mirror of https://github.com/go-gitea/gitea
Add Conda package registry (#22262)
This PR adds a [Conda](https://conda.io/) package registry.pull/22359/head^2
parent
5882e179a9
commit
6ba9ff7b48
@ -0,0 +1,85 @@ |
||||
--- |
||||
date: "2022-12-28T00:00:00+00:00" |
||||
title: "Conda Packages Repository" |
||||
slug: "packages/conda" |
||||
draft: false |
||||
toc: false |
||||
menu: |
||||
sidebar: |
||||
parent: "packages" |
||||
name: "Conda" |
||||
weight: 25 |
||||
identifier: "conda" |
||||
--- |
||||
|
||||
# Conda Packages Repository |
||||
|
||||
Publish [Conda](https://docs.conda.io/en/latest/) packages for your user or organization. |
||||
|
||||
**Table of Contents** |
||||
|
||||
{{< toc >}} |
||||
|
||||
## Requirements |
||||
|
||||
To work with the Conda package registry, you need to use [conda](https://docs.conda.io/projects/conda/en/stable/user-guide/install/index.html). |
||||
|
||||
## Configuring the package registry |
||||
|
||||
To register the package registry and provide credentials, edit your `.condarc` file: |
||||
|
||||
```yaml |
||||
channel_alias: https://gitea.example.com/api/packages/{owner}/conda |
||||
channels: |
||||
- https://gitea.example.com/api/packages/{owner}/conda |
||||
default_channels: |
||||
- https://gitea.example.com/api/packages/{owner}/conda |
||||
``` |
||||
|
||||
| Placeholder | Description | |
||||
| ------------ | ----------- | |
||||
| `owner` | The owner of the package. | |
||||
|
||||
See the [official documentation](https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html) for explanations of the individual settings. |
||||
|
||||
If you need to provide credentials, you may embed them as part of the channel url (`https://user:password@gitea.example.com/...`). |
||||
|
||||
## Publish a package |
||||
|
||||
To publish a package, perform a HTTP PUT operation with the package content in the request body. |
||||
|
||||
``` |
||||
PUT https://gitea.example.com/api/packages/{owner}/conda/{channel}/{filename} |
||||
``` |
||||
|
||||
| Placeholder | Description | |
||||
| ------------ | ----------- | |
||||
| `owner` | The owner of the package. | |
||||
| `channel` | The [channel](https://conda.io/projects/conda/en/latest/user-guide/concepts/channels.html) of the package. (optional) | |
||||
| `filename` | The name of the file. | |
||||
|
||||
Example request using HTTP Basic authentication: |
||||
|
||||
```shell |
||||
curl --user your_username:your_password_or_token \ |
||||
--upload-file path/to/package-1.0.conda \ |
||||
https://gitea.example.com/api/packages/testuser/conda/package-1.0.conda |
||||
``` |
||||
|
||||
You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first. |
||||
|
||||
## Install a package |
||||
|
||||
To install a package from the package registry, execute one of the following commands: |
||||
|
||||
```shell |
||||
conda install {package_name} |
||||
conda install {package_name}={package_version} |
||||
conda install -c {channel} {package_name} |
||||
``` |
||||
|
||||
| Parameter | Description | |
||||
| ----------------- | ----------- | |
||||
| `package_name` | The package name. | |
||||
| `package_version` | The package version. | |
||||
| `channel` | The channel of the package. (optional) | |
@ -0,0 +1,63 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package conda |
||||
|
||||
import ( |
||||
"context" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/models/packages" |
||||
conda_module "code.gitea.io/gitea/modules/packages/conda" |
||||
|
||||
"xorm.io/builder" |
||||
) |
||||
|
||||
type FileSearchOptions struct { |
||||
OwnerID int64 |
||||
Channel string |
||||
Subdir string |
||||
Filename string |
||||
} |
||||
|
||||
// SearchFiles gets all files matching the search options
|
||||
func SearchFiles(ctx context.Context, opts *FileSearchOptions) ([]*packages.PackageFile, error) { |
||||
var cond builder.Cond = builder.Eq{ |
||||
"package.type": packages.TypeConda, |
||||
"package.owner_id": opts.OwnerID, |
||||
"package_version.is_internal": false, |
||||
} |
||||
|
||||
if opts.Filename != "" { |
||||
cond = cond.And(builder.Eq{ |
||||
"package_file.lower_name": strings.ToLower(opts.Filename), |
||||
}) |
||||
} |
||||
|
||||
var versionPropsCond builder.Cond = builder.Eq{ |
||||
"package_property.ref_type": packages.PropertyTypePackage, |
||||
"package_property.name": conda_module.PropertyChannel, |
||||
"package_property.value": opts.Channel, |
||||
} |
||||
|
||||
cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(versionPropsCond).From("package_property"))) |
||||
|
||||
var filePropsCond builder.Cond = builder.Eq{ |
||||
"package_property.ref_type": packages.PropertyTypeFile, |
||||
"package_property.name": conda_module.PropertySubdir, |
||||
"package_property.value": opts.Subdir, |
||||
} |
||||
|
||||
cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(filePropsCond).From("package_property"))) |
||||
|
||||
sess := db.GetEngine(ctx). |
||||
Select("package_file.*"). |
||||
Table("package_file"). |
||||
Join("INNER", "package_version", "package_version.id = package_file.version_id"). |
||||
Join("INNER", "package", "package.id = package_version.package_id"). |
||||
Where(cond) |
||||
|
||||
pfs := make([]*packages.PackageFile, 0, 10) |
||||
return pfs, sess.Find(&pfs) |
||||
} |
@ -0,0 +1,243 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package conda |
||||
|
||||
import ( |
||||
"archive/tar" |
||||
"archive/zip" |
||||
"compress/bzip2" |
||||
"io" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/modules/json" |
||||
"code.gitea.io/gitea/modules/util" |
||||
"code.gitea.io/gitea/modules/validation" |
||||
|
||||
"github.com/klauspost/compress/zstd" |
||||
) |
||||
|
||||
var ( |
||||
ErrInvalidStructure = util.SilentWrap{Message: "package structure is invalid", Err: util.ErrInvalidArgument} |
||||
ErrInvalidName = util.SilentWrap{Message: "package name is invalid", Err: util.ErrInvalidArgument} |
||||
ErrInvalidVersion = util.SilentWrap{Message: "package version is invalid", Err: util.ErrInvalidArgument} |
||||
) |
||||
|
||||
const ( |
||||
PropertyName = "conda.name" |
||||
PropertyChannel = "conda.channel" |
||||
PropertySubdir = "conda.subdir" |
||||
PropertyMetadata = "conda.metdata" |
||||
) |
||||
|
||||
// Package represents a Conda package
|
||||
type Package struct { |
||||
Name string |
||||
Version string |
||||
Subdir string |
||||
VersionMetadata *VersionMetadata |
||||
FileMetadata *FileMetadata |
||||
} |
||||
|
||||
// VersionMetadata represents the metadata of a Conda package
|
||||
type VersionMetadata struct { |
||||
Description string `json:"description,omitempty"` |
||||
Summary string `json:"summary,omitempty"` |
||||
ProjectURL string `json:"project_url,omitempty"` |
||||
RepositoryURL string `json:"repository_url,omitempty"` |
||||
DocumentationURL string `json:"documentation_url,omitempty"` |
||||
License string `json:"license,omitempty"` |
||||
LicenseFamily string `json:"license_family,omitempty"` |
||||
} |
||||
|
||||
// FileMetadata represents the metadata of a Conda package file
|
||||
type FileMetadata struct { |
||||
IsCondaPackage bool `json:"is_conda"` |
||||
Architecture string `json:"architecture,omitempty"` |
||||
NoArch string `json:"noarch,omitempty"` |
||||
Build string `json:"build,omitempty"` |
||||
BuildNumber int64 `json:"build_number,omitempty"` |
||||
Dependencies []string `json:"dependencies,omitempty"` |
||||
Platform string `json:"platform,omitempty"` |
||||
Timestamp int64 `json:"timestamp,omitempty"` |
||||
} |
||||
|
||||
type index struct { |
||||
Name string `json:"name"` |
||||
Version string `json:"version"` |
||||
Architecture string `json:"arch"` |
||||
NoArch string `json:"noarch"` |
||||
Build string `json:"build"` |
||||
BuildNumber int64 `json:"build_number"` |
||||
Dependencies []string `json:"depends"` |
||||
License string `json:"license"` |
||||
LicenseFamily string `json:"license_family"` |
||||
Platform string `json:"platform"` |
||||
Subdir string `json:"subdir"` |
||||
Timestamp int64 `json:"timestamp"` |
||||
} |
||||
|
||||
type about struct { |
||||
Description string `json:"description"` |
||||
Summary string `json:"summary"` |
||||
ProjectURL string `json:"home"` |
||||
RepositoryURL string `json:"dev_url"` |
||||
DocumentationURL string `json:"doc_url"` |
||||
} |
||||
|
||||
type ReaderAndReaderAt interface { |
||||
io.Reader |
||||
io.ReaderAt |
||||
} |
||||
|
||||
// ParsePackageBZ2 parses the Conda package file compressed with bzip2
|
||||
func ParsePackageBZ2(r io.Reader) (*Package, error) { |
||||
gzr := bzip2.NewReader(r) |
||||
|
||||
return parsePackageTar(gzr) |
||||
} |
||||
|
||||
// ParsePackageConda parses the Conda package file compressed with zip and zstd
|
||||
func ParsePackageConda(r io.ReaderAt, size int64) (*Package, error) { |
||||
zr, err := zip.NewReader(r, size) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
for _, file := range zr.File { |
||||
if strings.HasPrefix(file.Name, "info-") && strings.HasSuffix(file.Name, ".tar.zst") { |
||||
f, err := zr.Open(file.Name) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer f.Close() |
||||
|
||||
dec, err := zstd.NewReader(f) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer dec.Close() |
||||
|
||||
p, err := parsePackageTar(dec) |
||||
if p != nil { |
||||
p.FileMetadata.IsCondaPackage = true |
||||
} |
||||
return p, err |
||||
} |
||||
} |
||||
|
||||
return nil, ErrInvalidStructure |
||||
} |
||||
|
||||
func parsePackageTar(r io.Reader) (*Package, error) { |
||||
var i *index |
||||
var a *about |
||||
|
||||
tr := tar.NewReader(r) |
||||
for { |
||||
hdr, err := tr.Next() |
||||
if err == io.EOF { |
||||
break |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if hdr.Typeflag != tar.TypeReg { |
||||
continue |
||||
} |
||||
|
||||
if hdr.Name == "info/index.json" { |
||||
if err := json.NewDecoder(tr).Decode(&i); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if !checkName(i.Name) { |
||||
return nil, ErrInvalidName |
||||
} |
||||
|
||||
if !checkVersion(i.Version) { |
||||
return nil, ErrInvalidVersion |
||||
} |
||||
|
||||
if a != nil { |
||||
break // stop loop if both files were found
|
||||
} |
||||
} else if hdr.Name == "info/about.json" { |
||||
if err := json.NewDecoder(tr).Decode(&a); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if !validation.IsValidURL(a.ProjectURL) { |
||||
a.ProjectURL = "" |
||||
} |
||||
if !validation.IsValidURL(a.RepositoryURL) { |
||||
a.RepositoryURL = "" |
||||
} |
||||
if !validation.IsValidURL(a.DocumentationURL) { |
||||
a.DocumentationURL = "" |
||||
} |
||||
|
||||
if i != nil { |
||||
break // stop loop if both files were found
|
||||
} |
||||
} |
||||
} |
||||
|
||||
if i == nil { |
||||
return nil, ErrInvalidStructure |
||||
} |
||||
if a == nil { |
||||
a = &about{} |
||||
} |
||||
|
||||
return &Package{ |
||||
Name: i.Name, |
||||
Version: i.Version, |
||||
Subdir: i.Subdir, |
||||
VersionMetadata: &VersionMetadata{ |
||||
License: i.License, |
||||
LicenseFamily: i.LicenseFamily, |
||||
Description: a.Description, |
||||
Summary: a.Summary, |
||||
ProjectURL: a.ProjectURL, |
||||
RepositoryURL: a.RepositoryURL, |
||||
DocumentationURL: a.DocumentationURL, |
||||
}, |
||||
FileMetadata: &FileMetadata{ |
||||
Architecture: i.Architecture, |
||||
NoArch: i.NoArch, |
||||
Build: i.Build, |
||||
BuildNumber: i.BuildNumber, |
||||
Dependencies: i.Dependencies, |
||||
Platform: i.Platform, |
||||
Timestamp: i.Timestamp, |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1393
|
||||
func checkName(name string) bool { |
||||
if name == "" { |
||||
return false |
||||
} |
||||
if name != strings.ToLower(name) { |
||||
return false |
||||
} |
||||
return !checkBadCharacters(name, "!") |
||||
} |
||||
|
||||
// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1403
|
||||
func checkVersion(version string) bool { |
||||
if version == "" { |
||||
return false |
||||
} |
||||
return !checkBadCharacters(version, "-") |
||||
} |
||||
|
||||
func checkBadCharacters(s, additional string) bool { |
||||
if strings.ContainsAny(s, "=@#$%^&*:;\"'\\|<>?/ ") { |
||||
return true |
||||
} |
||||
return strings.ContainsAny(s, additional) |
||||
} |
@ -0,0 +1,150 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package conda |
||||
|
||||
import ( |
||||
"archive/tar" |
||||
"archive/zip" |
||||
"bytes" |
||||
"io" |
||||
"testing" |
||||
|
||||
"github.com/dsnet/compress/bzip2" |
||||
"github.com/klauspost/compress/zstd" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
const ( |
||||
packageName = "gitea" |
||||
packageVersion = "1.0.1" |
||||
description = "Package Description" |
||||
projectURL = "https://gitea.io" |
||||
repositoryURL = "https://gitea.io/gitea/gitea" |
||||
documentationURL = "https://docs.gitea.io" |
||||
) |
||||
|
||||
func TestParsePackage(t *testing.T) { |
||||
createArchive := func(files map[string][]byte) *bytes.Buffer { |
||||
var buf bytes.Buffer |
||||
tw := tar.NewWriter(&buf) |
||||
for filename, content := range files { |
||||
hdr := &tar.Header{ |
||||
Name: filename, |
||||
Mode: 0o600, |
||||
Size: int64(len(content)), |
||||
} |
||||
tw.WriteHeader(hdr) |
||||
tw.Write(content) |
||||
} |
||||
tw.Close() |
||||
return &buf |
||||
} |
||||
|
||||
t.Run("MissingIndexFile", func(t *testing.T) { |
||||
buf := createArchive(map[string][]byte{"dummy.txt": {}}) |
||||
|
||||
p, err := parsePackageTar(buf) |
||||
assert.Nil(t, p) |
||||
assert.ErrorIs(t, err, ErrInvalidStructure) |
||||
}) |
||||
|
||||
t.Run("MissingAboutFile", func(t *testing.T) { |
||||
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"1.0"}`)}) |
||||
|
||||
p, err := parsePackageTar(buf) |
||||
assert.NotNil(t, p) |
||||
assert.NoError(t, err) |
||||
|
||||
assert.Equal(t, "name", p.Name) |
||||
assert.Equal(t, "1.0", p.Version) |
||||
assert.Empty(t, p.VersionMetadata.ProjectURL) |
||||
}) |
||||
|
||||
t.Run("InvalidName", func(t *testing.T) { |
||||
for _, name := range []string{"", "name!", "nAMe"} { |
||||
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"` + name + `","version":"1.0"}`)}) |
||||
|
||||
p, err := parsePackageTar(buf) |
||||
assert.Nil(t, p) |
||||
assert.ErrorIs(t, err, ErrInvalidName) |
||||
} |
||||
}) |
||||
|
||||
t.Run("InvalidVersion", func(t *testing.T) { |
||||
for _, version := range []string{"", "1.0-2"} { |
||||
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"` + version + `"}`)}) |
||||
|
||||
p, err := parsePackageTar(buf) |
||||
assert.Nil(t, p) |
||||
assert.ErrorIs(t, err, ErrInvalidVersion) |
||||
} |
||||
}) |
||||
|
||||
t.Run("Valid", func(t *testing.T) { |
||||
buf := createArchive(map[string][]byte{ |
||||
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `","subdir":"linux-64"}`), |
||||
"info/about.json": []byte(`{"description":"` + description + `","dev_url":"` + repositoryURL + `","doc_url":"` + documentationURL + `","home":"` + projectURL + `"}`), |
||||
}) |
||||
|
||||
p, err := parsePackageTar(buf) |
||||
assert.NotNil(t, p) |
||||
assert.NoError(t, err) |
||||
|
||||
assert.Equal(t, packageName, p.Name) |
||||
assert.Equal(t, packageVersion, p.Version) |
||||
assert.Equal(t, "linux-64", p.Subdir) |
||||
assert.Equal(t, description, p.VersionMetadata.Description) |
||||
assert.Equal(t, projectURL, p.VersionMetadata.ProjectURL) |
||||
assert.Equal(t, repositoryURL, p.VersionMetadata.RepositoryURL) |
||||
assert.Equal(t, documentationURL, p.VersionMetadata.DocumentationURL) |
||||
}) |
||||
|
||||
t.Run(".tar.bz2", func(t *testing.T) { |
||||
tarArchive := createArchive(map[string][]byte{ |
||||
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`), |
||||
}) |
||||
|
||||
var buf bytes.Buffer |
||||
bw, _ := bzip2.NewWriter(&buf, nil) |
||||
io.Copy(bw, tarArchive) |
||||
bw.Close() |
||||
|
||||
br := bytes.NewReader(buf.Bytes()) |
||||
|
||||
p, err := ParsePackageBZ2(br) |
||||
assert.NotNil(t, p) |
||||
assert.NoError(t, err) |
||||
|
||||
assert.Equal(t, packageName, p.Name) |
||||
assert.Equal(t, packageVersion, p.Version) |
||||
assert.False(t, p.FileMetadata.IsCondaPackage) |
||||
}) |
||||
|
||||
t.Run(".conda", func(t *testing.T) { |
||||
tarArchive := createArchive(map[string][]byte{ |
||||
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`), |
||||
}) |
||||
|
||||
var infoBuf bytes.Buffer |
||||
zsw, _ := zstd.NewWriter(&infoBuf) |
||||
io.Copy(zsw, tarArchive) |
||||
zsw.Close() |
||||
|
||||
var buf bytes.Buffer |
||||
zpw := zip.NewWriter(&buf) |
||||
w, _ := zpw.Create("info-x.tar.zst") |
||||
w.Write(infoBuf.Bytes()) |
||||
zpw.Close() |
||||
|
||||
br := bytes.NewReader(buf.Bytes()) |
||||
|
||||
p, err := ParsePackageConda(br, int64(br.Len())) |
||||
assert.NotNil(t, p) |
||||
assert.NoError(t, err) |
||||
|
||||
assert.Equal(t, packageName, p.Name) |
||||
assert.Equal(t, packageVersion, p.Version) |
||||
assert.True(t, p.FileMetadata.IsCondaPackage) |
||||
}) |
||||
} |
After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,306 @@ |
||||
// 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, 32*1024*1024) |
||||
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, _, err := packages_service.GetPackageFileStream(ctx, pf) |
||||
if err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
defer s.Close() |
||||
|
||||
ctx.ServeContent(s, &context.ServeHeaderOptions{ |
||||
Filename: pf.Name, |
||||
LastModified: pf.CreatedUnix.AsLocalTime(), |
||||
}) |
||||
} |
@ -0,0 +1,30 @@ |
||||
{{if eq .PackageDescriptor.Package.Type "conda"}} |
||||
<h4 class="ui top attached header">{{.locale.Tr "packages.installation"}}</h4> |
||||
<div class="ui attached segment"> |
||||
<div class="ui form"> |
||||
<div class="field"> |
||||
<label>{{svg "octicon-code"}} {{.locale.Tr "packages.conda.registry" | Safe}}</label> |
||||
<div class="markup"><pre class="code-block"><code>channel_alias: {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda |
||||
channels: |
||||
  - {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda |
||||
default_channels: |
||||
  - {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda</code></pre></div> |
||||
</div> |
||||
<div class="field"> |
||||
<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.conda.install"}}</label> |
||||
{{$channel := .PackageDescriptor.PackageProperties.GetByName "conda.channel"}} |
||||
<div class="markup"><pre class="code-block"><code>conda install{{if $channel}} -c {{$channel}}{{end}} {{.PackageDescriptor.PackageProperties.GetByName "conda.name"}}={{.PackageDescriptor.Version.Version}}</code></pre></div> |
||||
</div> |
||||
<div class="field"> |
||||
<label>{{.locale.Tr "packages.conda.documentation" | Safe}}</label> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Summary}} |
||||
<h4 class="ui top attached header">{{.locale.Tr "packages.about"}}</h4> |
||||
<div class="ui attached segment"> |
||||
{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{else}}{{.PackageDescriptor.Metadata.Summary}}{{end}} |
||||
</div> |
||||
{{end}} |
||||
{{end}} |
@ -0,0 +1,6 @@ |
||||
{{if eq .PackageDescriptor.Package.Type "conda"}} |
||||
{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.conda.details.repository_site"}}</a></div>{{end}} |
||||
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.conda.details.documentation_site"}}</a></div>{{end}} |
||||
{{end}} |
@ -0,0 +1,274 @@ |
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration |
||||
|
||||
import ( |
||||
"archive/tar" |
||||
"archive/zip" |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"testing" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/models/packages" |
||||
"code.gitea.io/gitea/models/unittest" |
||||
user_model "code.gitea.io/gitea/models/user" |
||||
conda_module "code.gitea.io/gitea/modules/packages/conda" |
||||
"code.gitea.io/gitea/tests" |
||||
|
||||
"github.com/dsnet/compress/bzip2" |
||||
"github.com/klauspost/compress/zstd" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestPackageConda(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
||||
|
||||
packageName := "test_package" |
||||
packageVersion := "1.0.1" |
||||
|
||||
channel := "test-channel" |
||||
root := fmt.Sprintf("/api/packages/%s/conda", user.Name) |
||||
|
||||
t.Run("Upload", func(t *testing.T) { |
||||
tarContent := func() []byte { |
||||
var buf bytes.Buffer |
||||
tw := tar.NewWriter(&buf) |
||||
|
||||
content := []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `","subdir":"noarch","build":"xxx"}`) |
||||
|
||||
hdr := &tar.Header{ |
||||
Name: "info/index.json", |
||||
Mode: 0o600, |
||||
Size: int64(len(content)), |
||||
} |
||||
tw.WriteHeader(hdr) |
||||
tw.Write(content) |
||||
tw.Close() |
||||
return buf.Bytes() |
||||
}() |
||||
|
||||
t.Run(".tar.bz2", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
var buf bytes.Buffer |
||||
bw, _ := bzip2.NewWriter(&buf, nil) |
||||
io.Copy(bw, bytes.NewReader(tarContent)) |
||||
bw.Close() |
||||
|
||||
filename := fmt.Sprintf("%s-%s.tar.bz2", packageName, packageVersion) |
||||
|
||||
req := NewRequestWithBody(t, "PUT", root+"/"+filename, bytes.NewReader(buf.Bytes())) |
||||
MakeRequest(t, req, http.StatusUnauthorized) |
||||
|
||||
req = NewRequestWithBody(t, "PUT", root+"/"+filename, bytes.NewReader(buf.Bytes())) |
||||
AddBasicAuthHeader(req, user.Name) |
||||
MakeRequest(t, req, http.StatusCreated) |
||||
|
||||
req = NewRequestWithBody(t, "PUT", root+"/"+filename, bytes.NewReader(buf.Bytes())) |
||||
AddBasicAuthHeader(req, user.Name) |
||||
MakeRequest(t, req, http.StatusConflict) |
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConda) |
||||
assert.NoError(t, err) |
||||
assert.Len(t, pvs, 1) |
||||
|
||||
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) |
||||
assert.NoError(t, err) |
||||
assert.Nil(t, pd.SemVer) |
||||
assert.IsType(t, &conda_module.VersionMetadata{}, pd.Metadata) |
||||
assert.Equal(t, packageName, pd.Package.Name) |
||||
assert.Equal(t, packageVersion, pd.Version.Version) |
||||
assert.Empty(t, pd.PackageProperties.GetByName(conda_module.PropertyChannel)) |
||||
}) |
||||
|
||||
t.Run(".conda", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
var infoBuf bytes.Buffer |
||||
zsw, _ := zstd.NewWriter(&infoBuf) |
||||
io.Copy(zsw, bytes.NewReader(tarContent)) |
||||
zsw.Close() |
||||
|
||||
var buf bytes.Buffer |
||||
zpw := zip.NewWriter(&buf) |
||||
w, _ := zpw.Create("info-x.tar.zst") |
||||
w.Write(infoBuf.Bytes()) |
||||
zpw.Close() |
||||
|
||||
fullName := channel + "/" + packageName |
||||
filename := fmt.Sprintf("%s-%s.conda", packageName, packageVersion) |
||||
|
||||
req := NewRequestWithBody(t, "PUT", root+"/"+channel+"/"+filename, bytes.NewReader(buf.Bytes())) |
||||
MakeRequest(t, req, http.StatusUnauthorized) |
||||
|
||||
req = NewRequestWithBody(t, "PUT", root+"/"+channel+"/"+filename, bytes.NewReader(buf.Bytes())) |
||||
AddBasicAuthHeader(req, user.Name) |
||||
MakeRequest(t, req, http.StatusCreated) |
||||
|
||||
req = NewRequestWithBody(t, "PUT", root+"/"+channel+"/"+filename, bytes.NewReader(buf.Bytes())) |
||||
AddBasicAuthHeader(req, user.Name) |
||||
MakeRequest(t, req, http.StatusConflict) |
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConda) |
||||
assert.NoError(t, err) |
||||
assert.Len(t, pvs, 2) |
||||
|
||||
pds, err := packages.GetPackageDescriptors(db.DefaultContext, pvs) |
||||
assert.NoError(t, err) |
||||
|
||||
assert.Condition(t, func() bool { |
||||
for _, pd := range pds { |
||||
if pd.Package.Name == fullName { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
}) |
||||
|
||||
for _, pd := range pds { |
||||
if pd.Package.Name == fullName { |
||||
assert.Nil(t, pd.SemVer) |
||||
assert.IsType(t, &conda_module.VersionMetadata{}, pd.Metadata) |
||||
assert.Equal(t, fullName, pd.Package.Name) |
||||
assert.Equal(t, packageVersion, pd.Version.Version) |
||||
assert.Equal(t, channel, pd.PackageProperties.GetByName(conda_module.PropertyChannel)) |
||||
} |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
t.Run("Download", func(t *testing.T) { |
||||
t.Run(".tar.bz2", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/noarch/%s-%s-xxx.tar.bz2", root, packageName, packageVersion)) |
||||
MakeRequest(t, req, http.StatusOK) |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/noarch/%s-%s-xxx.tar.bz2", root, channel, packageName, packageVersion)) |
||||
MakeRequest(t, req, http.StatusNotFound) |
||||
}) |
||||
|
||||
t.Run(".conda", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/noarch/%s-%s-xxx.conda", root, packageName, packageVersion)) |
||||
MakeRequest(t, req, http.StatusNotFound) |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/noarch/%s-%s-xxx.conda", root, channel, packageName, packageVersion)) |
||||
MakeRequest(t, req, http.StatusOK) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("EnumeratePackages", func(t *testing.T) { |
||||
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"` |
||||
} |
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/noarch/repodata.json", root)) |
||||
resp := MakeRequest(t, req, http.StatusOK) |
||||
assert.Equal(t, "application/json", resp.Header().Get("Content-Type")) |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/noarch/repodata.json.bz2", root)) |
||||
resp = MakeRequest(t, req, http.StatusOK) |
||||
assert.Equal(t, "application/x-bzip2", resp.Header().Get("Content-Type")) |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/noarch/current_repodata.json", root)) |
||||
resp = MakeRequest(t, req, http.StatusOK) |
||||
assert.Equal(t, "application/json", resp.Header().Get("Content-Type")) |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/noarch/current_repodata.json.bz2", root)) |
||||
resp = MakeRequest(t, req, http.StatusOK) |
||||
assert.Equal(t, "application/x-bzip2", resp.Header().Get("Content-Type")) |
||||
|
||||
t.Run(".tar.bz2", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
pv, err := packages.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages.TypeConda, packageName, packageVersion) |
||||
assert.NoError(t, err) |
||||
|
||||
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pv) |
||||
assert.NoError(t, err) |
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/noarch/repodata.json", root)) |
||||
resp := MakeRequest(t, req, http.StatusOK) |
||||
|
||||
var result RepoData |
||||
DecodeJSON(t, resp, &result) |
||||
|
||||
assert.Equal(t, "noarch", result.Info.Subdir) |
||||
assert.Empty(t, result.PackagesConda) |
||||
assert.Empty(t, result.Removed) |
||||
|
||||
filename := fmt.Sprintf("%s-%s-xxx.tar.bz2", packageName, packageVersion) |
||||
assert.Contains(t, result.Packages, filename) |
||||
packageInfo := result.Packages[filename] |
||||
assert.Equal(t, packageName, packageInfo.Name) |
||||
assert.Equal(t, packageVersion, packageInfo.Version) |
||||
assert.Equal(t, "noarch", packageInfo.Subdir) |
||||
assert.Equal(t, "xxx", packageInfo.Build) |
||||
assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5) |
||||
assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256) |
||||
assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size) |
||||
}) |
||||
|
||||
t.Run(".conda", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
pv, err := packages.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages.TypeConda, channel+"/"+packageName, packageVersion) |
||||
assert.NoError(t, err) |
||||
|
||||
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pv) |
||||
assert.NoError(t, err) |
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/noarch/repodata.json", root, channel)) |
||||
resp := MakeRequest(t, req, http.StatusOK) |
||||
|
||||
var result RepoData |
||||
DecodeJSON(t, resp, &result) |
||||
|
||||
assert.Equal(t, "noarch", result.Info.Subdir) |
||||
assert.Empty(t, result.Packages) |
||||
assert.Empty(t, result.Removed) |
||||
|
||||
filename := fmt.Sprintf("%s-%s-xxx.conda", packageName, packageVersion) |
||||
assert.Contains(t, result.PackagesConda, filename) |
||||
packageInfo := result.PackagesConda[filename] |
||||
assert.Equal(t, packageName, packageInfo.Name) |
||||
assert.Equal(t, packageVersion, packageInfo.Version) |
||||
assert.Equal(t, "noarch", packageInfo.Subdir) |
||||
assert.Equal(t, "xxx", packageInfo.Build) |
||||
assert.Equal(t, pd.Files[0].Blob.HashMD5, packageInfo.HashMD5) |
||||
assert.Equal(t, pd.Files[0].Blob.HashSHA256, packageInfo.HashSHA256) |
||||
assert.Equal(t, pd.Files[0].Blob.Size, packageInfo.Size) |
||||
}) |
||||
}) |
||||
} |
After Width: | Height: | Size: 3.8 KiB |
Loading…
Reference in new issue