Add Alpine package registry (#23714)
This PR adds an Alpine package registry. You can follow [this tutorial](https://wiki.alpinelinux.org/wiki/Creating_an_Alpine_package) to build a *.apk package for testing. This functionality is similar to the Debian registry (#22854) and therefore shares some methods. I marked this PR as blocked because it should be merged after #22854. ![grafik](https://user-images.githubusercontent.com/1666336/227779595-b76163aa-eea1-4a79-9583-775c24ad74e8.png) --------- Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Giteabot <teabot@gitea.io>forgejo-federated-star
parent
80bde0141b
commit
9173e079ae
@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
date: "2023-03-25T00:00:00+00:00"
|
||||||
|
title: "Alpine Packages Repository"
|
||||||
|
slug: "packages/alpine"
|
||||||
|
draft: false
|
||||||
|
toc: false
|
||||||
|
menu:
|
||||||
|
sidebar:
|
||||||
|
parent: "packages"
|
||||||
|
name: "Alpine"
|
||||||
|
weight: 4
|
||||||
|
identifier: "alpine"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Alpine Packages Repository
|
||||||
|
|
||||||
|
Publish [Alpine](https://pkgs.alpinelinux.org/) packages for your user or organization.
|
||||||
|
|
||||||
|
**Table of Contents**
|
||||||
|
|
||||||
|
{{< toc >}}
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
To work with the Alpine registry, you need to use a HTTP client like `curl` to upload and a package manager like `apk` to consume packages.
|
||||||
|
|
||||||
|
The following examples use `apk`.
|
||||||
|
|
||||||
|
## Configuring the package registry
|
||||||
|
|
||||||
|
To register the Alpine registry add the url to the list of known apk sources (`/etc/apk/repositories`):
|
||||||
|
|
||||||
|
```
|
||||||
|
https://gitea.example.com/api/packages/{owner}/alpine/<branch>/<repository>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Placeholder | Description |
|
||||||
|
| ------------ | ----------- |
|
||||||
|
| `owner` | The owner of the packages. |
|
||||||
|
| `branch` | The branch to use. |
|
||||||
|
| `repository` | The repository 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" >}}):
|
||||||
|
|
||||||
|
```
|
||||||
|
https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/alpine/<branch>/<repository>
|
||||||
|
```
|
||||||
|
|
||||||
|
The Alpine registry files are signed with a RSA key which must be known to apk. Download the public key and store it in `/etc/apk/keys/`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -JO https://gitea.example.com/api/packages/{owner}/alpine/key
|
||||||
|
```
|
||||||
|
|
||||||
|
Afterwards update the local package index:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
apk update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publish a package
|
||||||
|
|
||||||
|
To publish an Alpine package (`*.apk`), perform a HTTP `PUT` operation with the package content in the request body.
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT https://gitea.example.com/api/packages/{owner}/alpine/{branch}/{repository}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| ------------ | ----------- |
|
||||||
|
| `owner` | The owner of the package. |
|
||||||
|
| `branch` | The branch may match the release version of the OS, ex: `v3.17`. |
|
||||||
|
| `repository` | The repository can be used [to group packages](https://wiki.alpinelinux.org/wiki/Repositories) 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.apk \
|
||||||
|
https://gitea.example.com/api/packages/testuser/alpine/v3.17/main
|
||||||
|
```
|
||||||
|
|
||||||
|
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 file first.
|
||||||
|
|
||||||
|
The server responds with the following HTTP Status codes.
|
||||||
|
|
||||||
|
| HTTP Status Code | Meaning |
|
||||||
|
| ----------------- | ------- |
|
||||||
|
| `201 Created` | The package has been published. |
|
||||||
|
| `400 Bad Request` | The package name, version, branch, repository or architecture are invalid. |
|
||||||
|
| `409 Conflict` | A package file with the same combination of parameters exist already in the package. |
|
||||||
|
|
||||||
|
## Delete a package
|
||||||
|
|
||||||
|
To delete an Alpine 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}/alpine/{branch}/{repository}/{architecture}/{filename}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| -------------- | ----------- |
|
||||||
|
| `owner` | The owner of the package. |
|
||||||
|
| `branch` | The branch to use. |
|
||||||
|
| `repository` | The repository to use. |
|
||||||
|
| `architecture` | The package architecture. |
|
||||||
|
| `filename` | The file to delete.
|
||||||
|
|
||||||
|
Example request using HTTP Basic authentication:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl --user your_username:your_token_or_password -X DELETE \
|
||||||
|
https://gitea.example.com/api/packages/testuser/alpine/v3.17/main/test-package-1.0.0.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
The server responds 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 Alpine registry, execute the following commands:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# use latest version
|
||||||
|
apk add {package_name}
|
||||||
|
# use specific version
|
||||||
|
apk add {package_name}={package_version}
|
||||||
|
```
|
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package alpine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetBranches gets all available branches
|
||||||
|
func GetBranches(ctx context.Context, ownerID int64) ([]string, error) {
|
||||||
|
return packages_model.GetDistinctPropertyValues(
|
||||||
|
ctx,
|
||||||
|
packages_model.TypeAlpine,
|
||||||
|
ownerID,
|
||||||
|
packages_model.PropertyTypeFile,
|
||||||
|
alpine_module.PropertyBranch,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRepositories gets all available repositories for the given branch
|
||||||
|
func GetRepositories(ctx context.Context, ownerID int64, branch string) ([]string, error) {
|
||||||
|
return packages_model.GetDistinctPropertyValues(
|
||||||
|
ctx,
|
||||||
|
packages_model.TypeAlpine,
|
||||||
|
ownerID,
|
||||||
|
packages_model.PropertyTypeFile,
|
||||||
|
alpine_module.PropertyRepository,
|
||||||
|
&packages_model.DistinctPropertyDependency{
|
||||||
|
Name: alpine_module.PropertyBranch,
|
||||||
|
Value: branch,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArchitectures gets all available architectures for the given repository
|
||||||
|
func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) {
|
||||||
|
return packages_model.GetDistinctPropertyValues(
|
||||||
|
ctx,
|
||||||
|
packages_model.TypeAlpine,
|
||||||
|
ownerID,
|
||||||
|
packages_model.PropertyTypeFile,
|
||||||
|
alpine_module.PropertyArchitecture,
|
||||||
|
&packages_model.DistinctPropertyDependency{
|
||||||
|
Name: alpine_module.PropertyRepository,
|
||||||
|
Value: repository,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,236 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package alpine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bufio"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf("PKGINFO file is missing")
|
||||||
|
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||||||
|
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PropertyMetadata = "alpine.metadata"
|
||||||
|
PropertyBranch = "alpine.branch"
|
||||||
|
PropertyRepository = "alpine.repository"
|
||||||
|
PropertyArchitecture = "alpine.architecture"
|
||||||
|
|
||||||
|
SettingKeyPrivate = "alpine.key.private"
|
||||||
|
SettingKeyPublic = "alpine.key.public"
|
||||||
|
|
||||||
|
RepositoryPackage = "_alpine"
|
||||||
|
RepositoryVersion = "_repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://wiki.alpinelinux.org/wiki/Apk_spec
|
||||||
|
|
||||||
|
// Package represents an Alpine package
|
||||||
|
type Package struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
VersionMetadata VersionMetadata
|
||||||
|
FileMetadata FileMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata of an Alpine package
|
||||||
|
type VersionMetadata struct {
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
License string `json:"license,omitempty"`
|
||||||
|
ProjectURL string `json:"project_url,omitempty"`
|
||||||
|
Maintainer string `json:"maintainer,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileMetadata struct {
|
||||||
|
Checksum string `json:"checksum"`
|
||||||
|
Packager string `json:"packager,omitempty"`
|
||||||
|
BuildDate int64 `json:"build_date,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
Architecture string `json:"architecture,omitempty"`
|
||||||
|
Origin string `json:"origin,omitempty"`
|
||||||
|
CommitHash string `json:"commit_hash,omitempty"`
|
||||||
|
InstallIf string `json:"install_if,omitempty"`
|
||||||
|
Provides []string `json:"provides,omitempty"`
|
||||||
|
Dependencies []string `json:"dependencies,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePackage parses the Alpine package file
|
||||||
|
func ParsePackage(r io.Reader) (*Package, error) {
|
||||||
|
// Alpine packages are concated .tar.gz streams. Usually the first stream contains the package metadata.
|
||||||
|
|
||||||
|
br := bufio.NewReader(r) // needed for gzip Multistream
|
||||||
|
|
||||||
|
h := sha1.New()
|
||||||
|
|
||||||
|
gzr, err := gzip.NewReader(&teeByteReader{br, h})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
gzr.Multistream(false)
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
for {
|
||||||
|
hd, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hd.Name == ".PKGINFO" {
|
||||||
|
p, err := ParsePackageInfo(tr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// drain the reader
|
||||||
|
for {
|
||||||
|
if _, err := tr.Next(); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.FileMetadata.Checksum = "Q1" + base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h = sha1.New()
|
||||||
|
|
||||||
|
err = gzr.Reset(&teeByteReader{br, h})
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrMissingPKGINFOFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePackageInfo parses a PKGINFO file to retrieve the metadata of an Alpine package
|
||||||
|
func ParsePackageInfo(r io.Reader) (*Package, error) {
|
||||||
|
p := &Package{}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.IndexRune(line, '=')
|
||||||
|
if i == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(line[:i])
|
||||||
|
value := strings.TrimSpace(line[i+1:])
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "pkgname":
|
||||||
|
p.Name = value
|
||||||
|
case "pkgver":
|
||||||
|
p.Version = value
|
||||||
|
case "pkgdesc":
|
||||||
|
p.VersionMetadata.Description = value
|
||||||
|
case "url":
|
||||||
|
p.VersionMetadata.ProjectURL = value
|
||||||
|
case "builddate":
|
||||||
|
n, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
p.FileMetadata.BuildDate = n
|
||||||
|
}
|
||||||
|
case "size":
|
||||||
|
n, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
p.FileMetadata.Size = n
|
||||||
|
}
|
||||||
|
case "arch":
|
||||||
|
p.FileMetadata.Architecture = value
|
||||||
|
case "origin":
|
||||||
|
p.FileMetadata.Origin = value
|
||||||
|
case "commit":
|
||||||
|
p.FileMetadata.CommitHash = value
|
||||||
|
case "maintainer":
|
||||||
|
p.VersionMetadata.Maintainer = value
|
||||||
|
case "packager":
|
||||||
|
p.FileMetadata.Packager = value
|
||||||
|
case "license":
|
||||||
|
p.VersionMetadata.License = value
|
||||||
|
case "install_if":
|
||||||
|
p.FileMetadata.InstallIf = value
|
||||||
|
case "provides":
|
||||||
|
if value != "" {
|
||||||
|
p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
|
||||||
|
}
|
||||||
|
case "depend":
|
||||||
|
if value != "" {
|
||||||
|
p.FileMetadata.Dependencies = append(p.FileMetadata.Dependencies, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Name == "" {
|
||||||
|
return nil, ErrInvalidName
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Version == "" {
|
||||||
|
return nil, ErrInvalidVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
|
||||||
|
p.VersionMetadata.ProjectURL = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as io.TeeReader but implements io.ByteReader
|
||||||
|
type teeByteReader struct {
|
||||||
|
r *bufio.Reader
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *teeByteReader) Read(p []byte) (int, error) {
|
||||||
|
n, err := t.r.Read(p)
|
||||||
|
if n > 0 {
|
||||||
|
if n, err := t.w.Write(p[:n]); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *teeByteReader) ReadByte() (byte, error) {
|
||||||
|
b, err := t.r.ReadByte()
|
||||||
|
if err == nil {
|
||||||
|
if _, err := t.w.Write([]byte{b}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b, err
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package alpine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
packageName = "gitea"
|
||||||
|
packageVersion = "1.0.1"
|
||||||
|
packageDescription = "Package Description"
|
||||||
|
packageProjectURL = "https://gitea.io"
|
||||||
|
packageMaintainer = "KN4CK3R <dummy@gitea.io>"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createPKGINFOContent(name, version string) []byte {
|
||||||
|
return []byte(`pkgname = ` + name + `
|
||||||
|
pkgver = ` + version + `
|
||||||
|
pkgdesc = ` + packageDescription + `
|
||||||
|
url = ` + packageProjectURL + `
|
||||||
|
# comment
|
||||||
|
builddate = 1678834800
|
||||||
|
packager = Gitea <pack@ag.er>
|
||||||
|
size = 123456
|
||||||
|
arch = aarch64
|
||||||
|
origin = origin
|
||||||
|
commit = 1111e709613fbc979651b09ac2bc27c6591a9999
|
||||||
|
maintainer = ` + packageMaintainer + `
|
||||||
|
license = MIT
|
||||||
|
depend = common
|
||||||
|
install_if = value
|
||||||
|
depend = gitea
|
||||||
|
provides = common
|
||||||
|
provides = gitea`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePackage(t *testing.T) {
|
||||||
|
createPackage := func(name string, content []byte) io.Reader {
|
||||||
|
names := []string{"first.stream", name}
|
||||||
|
contents := [][]byte{{0}, content}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := gzip.NewWriter(&buf)
|
||||||
|
|
||||||
|
for i := range names {
|
||||||
|
if i != 0 {
|
||||||
|
zw.Close()
|
||||||
|
zw.Reset(&buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
tw := tar.NewWriter(zw)
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: names[i],
|
||||||
|
Mode: 0o600,
|
||||||
|
Size: int64(len(contents[i])),
|
||||||
|
}
|
||||||
|
tw.WriteHeader(hdr)
|
||||||
|
tw.Write(contents[i])
|
||||||
|
tw.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
zw.Close()
|
||||||
|
|
||||||
|
return &buf
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("MissingPKGINFOFile", func(t *testing.T) {
|
||||||
|
data := createPackage("dummy.txt", []byte{})
|
||||||
|
|
||||||
|
pp, err := ParsePackage(data)
|
||||||
|
assert.Nil(t, pp)
|
||||||
|
assert.ErrorIs(t, err, ErrMissingPKGINFOFile)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidPKGINFOFile", func(t *testing.T) {
|
||||||
|
data := createPackage(".PKGINFO", []byte{})
|
||||||
|
|
||||||
|
pp, err := ParsePackage(data)
|
||||||
|
assert.Nil(t, pp)
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Valid", func(t *testing.T) {
|
||||||
|
data := createPackage(".PKGINFO", createPKGINFOContent(packageName, packageVersion))
|
||||||
|
|
||||||
|
p, err := ParsePackage(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
|
||||||
|
assert.Equal(t, "Q1SRYURM5+uQDqfHSwTnNIOIuuDVQ=", p.FileMetadata.Checksum)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePackageInfo(t *testing.T) {
|
||||||
|
t.Run("InvalidName", func(t *testing.T) {
|
||||||
|
data := createPKGINFOContent("", packageVersion)
|
||||||
|
|
||||||
|
p, err := ParsePackageInfo(bytes.NewReader(data))
|
||||||
|
assert.Nil(t, p)
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidVersion", func(t *testing.T) {
|
||||||
|
data := createPKGINFOContent(packageName, "")
|
||||||
|
|
||||||
|
p, err := ParsePackageInfo(bytes.NewReader(data))
|
||||||
|
assert.Nil(t, p)
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidVersion)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Valid", func(t *testing.T) {
|
||||||
|
data := createPKGINFOContent(packageName, packageVersion)
|
||||||
|
|
||||||
|
p, err := ParsePackageInfo(bytes.NewReader(data))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
|
||||||
|
assert.Equal(t, packageName, p.Name)
|
||||||
|
assert.Equal(t, packageVersion, p.Version)
|
||||||
|
assert.Equal(t, packageDescription, p.VersionMetadata.Description)
|
||||||
|
assert.Equal(t, packageMaintainer, p.VersionMetadata.Maintainer)
|
||||||
|
assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL)
|
||||||
|
assert.Equal(t, "MIT", p.VersionMetadata.License)
|
||||||
|
assert.Empty(t, p.FileMetadata.Checksum)
|
||||||
|
assert.Equal(t, "Gitea <pack@ag.er>", p.FileMetadata.Packager)
|
||||||
|
assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate)
|
||||||
|
assert.EqualValues(t, 123456, p.FileMetadata.Size)
|
||||||
|
assert.Equal(t, "aarch64", p.FileMetadata.Architecture)
|
||||||
|
assert.Equal(t, "origin", p.FileMetadata.Origin)
|
||||||
|
assert.Equal(t, "1111e709613fbc979651b09ac2bc27c6591a9999", p.FileMetadata.CommitHash)
|
||||||
|
assert.Equal(t, "value", p.FileMetadata.InstallIf)
|
||||||
|
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides)
|
||||||
|
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Dependencies)
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 186 162" class="svg gitea-alpine" width="16" height="16" aria-hidden="true"><g fill="#0d597f"><path d="M67 100.75V81.125L52.875 95.25a41.588 41.588 0 0 0 4.3 2.637c1.35.71 2.612 1.25 3.787 1.676a21.12 21.12 0 0 0 3.275.887c1.006.184 1.926.266 2.763.278m72.25-1.625c.025.02.163.137.416.298.255.163.628.372 1.123.578.494.205 1.111.409 1.85.56.745.152 1.612.252 2.625.252.838 0 1.762-.073 2.775-.25a20.93 20.93 0 0 0 3.3-.873 29.25 29.25 0 0 0 3.837-1.676 41.805 41.805 0 0 0 4.375-2.674l-10.712-10.5-35.5-35.625-15.625 15.625-21-21.625-52.75 52.125c1.55 1.075 3 1.95 4.362 2.675a28.324 28.324 0 0 0 3.838 1.675c1.189.414 2.287.696 3.3.872 1.012.177 1.937.251 2.775.251 1.005 0 1.875-.1 2.625-.252a9.726 9.726 0 0 0 1.85-.561c.495-.205.866-.414 1.121-.577s.393-.278.418-.3l23.875-23.875 8.512-8.162 23.625 23.625 8.238 8.475c.024.021.162.137.417.299.255.162.626.37 1.121.577.495.205 1.113.409 1.85.56.745.153 1.625.253 2.625.253.838 0 1.763-.074 2.775-.25a20.966 20.966 0 0 0 3.3-.874 29.323 29.323 0 0 0 3.838-1.675 41.805 41.805 0 0 0 4.375-2.675l-18.875-18.5 3.525-3.525 16.375 16.375 9.55 9.462m-.204-98.75 46.5 80.625-46.5 80.625H46.05L-.45 81.066 46.05.441z"/><path d="M110.75 77 98.363 64.625l.88-.886 12.476 12.337z"/></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,253 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package alpine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
packages_module "code.gitea.io/gitea/modules/packages"
|
||||||
|
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||||
|
packages_service "code.gitea.io/gitea/services/packages"
|
||||||
|
alpine_service "code.gitea.io/gitea/services/packages/alpine"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := alpine_service.GetOrCreateKeyPair(ctx.Package.Owner.ID)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pubPem, _ := pem.Decode([]byte(pub))
|
||||||
|
if pubPem == nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, "failed to decode private key pem")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, err := x509.ParsePKIXPublicKey(pubPem.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprint, err := util.CreatePublicKeyFingerprint(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
|
||||||
|
ContentType: "application/x-pem-file",
|
||||||
|
Filename: fmt.Sprintf("%s@%s.rsa.pub", ctx.Package.Owner.LowerName, hex.EncodeToString(fingerprint)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRepositoryFile(ctx *context.Context) {
|
||||||
|
pv, err := alpine_service.GetOrCreateRepositoryVersion(ctx.Package.Owner.ID)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s, pf, err := packages_service.GetFileStreamByPackageVersion(
|
||||||
|
ctx,
|
||||||
|
pv,
|
||||||
|
&packages_service.PackageFileInfo{
|
||||||
|
Filename: alpine_service.IndexFilename,
|
||||||
|
CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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) {
|
||||||
|
branch := strings.TrimSpace(ctx.Params("branch"))
|
||||||
|
repository := strings.TrimSpace(ctx.Params("repository"))
|
||||||
|
if branch == "" || repository == "" {
|
||||||
|
apiError(ctx, http.StatusBadRequest, "invalid branch or repository")
|
||||||
|
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 := alpine_module.ParsePackage(buf)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.TypeAlpine,
|
||||||
|
Name: pck.Name,
|
||||||
|
Version: pck.Version,
|
||||||
|
},
|
||||||
|
Creator: ctx.Doer,
|
||||||
|
Metadata: pck.VersionMetadata,
|
||||||
|
},
|
||||||
|
&packages_service.PackageFileCreationInfo{
|
||||||
|
PackageFileInfo: packages_service.PackageFileInfo{
|
||||||
|
Filename: fmt.Sprintf("%s-%s.apk", pck.Name, pck.Version),
|
||||||
|
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, pck.FileMetadata.Architecture),
|
||||||
|
},
|
||||||
|
Creator: ctx.Doer,
|
||||||
|
Data: buf,
|
||||||
|
IsLead: true,
|
||||||
|
Properties: map[string]string{
|
||||||
|
alpine_module.PropertyBranch: branch,
|
||||||
|
alpine_module.PropertyRepository: repository,
|
||||||
|
alpine_module.PropertyArchitecture: pck.FileMetadata.Architecture,
|
||||||
|
alpine_module.PropertyMetadata: string(fileMetadataRaw),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile:
|
||||||
|
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 := alpine_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, pck.FileMetadata.Architecture); err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadPackageFile(ctx *context.Context) {
|
||||||
|
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||||
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
|
PackageType: packages_model.TypeAlpine,
|
||||||
|
Query: ctx.Params("filename"),
|
||||||
|
CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
|
||||||
|
})
|
||||||
|
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 DeletePackageFile(ctx *context.Context) {
|
||||||
|
branch, repository, architecture := ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")
|
||||||
|
|
||||||
|
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||||
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
|
PackageType: packages_model.TypeAlpine,
|
||||||
|
Query: ctx.Params("filename"),
|
||||||
|
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(pfs) != 1 {
|
||||||
|
apiError(ctx, http.StatusNotFound, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx.Doer, pfs[0]); err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
apiError(ctx, http.StatusNotFound, err)
|
||||||
|
} else {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := alpine_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, branch, repository, architecture); err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
@ -0,0 +1,328 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package alpine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
alpine_model "code.gitea.io/gitea/models/packages/alpine"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
packages_module "code.gitea.io/gitea/modules/packages"
|
||||||
|
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
packages_service "code.gitea.io/gitea/services/packages"
|
||||||
|
)
|
||||||
|
|
||||||
|
const IndexFilename = "APKINDEX.tar.gz"
|
||||||
|
|
||||||
|
// GetOrCreateRepositoryVersion gets or creates the internal repository package
|
||||||
|
// The Alpine registry needs multiple index files which are stored in this package.
|
||||||
|
func GetOrCreateRepositoryVersion(ownerID int64) (*packages_model.PackageVersion, error) {
|
||||||
|
return packages_service.GetOrCreateInternalPackageVersion(ownerID, packages_model.TypeAlpine, alpine_module.RepositoryPackage, alpine_module.RepositoryVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrCreateKeyPair gets or creates the RSA keys used to sign repository files
|
||||||
|
func GetOrCreateKeyPair(ownerID int64) (string, string, error) {
|
||||||
|
priv, err := user_model.GetSetting(ownerID, alpine_module.SettingKeyPrivate)
|
||||||
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := user_model.GetSetting(ownerID, alpine_module.SettingKeyPublic)
|
||||||
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if priv == "" || pub == "" {
|
||||||
|
priv, pub, err = util.GenerateKeyPair(4096)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user_model.SetUserSetting(ownerID, alpine_module.SettingKeyPrivate, priv); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user_model.SetUserSetting(ownerID, alpine_module.SettingKeyPublic, pub); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return priv, pub, 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
|
||||||
|
branches, err := alpine_model.GetBranches(ctx, ownerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, branch := range branches {
|
||||||
|
repositories, err := alpine_model.GetRepositories(ctx, ownerID, branch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, repository := range repositories {
|
||||||
|
architectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, architecture := range architectures {
|
||||||
|
if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
|
||||||
|
return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", branch, repository, architecture, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildSpecificRepositoryFiles builds index files for the repository
|
||||||
|
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) error {
|
||||||
|
pv, err := GetOrCreateRepositoryVersion(ownerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture)
|
||||||
|
}
|
||||||
|
|
||||||
|
type packageData struct {
|
||||||
|
Package *packages_model.Package
|
||||||
|
Version *packages_model.PackageVersion
|
||||||
|
Blob *packages_model.PackageBlob
|
||||||
|
VersionMetadata *alpine_module.VersionMetadata
|
||||||
|
FileMetadata *alpine_module.FileMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
type packageCache = map[*packages_model.PackageFile]*packageData
|
||||||
|
|
||||||
|
// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
|
||||||
|
func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
|
||||||
|
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||||
|
OwnerID: ownerID,
|
||||||
|
PackageType: packages_model.TypeAlpine,
|
||||||
|
Query: "%.apk",
|
||||||
|
Properties: map[string]string{
|
||||||
|
alpine_module.PropertyBranch: branch,
|
||||||
|
alpine_module.PropertyRepository: repository,
|
||||||
|
alpine_module.PropertyArchitecture: architecture,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the package indices if there are no packages
|
||||||
|
if len(pfs) == 0 {
|
||||||
|
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return packages_model.DeleteFileByID(ctx, pf.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache data needed for all repository files
|
||||||
|
cache := make(packageCache)
|
||||||
|
for _, pf := range pfs {
|
||||||
|
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, alpine_module.PropertyMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pd := &packageData{
|
||||||
|
Package: p,
|
||||||
|
Version: pv,
|
||||||
|
Blob: pb,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(pps) > 0 {
|
||||||
|
if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache[pf] = pd
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, pf := range pfs {
|
||||||
|
pd := cache[pf]
|
||||||
|
|
||||||
|
fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum)
|
||||||
|
fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name)
|
||||||
|
fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version)
|
||||||
|
fmt.Fprintf(&buf, "A:%s\n", pd.FileMetadata.Architecture)
|
||||||
|
if pd.VersionMetadata.Description != "" {
|
||||||
|
fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description)
|
||||||
|
}
|
||||||
|
if pd.VersionMetadata.ProjectURL != "" {
|
||||||
|
fmt.Fprintf(&buf, "U:%s\n", pd.VersionMetadata.ProjectURL)
|
||||||
|
}
|
||||||
|
if pd.VersionMetadata.License != "" {
|
||||||
|
fmt.Fprintf(&buf, "L:%s\n", pd.VersionMetadata.License)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, "S:%d\n", pd.Blob.Size)
|
||||||
|
fmt.Fprintf(&buf, "I:%d\n", pd.FileMetadata.Size)
|
||||||
|
fmt.Fprintf(&buf, "o:%s\n", pd.FileMetadata.Origin)
|
||||||
|
fmt.Fprintf(&buf, "m:%s\n", pd.VersionMetadata.Maintainer)
|
||||||
|
fmt.Fprintf(&buf, "t:%d\n", pd.FileMetadata.BuildDate)
|
||||||
|
if pd.FileMetadata.CommitHash != "" {
|
||||||
|
fmt.Fprintf(&buf, "c:%s\n", pd.FileMetadata.CommitHash)
|
||||||
|
}
|
||||||
|
if len(pd.FileMetadata.Dependencies) > 0 {
|
||||||
|
fmt.Fprintf(&buf, "D:%s\n", strings.Join(pd.FileMetadata.Dependencies, " "))
|
||||||
|
}
|
||||||
|
if len(pd.FileMetadata.Provides) > 0 {
|
||||||
|
fmt.Fprintf(&buf, "p:%s\n", strings.Join(pd.FileMetadata.Provides, " "))
|
||||||
|
}
|
||||||
|
fmt.Fprint(&buf, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
unsignedIndexContent, _ := packages_module.NewHashedBuffer()
|
||||||
|
h := sha1.New()
|
||||||
|
|
||||||
|
if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), "APKINDEX", buf.Bytes(), true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, _, err := GetOrCreateKeyPair(ownerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
privPem, _ := pem.Decode([]byte(priv))
|
||||||
|
if privPem == nil {
|
||||||
|
return fmt.Errorf("failed to decode private key pem")
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sign, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA1, h.Sum(nil))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
owner, err := user_model.GetUserByID(ctx, ownerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprint, err := util.CreatePublicKeyFingerprint(&privKey.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
signedIndexContent, _ := packages_module.NewHashedBuffer()
|
||||||
|
|
||||||
|
if err := writeGzipStream(
|
||||||
|
signedIndexContent,
|
||||||
|
fmt.Sprintf(".SIGN.RSA.%s@%s.rsa.pub", owner.LowerName, hex.EncodeToString(fingerprint)),
|
||||||
|
sign,
|
||||||
|
false,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(signedIndexContent, unsignedIndexContent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = packages_service.AddFileToPackageVersionInternal(
|
||||||
|
repoVersion,
|
||||||
|
&packages_service.PackageFileCreationInfo{
|
||||||
|
PackageFileInfo: packages_service.PackageFileInfo{
|
||||||
|
Filename: IndexFilename,
|
||||||
|
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
|
||||||
|
},
|
||||||
|
Creator: user_model.NewGhostUser(),
|
||||||
|
Data: signedIndexContent,
|
||||||
|
IsLead: false,
|
||||||
|
OverwriteExisting: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeGzipStream(w io.Writer, filename string, content []byte, addTarEnd bool) error {
|
||||||
|
zw := gzip.NewWriter(w)
|
||||||
|
defer zw.Close()
|
||||||
|
|
||||||
|
tw := tar.NewWriter(zw)
|
||||||
|
if addTarEnd {
|
||||||
|
defer tw.Close()
|
||||||
|
}
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: filename,
|
||||||
|
Mode: 0o600,
|
||||||
|
Size: int64(len(content)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
{{if eq .PackageDescriptor.Package.Type "alpine"}}
|
||||||
|
<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.alpine.registry" | Safe}}</label>
|
||||||
|
<div class="markup"><pre class="code-block"><code><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></gitea-origin-url>/$branch/$repository</code></pre></div>
|
||||||
|
<p>{{.locale.Tr "packages.alpine.registry.info" | Safe}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.alpine.registry.key" | Safe}}</label>
|
||||||
|
<div class="markup"><pre class="code-block"><code>curl -JO <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></gitea-origin-url></code></pre></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.alpine.install"}}</label>
|
||||||
|
<div class="markup">
|
||||||
|
<pre class="code-block"><code>sudo apk add {{$.PackageDescriptor.Package.Name}}={{$.PackageDescriptor.Version.Version}}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{.locale.Tr "packages.alpine.documentation" "https://docs.gitea.io/en-us/packages/alpine/" | Safe}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="ui top attached header">{{.locale.Tr "packages.alpine.repository"}}</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<table class="ui single line very basic table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="collapsing"><h5>{{.locale.Tr "packages.alpine.repository.branches"}}</h5></td>
|
||||||
|
<td>{{StringUtils.Join .Branches ", "}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="collapsing"><h5>{{.locale.Tr "packages.alpine.repository.repositories"}}</h5></td>
|
||||||
|
<td>{{StringUtils.Join .Repositories ", "}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="collapsing"><h5>{{.locale.Tr "packages.alpine.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}}
|
||||||
|
{{end}}
|
@ -0,0 +1,5 @@
|
|||||||
|
{{if eq .PackageDescriptor.Package.Type "alpine"}}
|
||||||
|
{{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}}
|
||||||
|
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
|
||||||
|
{{end}}
|
@ -0,0 +1,229 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
|
"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"
|
||||||
|
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPackageAlpine(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
packageName := "gitea-test"
|
||||||
|
packageVersion := "1.4.1-r3"
|
||||||
|
|
||||||
|
base64AlpinePackageContent := `H4sIAAAAAAACA9ML9nT30wsKdtTLzjNJzjYuckjPLElN1DUzMUxMNTa11CsqTtQrKE1ioAAYAIGZ
|
||||||
|
iQmYBgJ02hDENjQxMTAzMzQ1MTVjMDA0MTQ1ZlAwYKADKC0uSSxSUGAYoWDm4sZZtypv75+q2fVT
|
||||||
|
POD1bKkFB22ms+g1z+H4dk7AhC3HwUSj9EbT0Rk3Dn55dHxy/K7Q+Nl/i+L7Z036ypcRvvpZuMiN
|
||||||
|
s7wbZL/klqRGGshv9Gi0qHTgTZfw3HytnJdx9c3NTRp/PHn+Z50uq2pjkilzjtpfd+uzQMw1M7cY
|
||||||
|
i9RXJasnT2M+vDXCesLK7MilJt8sGplj4xUlLMUun9SzY+phFpxWxRXa06AseV9WvzH3jtGGoL5A
|
||||||
|
vQkea+VKPj5R+Cb461tIk97qpa9nJYsJujTNl2B/J1P52H/D2rPr/j19uU8p7cMSq5tmXk51ReXl
|
||||||
|
F/Yddr9XsMpEwFKlXSPo3QSGwnCOG8y2uadjm6ui998WYXNYubjg78N3a7bnXjhrl5fB8voI++LI
|
||||||
|
1FP5W44e2xf4Ou2wrtyic1Onz7MzMV5ksuno2V/LVG4eN/15X/n2/2vJ2VV+T68aT327dOrhd6e6
|
||||||
|
q5Y0V82Y83tdqkFa8TW2BvGCZ0ds/iibHVpzKuPcuSULO63/bNmfrnhjWqXzhMSXTb5Cv4vPaxSL
|
||||||
|
8LFMdqmxbN7+Y+Yi0ZyZhz4UxexLuHHFd1VFvk+kwvniq3P+f9rh52InWnL8Lpvedcecoh1GFSc5
|
||||||
|
xZ9VBGex2V269HZfwxSVCvP35wQfi2xKX+lYMXtF48n1R65O2PLWpm69RdESMa79dlrTGazsZacu
|
||||||
|
MbMLeSSScPORZde76/MBV6SFJAAEAAAfiwgAAAAAAAID7VRLaxsxEN6zfoUgZ++OVq+1aUIhUDeY
|
||||||
|
pKa49FhmJdkW3ofRysXpr69220t9SCk0gZJ+IGaY56eBmbxY4/m9Q+vCUOTr1fLu4d2H7O8CEpQQ
|
||||||
|
k0y4lAClypgQoBSTQqoMGBMgMnrOXgCnIWJIVLLXCcaoib5110CSij/V7D9eCZ5p5f9o/5VkF/tf
|
||||||
|
MqUzCi+5/6Hv41Nxv/Nffu4fwRVdus4FjM7S+pFiffKNpTxnkMMsALmin5PnHgMtS8rkgvGFBPpp
|
||||||
|
c0tLKDk5HnYdto5e052PDmfRDXE0fnUh2VgucjYLU5h1g0mm5RhGNymMrtEccOfIKTTJsY/xOCyK
|
||||||
|
YqqT+74gExWbmI2VlJ6LeQUcyPFH2lh/9SBuV/wjfXPohDnw8HZKviGD/zYmCZgrgsHsk36u1Bcl
|
||||||
|
SB/8zne/0jV92/qYbKRF38X0niiemN2QxhvXDWOL+7tNGhGeYt+m22mwaR6pddGZNM8FSeRxj8PY
|
||||||
|
X7PaqdqAVlqWXHKnmQGmK43VlqNlILRilbBSMI2jV5Vbu5XGSVsDyGc7yd8B/gK2qgAIAAAfiwgA
|
||||||
|
AAAAAAID7dNNSgMxGAbg7MSCOxcu5wJOv0x+OlkU7K5QoYXqVsxMMihlKMwP1Fu48QQewCN4DfEQ
|
||||||
|
egUz4sYuFKEtFN9n870hWSSQN+7P7GrsrfNV3Y9dW5Z3bNMo0FJ+zmB9EhcJ41KS1lxJpRnxbsWi
|
||||||
|
FduBtm5sFa7C/ifOo7y5Lf2QeiHar6jTaDSbnF5Mp+fzOL/x+aJuy3g+HvGhs8JY4b3yOpMZOZEo
|
||||||
|
lRW+MEoTTw3ZwqU0INNjsAe2VPk/9b/L3/s/kIKzqOtk+IbJGTtmr+bx7WoxOUoun98frk/un14O
|
||||||
|
Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA`
|
||||||
|
content, err := base64.StdEncoding.DecodeString(base64AlpinePackageContent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
branches := []string{"v3.16", "v3.17"}
|
||||||
|
repositories := []string{"main", "testing"}
|
||||||
|
|
||||||
|
rootURL := fmt.Sprintf("/api/packages/%s/alpine", user.Name)
|
||||||
|
|
||||||
|
t.Run("RepositoryKey", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", rootURL+"/key")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
assert.Equal(t, "application/x-pem-file", resp.Header().Get("Content-Type"))
|
||||||
|
assert.Contains(t, resp.Body.String(), "-----BEGIN PUBLIC KEY-----")
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, branch := range branches {
|
||||||
|
for _, repository := range repositories {
|
||||||
|
t.Run(fmt.Sprintf("[Branch:%s,Repository:%s]", branch, repository), func(t *testing.T) {
|
||||||
|
t.Run("Upload", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
uploadURL := fmt.Sprintf("%s/%s/%s", rootURL, branch, repository)
|
||||||
|
|
||||||
|
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, bytes.NewReader(content))
|
||||||
|
AddBasicAuthHeader(req, user.Name)
|
||||||
|
MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
|
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeAlpine)
|
||||||
|
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, &alpine_module.VersionMetadata{}, 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.apk", packageName, packageVersion)
|
||||||
|
expectedCompositeKey := fmt.Sprintf("%s|%s|x86_64", branch, repository)
|
||||||
|
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 alpine_module.PropertyBranch:
|
||||||
|
assert.Equal(t, branch, pfp.Value)
|
||||||
|
case alpine_module.PropertyRepository:
|
||||||
|
assert.Equal(t, repository, pfp.Value)
|
||||||
|
case alpine_module.PropertyArchitecture:
|
||||||
|
assert.Equal(t, "x86_64", pfp.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seen
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Index", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository)
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", url)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
assert.Condition(t, func() bool {
|
||||||
|
br := bufio.NewReader(resp.Body)
|
||||||
|
|
||||||
|
gzr, err := gzip.NewReader(br)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for {
|
||||||
|
gzr.Multistream(false)
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
for {
|
||||||
|
hd, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
if hd.Name == "APKINDEX" {
|
||||||
|
buf, err := io.ReadAll(tr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
s := string(buf)
|
||||||
|
|
||||||
|
assert.Contains(t, s, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n")
|
||||||
|
assert.Contains(t, s, "P:"+packageName+"\n")
|
||||||
|
assert.Contains(t, s, "V:"+packageVersion+"\n")
|
||||||
|
assert.Contains(t, s, "A:x86_64\n")
|
||||||
|
assert.Contains(t, s, "T:Gitea Test Package\n")
|
||||||
|
assert.Contains(t, s, "U:https://gitea.io/\n")
|
||||||
|
assert.Contains(t, s, "L:MIT\n")
|
||||||
|
assert.Contains(t, s, "S:1353\n")
|
||||||
|
assert.Contains(t, s, "I:4096\n")
|
||||||
|
assert.Contains(t, s, "o:gitea-test\n")
|
||||||
|
assert.Contains(t, s, "m:KN4CK3R <kn4ck3r@gitea.io>\n")
|
||||||
|
assert.Contains(t, s, "t:1679498030\n")
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gzr.Reset(br)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Download", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion))
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Delete", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
for _, branch := range branches {
|
||||||
|
for _, repository := range repositories {
|
||||||
|
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion))
|
||||||
|
MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
|
||||||
|
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion))
|
||||||
|
AddBasicAuthHeader(req, user.Name)
|
||||||
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
// Deleting the last file of an architecture should remove that index
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository))
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" viewBox="0 0 186 162" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.25 0 0 -1.25 -268 592)" fill="#0d597f"><g transform="translate(268 393)"><path d="m0 0v15.7l-11.3-11.3c1.22-0.847 2.36-1.54 3.44-2.11 1.08-0.567 2.09-1 3.03-1.34 0.941-0.334 1.81-0.562 2.62-0.71 0.804-0.147 1.54-0.213 2.21-0.222m57.8 1.3c0.02-0.017 0.13-0.11 0.333-0.239 0.204-0.13 0.502-0.297 0.898-0.462 0.395-0.164 0.889-0.327 1.48-0.448 0.596-0.122 1.29-0.202 2.1-0.202 0.671 0 1.41 0.059 2.22 0.2 0.812 0.142 1.69 0.367 2.64 0.699 0.953 0.333 1.98 0.773 3.07 1.34 1.09 0.572 2.26 1.28 3.5 2.14l-8.57 8.4-28.4 28.5-12.5-12.5-16.8 17.3-42.2-41.7c1.24-0.86 2.4-1.56 3.49-2.14 1.09-0.571 2.12-1.01 3.07-1.34 0.951-0.332 1.83-0.557 2.64-0.698 0.81-0.142 1.55-0.201 2.22-0.201 0.804 0 1.5 0.08 2.1 0.202 0.596 0.121 1.09 0.284 1.48 0.449 0.396 0.164 0.693 0.331 0.897 0.461s0.314 0.223 0.334 0.24l19.1 19.1 6.81 6.53 18.9-18.9 6.59-6.78c0.02-0.017 0.13-0.11 0.334-0.239 0.204-0.13 0.501-0.297 0.897-0.462 0.396-0.164 0.89-0.327 1.48-0.448 0.596-0.122 1.3-0.202 2.1-0.202 0.67 0 1.41 0.059 2.22 0.2 0.811 0.142 1.69 0.367 2.64 0.699 0.952 0.333 1.98 0.773 3.07 1.34 1.09 0.572 2.26 1.28 3.5 2.14l-15.1 14.8 2.82 2.82 13.1-13.1 7.64-7.57m-0.163 79 37.2-64.5-37.2-64.5h-74.5l-37.2 64.5 37.2 64.5z" fill="#0d597f"/></g><g transform="translate(303 412)"><path d="m0 0-9.91 9.9 0.705 0.709 9.98-9.87z" fill="#0d597f"/></g></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
Loading…
Reference in New Issue