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