@@ -0,0 +1,65 @@ | |||
--- | |||
date: "2016-11-08T16:00:00+02:00" | |||
title: "Arch Package Registry" | |||
weight: 10 | |||
toc: true | |||
draft: false | |||
menu: | |||
sidebar: | |||
parent: "packages" | |||
name: "Arch" | |||
weight: 10 | |||
identifier: "arch" | |||
--- | |||
# Arch package registry | |||
Gitea has a Arch Linux package registry, which can act as a fully working [Arch linux mirror](https://wiki.archlinux.org/title/mirrors) and connected directly in `/etc/pacman.conf`. Gitea automatically creates pacman database for packages in user/organization space when a new Arch package is uploaded. | |||
**Table of Contents** | |||
{{< toc >}} | |||
## Install packages | |||
First, you need to update your pacman configuration, adding following lines: | |||
```conf | |||
[{owner}.{domain}] | |||
SigLevel = Optional TrustAll | |||
Server = https://{domain}/api/packages/{owner}/arch/{distribution}/{architecture} | |||
``` | |||
Then, you can run pacman sync command (with -y flag to load connected database file), to install your package: | |||
```sh | |||
pacman -Sy package | |||
``` | |||
## Upload packages | |||
When uploading the package to gitea, you have to prepare package file with the `.pkg.tar.zst` extension and its `.pkg.tar.zst.sig` signature. You can use [curl](https://curl.se/) or any other HTTP client, Gitea supports multiple [authentication schemes](https://docs.gitea.com/usage/authentication). The upload command will create 3 files: package, signature and desc file for the pacman database (which will be created automatically on request). | |||
The following command will upload arch package and related signature to gitea with basic authentification: | |||
```sh | |||
curl -X PUT \ | |||
https://{domain}/api/packages/{owner}/arch/push/{package-1-1-x86_64.pkg.tar.zst}/{archlinux}/$(xxd -p package-1-1-x86_64.pkg.tar.zst.sig | tr -d '\n') \ | |||
--user your_username:your_token_or_password \ | |||
--header "Content-Type: application/octet-stream" \ | |||
--data-binary '@/path/to/package/file/package-1-1-x86_64.pkg.tar.zst' | |||
``` | |||
## Delete packages | |||
The `DELETE` method will remove specific package version, and all package files related to that version: | |||
```sh | |||
curl -X DELETE \ | |||
https://{domain}/api/packages/{user}/arch/remove/{package}/{version} \ | |||
--user your_username:your_token_or_password | |||
``` | |||
## Clients | |||
Any `pacman` compatible package manager or AUR-helper can be used to install packages from gitea ([yay](https://github.com/Jguer/yay), [paru](https://github.com/Morganamilo/paru), [pikaur](https://github.com/actionless/pikaur), [aura](https://github.com/fosskers/aura)). Alternatively, you can try [pack](https://fmnx.su/core/pack) which supports full gitea API (install/push/remove). Also, any HTTP client can be used to execute get/push/remove operations ([curl](https://curl.se/), [postman](https://www.postman.com/), [thunder-client](https://www.thunderclient.com/)). |
@@ -24,6 +24,7 @@ The following package managers are currently supported: | |||
| Name | Language | Package client | | |||
| ---- | -------- | -------------- | | |||
| [Alpine](usage/packages/alpine.md) | - | `apk` | | |||
| [Arch](usage/packages/arch.md) | - | `pacman` | | |||
| [Cargo](usage/packages/cargo.md) | Rust | `cargo` | | |||
| [Chef](usage/packages/chef.md) | - | `knife` | | |||
| [Composer](usage/packages/composer.md) | PHP | `composer` | |
@@ -13,6 +13,7 @@ import ( | |||
user_model "code.gitea.io/gitea/models/user" | |||
"code.gitea.io/gitea/modules/json" | |||
"code.gitea.io/gitea/modules/packages/alpine" | |||
"code.gitea.io/gitea/modules/packages/arch" | |||
"code.gitea.io/gitea/modules/packages/cargo" | |||
"code.gitea.io/gitea/modules/packages/chef" | |||
"code.gitea.io/gitea/modules/packages/composer" | |||
@@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc | |||
switch p.Type { | |||
case TypeAlpine: | |||
metadata = &alpine.VersionMetadata{} | |||
case TypeArch: | |||
metadata = &arch.VersionMetadata{} | |||
case TypeCargo: | |||
metadata = &cargo.Metadata{} | |||
case TypeChef: |
@@ -31,6 +31,7 @@ type Type string | |||
// List of supported packages | |||
const ( | |||
TypeAlpine Type = "alpine" | |||
TypeArch Type = "arch" | |||
TypeCargo Type = "cargo" | |||
TypeChef Type = "chef" | |||
TypeComposer Type = "composer" | |||
@@ -55,6 +56,7 @@ const ( | |||
var TypeList = []Type{ | |||
TypeAlpine, | |||
TypeArch, | |||
TypeCargo, | |||
TypeChef, | |||
TypeComposer, | |||
@@ -82,6 +84,8 @@ func (pt Type) Name() string { | |||
switch pt { | |||
case TypeAlpine: | |||
return "Alpine" | |||
case TypeArch: | |||
return "Arch" | |||
case TypeCargo: | |||
return "Cargo" | |||
case TypeChef: | |||
@@ -131,6 +135,8 @@ func (pt Type) SVGName() string { | |||
switch pt { | |||
case TypeAlpine: | |||
return "gitea-alpine" | |||
case TypeArch: | |||
return "gitea-arch" | |||
case TypeCargo: | |||
return "gitea-cargo" | |||
case TypeChef: |
@@ -0,0 +1,302 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package arch | |||
import ( | |||
"archive/tar" | |||
"bufio" | |||
"bytes" | |||
"compress/gzip" | |||
"encoding/hex" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"os" | |||
"regexp" | |||
"strconv" | |||
"strings" | |||
"code.gitea.io/gitea/modules/util" | |||
"code.gitea.io/gitea/modules/validation" | |||
"github.com/mholt/archiver/v3" | |||
) | |||
const ( | |||
PropertyDescription = "arch.description" | |||
PropertySignature = "arch.signature" | |||
) | |||
var ( | |||
// https://man.archlinux.org/man/PKGBUILD.5 | |||
reName = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$`) | |||
reVer = regexp.MustCompile(`^[a-zA-Z0-9:_.+]+-+[0-9]+$`) | |||
reOptDep = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$|^[a-zA-Z0-9@._+-]+(:.*)`) | |||
rePkgVer = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$|^[a-zA-Z0-9@._+-]+(>.*)|^[a-zA-Z0-9@._+-]+(<.*)|^[a-zA-Z0-9@._+-]+(=.*)`) | |||
) | |||
type Package struct { | |||
Name string `json:"name"` | |||
Version string `json:"version"` | |||
VersionMetadata VersionMetadata | |||
FileMetadata FileMetadata | |||
} | |||
// Arch package metadata related to specific version. | |||
// Version metadata the same across different architectures and distributions. | |||
type VersionMetadata struct { | |||
Base string `json:"base"` | |||
Description string `json:"description"` | |||
ProjectURL string `json:"project_url"` | |||
Groups []string `json:"groups,omitempty"` | |||
Provides []string `json:"provides,omitempty"` | |||
License []string `json:"license,omitempty"` | |||
Depends []string `json:"depends,omitempty"` | |||
OptDepends []string `json:"opt_depends,omitempty"` | |||
MakeDepends []string `json:"make_depends,omitempty"` | |||
CheckDepends []string `json:"check_depends,omitempty"` | |||
Backup []string `json:"backup,omitempty"` | |||
} | |||
// Metadata related to specific pakcage file. | |||
// This metadata might vary for different architecture and distribution. | |||
type FileMetadata struct { | |||
CompressedSize int64 `json:"compressed_size"` | |||
InstalledSize int64 `json:"installed_size"` | |||
MD5 string `json:"md5"` | |||
SHA256 string `json:"sha256"` | |||
BuildDate int64 `json:"build_date"` | |||
Packager string `json:"packager"` | |||
Arch string `json:"arch"` | |||
} | |||
// Function that receives arch package archive data and returns it's metadata. | |||
func ParsePackage(r io.Reader, md5, sha256 []byte, size int64) (*Package, error) { | |||
zstd := archiver.NewTarZstd() | |||
err := zstd.Open(r, 0) | |||
if err != nil { | |||
return nil, err | |||
} | |||
defer zstd.Close() | |||
var pkg *Package | |||
var mtree bool | |||
for { | |||
f, err := zstd.Read() | |||
if err == io.EOF { | |||
break | |||
} | |||
if err != nil { | |||
return nil, err | |||
} | |||
defer f.Close() | |||
switch f.Name() { | |||
case ".PKGINFO": | |||
pkg, err = ParsePackageInfo(f) | |||
if err != nil { | |||
return nil, err | |||
} | |||
case ".MTREE": | |||
mtree = true | |||
} | |||
} | |||
if pkg == nil { | |||
return nil, util.NewInvalidArgumentErrorf(".PKGINFO file not found") | |||
} | |||
if !mtree { | |||
return nil, util.NewInvalidArgumentErrorf(".MTREE file not found") | |||
} | |||
pkg.FileMetadata.CompressedSize = size | |||
pkg.FileMetadata.SHA256 = hex.EncodeToString(sha256) | |||
pkg.FileMetadata.MD5 = hex.EncodeToString(md5) | |||
return pkg, nil | |||
} | |||
// Function that accepts reader for .PKGINFO file from package archive, | |||
// validates all field according to PKGBUILD spec and returns 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 "pkgbase": | |||
p.VersionMetadata.Base = value | |||
case "pkgver": | |||
p.Version = value | |||
case "pkgdesc": | |||
p.VersionMetadata.Description = value | |||
case "url": | |||
p.VersionMetadata.ProjectURL = value | |||
case "packager": | |||
p.FileMetadata.Packager = value | |||
case "arch": | |||
p.FileMetadata.Arch = value | |||
case "provides": | |||
p.VersionMetadata.Provides = append(p.VersionMetadata.Provides, value) | |||
case "license": | |||
p.VersionMetadata.License = append(p.VersionMetadata.License, value) | |||
case "depend": | |||
p.VersionMetadata.Depends = append(p.VersionMetadata.Depends, value) | |||
case "optdepend": | |||
p.VersionMetadata.OptDepends = append(p.VersionMetadata.OptDepends, value) | |||
case "makedepend": | |||
p.VersionMetadata.MakeDepends = append(p.VersionMetadata.MakeDepends, value) | |||
case "checkdepend": | |||
p.VersionMetadata.CheckDepends = append(p.VersionMetadata.CheckDepends, value) | |||
case "backup": | |||
p.VersionMetadata.Backup = append(p.VersionMetadata.Backup, value) | |||
case "group": | |||
p.VersionMetadata.Groups = append(p.VersionMetadata.Groups, value) | |||
case "builddate": | |||
bd, err := strconv.ParseInt(value, 10, 64) | |||
if err != nil { | |||
return nil, err | |||
} | |||
p.FileMetadata.BuildDate = bd | |||
case "size": | |||
is, err := strconv.ParseInt(value, 10, 64) | |||
if err != nil { | |||
return nil, err | |||
} | |||
p.FileMetadata.InstalledSize = is | |||
} | |||
} | |||
return p, errors.Join(scanner.Err(), ValidatePackageSpec(p)) | |||
} | |||
// Arch package validation according to PKGBUILD specification. | |||
func ValidatePackageSpec(p *Package) error { | |||
if !reName.MatchString(p.Name) { | |||
return util.NewInvalidArgumentErrorf("invalid package name") | |||
} | |||
if !reName.MatchString(p.VersionMetadata.Base) { | |||
return util.NewInvalidArgumentErrorf("invalid package base") | |||
} | |||
if !reVer.MatchString(p.Version) { | |||
return util.NewInvalidArgumentErrorf("invalid package version") | |||
} | |||
if p.FileMetadata.Arch == "" { | |||
return util.NewInvalidArgumentErrorf("architecture should be specified") | |||
} | |||
if p.VersionMetadata.ProjectURL != "" { | |||
if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { | |||
return util.NewInvalidArgumentErrorf("invalid project URL") | |||
} | |||
} | |||
for _, cd := range p.VersionMetadata.CheckDepends { | |||
if !rePkgVer.MatchString(cd) { | |||
return util.NewInvalidArgumentErrorf("invalid check dependency: " + cd) | |||
} | |||
} | |||
for _, d := range p.VersionMetadata.Depends { | |||
if !rePkgVer.MatchString(d) { | |||
return util.NewInvalidArgumentErrorf("invalid dependency: " + d) | |||
} | |||
} | |||
for _, md := range p.VersionMetadata.MakeDepends { | |||
if !rePkgVer.MatchString(md) { | |||
return util.NewInvalidArgumentErrorf("invalid make dependency: " + md) | |||
} | |||
} | |||
for _, p := range p.VersionMetadata.Provides { | |||
if !rePkgVer.MatchString(p) { | |||
return util.NewInvalidArgumentErrorf("invalid provides: " + p) | |||
} | |||
} | |||
for _, od := range p.VersionMetadata.OptDepends { | |||
if !reOptDep.MatchString(od) { | |||
return util.NewInvalidArgumentErrorf("invalid optional dependency: " + od) | |||
} | |||
} | |||
for _, bf := range p.VersionMetadata.Backup { | |||
if strings.HasPrefix(bf, "/") { | |||
return util.NewInvalidArgumentErrorf("backup file contains leading forward slash") | |||
} | |||
} | |||
return nil | |||
} | |||
// Create pacman package description file. | |||
func (p *Package) Desc() string { | |||
entries := [40]string{ | |||
"FILENAME", fmt.Sprintf("%s-%s-%s.pkg.tar.zst", p.Name, p.Version, p.FileMetadata.Arch), | |||
"NAME", p.Name, | |||
"BASE", p.VersionMetadata.Base, | |||
"VERSION", p.Version, | |||
"DESC", p.VersionMetadata.Description, | |||
"GROUPS", strings.Join(p.VersionMetadata.Groups, "\n"), | |||
"CSIZE", fmt.Sprintf("%d", p.FileMetadata.CompressedSize), | |||
"ISIZE", fmt.Sprintf("%d", p.FileMetadata.InstalledSize), | |||
"MD5SUM", p.FileMetadata.MD5, | |||
"SHA256SUM", p.FileMetadata.SHA256, | |||
"URL", p.VersionMetadata.ProjectURL, | |||
"LICENSE", strings.Join(p.VersionMetadata.License, "\n"), | |||
"ARCH", p.FileMetadata.Arch, | |||
"BUILDDATE", fmt.Sprintf("%d", p.FileMetadata.BuildDate), | |||
"PACKAGER", p.FileMetadata.Packager, | |||
"PROVIDES", strings.Join(p.VersionMetadata.Provides, "\n"), | |||
"DEPENDS", strings.Join(p.VersionMetadata.Depends, "\n"), | |||
"OPTDEPENDS", strings.Join(p.VersionMetadata.OptDepends, "\n"), | |||
"MAKEDEPENDS", strings.Join(p.VersionMetadata.MakeDepends, "\n"), | |||
"CHECKDEPENDS", strings.Join(p.VersionMetadata.CheckDepends, "\n"), | |||
} | |||
var buf bytes.Buffer | |||
for i := 0; i < 40; i += 2 { | |||
if entries[i+1] != "" { | |||
fmt.Fprintf(&buf, "%%%s%%\n%s\n\n", entries[i], entries[i+1]) | |||
} | |||
} | |||
return buf.String() | |||
} | |||
// Create pacman database archive based on provided package metadata structs. | |||
func CreatePacmanDb(entries map[string][]byte) (*bytes.Buffer, error) { | |||
var b bytes.Buffer | |||
gw := gzip.NewWriter(&b) | |||
tw := tar.NewWriter(gw) | |||
for name, content := range entries { | |||
header := &tar.Header{ | |||
Name: name, | |||
Size: int64(len(content)), | |||
Mode: int64(os.ModePerm), | |||
} | |||
if err := tw.WriteHeader(header); err != nil { | |||
return nil, errors.Join(err, tw.Close(), gw.Close()) | |||
} | |||
if _, err := tw.Write(content); err != nil { | |||
return nil, errors.Join(err, tw.Close(), gw.Close()) | |||
} | |||
} | |||
return &b, errors.Join(tw.Close(), gw.Close()) | |||
} |
@@ -0,0 +1,452 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package arch | |||
import ( | |||
"bytes" | |||
"encoding/base64" | |||
"errors" | |||
"io" | |||
"os" | |||
"strings" | |||
"testing" | |||
"testing/fstest" | |||
"time" | |||
"github.com/mholt/archiver/v3" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestParsePackage(t *testing.T) { | |||
// Minimal PKGINFO contents and test FS | |||
const PKGINFO = `pkgname = a | |||
pkgbase = b | |||
pkgver = 1-2 | |||
arch = x86_64 | |||
` | |||
fs := fstest.MapFS{ | |||
"pkginfo": &fstest.MapFile{ | |||
Data: []byte(PKGINFO), | |||
Mode: os.ModePerm, | |||
ModTime: time.Now(), | |||
}, | |||
"mtree": &fstest.MapFile{ | |||
Data: []byte("data"), | |||
Mode: os.ModePerm, | |||
ModTime: time.Now(), | |||
}, | |||
} | |||
// Test .PKGINFO file | |||
pinf, err := fs.Stat("pkginfo") | |||
assert.NoError(t, err) | |||
pfile, err := fs.Open("pkginfo") | |||
assert.NoError(t, err) | |||
parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO") | |||
assert.NoError(t, err) | |||
// Test .MTREE file | |||
minf, err := fs.Stat("mtree") | |||
assert.NoError(t, err) | |||
mfile, err := fs.Open("mtree") | |||
assert.NoError(t, err) | |||
marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE") | |||
assert.NoError(t, err) | |||
t.Run("normal archive", func(t *testing.T) { | |||
var buf bytes.Buffer | |||
archive := archiver.NewTarZstd() | |||
archive.Create(&buf) | |||
err = archive.Write(archiver.File{ | |||
FileInfo: archiver.FileInfo{ | |||
FileInfo: pinf, | |||
CustomName: parcname, | |||
}, | |||
ReadCloser: pfile, | |||
}) | |||
assert.NoError(t, errors.Join(pfile.Close(), err)) | |||
err = archive.Write(archiver.File{ | |||
FileInfo: archiver.FileInfo{ | |||
FileInfo: minf, | |||
CustomName: marcname, | |||
}, | |||
ReadCloser: mfile, | |||
}) | |||
assert.NoError(t, errors.Join(mfile.Close(), archive.Close(), err)) | |||
_, err = ParsePackage(&buf, []byte{}, []byte{}, 0) | |||
assert.NoError(t, err) | |||
}) | |||
t.Run("missing .PKGINFO", func(t *testing.T) { | |||
var buf bytes.Buffer | |||
archive := archiver.NewTarZstd() | |||
archive.Create(&buf) | |||
assert.NoError(t, archive.Close()) | |||
_, err = ParsePackage(&buf, []byte{}, []byte{}, 0) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), ".PKGINFO file not found") | |||
}) | |||
t.Run("missing .MTREE", func(t *testing.T) { | |||
var buf bytes.Buffer | |||
pfile, err := fs.Open("pkginfo") | |||
assert.NoError(t, err) | |||
archive := archiver.NewTarZstd() | |||
archive.Create(&buf) | |||
err = archive.Write(archiver.File{ | |||
FileInfo: archiver.FileInfo{ | |||
FileInfo: pinf, | |||
CustomName: parcname, | |||
}, | |||
ReadCloser: pfile, | |||
}) | |||
assert.NoError(t, errors.Join(pfile.Close(), archive.Close(), err)) | |||
_, err = ParsePackage(&buf, []byte{}, []byte{}, 0) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), ".MTREE file not found") | |||
}) | |||
} | |||
func TestParsePackageInfo(t *testing.T) { | |||
const PKGINFO = `# Generated by makepkg 6.0.2 | |||
# using fakeroot version 1.31 | |||
pkgname = a | |||
pkgbase = b | |||
pkgver = 1-2 | |||
pkgdesc = comment | |||
url = https://example.com/ | |||
group = group | |||
builddate = 3 | |||
packager = Name Surname <login@example.com> | |||
size = 5 | |||
arch = x86_64 | |||
license = BSD | |||
provides = pvd | |||
depend = smth | |||
optdepend = hex | |||
checkdepend = ola | |||
makedepend = cmake | |||
backup = usr/bin/paket1 | |||
` | |||
p, err := ParsePackageInfo(strings.NewReader(PKGINFO)) | |||
assert.NoError(t, err) | |||
assert.Equal(t, Package{ | |||
Name: "a", | |||
Version: "1-2", | |||
VersionMetadata: VersionMetadata{ | |||
Base: "b", | |||
Description: "comment", | |||
ProjectURL: "https://example.com/", | |||
Groups: []string{"group"}, | |||
Provides: []string{"pvd"}, | |||
License: []string{"BSD"}, | |||
Depends: []string{"smth"}, | |||
OptDepends: []string{"hex"}, | |||
MakeDepends: []string{"cmake"}, | |||
CheckDepends: []string{"ola"}, | |||
Backup: []string{"usr/bin/paket1"}, | |||
}, | |||
FileMetadata: FileMetadata{ | |||
InstalledSize: 5, | |||
BuildDate: 3, | |||
Packager: "Name Surname <login@example.com>", | |||
Arch: "x86_64", | |||
}, | |||
}, *p) | |||
} | |||
func TestValidatePackageSpec(t *testing.T) { | |||
newpkg := func() Package { | |||
return Package{ | |||
Name: "abc", | |||
Version: "1-1", | |||
VersionMetadata: VersionMetadata{ | |||
Base: "ghx", | |||
Description: "whoami", | |||
ProjectURL: "https://example.com/", | |||
Groups: []string{"gnome"}, | |||
Provides: []string{"abc", "def"}, | |||
License: []string{"GPL"}, | |||
Depends: []string{"go", "gpg=1", "curl>=3", "git<=7"}, | |||
OptDepends: []string{"git: something", "make"}, | |||
MakeDepends: []string{"chrom"}, | |||
CheckDepends: []string{"bariy"}, | |||
Backup: []string{"etc/pacman.d/filo"}, | |||
}, | |||
FileMetadata: FileMetadata{ | |||
CompressedSize: 1, | |||
InstalledSize: 2, | |||
MD5: "abc", | |||
SHA256: "def", | |||
BuildDate: 3, | |||
Packager: "smon", | |||
Arch: "x86_64", | |||
}, | |||
} | |||
} | |||
t.Run("valid package", func(t *testing.T) { | |||
p := newpkg() | |||
err := ValidatePackageSpec(&p) | |||
assert.NoError(t, err) | |||
}) | |||
t.Run("invalid package name", func(t *testing.T) { | |||
p := newpkg() | |||
p.Name = "!$%@^!*&()" | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid package name") | |||
}) | |||
t.Run("invalid package base", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.Base = "!$%@^!*&()" | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid package base") | |||
}) | |||
t.Run("invalid package version", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.Base = "una-luna?" | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid package base") | |||
}) | |||
t.Run("invalid package version", func(t *testing.T) { | |||
p := newpkg() | |||
p.Version = "una-luna" | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid package version") | |||
}) | |||
t.Run("missing architecture", func(t *testing.T) { | |||
p := newpkg() | |||
p.FileMetadata.Arch = "" | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "architecture should be specified") | |||
}) | |||
t.Run("invalid URL", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.ProjectURL = "http%%$#" | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid project URL") | |||
}) | |||
t.Run("invalid check dependency", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.CheckDepends = []string{"Err^_^"} | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid check dependency") | |||
}) | |||
t.Run("invalid dependency", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.Depends = []string{"^^abc"} | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid dependency") | |||
}) | |||
t.Run("invalid make dependency", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.MakeDepends = []string{"^m^"} | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid make dependency") | |||
}) | |||
t.Run("invalid provides", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.Provides = []string{"^m^"} | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid provides") | |||
}) | |||
t.Run("invalid optional dependency", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.OptDepends = []string{"^m^:MM"} | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "invalid optional dependency") | |||
}) | |||
t.Run("invalid optional dependency", func(t *testing.T) { | |||
p := newpkg() | |||
p.VersionMetadata.Backup = []string{"/ola/cola"} | |||
err := ValidatePackageSpec(&p) | |||
assert.Error(t, err) | |||
assert.Contains(t, err.Error(), "backup file contains leading forward slash") | |||
}) | |||
} | |||
func TestDescString(t *testing.T) { | |||
const pkgdesc = `%FILENAME% | |||
zstd-1.5.5-1-x86_64.pkg.tar.zst | |||
%NAME% | |||
zstd | |||
%BASE% | |||
zstd | |||
%VERSION% | |||
1.5.5-1 | |||
%DESC% | |||
Zstandard - Fast real-time compression algorithm | |||
%GROUPS% | |||
dummy1 | |||
dummy2 | |||
%CSIZE% | |||
401 | |||
%ISIZE% | |||
1500453 | |||
%MD5SUM% | |||
5016660ef3d9aa148a7b72a08d3df1b2 | |||
%SHA256SUM% | |||
9fa4ede47e35f5971e4f26ecadcbfb66ab79f1d638317ac80334a3362dedbabd | |||
%URL% | |||
https://facebook.github.io/zstd/ | |||
%LICENSE% | |||
BSD | |||
GPL2 | |||
%ARCH% | |||
x86_64 | |||
%BUILDDATE% | |||
1681646714 | |||
%PACKAGER% | |||
Jelle van der Waa <jelle@archlinux.org> | |||
%PROVIDES% | |||
libzstd.so=1-64 | |||
%DEPENDS% | |||
glibc | |||
gcc-libs | |||
zlib | |||
xz | |||
lz4 | |||
%OPTDEPENDS% | |||
dummy3 | |||
dummy4 | |||
%MAKEDEPENDS% | |||
cmake | |||
gtest | |||
ninja | |||
%CHECKDEPENDS% | |||
dummy5 | |||
dummy6 | |||
` | |||
md := &Package{ | |||
Name: "zstd", | |||
Version: "1.5.5-1", | |||
VersionMetadata: VersionMetadata{ | |||
Base: "zstd", | |||
Description: "Zstandard - Fast real-time compression algorithm", | |||
ProjectURL: "https://facebook.github.io/zstd/", | |||
Groups: []string{"dummy1", "dummy2"}, | |||
Provides: []string{"libzstd.so=1-64"}, | |||
License: []string{"BSD", "GPL2"}, | |||
Depends: []string{"glibc", "gcc-libs", "zlib", "xz", "lz4"}, | |||
OptDepends: []string{"dummy3", "dummy4"}, | |||
MakeDepends: []string{"cmake", "gtest", "ninja"}, | |||
CheckDepends: []string{"dummy5", "dummy6"}, | |||
}, | |||
FileMetadata: FileMetadata{ | |||
CompressedSize: 401, | |||
InstalledSize: 1500453, | |||
MD5: "5016660ef3d9aa148a7b72a08d3df1b2", | |||
SHA256: "9fa4ede47e35f5971e4f26ecadcbfb66ab79f1d638317ac80334a3362dedbabd", | |||
BuildDate: 1681646714, | |||
Packager: "Jelle van der Waa <jelle@archlinux.org>", | |||
Arch: "x86_64", | |||
}, | |||
} | |||
assert.Equal(t, pkgdesc, md.Desc()) | |||
} | |||
func TestCreatePacmanDb(t *testing.T) { | |||
const dbarchive = "H4sIAAAAAAAA/0rLzEnVS60oYaAhMDAwMDA3NwfTBgYG6LSBgYEpEtuAwcDQwMzUgEHBgJaOgoHS4pLEIgYDiu1C99wQASmlubmVA+2IUTAKRsEoGAV0B4AAAAD//2VF3KIACAAA" | |||
db, err := CreatePacmanDb(map[string][]byte{ | |||
"file.ext": []byte("dummy"), | |||
}) | |||
assert.NoError(t, err) | |||
actual, err := io.ReadAll(db) | |||
assert.NoError(t, err) | |||
expected, err := base64.RawStdEncoding.DecodeString(dbarchive) | |||
assert.NoError(t, err) | |||
assert.Equal(t, expected, actual) | |||
} |
@@ -24,6 +24,7 @@ var ( | |||
LimitTotalOwnerCount int64 | |||
LimitTotalOwnerSize int64 | |||
LimitSizeAlpine int64 | |||
LimitSizeArch int64 | |||
LimitSizeCargo int64 | |||
LimitSizeChef int64 | |||
LimitSizeComposer int64 | |||
@@ -82,6 +83,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) { | |||
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") | |||
Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE") | |||
Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH") | |||
Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") | |||
Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") | |||
Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") |
@@ -3448,6 +3448,16 @@ alpine.repository = Repository Info | |||
alpine.repository.branches = Branches | |||
alpine.repository.repositories = Repositories | |||
alpine.repository.architectures = Architectures | |||
arch.pacmanconf = Add server with related distribution and architecture to <code>/etc/pacman.conf</code>: | |||
arch.pacmansync = Sync package with pacman: | |||
arch.documentation = For more information on the arch mirrors, see %sthe documentation%s. | |||
arch.properties = Package properties | |||
arch.description = Description | |||
arch.provides = Provides | |||
arch.depends = Depends | |||
arch.optdepends = Optional depends | |||
arch.makedepends = Make depends | |||
arch.checkdepends = Check depends | |||
cargo.registry = Setup this registry in the Cargo configuration file (for example <code>~/.cargo/config.toml</code>): | |||
cargo.install = To install the package using Cargo, run the following command: | |||
chef.registry = Setup this registry in your <code>~/.chef/config.rb</code> file: |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg gitea-arch" width="16" height="16" aria-hidden="true"><path fill="#1793d1" d="M256 72c-14 35-23 57-39 91 10 11 22 23 41 36-21-8-35-17-45-26-21 43-53 103-117 220 50-30 90-48 127-55-2-7-3-14-3-22v-1c1-33 18-58 38-56 20 1 36 29 35 62l-2 17c36 7 75 26 125 54l-27-50c-13-10-27-23-55-38 19 5 33 11 44 17-86-159-93-180-122-250z"/></svg> |
@@ -14,6 +14,7 @@ import ( | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/web" | |||
"code.gitea.io/gitea/routers/api/packages/alpine" | |||
"code.gitea.io/gitea/routers/api/packages/arch" | |||
"code.gitea.io/gitea/routers/api/packages/cargo" | |||
"code.gitea.io/gitea/routers/api/packages/chef" | |||
"code.gitea.io/gitea/routers/api/packages/composer" | |||
@@ -121,6 +122,12 @@ func CommonRoutes() *web.Route { | |||
}) | |||
}) | |||
}, reqPackageAccess(perm.AccessModeRead)) | |||
r.Group("/arch", func() { | |||
r.Put("/push/{filename}/{distro}", reqPackageAccess(perm.AccessModeWrite), arch.Push) | |||
r.Put("/push/{filename}/{distro}/{sign}", reqPackageAccess(perm.AccessModeWrite), arch.Push) | |||
r.Delete("/remove/{package}/{version}", reqPackageAccess(perm.AccessModeWrite), arch.Remove) | |||
r.Get("/{distro}/{arch}/{file}", arch.Get) | |||
}) | |||
r.Group("/cargo", func() { | |||
r.Group("/api/v1/crates", func() { | |||
r.Get("", cargo.SearchPackages) |
@@ -0,0 +1,135 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package arch | |||
import ( | |||
"bytes" | |||
"net/http" | |||
"strings" | |||
packages_model "code.gitea.io/gitea/models/packages" | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/routers/api/packages/helper" | |||
packages_service "code.gitea.io/gitea/services/packages" | |||
arch_service "code.gitea.io/gitea/services/packages/arch" | |||
) | |||
func apiError(ctx *context.Context, status int, obj any) { | |||
helper.LogAndProcessError(ctx, status, obj, func(message string) { | |||
ctx.PlainText(status, message) | |||
}) | |||
} | |||
// Push new package to arch package registry. | |||
func Push(ctx *context.Context) { | |||
var ( | |||
filename = ctx.Params("filename") | |||
distro = ctx.Params("distro") | |||
sign = ctx.Params("sign") | |||
) | |||
upload, close, err := ctx.UploadStream() | |||
if err != nil { | |||
apiError(ctx, http.StatusInternalServerError, err) | |||
return | |||
} | |||
if close { | |||
defer upload.Close() | |||
} | |||
_, _, err = arch_service.UploadArchPackage(ctx, upload, filename, distro, sign) | |||
if err != nil { | |||
switch err { | |||
case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: | |||
apiError(ctx, http.StatusConflict, err) | |||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: | |||
apiError(ctx, http.StatusForbidden, err) | |||
default: | |||
apiError(ctx, http.StatusInternalServerError, err) | |||
} | |||
return | |||
} | |||
ctx.Status(http.StatusOK) | |||
} | |||
// Get file from arch package registry. | |||
func Get(ctx *context.Context) { | |||
var ( | |||
file = ctx.Params("file") | |||
owner = ctx.Params("username") | |||
distro = ctx.Params("distro") | |||
arch = ctx.Params("arch") | |||
) | |||
if strings.HasSuffix(file, ".pkg.tar.zst") { | |||
pkg, err := arch_service.GetPackageFile(ctx, distro, file) | |||
if err != nil { | |||
apiError(ctx, http.StatusNotFound, err) | |||
return | |||
} | |||
ctx.ServeContent(pkg, &context.ServeHeaderOptions{ | |||
Filename: file, | |||
}) | |||
return | |||
} | |||
if strings.HasSuffix(file, ".pkg.tar.zst.sig") { | |||
sig, err := arch_service.GetPackageSignature(ctx, distro, file) | |||
if err != nil { | |||
apiError(ctx, http.StatusNotFound, err) | |||
return | |||
} | |||
ctx.ServeContent(sig, &context.ServeHeaderOptions{ | |||
Filename: file, | |||
}) | |||
return | |||
} | |||
if strings.HasSuffix(file, ".db.tar.gz") || strings.HasSuffix(file, ".db") { | |||
db, err := arch_service.CreatePacmanDb(ctx, owner, arch, distro) | |||
if err != nil { | |||
apiError(ctx, http.StatusInternalServerError, err) | |||
return | |||
} | |||
ctx.ServeContent(bytes.NewReader(db.Bytes()), &context.ServeHeaderOptions{ | |||
Filename: file, | |||
}) | |||
return | |||
} | |||
ctx.Status(http.StatusNotFound) | |||
} | |||
// Remove specific package version, related files with properties. | |||
func Remove(ctx *context.Context) { | |||
var ( | |||
pkg = ctx.Params("package") | |||
ver = ctx.Params("version") | |||
) | |||
version, err := packages_model.GetVersionByNameAndVersion( | |||
ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg, ver, | |||
) | |||
if err != nil { | |||
switch err { | |||
case packages_model.ErrPackageNotExist: | |||
apiError(ctx, http.StatusNotFound, err) | |||
default: | |||
apiError(ctx, http.StatusInternalServerError, err) | |||
} | |||
return | |||
} | |||
err = packages_service.RemovePackageVersion(ctx, ctx.Package.Owner, version) | |||
if err != nil { | |||
apiError(ctx, http.StatusInternalServerError, err) | |||
return | |||
} | |||
ctx.Status(http.StatusOK) | |||
} |
@@ -177,7 +177,7 @@ func ViewPackageVersion(ctx *context.Context) { | |||
ctx.Data["PackageDescriptor"] = pd | |||
switch pd.Package.Type { | |||
case packages_model.TypeContainer: | |||
case packages_model.TypeContainer, packages_model.TypeArch: | |||
ctx.Data["RegistryHost"] = setting.Packages.RegistryHost | |||
case packages_model.TypeAlpine: | |||
branches := make(container.Set[string]) |
@@ -0,0 +1,133 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package arch | |||
import ( | |||
"bytes" | |||
"encoding/hex" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"sort" | |||
"strings" | |||
packages_model "code.gitea.io/gitea/models/packages" | |||
"code.gitea.io/gitea/modules/context" | |||
arch_module "code.gitea.io/gitea/modules/packages/arch" | |||
packages_service "code.gitea.io/gitea/services/packages" | |||
) | |||
// Get data related to provided filename and distribution, for package files | |||
// update download counter. | |||
func GetPackageFile(ctx *context.Context, distro, file string) (io.ReadSeekCloser, error) { | |||
pf, err := getPackageFile(ctx, distro, file) | |||
if err != nil { | |||
return nil, err | |||
} | |||
filestream, _, _, err := packages_service.GetPackageFileStream(ctx, pf) | |||
return filestream, err | |||
} | |||
// This function will search for package signature and if present, will load it | |||
// from package file properties, and return its byte reader. | |||
func GetPackageSignature(ctx *context.Context, distro, file string) (*bytes.Reader, error) { | |||
pf, err := getPackageFile(ctx, distro, strings.TrimSuffix(file, ".sig")) | |||
if err != nil { | |||
return nil, err | |||
} | |||
proprs, err := packages_model.GetProperties(ctx, packages_model.PropertyTypeFile, pf.ID) | |||
if err != nil { | |||
return nil, err | |||
} | |||
for _, pp := range proprs { | |||
if pp.Name == arch_module.PropertySignature { | |||
b, err := hex.DecodeString(pp.Value) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return bytes.NewReader(b), nil | |||
} | |||
} | |||
return nil, errors.New("signature for requested package not found") | |||
} | |||
// Ejects parameters required to get package file property from file name. | |||
func getPackageFile(ctx *context.Context, distro, file string) (*packages_model.PackageFile, error) { | |||
var ( | |||
splt = strings.Split(file, "-") | |||
pkgname = strings.Join(splt[0:len(splt)-3], "-") | |||
vername = splt[len(splt)-3] + "-" + splt[len(splt)-2] | |||
) | |||
version, err := packages_model.GetVersionByNameAndVersion( | |||
ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkgname, vername, | |||
) | |||
if err != nil { | |||
return nil, err | |||
} | |||
pkgfile, err := packages_model.GetFileForVersionByName(ctx, version.ID, file, distro) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return pkgfile, nil | |||
} | |||
// Finds all arch packages in user/organization scope, each package version | |||
// starting from latest in descending order is checked to be compatible with | |||
// requested combination of architecture and distribution. When/If the first | |||
// compatible version is found, related desc file will be loaded from package | |||
// properties and added to resulting .db.tar.gz archive. | |||
func CreatePacmanDb(ctx *context.Context, owner, arch, distro string) (*bytes.Buffer, error) { | |||
pkgs, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeArch) | |||
if err != nil { | |||
return nil, err | |||
} | |||
entries := make(map[string][]byte) | |||
for _, pkg := range pkgs { | |||
versions, err := packages_model.GetVersionsByPackageName( | |||
ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg.Name, | |||
) | |||
if err != nil { | |||
return nil, err | |||
} | |||
sort.Slice(versions, func(i, j int) bool { | |||
return versions[i].CreatedUnix > versions[j].CreatedUnix | |||
}) | |||
for _, ver := range versions { | |||
file := fmt.Sprintf("%s-%s-%s.pkg.tar.zst", pkg.Name, ver.Version, arch) | |||
pf, err := packages_model.GetFileForVersionByName(ctx, ver.ID, file, distro) | |||
if err != nil { | |||
file = fmt.Sprintf("%s-%s-any.pkg.tar.zst", pkg.Name, ver.Version) | |||
pf, err = packages_model.GetFileForVersionByName(ctx, ver.ID, file, distro) | |||
if err != nil { | |||
continue | |||
} | |||
} | |||
pps, err := packages_model.GetPropertiesByName( | |||
ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyDescription, | |||
) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if len(pps) >= 1 { | |||
entries[pkg.Name+"-"+ver.Version+"/desc"] = []byte(pps[0].Value) | |||
break | |||
} | |||
} | |||
} | |||
return arch_module.CreatePacmanDb(entries) | |||
} |
@@ -0,0 +1,83 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package arch | |||
import ( | |||
"encoding/hex" | |||
"errors" | |||
"io" | |||
packages_model "code.gitea.io/gitea/models/packages" | |||
"code.gitea.io/gitea/modules/context" | |||
packages_module "code.gitea.io/gitea/modules/packages" | |||
arch_module "code.gitea.io/gitea/modules/packages/arch" | |||
packages_service "code.gitea.io/gitea/services/packages" | |||
) | |||
// UploadArchPackage adds an Arch Package to the registry. | |||
// The first return value indictaes if the error is a user error. | |||
func UploadArchPackage(ctx *context.Context, upload io.Reader, filename, distro, sign string) (bool, *packages_model.PackageVersion, error) { | |||
buf, err := packages_module.CreateHashedBufferFromReader(upload) | |||
if err != nil { | |||
return false, nil, err | |||
} | |||
defer buf.Close() | |||
md5, _, sha256, _ := buf.Sums() | |||
p, err := arch_module.ParsePackage(buf, md5, sha256, buf.Size()) | |||
if err != nil { | |||
return false, nil, err | |||
} | |||
_, err = buf.Seek(0, io.SeekStart) | |||
if err != nil { | |||
return false, nil, err | |||
} | |||
properties := map[string]string{ | |||
arch_module.PropertyDescription: p.Desc(), | |||
} | |||
if sign != "" { | |||
_, err := hex.DecodeString(sign) | |||
if err != nil { | |||
return true, nil, errors.New("unable to decode package signature") | |||
} | |||
properties[arch_module.PropertySignature] = sign | |||
} | |||
ver, _, err := packages_service.CreatePackageOrAddFileToExisting( | |||
ctx, &packages_service.PackageCreationInfo{ | |||
PackageInfo: packages_service.PackageInfo{ | |||
Owner: ctx.Package.Owner, | |||
PackageType: packages_model.TypeArch, | |||
Name: p.Name, | |||
Version: p.Version, | |||
}, | |||
Creator: ctx.Doer, | |||
Metadata: p.VersionMetadata, | |||
}, | |||
&packages_service.PackageFileCreationInfo{ | |||
PackageFileInfo: packages_service.PackageFileInfo{ | |||
Filename: filename, | |||
CompositeKey: distro, | |||
}, | |||
OverwriteExisting: true, | |||
IsLead: true, | |||
Creator: ctx.Doer, | |||
Data: buf, | |||
Properties: properties, | |||
}, | |||
) | |||
if err != nil { | |||
switch err { | |||
case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile, packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: | |||
return true, nil, err | |||
default: | |||
return false, nil, err | |||
} | |||
} | |||
return false, ver, nil | |||
} |
@@ -355,6 +355,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p | |||
switch packageType { | |||
case packages_model.TypeAlpine: | |||
typeSpecificSize = setting.Packages.LimitSizeAlpine | |||
case packages_model.TypeArch: | |||
typeSpecificSize = setting.Packages.LimitSizeArch | |||
case packages_model.TypeCargo: | |||
typeSpecificSize = setting.Packages.LimitSizeCargo | |||
case packages_model.TypeChef: |
@@ -0,0 +1,70 @@ | |||
{{if eq .PackageDescriptor.Package.Type "arch"}} | |||
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.installation"}}</h4> | |||
<div class="ui attached segment"> | |||
<div class="ui form"> | |||
<div class="field"> | |||
<label>{{svg "octicon-gear"}} {{ctx.Locale.Tr "packages.arch.pacmanconf" | Safe}}</label> | |||
<div class="markup"><pre class="code-block"><code>[{{.PackageDescriptor.Owner.LowerName}}.{{.RegistryHost}}] | |||
SigLevel = Optional TrustAll | |||
Server = <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/arch/$distribution/$arch"></gitea-origin-url></code></pre></div> | |||
</div> | |||
<div class="field"> | |||
<label>{{svg "octicon-sync"}} {{ctx.Locale.Tr "packages.arch.pacmansync"}}</label> | |||
<div class="markup"><pre class="code-block"><code>pacman -Sy {{.PackageDescriptor.Package.LowerName}}</code></pre></div> | |||
</div> | |||
<div class="field"> | |||
{{ctx.Locale.Tr "packages.arch.documentation" (printf `<a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/usage/packages/arch/">`) (printf `</a>`) | Safe}} | |||
</div> | |||
</div> | |||
</div> | |||
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.arch.properties"}}</h4> | |||
<div class="ui attached segment"> | |||
<table class="ui very basic compact table"> | |||
<tbody> | |||
<tr> | |||
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.description"}}</h5></td> | |||
<td>{{.PackageDescriptor.Metadata.Description}}</td> | |||
</tr> | |||
{{if .PackageDescriptor.Metadata.Provides}} | |||
<tr> | |||
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.provides"}}</h5></td> | |||
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Provides ", "}}</td> | |||
</tr> | |||
{{end}} | |||
{{if .PackageDescriptor.Metadata.Depends}} | |||
<tr> | |||
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.depends"}}</h5></td> | |||
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Depends ", "}}</td> | |||
</tr> | |||
{{end}} | |||
{{if .PackageDescriptor.Metadata.OptDepends}} | |||
<tr> | |||
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.optdepends"}}</h5></td> | |||
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.OptDepends ", "}}</td> | |||
</tr> | |||
{{end}} | |||
{{if .PackageDescriptor.Metadata.MakeDepends}} | |||
<tr> | |||
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.makedepends"}}</h5></td> | |||
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.MakeDepends ", "}}</td> | |||
</tr> | |||
{{end}} | |||
{{if .PackageDescriptor.Metadata.CheckDepends}} | |||
<tr> | |||
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.checkdepends"}}</h5></td> | |||
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.CheckDepends ", "}}</td> | |||
</tr> | |||
{{end}} | |||
</tbody> | |||
</table> | |||
</div> | |||
{{end}} |
@@ -0,0 +1,4 @@ | |||
{{if eq .PackageDescriptor.Package.Type "arch"}} | |||
{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</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">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} | |||
{{end}} |
@@ -19,6 +19,7 @@ | |||
<div class="issue-content"> | |||
<div class="issue-content-left"> | |||
{{template "package/content/alpine" .}} | |||
{{template "package/content/arch" .}} | |||
{{template "package/content/cargo" .}} | |||
{{template "package/content/chef" .}} | |||
{{template "package/content/composer" .}} | |||
@@ -50,6 +51,7 @@ | |||
<div class="item">{{svg "octicon-calendar" 16 "tw-mr-2"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}</div> | |||
<div class="item">{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}</div> | |||
{{template "package/metadata/alpine" .}} | |||
{{template "package/metadata/arch" .}} | |||
{{template "package/metadata/cargo" .}} | |||
{{template "package/metadata/chef" .}} | |||
{{template "package/metadata/composer" .}} |
@@ -0,0 +1,454 @@ | |||
// Copyright 2023 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package integration | |||
import ( | |||
"archive/tar" | |||
"bufio" | |||
"bytes" | |||
"compress/gzip" | |||
"crypto/md5" | |||
"encoding/hex" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"net/http" | |||
"os" | |||
"testing" | |||
"testing/fstest" | |||
"time" | |||
"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/packages/arch" | |||
"code.gitea.io/gitea/tests" | |||
"github.com/mholt/archiver/v3" | |||
"github.com/minio/sha256-simd" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestPackageArch(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
var ( | |||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) | |||
pushBatch = []*TestArchPackage{ | |||
BuildArchPackage(t, "git", "1-1", "x86_64"), | |||
BuildArchPackage(t, "git", "2-1", "x86_64"), | |||
BuildArchPackage(t, "git", "1-1", "i686"), | |||
BuildArchPackage(t, "adwaita", "1-1", "any"), | |||
BuildArchPackage(t, "adwaita", "2-1", "any"), | |||
} | |||
removeBatch = []*TestArchPackage{ | |||
BuildArchPackage(t, "curl", "1-1", "x86_64"), | |||
BuildArchPackage(t, "curl", "2-1", "x86_64"), | |||
BuildArchPackage(t, "dock", "1-1", "any"), | |||
BuildArchPackage(t, "dock", "2-1", "any"), | |||
} | |||
firstDatabaseBatch = []*TestArchPackage{ | |||
BuildArchPackage(t, "pacman", "1-1", "x86_64"), | |||
BuildArchPackage(t, "pacman", "1-1", "i686"), | |||
BuildArchPackage(t, "htop", "1-1", "x86_64"), | |||
BuildArchPackage(t, "htop", "1-1", "i686"), | |||
BuildArchPackage(t, "dash", "1-1", "any"), | |||
} | |||
secondDatabaseBatch = []*TestArchPackage{ | |||
BuildArchPackage(t, "pacman", "2-1", "x86_64"), | |||
BuildArchPackage(t, "htop", "2-1", "i686"), | |||
BuildArchPackage(t, "dash", "2-1", "any"), | |||
} | |||
PacmanDBx86 = BuildPacmanDb(t, | |||
secondDatabaseBatch[0].Pkg, | |||
firstDatabaseBatch[2].Pkg, | |||
secondDatabaseBatch[2].Pkg, | |||
) | |||
PacmanDBi686 = BuildPacmanDb(t, | |||
firstDatabaseBatch[0].Pkg, | |||
secondDatabaseBatch[1].Pkg, | |||
secondDatabaseBatch[2].Pkg, | |||
) | |||
signdata = []byte{1, 2, 3, 4} | |||
) | |||
t.Run("PushWithSignature", func(t *testing.T) { | |||
for _, p := range pushBatch { | |||
t.Run(p.File, func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/push/%s/archlinux/%s", | |||
user.Name, p.File, hex.EncodeToString(signdata), | |||
) | |||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
MakeRequest(t, req, http.StatusOK) | |||
pv, err := packages.GetVersionByNameAndVersion( | |||
db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, | |||
) | |||
assert.NoError(t, err) | |||
pf, err := packages.GetFileForVersionByName( | |||
db.DefaultContext, pv.ID, p.File, "archlinux", | |||
) | |||
assert.NoError(t, err) | |||
assert.NotNil(t, pf) | |||
pps, err := packages.GetPropertiesByName( | |||
db.DefaultContext, packages.PropertyTypeFile, | |||
pf.ID, arch.PropertySignature, | |||
) | |||
assert.NoError(t, err) | |||
assert.Len(t, pps, 1) | |||
}) | |||
} | |||
}) | |||
t.Run("PushWithoutSignature", func(t *testing.T) { | |||
for _, p := range pushBatch { | |||
t.Run(p.File, func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/push/%s/parabola", | |||
user.Name, p.File, | |||
) | |||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
MakeRequest(t, req, http.StatusOK) | |||
pv, err := packages.GetVersionByNameAndVersion( | |||
db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, | |||
) | |||
assert.NoError(t, err) | |||
pf, err := packages.GetFileForVersionByName( | |||
db.DefaultContext, pv.ID, p.File, "parabola", | |||
) | |||
assert.NoError(t, err) | |||
assert.NotNil(t, pf) | |||
}) | |||
} | |||
}) | |||
t.Run("GetPackage", func(t *testing.T) { | |||
for _, p := range pushBatch { | |||
t.Run(p.File, func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/push/%s/artix/%s", | |||
user.Name, p.File, hex.EncodeToString(signdata), | |||
) | |||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
MakeRequest(t, req, http.StatusOK) | |||
url = fmt.Sprintf( | |||
"/api/packages/%s/arch/artix/%s/%s", | |||
user.Name, p.Arch, p.File, | |||
) | |||
req = NewRequest(t, "GET", url) | |||
resp := MakeRequest(t, req, http.StatusOK) | |||
assert.Equal(t, p.Data, resp.Body.Bytes()) | |||
}) | |||
} | |||
}) | |||
t.Run("GetSignature", func(t *testing.T) { | |||
for _, p := range pushBatch { | |||
t.Run(p.File, func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/push/%s/arco/%s", | |||
user.Name, p.File, hex.EncodeToString(signdata), | |||
) | |||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
MakeRequest(t, req, http.StatusOK) | |||
url = fmt.Sprintf( | |||
"/api/packages/%s/arch/arco/%s/%s.sig", | |||
user.Name, p.Arch, p.File, | |||
) | |||
req = NewRequest(t, "GET", url) | |||
resp := MakeRequest(t, req, http.StatusOK) | |||
assert.Equal(t, signdata, resp.Body.Bytes()) | |||
}) | |||
} | |||
}) | |||
t.Run("Remove", func(t *testing.T) { | |||
for _, p := range removeBatch { | |||
t.Run(p.File, func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/push/%s/manjaro/%s", | |||
user.Name, p.File, hex.EncodeToString(signdata), | |||
) | |||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
MakeRequest(t, req, http.StatusOK) | |||
url = fmt.Sprintf( | |||
"/api/packages/%s/arch/remove/%s/%s", | |||
user.Name, p.Name, p.Ver, | |||
) | |||
req = NewRequest(t, "DELETE", url) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
MakeRequest(t, req, http.StatusOK) | |||
_, err := packages.GetVersionByNameAndVersion( | |||
db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, | |||
) | |||
assert.ErrorIs(t, err, packages.ErrPackageNotExist) | |||
}) | |||
} | |||
}) | |||
t.Run("PacmanDatabase", func(t *testing.T) { | |||
prepareDatabasePackages := func(t *testing.T) { | |||
for _, p := range firstDatabaseBatch { | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/push/%s/ion/%s", | |||
user.Name, p.File, hex.EncodeToString(signdata), | |||
) | |||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
MakeRequest(t, req, http.StatusOK) | |||
} | |||
// While creating pacman database, package versions are sorted by | |||
// UnixTime, second delay is required to ensure that newer package | |||
// version creation time differs from older packages. | |||
time.Sleep(time.Second) | |||
for _, p := range secondDatabaseBatch { | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/push/%s/ion/%s", | |||
user.Name, p.File, hex.EncodeToString(signdata), | |||
) | |||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
MakeRequest(t, req, http.StatusOK) | |||
} | |||
} | |||
t.Run("x86_64", func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
prepareDatabasePackages(t) | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/ion/x86_64/user.db.tar.gz", user.Name, | |||
) | |||
req := NewRequest(t, "GET", url) | |||
resp := MakeRequest(t, req, http.StatusOK) | |||
CompareTarGzEntries(t, PacmanDBx86, resp.Body.Bytes()) | |||
}) | |||
t.Run("i686", func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
prepareDatabasePackages(t) | |||
url := fmt.Sprintf( | |||
"/api/packages/%s/arch/ion/i686/user.db", user.Name, | |||
) | |||
req := NewRequest(t, "GET", url) | |||
resp := MakeRequest(t, req, http.StatusOK) | |||
CompareTarGzEntries(t, PacmanDBi686, resp.Body.Bytes()) | |||
}) | |||
}) | |||
} | |||
type TestArchPackage struct { | |||
Pkg arch.Package | |||
Data []byte | |||
File string | |||
Name string | |||
Ver string | |||
Arch string | |||
} | |||
func BuildArchPackage(t *testing.T, name, ver, architecture string) *TestArchPackage { | |||
fs := fstest.MapFS{ | |||
"pkginfo": &fstest.MapFile{ | |||
Data: []byte(fmt.Sprintf( | |||
"pkgname = %s\npkgbase = %s\npkgver = %s\narch = %s\n", | |||
name, name, ver, architecture, | |||
)), | |||
Mode: os.ModePerm, | |||
ModTime: time.Now(), | |||
}, | |||
"mtree": &fstest.MapFile{ | |||
Data: []byte("test"), | |||
Mode: os.ModePerm, | |||
ModTime: time.Now(), | |||
}, | |||
} | |||
pinf, err := fs.Stat("pkginfo") | |||
assert.NoError(t, err) | |||
pfile, err := fs.Open("pkginfo") | |||
assert.NoError(t, err) | |||
parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO") | |||
assert.NoError(t, err) | |||
minf, err := fs.Stat("mtree") | |||
assert.NoError(t, err) | |||
mfile, err := fs.Open("mtree") | |||
assert.NoError(t, err) | |||
marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE") | |||
assert.NoError(t, err) | |||
var buf bytes.Buffer | |||
archive := archiver.NewTarZstd() | |||
archive.Create(&buf) | |||
err = archive.Write(archiver.File{ | |||
FileInfo: archiver.FileInfo{ | |||
FileInfo: pinf, | |||
CustomName: parcname, | |||
}, | |||
ReadCloser: pfile, | |||
}) | |||
assert.NoError(t, errors.Join(pfile.Close(), err)) | |||
err = archive.Write(archiver.File{ | |||
FileInfo: archiver.FileInfo{ | |||
FileInfo: minf, | |||
CustomName: marcname, | |||
}, | |||
ReadCloser: mfile, | |||
}) | |||
assert.NoError(t, errors.Join(mfile.Close(), archive.Close(), err)) | |||
md5, sha256, size := archPkgParams(buf.Bytes()) | |||
return &TestArchPackage{ | |||
Data: buf.Bytes(), | |||
Name: name, | |||
Ver: ver, | |||
Arch: architecture, | |||
File: fmt.Sprintf("%s-%s-%s.pkg.tar.zst", name, ver, architecture), | |||
Pkg: arch.Package{ | |||
Name: name, | |||
Version: ver, | |||
VersionMetadata: arch.VersionMetadata{ | |||
Base: name, | |||
}, | |||
FileMetadata: arch.FileMetadata{ | |||
CompressedSize: size, | |||
MD5: hex.EncodeToString(md5), | |||
SHA256: hex.EncodeToString(sha256), | |||
Arch: architecture, | |||
}, | |||
}, | |||
} | |||
} | |||
func archPkgParams(b []byte) ([]byte, []byte, int64) { | |||
md5 := md5.New() | |||
sha256 := sha256.New() | |||
c := counter{bytes.NewReader(b), 0} | |||
br := bufio.NewReader(io.TeeReader(&c, io.MultiWriter(md5, sha256))) | |||
io.ReadAll(br) | |||
return md5.Sum(nil), sha256.Sum(nil), int64(c.n) | |||
} | |||
type counter struct { | |||
io.Reader | |||
n int | |||
} | |||
func (w *counter) Read(p []byte) (int, error) { | |||
n, err := w.Reader.Read(p) | |||
w.n += n | |||
return n, err | |||
} | |||
func BuildPacmanDb(t *testing.T, pkgs ...arch.Package) []byte { | |||
entries := map[string][]byte{} | |||
for _, p := range pkgs { | |||
entries[fmt.Sprintf("%s-%s/desc", p.Name, p.Version)] = []byte(p.Desc()) | |||
} | |||
b, err := arch.CreatePacmanDb(entries) | |||
if err != nil { | |||
assert.NoError(t, err) | |||
return nil | |||
} | |||
return b.Bytes() | |||
} | |||
func CompareTarGzEntries(t *testing.T, expected, actual []byte) { | |||
fgz, err := gzip.NewReader(bytes.NewReader(expected)) | |||
if err != nil { | |||
assert.NoError(t, err) | |||
return | |||
} | |||
ftar := tar.NewReader(fgz) | |||
validatemap := map[string]struct{}{} | |||
for { | |||
h, err := ftar.Next() | |||
if err != nil { | |||
break | |||
} | |||
validatemap[h.Name] = struct{}{} | |||
} | |||
sgz, err := gzip.NewReader(bytes.NewReader(actual)) | |||
if err != nil { | |||
assert.NoError(t, err) | |||
return | |||
} | |||
star := tar.NewReader(sgz) | |||
for { | |||
h, err := star.Next() | |||
if err != nil { | |||
break | |||
} | |||
_, ok := validatemap[h.Name] | |||
if !ok { | |||
assert.Fail(t, "Unexpected entry in archive: "+h.Name) | |||
} | |||
delete(validatemap, h.Name) | |||
} | |||
if len(validatemap) == 0 { | |||
return | |||
} | |||
for e := range validatemap { | |||
assert.Fail(t, "Entry not found in archive: "+e) | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#1793d1" d="M256 72c-14 35-23 57-39 91 10 11 22 23 41 36-21-8-35-17-45-26-21 43-53 103-117 220 50-30 90-48 127-55-2-7-3-14-3-22v-1c1-33 18-58 38-56 20 1 36 29 35 62l-2 17c36 7 75 26 125 54l-27-50c-13-10-27-23-55-38 19 5 33 11 44 17-86-159-93-180-122-250z"/></svg> |