mirror of https://github.com/go-gitea/gitea
Add Debian package registry (#24426)
Co-authored-by: @awkwardbunny This PR adds a Debian package registry. You can follow [this tutorial](https://www.baeldung.com/linux/create-debian-package) to build a *.deb package for testing. Source packages are not supported at the moment and I did not find documentation of the architecture "all" and how these packages should be treated. ![grafik](https://user-images.githubusercontent.com/1666336/218126879-eb80a866-775c-4c8e-8529-5797203a64e6.png) Part of #20751. Revised copy of #22854. --------- Co-authored-by: Brian Hong <brian@hongs.me> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Giteabot <teabot@gitea.io>pull/24478/head^2
parent
1f52560ca4
commit
bf999e4069
@ -0,0 +1,134 @@ |
||||
--- |
||||
date: "2023-01-07T00:00:00+00:00" |
||||
title: "Debian Packages Repository" |
||||
slug: "packages/debian" |
||||
draft: false |
||||
toc: false |
||||
menu: |
||||
sidebar: |
||||
parent: "packages" |
||||
name: "Debian" |
||||
weight: 35 |
||||
identifier: "debian" |
||||
--- |
||||
|
||||
# Debian Packages Repository |
||||
|
||||
Publish [Debian](https://www.debian.org/distrib/packages) packages for your user or organization. |
||||
|
||||
**Table of Contents** |
||||
|
||||
{{< toc >}} |
||||
|
||||
## Requirements |
||||
|
||||
To work with the Debian registry, you need to use a HTTP client like `curl` to upload and a package manager like `apt` to consume packages. |
||||
|
||||
The following examples use `apt`. |
||||
|
||||
## Configuring the package registry |
||||
|
||||
To register the Debian registry add the url to the list of known apt sources: |
||||
|
||||
```shell |
||||
echo "deb https://gitea.example.com/api/packages/{owner}/debian {distribution} {component}" | sudo tee -a /etc/apt/sources.list.d/gitea.list |
||||
``` |
||||
|
||||
| Placeholder | Description | |
||||
| -------------- | ----------- | |
||||
| `owner` | The owner of the package. | |
||||
| `distribution` | The distribution to use. | |
||||
| `component` | The component to use. | |
||||
|
||||
If the registry is private, provide credentials in the url. You can use a password or a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}): |
||||
|
||||
```shell |
||||
echo "deb https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/debian {distribution} {component}" | sudo tee -a /etc/apt/sources.list.d/gitea.list |
||||
``` |
||||
|
||||
The Debian registry files are signed with a PGP key which must be known to apt: |
||||
|
||||
```shell |
||||
sudo curl https://gitea.example.com/api/packages/{owner}/debian/repository.key -o /etc/apt/trusted.gpg.d/gitea-{owner}.asc |
||||
``` |
||||
|
||||
Afterwards update the local package index: |
||||
|
||||
```shell |
||||
apt update |
||||
``` |
||||
|
||||
## Publish a package |
||||
|
||||
To publish a Debian package (`*.deb`), perform a HTTP `PUT` operation with the package content in the request body. |
||||
|
||||
``` |
||||
PUT https://gitea.example.com/api/packages/{owner}/debian/pool/{distribution}/{component}/upload |
||||
``` |
||||
|
||||
| Parameter | Description | |
||||
| -------------- | ----------- | |
||||
| `owner` | The owner of the package. | |
||||
| `distribution` | The distribution may match the release name of the OS, ex: `bionic`. | |
||||
| `component` | The component can be used to group packages or just `main` or similar. | |
||||
|
||||
Example request using HTTP Basic authentication: |
||||
|
||||
```shell |
||||
curl --user your_username:your_password_or_token \ |
||||
--upload-file path/to/file.deb \ |
||||
https://gitea.example.com/api/packages/testuser/debian/pool/bionic/main/upload |
||||
``` |
||||
|
||||
If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/development/api-usage.en-us.md#authentication" >}}) instead of the password. |
||||
You cannot publish a file with the same name twice to a package. You must delete the existing package version first. |
||||
|
||||
The server reponds with the following HTTP Status codes. |
||||
|
||||
| HTTP Status Code | Meaning | |
||||
| ----------------- | ------- | |
||||
| `201 Created` | The package has been published. | |
||||
| `400 Bad Request` | The package name, version, distribution, component or architecture are invalid. | |
||||
| `409 Conflict` | A package file with the same combination of parameters exists already. | |
||||
|
||||
## Delete a package |
||||
|
||||
To delete a Debian package perform a HTTP `DELETE` operation. This will delete the package version too if there is no file left. |
||||
|
||||
``` |
||||
DELETE https://gitea.example.com/api/packages/{owner}/debian/pool/{distribution}/{component}/{package_name}/{package_version}/{architecture} |
||||
``` |
||||
|
||||
| Parameter | Description | |
||||
| ----------------- | ----------- | |
||||
| `owner` | The owner of the package. | |
||||
| `package_name` | The package name. | |
||||
| `package_version` | The package version. | |
||||
| `distribution` | The package distribution. | |
||||
| `component` | The package component. | |
||||
| `architecture` | The package architecture. | |
||||
|
||||
Example request using HTTP Basic authentication: |
||||
|
||||
```shell |
||||
curl --user your_username:your_token_or_password -X DELETE \ |
||||
https://gitea.example.com/api/packages/testuser/debian/pools/bionic/main/test-package/1.0.0/amd64 |
||||
``` |
||||
|
||||
The server reponds with the following HTTP Status codes. |
||||
|
||||
| HTTP Status Code | Meaning | |
||||
| ----------------- | ------- | |
||||
| `204 No Content` | Success | |
||||
| `404 Not Found` | The package or file was not found. | |
||||
|
||||
## Install a package |
||||
|
||||
To install a package from the Debian registry, execute the following commands: |
||||
|
||||
```shell |
||||
# use latest version |
||||
apt install {package_name} |
||||
# use specific version |
||||
apt install {package_name}={package_version} |
||||
``` |
@ -0,0 +1,23 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_20 //nolint
|
||||
|
||||
import ( |
||||
"xorm.io/xorm" |
||||
) |
||||
|
||||
func AddIsInternalColumnToPackage(x *xorm.Engine) error { |
||||
type Package struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
OwnerID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` |
||||
RepoID int64 `xorm:"INDEX"` |
||||
Type string `xorm:"UNIQUE(s) INDEX NOT NULL"` |
||||
Name string `xorm:"NOT NULL"` |
||||
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` |
||||
SemverCompatible bool `xorm:"NOT NULL DEFAULT false"` |
||||
IsInternal bool `xorm:"NOT NULL DEFAULT false"` |
||||
} |
||||
|
||||
return x.Sync(new(Package)) |
||||
} |
@ -0,0 +1,131 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian |
||||
|
||||
import ( |
||||
"context" |
||||
"strconv" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
"code.gitea.io/gitea/models/packages" |
||||
debian_module "code.gitea.io/gitea/modules/packages/debian" |
||||
|
||||
"xorm.io/builder" |
||||
) |
||||
|
||||
type PackageSearchOptions struct { |
||||
OwnerID int64 |
||||
Distribution string |
||||
Component string |
||||
Architecture string |
||||
} |
||||
|
||||
// SearchLatestPackages gets the latest packages matching the search options
|
||||
func SearchLatestPackages(ctx context.Context, opts *PackageSearchOptions) ([]*packages.PackageFileDescriptor, error) { |
||||
var cond builder.Cond = builder.Eq{ |
||||
"package_file.is_lead": true, |
||||
"package.type": packages.TypeDebian, |
||||
"package.owner_id": opts.OwnerID, |
||||
"package.is_internal": false, |
||||
"package_version.is_internal": false, |
||||
} |
||||
|
||||
props := make(map[string]string) |
||||
if opts.Distribution != "" { |
||||
props[debian_module.PropertyDistribution] = opts.Distribution |
||||
} |
||||
if opts.Component != "" { |
||||
props[debian_module.PropertyComponent] = opts.Component |
||||
} |
||||
if opts.Architecture != "" { |
||||
props[debian_module.PropertyArchitecture] = opts.Architecture |
||||
} |
||||
|
||||
if len(props) > 0 { |
||||
var propsCond builder.Cond = builder.Eq{ |
||||
"package_property.ref_type": packages.PropertyTypeFile, |
||||
} |
||||
propsCond = propsCond.And(builder.Expr("package_property.ref_id = package_file.id")) |
||||
|
||||
propsCondBlock := builder.NewCond() |
||||
for name, value := range props { |
||||
propsCondBlock = propsCondBlock.Or(builder.Eq{ |
||||
"package_property.name": name, |
||||
"package_property.value": value, |
||||
}) |
||||
} |
||||
propsCond = propsCond.And(propsCondBlock) |
||||
|
||||
cond = cond.And(builder.Eq{ |
||||
strconv.Itoa(len(props)): builder.Select("COUNT(*)").Where(propsCond).From("package_property"), |
||||
}) |
||||
} |
||||
|
||||
cond = cond. |
||||
And(builder.Expr("pv2.id IS NULL")) |
||||
|
||||
joinCond := builder. |
||||
Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))"). |
||||
And(builder.Eq{"pv2.is_internal": false}) |
||||
|
||||
pfs := make([]*packages.PackageFile, 0, 10) |
||||
err := db.GetEngine(ctx). |
||||
Table("package_file"). |
||||
Select("package_file.*"). |
||||
Join("INNER", "package_version", "package_version.id = package_file.version_id"). |
||||
Join("LEFT", "package_version pv2", joinCond). |
||||
Join("INNER", "package", "package.id = package_version.package_id"). |
||||
Where(cond). |
||||
Desc("package_version.created_unix"). |
||||
Find(&pfs) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return packages.GetPackageFileDescriptors(ctx, pfs) |
||||
} |
||||
|
||||
// GetDistributions gets all available distributions
|
||||
func GetDistributions(ctx context.Context, ownerID int64) ([]string, error) { |
||||
return getDistinctPropertyValues(ctx, ownerID, "", debian_module.PropertyDistribution) |
||||
} |
||||
|
||||
// GetComponents gets all available components for the given distribution
|
||||
func GetComponents(ctx context.Context, ownerID int64, distribution string) ([]string, error) { |
||||
return getDistinctPropertyValues(ctx, ownerID, distribution, debian_module.PropertyComponent) |
||||
} |
||||
|
||||
// GetArchitectures gets all available architectures for the given distribution
|
||||
func GetArchitectures(ctx context.Context, ownerID int64, distribution string) ([]string, error) { |
||||
return getDistinctPropertyValues(ctx, ownerID, distribution, debian_module.PropertyArchitecture) |
||||
} |
||||
|
||||
func getDistinctPropertyValues(ctx context.Context, ownerID int64, distribution, propName string) ([]string, error) { |
||||
var cond builder.Cond = builder.Eq{ |
||||
"package_property.ref_type": packages.PropertyTypeFile, |
||||
"package_property.name": propName, |
||||
"package.type": packages.TypeDebian, |
||||
"package.owner_id": ownerID, |
||||
} |
||||
if distribution != "" { |
||||
innerCond := builder. |
||||
Expr("pp.ref_id = package_property.ref_id"). |
||||
And(builder.Eq{ |
||||
"pp.ref_type": packages.PropertyTypeFile, |
||||
"pp.name": debian_module.PropertyDistribution, |
||||
"pp.value": distribution, |
||||
}) |
||||
cond = cond.And(builder.Exists(builder.Select("pp.ref_id").From("package_property pp").Where(innerCond))) |
||||
} |
||||
|
||||
values := make([]string, 0, 5) |
||||
return values, db.GetEngine(ctx). |
||||
Table("package_property"). |
||||
Distinct("package_property.value"). |
||||
Join("INNER", "package_file", "package_file.id = package_property.ref_id"). |
||||
Join("INNER", "package_version", "package_version.id = package_file.version_id"). |
||||
Join("INNER", "package", "package.id = package_version.package_id"). |
||||
Where(cond). |
||||
Find(&values) |
||||
} |
@ -0,0 +1,218 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian |
||||
|
||||
import ( |
||||
"archive/tar" |
||||
"bufio" |
||||
"compress/gzip" |
||||
"io" |
||||
"net/mail" |
||||
"regexp" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/modules/util" |
||||
"code.gitea.io/gitea/modules/validation" |
||||
|
||||
"github.com/blakesmith/ar" |
||||
"github.com/klauspost/compress/zstd" |
||||
"github.com/ulikunitz/xz" |
||||
) |
||||
|
||||
const ( |
||||
PropertyDistribution = "debian.distribution" |
||||
PropertyComponent = "debian.component" |
||||
PropertyArchitecture = "debian.architecture" |
||||
PropertyControl = "debian.control" |
||||
PropertyRepositoryIncludeInRelease = "debian.repository.include_in_release" |
||||
|
||||
SettingKeyPrivate = "debian.key.private" |
||||
SettingKeyPublic = "debian.key.public" |
||||
|
||||
RepositoryPackage = "_debian" |
||||
RepositoryVersion = "_repository" |
||||
|
||||
controlTar = "control.tar" |
||||
) |
||||
|
||||
var ( |
||||
ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing") |
||||
ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm") |
||||
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") |
||||
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") |
||||
ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") |
||||
|
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#source
|
||||
namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) |
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
|
||||
versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) |
||||
) |
||||
|
||||
type Package struct { |
||||
Name string |
||||
Version string |
||||
Architecture string |
||||
Control string |
||||
Metadata *Metadata |
||||
} |
||||
|
||||
type Metadata struct { |
||||
Maintainer string `json:"maintainer,omitempty"` |
||||
ProjectURL string `json:"project_url,omitempty"` |
||||
Description string `json:"description,omitempty"` |
||||
Dependencies []string `json:"dependencies,omitempty"` |
||||
} |
||||
|
||||
// ParsePackage parses the Debian package file
|
||||
// https://manpages.debian.org/bullseye/dpkg-dev/deb.5.en.html
|
||||
func ParsePackage(r io.Reader) (*Package, error) { |
||||
arr := ar.NewReader(r) |
||||
|
||||
for { |
||||
hd, err := arr.Next() |
||||
if err == io.EOF { |
||||
break |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if strings.HasPrefix(hd.Name, controlTar) { |
||||
var inner io.Reader |
||||
switch hd.Name[len(controlTar):] { |
||||
case "": |
||||
inner = arr |
||||
case ".gz": |
||||
gzr, err := gzip.NewReader(arr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer gzr.Close() |
||||
|
||||
inner = gzr |
||||
case ".xz": |
||||
xzr, err := xz.NewReader(arr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
inner = xzr |
||||
case ".zst": |
||||
zr, err := zstd.NewReader(arr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer zr.Close() |
||||
|
||||
inner = zr |
||||
default: |
||||
return nil, ErrUnsupportedCompression |
||||
} |
||||
|
||||
tr := tar.NewReader(inner) |
||||
for { |
||||
hd, err := tr.Next() |
||||
if err == io.EOF { |
||||
break |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if hd.Typeflag != tar.TypeReg { |
||||
continue |
||||
} |
||||
|
||||
if hd.FileInfo().Name() == "control" { |
||||
return ParseControlFile(tr) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil, ErrMissingControlFile |
||||
} |
||||
|
||||
// ParseControlFile parses a Debian control file to retrieve the metadata
|
||||
func ParseControlFile(r io.Reader) (*Package, error) { |
||||
p := &Package{ |
||||
Metadata: &Metadata{}, |
||||
} |
||||
|
||||
key := "" |
||||
var depends strings.Builder |
||||
var control strings.Builder |
||||
|
||||
s := bufio.NewScanner(io.TeeReader(r, &control)) |
||||
for s.Scan() { |
||||
line := s.Text() |
||||
|
||||
trimmed := strings.TrimSpace(line) |
||||
if trimmed == "" { |
||||
continue |
||||
} |
||||
|
||||
if line[0] == ' ' || line[0] == '\t' { |
||||
switch key { |
||||
case "Description": |
||||
p.Metadata.Description += line |
||||
case "Depends": |
||||
depends.WriteString(trimmed) |
||||
} |
||||
} else { |
||||
parts := strings.SplitN(trimmed, ":", 2) |
||||
if len(parts) < 2 { |
||||
continue |
||||
} |
||||
|
||||
key = parts[0] |
||||
value := strings.TrimSpace(parts[1]) |
||||
switch key { |
||||
case "Package": |
||||
if !namePattern.MatchString(value) { |
||||
return nil, ErrInvalidName |
||||
} |
||||
p.Name = value |
||||
case "Version": |
||||
if !versionPattern.MatchString(value) { |
||||
return nil, ErrInvalidVersion |
||||
} |
||||
p.Version = value |
||||
case "Architecture": |
||||
if value == "" { |
||||
return nil, ErrInvalidArchitecture |
||||
} |
||||
p.Architecture = value |
||||
case "Maintainer": |
||||
a, err := mail.ParseAddress(value) |
||||
if err != nil || a.Name == "" { |
||||
p.Metadata.Maintainer = value |
||||
} else { |
||||
p.Metadata.Maintainer = a.Name |
||||
} |
||||
case "Description": |
||||
p.Metadata.Description = value |
||||
case "Depends": |
||||
depends.WriteString(value) |
||||
case "Homepage": |
||||
if validation.IsValidURL(value) { |
||||
p.Metadata.ProjectURL = value |
||||
} |
||||
} |
||||
} |
||||
} |
||||
if err := s.Err(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
dependencies := strings.Split(depends.String(), ",") |
||||
for i := range dependencies { |
||||
dependencies[i] = strings.TrimSpace(dependencies[i]) |
||||
} |
||||
p.Metadata.Dependencies = dependencies |
||||
|
||||
p.Control = control.String() |
||||
|
||||
return p, nil |
||||
} |
@ -0,0 +1,171 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian |
||||
|
||||
import ( |
||||
"archive/tar" |
||||
"bytes" |
||||
"compress/gzip" |
||||
"io" |
||||
"testing" |
||||
|
||||
"github.com/blakesmith/ar" |
||||
"github.com/klauspost/compress/zstd" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/ulikunitz/xz" |
||||
) |
||||
|
||||
const ( |
||||
packageName = "gitea" |
||||
packageVersion = "0:1.0.1-te~st" |
||||
packageArchitecture = "amd64" |
||||
packageAuthor = "KN4CK3R" |
||||
description = "Description with multiple lines." |
||||
projectURL = "https://gitea.io" |
||||
) |
||||
|
||||
func TestParsePackage(t *testing.T) { |
||||
createArchive := func(files map[string][]byte) io.Reader { |
||||
var buf bytes.Buffer |
||||
aw := ar.NewWriter(&buf) |
||||
aw.WriteGlobalHeader() |
||||
for filename, content := range files { |
||||
hdr := &ar.Header{ |
||||
Name: filename, |
||||
Mode: 0o600, |
||||
Size: int64(len(content)), |
||||
} |
||||
aw.WriteHeader(hdr) |
||||
aw.Write(content) |
||||
} |
||||
return &buf |
||||
} |
||||
|
||||
t.Run("MissingControlFile", func(t *testing.T) { |
||||
data := createArchive(map[string][]byte{"dummy.txt": {}}) |
||||
|
||||
p, err := ParsePackage(data) |
||||
assert.Nil(t, p) |
||||
assert.ErrorIs(t, err, ErrMissingControlFile) |
||||
}) |
||||
|
||||
t.Run("Compression", func(t *testing.T) { |
||||
t.Run("Unsupported", func(t *testing.T) { |
||||
data := createArchive(map[string][]byte{"control.tar.foo": {}}) |
||||
|
||||
p, err := ParsePackage(data) |
||||
assert.Nil(t, p) |
||||
assert.ErrorIs(t, err, ErrUnsupportedCompression) |
||||
}) |
||||
|
||||
var buf bytes.Buffer |
||||
tw := tar.NewWriter(&buf) |
||||
tw.WriteHeader(&tar.Header{ |
||||
Name: "control", |
||||
Mode: 0o600, |
||||
Size: 50, |
||||
}) |
||||
tw.Write([]byte("Package: gitea\nVersion: 1.0.0\nArchitecture: amd64\n")) |
||||
tw.Close() |
||||
|
||||
t.Run("None", func(t *testing.T) { |
||||
data := createArchive(map[string][]byte{"control.tar": buf.Bytes()}) |
||||
|
||||
p, err := ParsePackage(data) |
||||
assert.NotNil(t, p) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, "gitea", p.Name) |
||||
}) |
||||
|
||||
t.Run("gz", func(t *testing.T) { |
||||
var zbuf bytes.Buffer |
||||
zw := gzip.NewWriter(&zbuf) |
||||
zw.Write(buf.Bytes()) |
||||
zw.Close() |
||||
|
||||
data := createArchive(map[string][]byte{"control.tar.gz": zbuf.Bytes()}) |
||||
|
||||
p, err := ParsePackage(data) |
||||
assert.NotNil(t, p) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, "gitea", p.Name) |
||||
}) |
||||
|
||||
t.Run("xz", func(t *testing.T) { |
||||
var xbuf bytes.Buffer |
||||
xw, _ := xz.NewWriter(&xbuf) |
||||
xw.Write(buf.Bytes()) |
||||
xw.Close() |
||||
|
||||
data := createArchive(map[string][]byte{"control.tar.xz": xbuf.Bytes()}) |
||||
|
||||
p, err := ParsePackage(data) |
||||
assert.NotNil(t, p) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, "gitea", p.Name) |
||||
}) |
||||
|
||||
t.Run("zst", func(t *testing.T) { |
||||
var zbuf bytes.Buffer |
||||
zw, _ := zstd.NewWriter(&zbuf) |
||||
zw.Write(buf.Bytes()) |
||||
zw.Close() |
||||
|
||||
data := createArchive(map[string][]byte{"control.tar.zst": zbuf.Bytes()}) |
||||
|
||||
p, err := ParsePackage(data) |
||||
assert.NotNil(t, p) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, "gitea", p.Name) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func TestParseControlFile(t *testing.T) { |
||||
buildContent := func(name, version, architecture string) *bytes.Buffer { |
||||
var buf bytes.Buffer |
||||
buf.WriteString("Package: " + name + "\nVersion: " + version + "\nArchitecture: " + architecture + "\nMaintainer: " + packageAuthor + " <kn4ck3r@gitea.io>\nHomepage: " + projectURL + "\nDepends: a,\n b\nDescription: Description\n with multiple\n lines.") |
||||
return &buf |
||||
} |
||||
|
||||
t.Run("InvalidName", func(t *testing.T) { |
||||
for _, name := range []string{"", "-cd"} { |
||||
p, err := ParseControlFile(buildContent(name, packageVersion, packageArchitecture)) |
||||
assert.Nil(t, p) |
||||
assert.ErrorIs(t, err, ErrInvalidName) |
||||
} |
||||
}) |
||||
|
||||
t.Run("InvalidVersion", func(t *testing.T) { |
||||
for _, version := range []string{"", "1-", ":1.0", "1_0"} { |
||||
p, err := ParseControlFile(buildContent(packageName, version, packageArchitecture)) |
||||
assert.Nil(t, p) |
||||
assert.ErrorIs(t, err, ErrInvalidVersion) |
||||
} |
||||
}) |
||||
|
||||
t.Run("InvalidArchitecture", func(t *testing.T) { |
||||
p, err := ParseControlFile(buildContent(packageName, packageVersion, "")) |
||||
assert.Nil(t, p) |
||||
assert.ErrorIs(t, err, ErrInvalidArchitecture) |
||||
}) |
||||
|
||||
t.Run("Valid", func(t *testing.T) { |
||||
content := buildContent(packageName, packageVersion, packageArchitecture) |
||||
full := content.String() |
||||
|
||||
p, err := ParseControlFile(content) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, p) |
||||
|
||||
assert.Equal(t, packageName, p.Name) |
||||
assert.Equal(t, packageVersion, p.Version) |
||||
assert.Equal(t, packageArchitecture, p.Architecture) |
||||
assert.Equal(t, description, p.Metadata.Description) |
||||
assert.Equal(t, projectURL, p.Metadata.ProjectURL) |
||||
assert.Equal(t, packageAuthor, p.Metadata.Maintainer) |
||||
assert.Equal(t, []string{"a", "b"}, p.Metadata.Dependencies) |
||||
assert.Equal(t, full, p.Control) |
||||
}) |
||||
} |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,317 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian |
||||
|
||||
import ( |
||||
stdctx "context" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
packages_model "code.gitea.io/gitea/models/packages" |
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/notification" |
||||
packages_module "code.gitea.io/gitea/modules/packages" |
||||
debian_module "code.gitea.io/gitea/modules/packages/debian" |
||||
"code.gitea.io/gitea/modules/util" |
||||
"code.gitea.io/gitea/routers/api/packages/helper" |
||||
packages_service "code.gitea.io/gitea/services/packages" |
||||
debian_service "code.gitea.io/gitea/services/packages/debian" |
||||
) |
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) { |
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) { |
||||
ctx.PlainText(status, message) |
||||
}) |
||||
} |
||||
|
||||
func GetRepositoryKey(ctx *context.Context) { |
||||
_, pub, err := debian_service.GetOrCreateKeyPair(ctx.Package.Owner.ID) |
||||
if err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
|
||||
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ |
||||
ContentType: "application/pgp-keys", |
||||
Filename: "repository.key", |
||||
}) |
||||
} |
||||
|
||||
// https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
|
||||
// https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
|
||||
func GetRepositoryFile(ctx *context.Context) { |
||||
pv, err := debian_service.GetOrCreateRepositoryVersion(ctx.Package.Owner.ID) |
||||
if err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
|
||||
key := ctx.Params("distribution") |
||||
|
||||
component := ctx.Params("component") |
||||
architecture := strings.TrimPrefix(ctx.Params("architecture"), "binary-") |
||||
if component != "" && architecture != "" { |
||||
key += "|" + component + "|" + architecture |
||||
} |
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageVersion( |
||||
ctx, |
||||
pv, |
||||
&packages_service.PackageFileInfo{ |
||||
Filename: ctx.Params("filename"), |
||||
CompositeKey: key, |
||||
}, |
||||
) |
||||
if err != nil { |
||||
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { |
||||
apiError(ctx, http.StatusNotFound, err) |
||||
} else { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
} |
||||
return |
||||
} |
||||
defer s.Close() |
||||
|
||||
ctx.ServeContent(s, &context.ServeHeaderOptions{ |
||||
Filename: pf.Name, |
||||
LastModified: pf.CreatedUnix.AsLocalTime(), |
||||
}) |
||||
} |
||||
|
||||
// https://wiki.debian.org/DebianRepository/Format#indices_acquisition_via_hashsums_.28by-hash.29
|
||||
func GetRepositoryFileByHash(ctx *context.Context) { |
||||
pv, err := debian_service.GetOrCreateRepositoryVersion(ctx.Package.Owner.ID) |
||||
if err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
|
||||
algorithm := strings.ToLower(ctx.Params("algorithm")) |
||||
if algorithm == "md5sum" { |
||||
algorithm = "md5" |
||||
} |
||||
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ |
||||
VersionID: pv.ID, |
||||
Hash: strings.ToLower(ctx.Params("hash")), |
||||
HashAlgorithm: algorithm, |
||||
}) |
||||
if err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
if len(pfs) != 1 { |
||||
apiError(ctx, http.StatusNotFound, nil) |
||||
return |
||||
} |
||||
|
||||
s, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) |
||||
if err != nil { |
||||
if errors.Is(err, util.ErrNotExist) { |
||||
apiError(ctx, http.StatusNotFound, err) |
||||
} else { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
} |
||||
return |
||||
} |
||||
defer s.Close() |
||||
|
||||
ctx.ServeContent(s, &context.ServeHeaderOptions{ |
||||
Filename: pf.Name, |
||||
LastModified: pf.CreatedUnix.AsLocalTime(), |
||||
}) |
||||
} |
||||
|
||||
func UploadPackageFile(ctx *context.Context) { |
||||
distribution := strings.TrimSpace(ctx.Params("distribution")) |
||||
component := strings.TrimSpace(ctx.Params("component")) |
||||
if distribution == "" || component == "" { |
||||
apiError(ctx, http.StatusBadRequest, "invalid distribution or component") |
||||
return |
||||
} |
||||
|
||||
upload, close, err := ctx.UploadStream() |
||||
if err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
if close { |
||||
defer upload.Close() |
||||
} |
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload) |
||||
if err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
defer buf.Close() |
||||
|
||||
pck, err := debian_module.ParsePackage(buf) |
||||
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 |
||||
} |
||||
|
||||
_, _, err = packages_service.CreatePackageOrAddFileToExisting( |
||||
&packages_service.PackageCreationInfo{ |
||||
PackageInfo: packages_service.PackageInfo{ |
||||
Owner: ctx.Package.Owner, |
||||
PackageType: packages_model.TypeDebian, |
||||
Name: pck.Name, |
||||
Version: pck.Version, |
||||
}, |
||||
Creator: ctx.Doer, |
||||
Metadata: pck.Metadata, |
||||
}, |
||||
&packages_service.PackageFileCreationInfo{ |
||||
PackageFileInfo: packages_service.PackageFileInfo{ |
||||
Filename: fmt.Sprintf("%s_%s_%s.deb", pck.Name, pck.Version, pck.Architecture), |
||||
CompositeKey: fmt.Sprintf("%s|%s", distribution, component), |
||||
}, |
||||
Creator: ctx.Doer, |
||||
Data: buf, |
||||
IsLead: true, |
||||
Properties: map[string]string{ |
||||
debian_module.PropertyDistribution: distribution, |
||||
debian_module.PropertyComponent: component, |
||||
debian_module.PropertyArchitecture: pck.Architecture, |
||||
debian_module.PropertyControl: pck.Control, |
||||
}, |
||||
}, |
||||
) |
||||
if err != nil { |
||||
switch err { |
||||
case packages_model.ErrDuplicatePackageVersion: |
||||
apiError(ctx, http.StatusBadRequest, err) |
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: |
||||
apiError(ctx, http.StatusForbidden, err) |
||||
default: |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
} |
||||
return |
||||
} |
||||
|
||||
if err := debian_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, distribution, component, pck.Architecture); err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
|
||||
ctx.Status(http.StatusCreated) |
||||
} |
||||
|
||||
func DownloadPackageFile(ctx *context.Context) { |
||||
name := ctx.Params("name") |
||||
version := ctx.Params("version") |
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( |
||||
ctx, |
||||
&packages_service.PackageInfo{ |
||||
Owner: ctx.Package.Owner, |
||||
PackageType: packages_model.TypeDebian, |
||||
Name: name, |
||||
Version: version, |
||||
}, |
||||
&packages_service.PackageFileInfo{ |
||||
Filename: fmt.Sprintf("%s_%s_%s.deb", name, version, ctx.Params("architecture")), |
||||
CompositeKey: fmt.Sprintf("%s|%s", ctx.Params("distribution"), ctx.Params("component")), |
||||
}, |
||||
) |
||||
if err != nil { |
||||
if errors.Is(err, util.ErrNotExist) { |
||||
apiError(ctx, http.StatusNotFound, err) |
||||
} else { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
} |
||||
return |
||||
} |
||||
defer s.Close() |
||||
|
||||
ctx.ServeContent(s, &context.ServeHeaderOptions{ |
||||
ContentType: "application/vnd.debian.binary-package", |
||||
Filename: pf.Name, |
||||
LastModified: pf.CreatedUnix.AsLocalTime(), |
||||
}) |
||||
} |
||||
|
||||
func DeletePackageFile(ctx *context.Context) { |
||||
distribution := ctx.Params("distribution") |
||||
component := ctx.Params("component") |
||||
name := ctx.Params("name") |
||||
version := ctx.Params("version") |
||||
architecture := ctx.Params("architecture") |
||||
|
||||
owner := ctx.Package.Owner |
||||
|
||||
var pd *packages_model.PackageDescriptor |
||||
|
||||
err := db.WithTx(ctx, func(ctx stdctx.Context) error { |
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, owner.ID, packages_model.TypeDebian, name, version) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pf, err := packages_model.GetFileForVersionByName( |
||||
ctx, |
||||
pv.ID, |
||||
fmt.Sprintf("%s_%s_%s.deb", name, version, architecture), |
||||
fmt.Sprintf("%s|%s", distribution, component), |
||||
) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := packages_service.DeletePackageFile(ctx, pf); err != nil { |
||||
return err |
||||
} |
||||
|
||||
has, err := packages_model.HasVersionFileReferences(ctx, pv.ID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !has { |
||||
pd, err = packages_model.GetPackageDescriptor(ctx, pv) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
if err != nil { |
||||
if errors.Is(err, util.ErrNotExist) { |
||||
apiError(ctx, http.StatusNotFound, err) |
||||
} else { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
} |
||||
return |
||||
} |
||||
|
||||
if pd != nil { |
||||
notification.NotifyPackageDelete(ctx, ctx.Doer, pd) |
||||
} |
||||
|
||||
if err := debian_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, distribution, component, architecture); err != nil { |
||||
apiError(ctx, http.StatusInternalServerError, err) |
||||
return |
||||
} |
||||
|
||||
ctx.Status(http.StatusNoContent) |
||||
} |
@ -0,0 +1,443 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian |
||||
|
||||
import ( |
||||
"bytes" |
||||
"compress/gzip" |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"sort" |
||||
"strings" |
||||
"time" |
||||
|
||||
"code.gitea.io/gitea/models/db" |
||||
packages_model "code.gitea.io/gitea/models/packages" |
||||
debian_model "code.gitea.io/gitea/models/packages/debian" |
||||
user_model "code.gitea.io/gitea/models/user" |
||||
"code.gitea.io/gitea/modules/log" |
||||
packages_module "code.gitea.io/gitea/modules/packages" |
||||
debian_module "code.gitea.io/gitea/modules/packages/debian" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
"code.gitea.io/gitea/modules/util" |
||||
packages_service "code.gitea.io/gitea/services/packages" |
||||
|
||||
"github.com/keybase/go-crypto/openpgp" |
||||
"github.com/keybase/go-crypto/openpgp/armor" |
||||
"github.com/keybase/go-crypto/openpgp/clearsign" |
||||
"github.com/keybase/go-crypto/openpgp/packet" |
||||
"github.com/ulikunitz/xz" |
||||
) |
||||
|
||||
// GetOrCreateRepositoryVersion gets or creates the internal repository package
|
||||
// The Debian registry needs multiple index files which are stored in this package.
|
||||
func GetOrCreateRepositoryVersion(ownerID int64) (*packages_model.PackageVersion, error) { |
||||
var repositoryVersion *packages_model.PackageVersion |
||||
|
||||
return repositoryVersion, db.WithTx(db.DefaultContext, func(ctx context.Context) error { |
||||
p := &packages_model.Package{ |
||||
OwnerID: ownerID, |
||||
Type: packages_model.TypeDebian, |
||||
Name: debian_module.RepositoryPackage, |
||||
LowerName: debian_module.RepositoryPackage, |
||||
IsInternal: true, |
||||
} |
||||
var err error |
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { |
||||
if err != packages_model.ErrDuplicatePackage { |
||||
log.Error("Error inserting package: %v", err) |
||||
return err |
||||
} |
||||
} |
||||
|
||||
pv := &packages_model.PackageVersion{ |
||||
PackageID: p.ID, |
||||
CreatorID: ownerID, |
||||
Version: debian_module.RepositoryVersion, |
||||
LowerVersion: debian_module.RepositoryVersion, |
||||
IsInternal: true, |
||||
MetadataJSON: "null", |
||||
} |
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { |
||||
if err != packages_model.ErrDuplicatePackageVersion { |
||||
log.Error("Error inserting package version: %v", err) |
||||
return err |
||||
} |
||||
} |
||||
|
||||
repositoryVersion = pv |
||||
|
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository files
|
||||
func GetOrCreateKeyPair(ownerID int64) (string, string, error) { |
||||
priv, err := user_model.GetSetting(ownerID, debian_module.SettingKeyPrivate) |
||||
if err != nil && !errors.Is(err, util.ErrNotExist) { |
||||
return "", "", err |
||||
} |
||||
|
||||
pub, err := user_model.GetSetting(ownerID, debian_module.SettingKeyPublic) |
||||
if err != nil && !errors.Is(err, util.ErrNotExist) { |
||||
return "", "", err |
||||
} |
||||
|
||||
if priv == "" || pub == "" { |
||||
priv, pub, err = generateKeypair() |
||||
if err != nil { |
||||
return "", "", err |
||||
} |
||||
|
||||
if err := user_model.SetUserSetting(ownerID, debian_module.SettingKeyPrivate, priv); err != nil { |
||||
return "", "", err |
||||
} |
||||
|
||||
if err := user_model.SetUserSetting(ownerID, debian_module.SettingKeyPublic, pub); err != nil { |
||||
return "", "", err |
||||
} |
||||
} |
||||
|
||||
return priv, pub, nil |
||||
} |
||||
|
||||
func generateKeypair() (string, string, error) { |
||||
e, err := openpgp.NewEntity(setting.AppName, "Debian Registry", "", nil) |
||||
if err != nil { |
||||
return "", "", err |
||||
} |
||||
|
||||
var priv strings.Builder |
||||
var pub strings.Builder |
||||
|
||||
w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil) |
||||
if err != nil { |
||||
return "", "", err |
||||
} |
||||
if err := e.SerializePrivate(w, nil); err != nil { |
||||
return "", "", err |
||||
} |
||||
w.Close() |
||||
|
||||
w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil) |
||||
if err != nil { |
||||
return "", "", err |
||||
} |
||||
if err := e.Serialize(w); err != nil { |
||||
return "", "", err |
||||
} |
||||
w.Close() |
||||
|
||||
return priv.String(), pub.String(), nil |
||||
} |
||||
|
||||
// BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures
|
||||
func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { |
||||
pv, err := GetOrCreateRepositoryVersion(ownerID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 1. Delete all existing repository files
|
||||
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, pf := range pfs { |
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil { |
||||
return err |
||||
} |
||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
// 2. (Re)Build repository files for existing packages
|
||||
distributions, err := debian_model.GetDistributions(ctx, ownerID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, distribution := range distributions { |
||||
components, err := debian_model.GetComponents(ctx, ownerID, distribution) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, component := range components { |
||||
for _, architecture := range architectures { |
||||
if err := buildRepositoryFiles(ctx, ownerID, pv, distribution, component, architecture); err != nil { |
||||
return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", distribution, component, architecture, err) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// BuildSpecificRepositoryFiles builds index files for the repository
|
||||
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, distribution, component, architecture string) error { |
||||
pv, err := GetOrCreateRepositoryVersion(ownerID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return buildRepositoryFiles(ctx, ownerID, pv, distribution, component, architecture) |
||||
} |
||||
|
||||
func buildRepositoryFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution, component, architecture string) error { |
||||
if err := buildPackagesIndices(ctx, ownerID, repoVersion, distribution, component, architecture); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return buildReleaseFiles(ctx, ownerID, repoVersion, distribution) |
||||
} |
||||
|
||||
// https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
|
||||
func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution, component, architecture string) error { |
||||
pfds, err := debian_model.SearchLatestPackages(ctx, &debian_model.PackageSearchOptions{ |
||||
OwnerID: ownerID, |
||||
Distribution: distribution, |
||||
Component: component, |
||||
Architecture: architecture, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Delete the package indices if there are no packages
|
||||
if len(pfds) == 0 { |
||||
key := fmt.Sprintf("%s|%s|%s", distribution, component, architecture) |
||||
for _, filename := range []string{"Packages", "Packages.gz", "Packages.xz"} { |
||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, key) |
||||
if err != nil && !errors.Is(err, util.ErrNotExist) { |
||||
return err |
||||
} |
||||
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil { |
||||
return err |
||||
} |
||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
packagesContent, _ := packages_module.NewHashedBuffer() |
||||
|
||||
packagesGzipContent, _ := packages_module.NewHashedBuffer() |
||||
gzw := gzip.NewWriter(packagesGzipContent) |
||||
|
||||
packagesXzContent, _ := packages_module.NewHashedBuffer() |
||||
xzw, _ := xz.NewWriter(packagesXzContent) |
||||
|
||||
w := io.MultiWriter(packagesContent, gzw, xzw) |
||||
|
||||
addSeparator := false |
||||
for _, pfd := range pfds { |
||||
if addSeparator { |
||||
fmt.Fprintln(w) |
||||
} |
||||
addSeparator = true |
||||
|
||||
fmt.Fprint(w, pfd.Properties.GetByName(debian_module.PropertyControl)) |
||||
|
||||
fmt.Fprintf(w, "Filename: pool/%s/%s/%s\n", distribution, component, pfd.File.Name) |
||||
fmt.Fprintf(w, "Size: %d\n", pfd.Blob.Size) |
||||
fmt.Fprintf(w, "MD5sum: %s\n", pfd.Blob.HashMD5) |
||||
fmt.Fprintf(w, "SHA1: %s\n", pfd.Blob.HashSHA1) |
||||
fmt.Fprintf(w, "SHA256: %s\n", pfd.Blob.HashSHA256) |
||||
fmt.Fprintf(w, "SHA512: %s\n", pfd.Blob.HashSHA512) |
||||
} |
||||
|
||||
gzw.Close() |
||||
xzw.Close() |
||||
|
||||
for _, file := range []struct { |
||||
Name string |
||||
Data packages_module.HashedSizeReader |
||||
}{ |
||||
{"Packages", packagesContent}, |
||||
{"Packages.gz", packagesGzipContent}, |
||||
{"Packages.xz", packagesXzContent}, |
||||
} { |
||||
_, err = packages_service.AddFileToPackageVersionInternal( |
||||
repoVersion, |
||||
&packages_service.PackageFileCreationInfo{ |
||||
PackageFileInfo: packages_service.PackageFileInfo{ |
||||
Filename: file.Name, |
||||
CompositeKey: fmt.Sprintf("%s|%s|%s", distribution, component, architecture), |
||||
}, |
||||
Creator: user_model.NewGhostUser(), |
||||
Data: file.Data, |
||||
IsLead: false, |
||||
OverwriteExisting: true, |
||||
Properties: map[string]string{ |
||||
debian_module.PropertyRepositoryIncludeInRelease: "", |
||||
debian_module.PropertyDistribution: distribution, |
||||
debian_module.PropertyComponent: component, |
||||
debian_module.PropertyArchitecture: architecture, |
||||
}, |
||||
}, |
||||
) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
|
||||
func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution string) error { |
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ |
||||
VersionID: repoVersion.ID, |
||||
Properties: map[string]string{ |
||||
debian_module.PropertyRepositoryIncludeInRelease: "", |
||||
debian_module.PropertyDistribution: distribution, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Delete the release files if there are no packages
|
||||
if len(pfs) == 0 { |
||||
for _, filename := range []string{"Release", "Release.gpg", "InRelease"} { |
||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, distribution) |
||||
if err != nil && !errors.Is(err, util.ErrNotExist) { |
||||
return err |
||||
} |
||||
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil { |
||||
return err |
||||
} |
||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
components, err := debian_model.GetComponents(ctx, ownerID, distribution) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
sort.Strings(components) |
||||
|
||||
architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
sort.Strings(architectures) |
||||
|
||||
priv, _, err := GetOrCreateKeyPair(ownerID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
block, err := armor.Decode(strings.NewReader(priv)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
e, err := openpgp.ReadEntity(packet.NewReader(block.Body)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
inReleaseContent, _ := packages_module.NewHashedBuffer() |
||||
sw, err := clearsign.Encode(inReleaseContent, e.PrivateKey, nil) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var buf bytes.Buffer |
||||
|
||||
w := io.MultiWriter(sw, &buf) |
||||
|
||||
fmt.Fprintf(w, "Origin: %s\n", setting.AppName) |
||||
fmt.Fprintf(w, "Label: %s\n", setting.AppName) |
||||
fmt.Fprintf(w, "Suite: %s\n", distribution) |
||||
fmt.Fprintf(w, "Codename: %s\n", distribution) |
||||
fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " ")) |
||||
fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " ")) |
||||
fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123)) |
||||
fmt.Fprint(w, "Acquire-By-Hash: yes") |
||||
|
||||
pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var md5, sha1, sha256, sha512 strings.Builder |
||||
for _, pfd := range pfds { |
||||
path := fmt.Sprintf("%s/binary-%s/%s", pfd.Properties.GetByName(debian_module.PropertyComponent), pfd.Properties.GetByName(debian_module.PropertyArchitecture), pfd.File.Name) |
||||
fmt.Fprintf(&md5, " %s %d %s\n", pfd.Blob.HashMD5, pfd.Blob.Size, path) |
||||
fmt.Fprintf(&sha1, " %s %d %s\n", pfd.Blob.HashSHA1, pfd.Blob.Size, path) |
||||
fmt.Fprintf(&sha256, " %s %d %s\n", pfd.Blob.HashSHA256, pfd.Blob.Size, path) |
||||
fmt.Fprintf(&sha512, " %s %d %s\n", pfd.Blob.HashSHA512, pfd.Blob.Size, path) |
||||
} |
||||
|
||||
fmt.Fprintln(w, "MD5Sum:") |
||||
fmt.Fprint(w, md5.String()) |
||||
fmt.Fprintln(w, "SHA1:") |
||||
fmt.Fprint(w, sha1.String()) |
||||
fmt.Fprintln(w, "SHA256:") |
||||
fmt.Fprint(w, sha256.String()) |
||||
fmt.Fprintln(w, "SHA512:") |
||||
fmt.Fprint(w, sha512.String()) |
||||
|
||||
sw.Close() |
||||
|
||||
releaseGpgContent, _ := packages_module.NewHashedBuffer() |
||||
if err := openpgp.ArmoredDetachSign(releaseGpgContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil { |
||||
return err |
||||
} |
||||
|
||||
releaseContent, _ := packages_module.CreateHashedBufferFromReader(&buf) |
||||
|
||||
for _, file := range []struct { |
||||
Name string |
||||
Data packages_module.HashedSizeReader |
||||
}{ |
||||
{"Release", releaseContent}, |
||||
{"Release.gpg", releaseGpgContent}, |
||||
{"InRelease", inReleaseContent}, |
||||
} { |
||||
_, err = packages_service.AddFileToPackageVersionInternal( |
||||
repoVersion, |
||||
&packages_service.PackageFileCreationInfo{ |
||||
PackageFileInfo: packages_service.PackageFileInfo{ |
||||
Filename: file.Name, |
||||
CompositeKey: distribution, |
||||
}, |
||||
Creator: user_model.NewGhostUser(), |
||||
Data: file.Data, |
||||
IsLead: false, |
||||
OverwriteExisting: true, |
||||
Properties: map[string]string{ |
||||
debian_module.PropertyDistribution: distribution, |
||||
}, |
||||
}, |
||||
) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,65 @@ |
||||
{{if eq .PackageDescriptor.Package.Type "debian"}} |
||||
<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-terminal"}} {{.locale.Tr "packages.debian.registry"}}</label> |
||||
<div class="markup"><pre class="code-block"><code>sudo curl <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></gitea-origin-url> -o /etc/apt/trusted.gpg.d/gitea-{{$.PackageDescriptor.Owner.Name}}.asc |
||||
echo "deb <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></gitea-origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list |
||||
sudo apt update</code></pre></div> |
||||
<p>{{.locale.Tr "packages.debian.registry.info" | Safe}}</p> |
||||
</div> |
||||
<div class="field"> |
||||
<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.debian.install"}}</label> |
||||
<div class="markup"> |
||||
<pre class="code-block"><code>sudo apt install {{$.PackageDescriptor.Package.Name}}={{$.PackageDescriptor.Version.Version}}</code></pre> |
||||
</div> |
||||
</div> |
||||
<div class="field"> |
||||
<label>{{.locale.Tr "packages.debian.documentation" "https://docs.gitea.io/en-us/packages/debian/" | Safe}}</label> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<h4 class="ui top attached header">{{.locale.Tr "packages.debian.repository"}}</h4> |
||||
<div class="ui attached segment"> |
||||
<table class="ui single line very basic table"> |
||||
<tbody> |
||||
<tr> |
||||
<td class="collapsing"><h5>{{.locale.Tr "packages.debian.repository.distributions"}}</h5></td> |
||||
<td>{{StringUtils.Join .Distributions ", "}}</td> |
||||
</tr> |
||||
<tr> |
||||
<td class="collapsing"><h5>{{.locale.Tr "packages.debian.repository.components"}}</h5></td> |
||||
<td>{{StringUtils.Join .Components ", "}}</td> |
||||
</tr> |
||||
<tr> |
||||
<td class="collapsing"><h5>{{.locale.Tr "packages.debian.repository.architectures"}}</h5></td> |
||||
<td>{{StringUtils.Join .Architectures ", "}}</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
|
||||
{{if .PackageDescriptor.Metadata.Description}} |
||||
<h4 class="ui top attached header">{{.locale.Tr "packages.about"}}</h4> |
||||
<div class="ui attached segment"> |
||||
{{.PackageDescriptor.Metadata.Description}} |
||||
</div> |
||||
{{end}} |
||||
|
||||
{{if .PackageDescriptor.Metadata.Dependencies}} |
||||
<h4 class="ui top attached header">{{.locale.Tr "packages.dependencies"}}</h4> |
||||
<div class="ui attached segment"> |
||||
<table class="ui single line very basic table"> |
||||
<tbody> |
||||
{{range .PackageDescriptor.Metadata.Dependencies}} |
||||
<tr> |
||||
<td>{{.}}</td> |
||||
</tr> |
||||
{{end}} |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
{{end}} |
||||
{{end}} |
@ -0,0 +1,4 @@ |
||||
{{if eq .PackageDescriptor.Package.Type "debian"}} |
||||
{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{.locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</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}} |
||||
{{end}} |
@ -0,0 +1,252 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration |
||||
|
||||
import ( |
||||
"archive/tar" |
||||
"bytes" |
||||
"compress/gzip" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"strings" |
||||
"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" |
||||
"code.gitea.io/gitea/modules/base" |
||||
debian_module "code.gitea.io/gitea/modules/packages/debian" |
||||
"code.gitea.io/gitea/tests" |
||||
|
||||
"github.com/blakesmith/ar" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestPackageDebian(t *testing.T) { |
||||
defer tests.PrepareTestEnv(t)() |
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) |
||||
|
||||
packageName := "gitea" |
||||
packageVersion := "1.0.3" |
||||
packageDescription := "Package Description" |
||||
|
||||
createArchive := func(name, version, architecture string) io.Reader { |
||||
var cbuf bytes.Buffer |
||||
zw := gzip.NewWriter(&cbuf) |
||||
tw := tar.NewWriter(zw) |
||||
tw.WriteHeader(&tar.Header{ |
||||
Name: "control", |
||||
Mode: 0o600, |
||||
Size: 50, |
||||
}) |
||||
fmt.Fprintf(tw, "Package: %s\nVersion: %s\nArchitecture: %s\nDescription: %s\n", name, version, architecture, packageDescription) |
||||
tw.Close() |
||||
zw.Close() |
||||
|
||||
var buf bytes.Buffer |
||||
aw := ar.NewWriter(&buf) |
||||
aw.WriteGlobalHeader() |
||||
hdr := &ar.Header{ |
||||
Name: "control.tar.gz", |
||||
Mode: 0o600, |
||||
Size: int64(cbuf.Len()), |
||||
} |
||||
aw.WriteHeader(hdr) |
||||
aw.Write(cbuf.Bytes()) |
||||
return &buf |
||||
} |
||||
|
||||
distributions := []string{"test", "gitea"} |
||||
components := []string{"main", "stable"} |
||||
architectures := []string{"all", "amd64"} |
||||
|
||||
rootURL := fmt.Sprintf("/api/packages/%s/debian", user.Name) |
||||
|
||||
t.Run("RepositoryKey", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
req := NewRequest(t, "GET", rootURL+"/repository.key") |
||||
resp := MakeRequest(t, req, http.StatusOK) |
||||
|
||||
assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type")) |
||||
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----") |
||||
}) |
||||
|
||||
for _, distribution := range distributions { |
||||
t.Run(fmt.Sprintf("[Distribution:%s]", distribution), func(t *testing.T) { |
||||
for _, component := range components { |
||||
for _, architecture := range architectures { |
||||
t.Run(fmt.Sprintf("[Component:%s,Architecture:%s]", component, architecture), func(t *testing.T) { |
||||
t.Run("Upload", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
uploadURL := fmt.Sprintf("%s/pool/%s/%s/upload", rootURL, distribution, component) |
||||
|
||||
req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) |
||||
MakeRequest(t, req, http.StatusUnauthorized) |
||||
|
||||
req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) |
||||
AddBasicAuthHeader(req, user.Name) |
||||
MakeRequest(t, req, http.StatusBadRequest) |
||||
|
||||
req = NewRequestWithBody(t, "PUT", uploadURL, createArchive("", "", "")) |
||||
AddBasicAuthHeader(req, user.Name) |
||||
MakeRequest(t, req, http.StatusBadRequest) |
||||
|
||||
req = NewRequestWithBody(t, "PUT", uploadURL, createArchive(packageName, packageVersion, architecture)) |
||||
AddBasicAuthHeader(req, user.Name) |
||||
MakeRequest(t, req, http.StatusCreated) |
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeDebian) |
||||
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, &debian_module.Metadata{}, pd.Metadata) |
||||
assert.Equal(t, packageName, pd.Package.Name) |
||||
assert.Equal(t, packageVersion, pd.Version.Version) |
||||
|
||||
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) |
||||
assert.NoError(t, err) |
||||
assert.NotEmpty(t, pfs) |
||||
assert.Condition(t, func() bool { |
||||
seen := false |
||||
expectedFilename := fmt.Sprintf("%s_%s_%s.deb", packageName, packageVersion, architecture) |
||||
expectedCompositeKey := fmt.Sprintf("%s|%s", distribution, component) |
||||
for _, pf := range pfs { |
||||
if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey { |
||||
if seen { |
||||
return false |
||||
} |
||||
seen = true |
||||
|
||||
assert.True(t, pf.IsLead) |
||||
|
||||
pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID) |
||||
assert.NoError(t, err) |
||||
|
||||
for _, pfp := range pfps { |
||||
switch pfp.Name { |
||||
case debian_module.PropertyDistribution: |
||||
assert.Equal(t, distribution, pfp.Value) |
||||
case debian_module.PropertyComponent: |
||||
assert.Equal(t, component, pfp.Value) |
||||
case debian_module.PropertyArchitecture: |
||||
assert.Equal(t, architecture, pfp.Value) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return seen |
||||
}) |
||||
}) |
||||
|
||||
t.Run("Download", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/pool/%s/%s/%s_%s_%s.deb", rootURL, distribution, component, packageName, packageVersion, architecture)) |
||||
resp := MakeRequest(t, req, http.StatusOK) |
||||
|
||||
assert.Equal(t, "application/vnd.debian.binary-package", resp.Header().Get("Content-Type")) |
||||
}) |
||||
|
||||
t.Run("Packages", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
url := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distribution, component, architecture) |
||||
|
||||
req := NewRequest(t, "GET", url) |
||||
resp := MakeRequest(t, req, http.StatusOK) |
||||
|
||||
body := resp.Body.String() |
||||
|
||||
assert.Contains(t, body, "Package: "+packageName) |
||||
assert.Contains(t, body, "Version: "+packageVersion) |
||||
assert.Contains(t, body, "Architecture: "+architecture) |
||||
assert.Contains(t, body, fmt.Sprintf("Filename: pool/%s/%s/%s_%s_%s.deb", distribution, component, packageName, packageVersion, architecture)) |
||||
|
||||
req = NewRequest(t, "GET", url+".gz") |
||||
MakeRequest(t, req, http.StatusOK) |
||||
|
||||
req = NewRequest(t, "GET", url+".xz") |
||||
MakeRequest(t, req, http.StatusOK) |
||||
|
||||
url = fmt.Sprintf("%s/dists/%s/%s/%s/by-hash/SHA256/%s", rootURL, distribution, component, architecture, base.EncodeSha256(body)) |
||||
req = NewRequest(t, "GET", url) |
||||
resp = MakeRequest(t, req, http.StatusOK) |
||||
|
||||
assert.Equal(t, body, resp.Body.String()) |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
t.Run("Release", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, distribution)) |
||||
resp := MakeRequest(t, req, http.StatusOK) |
||||
|
||||
body := resp.Body.String() |
||||
|
||||
assert.Contains(t, body, "Components: "+strings.Join(components, " ")) |
||||
assert.Contains(t, body, "Architectures: "+strings.Join(architectures, " ")) |
||||
|
||||
for _, component := range components { |
||||
for _, architecture := range architectures { |
||||
assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages", component, architecture)) |
||||
assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages.gz", component, architecture)) |
||||
assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages.xz", component, architecture)) |
||||
} |
||||
} |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/by-hash/SHA256/%s", rootURL, distribution, base.EncodeSha256(body))) |
||||
resp = MakeRequest(t, req, http.StatusOK) |
||||
|
||||
assert.Equal(t, body, resp.Body.String()) |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release.gpg", rootURL, distribution)) |
||||
resp = MakeRequest(t, req, http.StatusOK) |
||||
|
||||
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----") |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/InRelease", rootURL, distribution)) |
||||
resp = MakeRequest(t, req, http.StatusOK) |
||||
|
||||
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNED MESSAGE-----") |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
t.Run("Delete", func(t *testing.T) { |
||||
defer tests.PrintCurrentTest(t)() |
||||
|
||||
distribution := distributions[0] |
||||
architecture := architectures[0] |
||||
|
||||
for _, component := range components { |
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/pool/%s/%s/%s/%s/%s", rootURL, distribution, component, packageName, packageVersion, architecture)) |
||||
MakeRequest(t, req, http.StatusUnauthorized) |
||||
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/pool/%s/%s/%s/%s/%s", rootURL, distribution, component, packageName, packageVersion, architecture)) |
||||
AddBasicAuthHeader(req, user.Name) |
||||
MakeRequest(t, req, http.StatusNoContent) |
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distribution, component, architecture)) |
||||
MakeRequest(t, req, http.StatusNotFound) |
||||
} |
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, distribution)) |
||||
resp := MakeRequest(t, req, http.StatusOK) |
||||
|
||||
body := resp.Body.String() |
||||
|
||||
assert.Contains(t, body, "Components: "+strings.Join(components, " ")) |
||||
assert.Contains(t, body, "Architectures: "+architectures[1]) |
||||
}) |
||||
} |
After Width: | Height: | Size: 4.2 KiB |
Loading…
Reference in new issue