Arch packages implementation (#4785)
Some checks are pending
/ release (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-remote-cacher (map[image:docker.io/valkey/valkey:7.2.5-alpine3.19 port:6379]) (push) Blocked by required conditions
testing / test-remote-cacher (map[image:ghcr.io/microsoft/garnet-alpine:1.0.14 port:6379]) (push) Blocked by required conditions
testing / test-remote-cacher (map[image:redis:7.2 port:6379]) (push) Blocked by required conditions
testing / test-remote-cacher (map[image:registry.redict.io/redict:7.3.0-scratch port:6379]) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions

This PR is from https://github.com/go-gitea/gitea/pull/31037

This PR was originally created by @d1nch8g , and the original source code comes from https://ion.lc/core/gitea.

This PR adds a package registry for [Arch Linux](https://archlinux.org/) packages with support for package files, [signatures](https://wiki.archlinux.org/title/Pacman/Package_signing), and automatic [pacman-database](https://archlinux.org/pacman/repo-add.8.html) management.

Features:

1. Push any ` tar.zst ` package and Gitea sign it.
2. Delete endpoint for specific package version and all related files
3. Supports trust levels with `SigLevel = Required`.
4. Package UI with instructions to connect to the new pacman database and visualised package metadata

![](/attachments/810ca6df-bd20-44c2-bdf7-95e94886d750)

You can follow [this tutorial](https://wiki.archlinux.org/title/Creating_packages) to build a *.pkg.tar.zst package for testing

docs pr: https://codeberg.org/forgejo/docs/pulls/791

Co-authored-by: d1nch8g@ion.lc
Co-authored-by: @KN4CK3R
Co-authored-by: @mahlzahn
Co-authored-by: @silverwind
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4785
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Exploding Dragon <explodingfkl@gmail.com>
Co-committed-by: Exploding Dragon <explodingfkl@gmail.com>
This commit is contained in:
Exploding Dragon 2024-08-04 06:16:29 +00:00 committed by Earl Warren
parent 22d3659803
commit f17194ca91
18 changed files with 1896 additions and 0 deletions

View file

@ -13,6 +13,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/packages/alpine" "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/cargo"
"code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/packages/composer" "code.gitea.io/gitea/modules/packages/composer"
@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
switch p.Type { switch p.Type {
case TypeAlpine: case TypeAlpine:
metadata = &alpine.VersionMetadata{} metadata = &alpine.VersionMetadata{}
case TypeArch:
metadata = &arch.VersionMetadata{}
case TypeCargo: case TypeCargo:
metadata = &cargo.Metadata{} metadata = &cargo.Metadata{}
case TypeChef: case TypeChef:

View file

@ -33,6 +33,7 @@ type Type string
// List of supported packages // List of supported packages
const ( const (
TypeAlpine Type = "alpine" TypeAlpine Type = "alpine"
TypeArch Type = "arch"
TypeCargo Type = "cargo" TypeCargo Type = "cargo"
TypeChef Type = "chef" TypeChef Type = "chef"
TypeComposer Type = "composer" TypeComposer Type = "composer"
@ -57,6 +58,7 @@ const (
var TypeList = []Type{ var TypeList = []Type{
TypeAlpine, TypeAlpine,
TypeArch,
TypeCargo, TypeCargo,
TypeChef, TypeChef,
TypeComposer, TypeComposer,
@ -84,6 +86,8 @@ func (pt Type) Name() string {
switch pt { switch pt {
case TypeAlpine: case TypeAlpine:
return "Alpine" return "Alpine"
case TypeArch:
return "Arch"
case TypeCargo: case TypeCargo:
return "Cargo" return "Cargo"
case TypeChef: case TypeChef:
@ -133,6 +137,8 @@ func (pt Type) SVGName() string {
switch pt { switch pt {
case TypeAlpine: case TypeAlpine:
return "gitea-alpine" return "gitea-alpine"
case TypeArch:
return "gitea-arch"
case TypeCargo: case TypeCargo:
return "gitea-cargo" return "gitea-cargo"
case TypeChef: case TypeChef:

View file

@ -0,0 +1,316 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"bufio"
"bytes"
"encoding/hex"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"github.com/mholt/archiver/v3"
)
// Arch Linux Packages
// https://man.archlinux.org/man/PKGBUILD.5
const (
PropertyDescription = "arch.description"
PropertyArch = "arch.architecture"
PropertyDistribution = "arch.distribution"
SettingKeyPrivate = "arch.key.private"
SettingKeyPublic = "arch.key.public"
RepositoryPackage = "_arch"
RepositoryVersion = "_repository"
)
var (
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"` // Includes version, release and epoch
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"`
Conflicts []string `json:"conflicts,omitempty"`
Replaces []string `json:"replaces,omitempty"`
Backup []string `json:"backup,omitempty"`
Xdata []string `json:"xdata,omitempty"`
}
// FileMetadata Metadata related to specific package 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"`
PgpSigned string `json:"pgp"`
}
// ParsePackage Function that receives arch package archive data and returns it's metadata.
func ParsePackage(r *packages.HashedBuffer) (*Package, error) {
md5, _, sha256, _ := r.Sums()
_, err := r.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
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 = r.Size()
pkg.FileMetadata.MD5 = hex.EncodeToString(md5)
pkg.FileMetadata.SHA256 = hex.EncodeToString(sha256)
return pkg, nil
}
// ParsePackageInfo 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
}
key, value, find := strings.Cut(line, "=")
if !find {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
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 "conflict":
p.VersionMetadata.Conflicts = append(p.VersionMetadata.Conflicts, value)
case "replaces":
p.VersionMetadata.Replaces = append(p.VersionMetadata.Replaces, value)
case "xdata":
p.VersionMetadata.Xdata = append(p.VersionMetadata.Xdata, 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
default:
return nil, util.NewInvalidArgumentErrorf("property is not supported %s", key)
}
}
return p, errors.Join(scanner.Err(), ValidatePackageSpec(p))
}
// ValidatePackageSpec 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 _, p := range p.VersionMetadata.Conflicts {
if !rePkgVer.MatchString(p) {
return util.NewInvalidArgumentErrorf("invalid conflicts: " + p)
}
}
for _, p := range p.VersionMetadata.Replaces {
if !rePkgVer.MatchString(p) {
return util.NewInvalidArgumentErrorf("invalid replaces: " + p)
}
}
for _, p := range p.VersionMetadata.Replaces {
if !rePkgVer.MatchString(p) {
return util.NewInvalidArgumentErrorf("invalid xdata: " + 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
}
// Desc Create pacman package description file.
func (p *Package) Desc() string {
entries := []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,
"PGPSIG", p.FileMetadata.PgpSigned,
"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,
"REPLACES", strings.Join(p.VersionMetadata.Replaces, "\n"),
"CONFLICTS", strings.Join(p.VersionMetadata.Conflicts, "\n"),
"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 < len(entries); i += 2 {
if entries[i+1] != "" {
_, _ = fmt.Fprintf(&buf, "%%%s%%\n%s\n\n", entries[i], entries[i+1])
}
}
return buf.String()
}

View file

@ -0,0 +1,445 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"bytes"
"errors"
"os"
"strings"
"testing"
"testing/fstest"
"time"
"code.gitea.io/gitea/modules/packages"
"github.com/mholt/archiver/v3"
"github.com/stretchr/testify/require"
)
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")
require.NoError(t, err)
pfile, err := fs.Open("pkginfo")
require.NoError(t, err)
parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO")
require.NoError(t, err)
// Test .MTREE file
minf, err := fs.Stat("mtree")
require.NoError(t, err)
mfile, err := fs.Open("mtree")
require.NoError(t, err)
marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE")
require.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,
})
require.NoError(t, errors.Join(pfile.Close(), err))
err = archive.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: minf,
CustomName: marcname,
},
ReadCloser: mfile,
})
require.NoError(t, errors.Join(mfile.Close(), archive.Close(), err))
reader, err := packages.CreateHashedBufferFromReader(&buf)
if err != nil {
t.Fatal(err)
}
defer reader.Close()
_, err = ParsePackage(reader)
require.NoError(t, err)
})
t.Run("missing .PKGINFO", func(t *testing.T) {
var buf bytes.Buffer
archive := archiver.NewTarZstd()
archive.Create(&buf)
require.NoError(t, archive.Close())
reader, err := packages.CreateHashedBufferFromReader(&buf)
require.NoError(t, err)
defer reader.Close()
_, err = ParsePackage(reader)
require.Error(t, err)
require.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")
require.NoError(t, err)
archive := archiver.NewTarZstd()
archive.Create(&buf)
err = archive.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: pinf,
CustomName: parcname,
},
ReadCloser: pfile,
})
require.NoError(t, errors.Join(pfile.Close(), archive.Close(), err))
reader, err := packages.CreateHashedBufferFromReader(&buf)
require.NoError(t, err)
defer reader.Close()
_, err = ParsePackage(reader)
require.Error(t, err)
require.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))
require.NoError(t, err)
require.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,
SHA256: "def",
BuildDate: 3,
Packager: "smon",
Arch: "x86_64",
},
}
}
t.Run("valid package", func(t *testing.T) {
p := newpkg()
err := ValidatePackageSpec(&p)
require.NoError(t, err)
})
t.Run("invalid package name", func(t *testing.T) {
p := newpkg()
p.Name = "!$%@^!*&()"
err := ValidatePackageSpec(&p)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid package name")
})
t.Run("invalid package base", func(t *testing.T) {
p := newpkg()
p.VersionMetadata.Base = "!$%@^!*&()"
err := ValidatePackageSpec(&p)
require.Error(t, err)
require.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)
require.Error(t, err)
require.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)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid package version")
})
t.Run("missing architecture", func(t *testing.T) {
p := newpkg()
p.FileMetadata.Arch = ""
err := ValidatePackageSpec(&p)
require.Error(t, err)
require.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)
require.Error(t, err)
require.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)
require.Error(t, err)
require.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)
require.Error(t, err)
require.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)
require.Error(t, err)
require.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)
require.Error(t, err)
require.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)
require.Error(t, err)
require.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)
require.Error(t, err)
require.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",
},
}
require.Equal(t, pkgdesc, md.Desc())
}

View file

@ -24,6 +24,7 @@ var (
LimitTotalOwnerCount int64 LimitTotalOwnerCount int64
LimitTotalOwnerSize int64 LimitTotalOwnerSize int64
LimitSizeAlpine int64 LimitSizeAlpine int64
LimitSizeArch int64
LimitSizeCargo int64 LimitSizeCargo int64
LimitSizeChef int64 LimitSizeChef int64
LimitSizeComposer int64 LimitSizeComposer int64
@ -83,6 +84,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE") Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE")
Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH")
Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO")
Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF")
Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")

View file

@ -3611,6 +3611,22 @@ alpine.repository = Repository Info
alpine.repository.branches = Branches alpine.repository.branches = Branches
alpine.repository.repositories = Repositories alpine.repository.repositories = Repositories
alpine.repository.architectures = Architectures alpine.repository.architectures = Architectures
arch.pacman.helper.gpg = Add trust certificate for pacman:
arch.pacman.repo.multi = %s has the same version in different distributions.
arch.pacman.repo.multi.item = Configuration for %s
arch.pacman.conf = Add server with related distribution and architecture to <code>/etc/pacman.conf</code> :
arch.pacman.sync = Sync package with pacman:
arch.version.properties = Version Properties
arch.version.description = Description
arch.version.provides = Provides
arch.version.groups = Group
arch.version.depends = Depends
arch.version.optdepends = Optional depends
arch.version.makedepends = Make depends
arch.version.checkdepends = Check depends
arch.version.conflicts = Conflicts
arch.version.replaces = Replaces
arch.version.backup = Backup
cargo.registry = Setup this registry in the Cargo configuration file (for example <code>~/.cargo/config.toml</code>): 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: 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: chef.registry = Setup this registry in your <code>~/.chef/config.rb</code> file:

1
public/assets/img/svg/gitea-arch.svg generated Normal file
View 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>

After

Width:  |  Height:  |  Size: 402 B

View file

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/packages/alpine" "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/cargo"
"code.gitea.io/gitea/routers/api/packages/chef" "code.gitea.io/gitea/routers/api/packages/chef"
"code.gitea.io/gitea/routers/api/packages/composer" "code.gitea.io/gitea/routers/api/packages/composer"
@ -137,6 +138,17 @@ func CommonRoutes() *web.Route {
}) })
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/arch", func() {
r.Group("/repository.key", func() {
r.Head("", arch.GetRepositoryKey)
r.Get("", arch.GetRepositoryKey)
})
r.Group("/{distro}", func() {
r.Put("", reqPackageAccess(perm.AccessModeWrite), arch.PushPackage)
r.Get("/{arch}/{file}", arch.GetPackageOrDB)
r.Delete("/{package}/{version}", reqPackageAccess(perm.AccessModeWrite), arch.RemovePackage)
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/cargo", func() { r.Group("/cargo", func() {
r.Group("/api/v1/crates", func() { r.Group("/api/v1/crates", func() {
r.Get("", cargo.SearchPackages) r.Get("", cargo.SearchPackages)

View file

@ -0,0 +1,248 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
packages_module "code.gitea.io/gitea/modules/packages"
arch_module "code.gitea.io/gitea/modules/packages/arch"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
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)
})
}
func GetRepositoryKey(ctx *context.Context) {
_, pub, err := arch_service.GetOrCreateKeyPair(ctx, ctx.Package.Owner.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
ContentType: "application/pgp-keys",
Filename: "repository.key",
})
}
func PushPackage(ctx *context.Context) {
distro := ctx.Params("distro")
upload, needToClose, err := ctx.UploadStream()
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if needToClose {
defer upload.Close()
}
buf, err := packages_module.CreateHashedBufferFromReader(upload)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
p, err := arch_module.ParsePackage(buf)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
_, err = buf.Seek(0, io.SeekStart)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
sign, err := arch_service.NewFileSign(ctx, ctx.Package.Owner.ID, buf)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer sign.Close()
_, err = buf.Seek(0, io.SeekStart)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
// update gpg sign
pgp, err := io.ReadAll(sign)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
p.FileMetadata.PgpSigned = base64.StdEncoding.EncodeToString(pgp)
_, err = sign.Seek(0, io.SeekStart)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
properties := map[string]string{
arch_module.PropertyDescription: p.Desc(),
arch_module.PropertyArch: p.FileMetadata.Arch,
arch_module.PropertyDistribution: distro,
}
version, _, 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: fmt.Sprintf("%s-%s-%s.pkg.tar.zst", p.Name, p.Version, p.FileMetadata.Arch),
CompositeKey: distro,
},
OverwriteExisting: false,
IsLead: true,
Creator: ctx.ContextUser,
Data: buf,
Properties: properties,
},
)
if err != nil {
switch {
case errors.Is(err, packages_model.ErrDuplicatePackageVersion), errors.Is(err, packages_model.ErrDuplicatePackageFile):
apiError(ctx, http.StatusConflict, err)
case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
// add sign file
_, err = packages_service.AddFileToPackageVersionInternal(ctx, version, &packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
CompositeKey: distro,
Filename: fmt.Sprintf("%s-%s-%s.pkg.tar.zst.sig", p.Name, p.Version, p.FileMetadata.Arch),
},
OverwriteExisting: true,
IsLead: false,
Creator: ctx.Doer,
Data: sign,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
}
if err = arch_service.BuildPacmanDB(ctx, ctx.Package.Owner.ID, distro, p.FileMetadata.Arch); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusCreated)
}
func GetPackageOrDB(ctx *context.Context) {
var (
file = ctx.Params("file")
distro = ctx.Params("distro")
arch = ctx.Params("arch")
)
if strings.HasSuffix(file, ".pkg.tar.zst") || strings.HasSuffix(file, ".pkg.tar.zst.sig") {
pkg, err := arch_service.GetPackageFile(ctx, distro, file, ctx.Package.Owner.ID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
ctx.ServeContent(pkg, &context.ServeHeaderOptions{
Filename: file,
})
return
}
if strings.HasSuffix(file, ".db.tar.gz") ||
strings.HasSuffix(file, ".db") ||
strings.HasSuffix(file, ".db.tar.gz.sig") ||
strings.HasSuffix(file, ".db.sig") {
pkg, err := arch_service.GetPackageDBFile(ctx, distro, arch, ctx.Package.Owner.ID,
strings.HasSuffix(file, ".sig"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
ctx.ServeContent(pkg, &context.ServeHeaderOptions{
Filename: file,
})
return
}
ctx.Status(http.StatusNotFound)
}
func RemovePackage(ctx *context.Context) {
var (
distro = ctx.Params("distro")
pkg = ctx.Params("package")
ver = ctx.Params("version")
)
pv, err := packages_model.GetVersionByNameAndVersion(
ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg, ver,
)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
files, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
deleted := false
for _, file := range files {
if file.CompositeKey == distro {
deleted = true
err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx, ctx.ContextUser, file)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
}
}
if deleted {
err = arch_service.BuildCustomRepositoryFiles(ctx, ctx.Package.Owner.ID, distro)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
}
ctx.Status(http.StatusNoContent)
} else {
ctx.Error(http.StatusNotFound)
}
}

View file

@ -4,6 +4,7 @@
package user package user
import ( import (
"fmt"
"net/http" "net/http"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -18,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
alpine_module "code.gitea.io/gitea/modules/packages/alpine" alpine_module "code.gitea.io/gitea/modules/packages/alpine"
arch_model "code.gitea.io/gitea/modules/packages/arch"
debian_module "code.gitea.io/gitea/modules/packages/debian" debian_module "code.gitea.io/gitea/modules/packages/debian"
rpm_module "code.gitea.io/gitea/modules/packages/rpm" rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -200,6 +202,19 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.Data["Branches"] = util.Sorted(branches.Values()) ctx.Data["Branches"] = util.Sorted(branches.Values())
ctx.Data["Repositories"] = util.Sorted(repositories.Values()) ctx.Data["Repositories"] = util.Sorted(repositories.Values())
ctx.Data["Architectures"] = util.Sorted(architectures.Values()) ctx.Data["Architectures"] = util.Sorted(architectures.Values())
case packages_model.TypeArch:
ctx.Data["RegistryHost"] = setting.Packages.RegistryHost
ctx.Data["SignMail"] = fmt.Sprintf("%s@noreply.%s", ctx.Package.Owner.Name, setting.Packages.RegistryHost)
groups := make(container.Set[string])
for _, f := range pd.Files {
for _, pp := range f.Properties {
switch pp.Name {
case arch_model.PropertyDistribution:
groups.Add(pp.Value)
}
}
}
ctx.Data["Groups"] = util.Sorted(groups.Values())
case packages_model.TypeDebian: case packages_model.TypeDebian:
distributions := make(container.Set[string]) distributions := make(container.Set[string])
components := make(container.Set[string]) components := make(container.Set[string])

View file

@ -0,0 +1,348 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"archive/tar"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"os"
"sort"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
user_model "code.gitea.io/gitea/models/user"
packages_module "code.gitea.io/gitea/modules/packages"
arch_module "code.gitea.io/gitea/modules/packages/arch"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) {
return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeArch, arch_module.RepositoryPackage, arch_module.RepositoryVersion)
}
func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
if err != nil {
return err
}
// remove old db files
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
if err != nil {
return err
}
for _, pf := range pfs {
if strings.HasSuffix(pf.Name, ".db") {
arch := strings.TrimSuffix(strings.TrimPrefix(pf.Name, fmt.Sprintf("%s-", pf.CompositeKey)), ".db")
if err := BuildPacmanDB(ctx, ownerID, pf.CompositeKey, arch); err != nil {
return err
}
}
}
return nil
}
func BuildCustomRepositoryFiles(ctx context.Context, ownerID int64, disco string) error {
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
if err != nil {
return err
}
// remove old db files
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
if err != nil {
return err
}
for _, pf := range pfs {
if strings.HasSuffix(pf.Name, ".db") && pf.CompositeKey == disco {
arch := strings.TrimSuffix(strings.TrimPrefix(pf.Name, fmt.Sprintf("%s-", pf.CompositeKey)), ".db")
if err := BuildPacmanDB(ctx, ownerID, pf.CompositeKey, arch); err != nil {
return err
}
}
}
return nil
}
func NewFileSign(ctx context.Context, ownerID int64, input io.Reader) (*packages_module.HashedBuffer, error) {
// If no signature is specified, it will be generated by Gitea.
priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
if err != nil {
return nil, err
}
block, err := armor.Decode(strings.NewReader(priv))
if err != nil {
return nil, err
}
e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
if err != nil {
return nil, err
}
pkgSig, err := packages_module.NewHashedBuffer()
if err != nil {
return nil, err
}
defer pkgSig.Close()
if err := openpgp.DetachSign(pkgSig, e, input, nil); err != nil {
return nil, err
}
return pkgSig, nil
}
// BuildPacmanDB Create db signature cache
func BuildPacmanDB(ctx context.Context, ownerID int64, distro, arch string) error {
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
if err != nil {
return err
}
// remove old db files
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
if err != nil {
return err
}
for _, pf := range pfs {
if pf.CompositeKey == distro && strings.HasPrefix(pf.Name, fmt.Sprintf("%s-%s", distro, arch)) {
// remove distro and arch
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
return err
}
}
}
db, err := flushDB(ctx, ownerID, distro, arch)
if errors.Is(err, io.EOF) {
return nil
} else if err != nil {
return err
}
defer db.Close()
// Create db signature cache
_, err = db.Seek(0, io.SeekStart)
if err != nil {
return err
}
sig, err := NewFileSign(ctx, ownerID, db)
if err != nil {
return err
}
defer sig.Close()
_, err = db.Seek(0, io.SeekStart)
if err != nil {
return err
}
for name, data := range map[string]*packages_module.HashedBuffer{
fmt.Sprintf("%s-%s.db", distro, arch): db,
fmt.Sprintf("%s-%s.db.sig", distro, arch): sig,
} {
_, err = packages_service.AddFileToPackageVersionInternal(ctx, pv, &packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: name,
CompositeKey: distro,
},
Creator: user_model.NewGhostUser(),
Data: data,
IsLead: false,
OverwriteExisting: true,
})
if err != nil {
return err
}
}
return nil
}
func flushDB(ctx context.Context, ownerID int64, distro, arch string) (*packages_module.HashedBuffer, error) {
pkgs, err := packages_model.GetPackagesByType(ctx, ownerID, packages_model.TypeArch)
if err != nil {
return nil, err
}
if len(pkgs) == 0 {
return nil, io.EOF
}
db, err := packages_module.NewHashedBuffer()
if err != nil {
return nil, err
}
gw := gzip.NewWriter(db)
tw := tar.NewWriter(gw)
count := 0
for _, pkg := range pkgs {
versions, err := packages_model.GetVersionsByPackageName(
ctx, ownerID, packages_model.TypeArch, pkg.Name,
)
if err != nil {
return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), 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 {
// add any arch package
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, errors.Join(tw.Close(), gw.Close(), db.Close(), err)
}
if len(pps) >= 1 {
meta := []byte(pps[0].Value)
header := &tar.Header{
Name: pkg.Name + "-" + ver.Version + "/desc",
Size: int64(len(meta)),
Mode: int64(os.ModePerm),
}
if err = tw.WriteHeader(header); err != nil {
return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err)
}
if _, err := tw.Write(meta); err != nil {
return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err)
}
count++
break
}
}
}
defer gw.Close()
defer tw.Close()
if count == 0 {
return nil, errors.Join(db.Close(), io.EOF)
}
return db, nil
}
// GetPackageFile Get data related to provided filename and distribution, for package files
// update download counter.
func GetPackageFile(ctx context.Context, distro, file string, ownerID int64) (io.ReadSeekCloser, error) {
pf, err := getPackageFile(ctx, distro, file, ownerID)
if err != nil {
return nil, err
}
filestream, _, _, err := packages_service.GetPackageFileStream(ctx, pf)
return filestream, err
}
// Ejects parameters required to get package file property from file name.
func getPackageFile(ctx context.Context, distro, file string, ownerID int64) (*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, ownerID, 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
}
func GetPackageDBFile(ctx context.Context, distro, arch string, ownerID int64, signFile bool) (io.ReadSeekCloser, error) {
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
if err != nil {
return nil, err
}
fileName := fmt.Sprintf("%s-%s.db", distro, arch)
if signFile {
fileName = fmt.Sprintf("%s-%s.db.sig", distro, arch)
}
file, err := packages_model.GetFileForVersionByName(ctx, pv.ID, fileName, distro)
if err != nil {
return nil, err
}
filestream, _, _, err := packages_service.GetPackageFileStream(ctx, file)
return filestream, err
}
// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
priv, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPrivate)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
pub, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPublic)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
if priv == "" || pub == "" {
user, err := user_model.GetUserByID(ctx, ownerID)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
priv, pub, err = generateKeypair(user.Name)
if err != nil {
return "", "", err
}
if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPrivate, priv); err != nil {
return "", "", err
}
if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPublic, pub); err != nil {
return "", "", err
}
}
return priv, pub, nil
}
func generateKeypair(owner string) (string, string, error) {
e, err := openpgp.NewEntity(
owner,
"Arch Package signature only",
fmt.Sprintf("%s@noreply.%s", owner, setting.Packages.RegistryHost), &packet.Config{
RSABits: 4096,
})
if err != nil {
return "", "", err
}
var priv strings.Builder
var pub strings.Builder
w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil)
if err != nil {
return "", "", err
}
if err := e.SerializePrivate(w, nil); err != nil {
return "", "", err
}
w.Close()
w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil)
if err != nil {
return "", "", err
}
if err := e.Serialize(w); err != nil {
return "", "", err
}
w.Close()
return priv.String(), pub.String(), nil
}

View file

@ -16,6 +16,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
alpine_service "code.gitea.io/gitea/services/packages/alpine" alpine_service "code.gitea.io/gitea/services/packages/alpine"
arch_service "code.gitea.io/gitea/services/packages/arch"
cargo_service "code.gitea.io/gitea/services/packages/cargo" cargo_service "code.gitea.io/gitea/services/packages/cargo"
container_service "code.gitea.io/gitea/services/packages/container" container_service "code.gitea.io/gitea/services/packages/container"
debian_service "code.gitea.io/gitea/services/packages/debian" debian_service "code.gitea.io/gitea/services/packages/debian"
@ -132,6 +133,10 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err) return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
} }
} else if pcr.Type == packages_model.TypeArch {
if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
}
} }
} }
return nil return nil

View file

@ -359,6 +359,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
switch packageType { switch packageType {
case packages_model.TypeAlpine: case packages_model.TypeAlpine:
typeSpecificSize = setting.Packages.LimitSizeAlpine typeSpecificSize = setting.Packages.LimitSizeAlpine
case packages_model.TypeArch:
typeSpecificSize = setting.Packages.LimitSizeArch
case packages_model.TypeCargo: case packages_model.TypeCargo:
typeSpecificSize = setting.Packages.LimitSizeCargo typeSpecificSize = setting.Packages.LimitSizeCargo
case packages_model.TypeChef: case packages_model.TypeChef:

View file

@ -0,0 +1,143 @@
{{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-terminal"}} {{ctx.Locale.Tr "packages.arch.pacman.helper.gpg"}}</label>
<div class="markup">
<pre class="code-block"><code>wget -O sign.gpg <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/arch/repository.key"></origin-url>
pacman-key --add sign.gpg
pacman-key --lsign-key '{{$.SignMail}}'</code></pre>
</div>
</div>
<div class="field">
<label>{{svg "octicon-gear"}} {{ctx.Locale.Tr "packages.arch.pacman.conf"}}</label>
<div class="markup">
<pre
class="code-block"><code>
{{- if gt (len $.Groups) 1 -}}
# {{ctx.Locale.Tr "packages.arch.pacman.repo.multi" $.PackageDescriptor.Package.LowerName}}
{{end -}}
{{- $GroupSize := (len .Groups) -}}
{{- range $i,$v := .Groups -}}
{{- if gt $i 0}}
{{end -}}{{- if gt $GroupSize 1 -}}
# {{ctx.Locale.Tr "packages.arch.pacman.repo.multi.item" .}}
{{end -}}
[{{$.PackageDescriptor.Owner.LowerName}}.{{$.RegistryHost}}]
SigLevel = Required
Server = <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/arch/{{.}}/$arch"></origin-url>
{{end -}}
</code></pre>
</div>
</div>
<div class="field">
<label>{{svg "octicon-sync"}} {{ctx.Locale.Tr "packages.arch.pacman.sync"}}</label>
<div class="markup">
<pre class="code-block"><code>pacman -Sy {{.PackageDescriptor.Package.LowerName}}</code></pre>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Arch"
"https://forgejo.org/docs/latest/user/packages/arch/"}}</label>
</div>
</div>
</div>
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.arch.version.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.version.description"}}</h5>
</td>
<td>{{.PackageDescriptor.Metadata.Description}}</td>
</tr>
{{if .PackageDescriptor.Metadata.Groups}}
<tr>
<td class="collapsing">
<h5>{{ctx.Locale.Tr "packages.arch.version.groups"}}</h5>
</td>
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Groups ", "}}</td>
</tr>
{{end}}
{{if .PackageDescriptor.Metadata.Provides}}
<tr>
<td class="collapsing">
<h5>{{ctx.Locale.Tr "packages.arch.version.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.version.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.version.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.version.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.version.checkdepends"}}</h5>
</td>
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.CheckDepends ", "}}</td>
</tr>
{{end}}
{{if .PackageDescriptor.Metadata.Conflicts}}
<tr>
<td class="collapsing">
<h5>{{ctx.Locale.Tr "packages.arch.version.conflicts"}}</h5>
</td>
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Conflicts ", "}}</td>
</tr>
{{end}}
{{if .PackageDescriptor.Metadata.Replaces}}
<tr>
<td class="collapsing">
<h5>{{ctx.Locale.Tr "packages.arch.version.replaces"}}</h5>
</td>
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Replaces ", "}}</td>
</tr>
{{end}}
{{if .PackageDescriptor.Metadata.Backup}}
<tr>
<td class="collapsing">
<h5>{{ctx.Locale.Tr "packages.arch.version.backup"}}</h5>
</td>
<td>{{StringUtils.Join $.PackageDescriptor.Metadata.Backup ", "}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}

View file

@ -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}}

View file

@ -19,6 +19,7 @@
<div class="issue-content"> <div class="issue-content">
<div class="issue-content-left"> <div class="issue-content-left">
{{template "package/content/alpine" .}} {{template "package/content/alpine" .}}
{{template "package/content/arch" .}}
{{template "package/content/cargo" .}} {{template "package/content/cargo" .}}
{{template "package/content/chef" .}} {{template "package/content/chef" .}}
{{template "package/content/composer" .}} {{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-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> <div class="item">{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
{{template "package/metadata/alpine" .}} {{template "package/metadata/alpine" .}}
{{template "package/metadata/arch" .}}
{{template "package/metadata/cargo" .}} {{template "package/metadata/cargo" .}}
{{template "package/metadata/chef" .}} {{template "package/metadata/chef" .}}
{{template "package/metadata/composer" .}} {{template "package/metadata/composer" .}}

View file

@ -0,0 +1,327 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strings"
"testing"
"testing/fstest"
"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"
arch_model "code.gitea.io/gitea/modules/packages/arch"
"code.gitea.io/gitea/tests"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/stretchr/testify/require"
)
func TestPackageArch(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
unPack := func(s string) []byte {
data, _ := base64.StdEncoding.DecodeString(strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(s), "\n", ""), "\r", ""))
return data
}
rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name)
pkgs := map[string][]byte{
"any": unPack(`
KLUv/QBYXRMABmOHSbCWag6dY6d8VNtVR3rpBnWdBbkDAxM38Dj3XG3FK01TCKlWtMV9QpskYdsm
e6fh5gWqM8edeurYNESoIUz/RmtyQy68HVrBj1p+AIoAYABFSJh4jcDyWNQgHIKIuNgIll64S4oY
FFIUk6vJQBMIIl2iYtIysqKWVYMCYvXDpAKTMzVGwZTUWhbciFCglIMH1QMbEtjHpohSi8XRYwPr
AwACSy/fzxO1FobizlP7sFgHcpx90Pus94Edjcc9GOustbD3PBprLUxH50IGC1sfw31c7LOfT4Qe
nh0KP1uKywwdPrRYmuyIkWBHRlcLfeBIDpKKqw44N0K2nNAfFW5grHRfSShyVgaEIZwIVVmFGL7O
88XDE5whJm4NkwA91dRoPBCcrgqozKSyah1QygsWkCshAaYrvbHCFdUTJCOgBpeUTMuJJ6+SRtcj
wIRua8mGJyg7qWoqJQq9z/4+DU1rHrEO8f6QZ3HUu3IM7GY37u+jeWjUu45637yN+qj338cdi0Uc
y0a9a+e5//1cYnPUu37dxr15khzNQ9/PE80aC/1okjz9mGo3bqP5Ue+scflGshdzx2g28061k2PW
uKwzjmV/XzTzzmKdcfz3eRbJoRPddcaP/n4PSZqQeYa1PDtPQzOHJK0amfjvz0IUV/v38xHJK/rz
JtFpalPD30drDWi7Bl8NB3J/P3csijQyldWZ8gy3TNslLsozMw74DhoAXoAfnE8xydUUHPZ3hML4
2zVDGiEXSGYRx4BKQDcDJA5S9Ca25FRgPtSWSowZJpJTYAR9WCPHUDgACm6+hBecGDPNClpwHZ2A
EQ==
`),
"x86_64": unPack(`
KLUv/QBYnRMAFmOJS7BUbg7Un8q21hxCopsOMn6UGTzJRbHI753uOeMdxZ+V7ajoETVxl9CSBCR5
2a3K1vr1gwyp9gCTH422bRNxHEg7Z0z9HV4rH/DGFn8AjABjAFQ2oaUVMRRGViVoqmxAVKuoKQVM
NJRwTDl9NcHCClliWjTpWin6sRUZsXSipWlAipQnleThRgFF5QTAzpth0UPFkhQeJRnYOaqSScEC
djCPDwE8pQTfVXW9F7bmznX3YTNZDeP7IHgxDazNQhp+UDa798KeRgvvvbCamgsYdL461TfvcmlY
djFowWYH5yaH5ztZcemh4omAkm7iQIWvGypNIXJQNgc7DVuHjx06I4MZGTIkeEBIOIL0OxcvnGps
0TwxycqKYESrwwQYEDKI2F0hNXH1/PCQ2BS4Ykki48EAaflAbRHxYrRQbdAZ4oXVAMGCkYOXkBRb
NkwjNCoIF07ByTlyfJhmoHQtCbFYDN+941783KqzusznmPePXJPluS1+cL/74Rd/1UHluW15blFv
ol6e+8XPPZNDPN/Kc9vOdX/xNZrT8twWnH34U9Xkqw76rqqrPjPQl6nJde9i74e/8Mtz6zOjT3R7
Uve8BrabpT4zanE83158MtVbkxbH84vPNWkGqeu2OF704vfRzAGl6mhRtXPdmOrRzFla+BO+DL34
uHHN9r74usjkduX5VEhNz9TnxV9trSabvYAwuIZffN0zSeZM3c3GUHX8dG6jeUgHGgBbgB9cUDHJ
1RR09teBwvjbNUMaIRdIZhHHgEpANwMkDpL0JsbkVFA+0JZKjBkmklNgBH1YI8dQOAAKbr6EF5wY
M80KWnAdnYAR
`),
"aarch64": unPack(`
KLUv/QBYdRQAVuSMS7BUbg7Un8q21hxCopsOMn6UGTzJRbHI753uOeMdxZ+V7ajoEbUkUXbXhXW/
7FanWzv7B/EcMxhodFqyZkUcB9LOGVN/h9MqG7zFFmoAaQB8AEFrvpXntn3V/cXXaE7Lc9uP5uFP
VXPl+ue7qnJ9Zp8vU3PVvYu9HvbAL8+tz4y+0O1J3TPXqbZ5l3+lapk5ee+L577qXvdf+Atn+P69
4Qz8QhpYw4/xd78Q3/v6Wg28974u1Ojc2ODseAGpHs2crYG4kef84uNGnu198fWQuVq+8ymQmp5p
z4vPbRjOaBC+FxziF1/3TJI5U3ezMlQdPZ3baA7SMhnMunvHvfg5rrO6zOeY94+rJstzW/zgetfD
Lz7XP+W5bXluUW+hXp77xc89kwFRTF1PrKxAFpgXT7ZWhjzYjpRIStGyNCAGBYM6AnGrkKKCAmAH
k3HBI8VyBBYdGdApmoqJYQE62EeIADCkBF1VOW0WYnz/+y6ufTMaDQ2GDDme7Wapz4xa3JpvLz6Z
6q1Ji1vzi79q0vxR+ba4dejF76OZ80nV0aJqX3VjKCsuP1g0EWDSURyw0JVDZWlEzsnmYLdh8wDS
I2dkIEMjxsSOiAlJjH4HIwbTjayZJidXVxKQYH2gICOCBhK7KqMlLZ4gMCU1BapYlsTAXnywepyy
jMBmtEhxyCnCZdUAwYKxAxeRFVk4TCL0aYgWjt3kHTg9SjVStppI2YCSWshUEFGdmJmyCVGpnqIU
KNlA0hEjIOACGSLqYpXAD5SSNVT2MJRJwREAF4FRHPBlCJMSNwFguGAWDJBg+KIArkIJGNtCydUL
TuN1oBh/+zKkEblAsgjGqVgUwKLP+UOMOGCpAhICtg6ncFJH`),
"other": unPack(`
KLUv/QBYbRMABuOHS9BSNQdQ56F+xNFoV3CijY54JYt3VqV1iUU3xmj00y2pyBOCuokbhDYpvNsj
ZJeCxqH+nQFpMf4Wa92okaZoF4eH6HsXXCBo+qy3Fn4AigBgAEaYrLCQEuAom6YbHyuKZAFYksqi
sSOFiRs0WDmlACk0CnpnaAeKiCS3BlwVkViJEbDS43lFNbLkZEmGhc305Nn4AMLGiUkBDiMTG5Vz
q4ZISjCofEfR1NpXijvP2X95Hu1e+zLalc0+mjeT3Z/FPGvt62WymbX2dXMDIYKDLjjP8n03RrPf
A1vOApwGOh2MgE2LpgZrgXLDF2CUJ15idG2J8GCSgcc2ZVRgA8+RHD0k2VJjg6mRUgGGhBWEyEcz
5EePLhUeWlYhoFCKONxUiBiIUiQeDIqiQwkjLiyqnF5eGs6a2gGRapbU9JRyuXAlPemYajlJojJd
GBBJjo5GxFRkITOAvLhSCr2TDz4uzdU8Yh3i/SHP4qh3vTG2s9198NP8M+pdR73BvIP6qPeDjzsW
gTi+jXrXWOe5P/jZxOeod/287v6JljzNP99RNM0a+/x4ljz3LNV2t5v9qHfW2Pyg24u54zSfObWX
Y9bYrCTHtwdfPPPOYiU5fvB5FssfNN2V5EIPfg9LnM+JhtVEO8+FZw5LXA068YNPhimu9sHPQiWv
qc6fE9BTnxIe/LTKatab+WYu7T74uWNRxJW5W5Ux0bDLuG1ioCwjg4DvGgBcgB8cUDHJ1RQ89neE
wvjbNUMiIZdo5hbHgEpANwMkDnL0Jr7kVFg+0pZKjBkmklNgBH1YI8dQOAAKbr6EF5wYM80KWnAd
nYAR`),
}
t.Run("RepositoryKey", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", rootURL+"/repository.key")
resp := MakeRequest(t, req, http.StatusOK)
require.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
require.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
})
t.Run("Upload", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithBody(t, "PUT", rootURL+"/default", bytes.NewReader(pkgs["any"]))
MakeRequest(t, req, http.StatusUnauthorized)
req = NewRequestWithBody(t, "PUT", rootURL+"/default", bytes.NewReader(pkgs["any"])).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch)
require.NoError(t, err)
require.Len(t, pvs, 1)
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
require.NoError(t, err)
require.Nil(t, pd.SemVer)
require.IsType(t, &arch_model.VersionMetadata{}, pd.Metadata)
require.Equal(t, "test", pd.Package.Name)
require.Equal(t, "1.0.0-1", pd.Version.Version)
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
require.NoError(t, err)
require.Len(t, pfs, 2) // zst and zst.sig
require.True(t, pfs[0].IsLead)
pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
require.NoError(t, err)
require.Equal(t, int64(len(pkgs["any"])), pb.Size)
req = NewRequestWithBody(t, "PUT", rootURL+"/default", bytes.NewReader(pkgs["any"])).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusConflict)
req = NewRequestWithBody(t, "PUT", rootURL+"/default", bytes.NewReader(pkgs["x86_64"])).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithBody(t, "PUT", rootURL+"/other", bytes.NewReader(pkgs["any"])).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithBody(t, "PUT", rootURL+"/other", bytes.NewReader(pkgs["aarch64"])).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithBody(t, "PUT", rootURL+"/base", bytes.NewReader(pkgs["other"])).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithBody(t, "PUT", rootURL+"/base", bytes.NewReader(pkgs["x86_64"])).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
req = NewRequestWithBody(t, "PUT", rootURL+"/base", bytes.NewReader(pkgs["aarch64"])).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
})
t.Run("Download", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", rootURL+"/default/x86_64/test-1.0.0-1-x86_64.pkg.tar.zst")
resp := MakeRequest(t, req, http.StatusOK)
require.Equal(t, pkgs["x86_64"], resp.Body.Bytes())
req = NewRequest(t, "GET", rootURL+"/default/x86_64/test-1.0.0-1-any.pkg.tar.zst")
resp = MakeRequest(t, req, http.StatusOK)
require.Equal(t, pkgs["any"], resp.Body.Bytes())
req = NewRequest(t, "GET", rootURL+"/default/x86_64/test-1.0.0-1-aarch64.pkg.tar.zst")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", rootURL+"/other/x86_64/test-1.0.0-1-x86_64.pkg.tar.zst")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", rootURL+"/other/x86_64/test-1.0.0-1-any.pkg.tar.zst")
resp = MakeRequest(t, req, http.StatusOK)
require.Equal(t, pkgs["any"], resp.Body.Bytes())
})
t.Run("SignVerify", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", rootURL+"/repository.key")
respPub := MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", rootURL+"/other/x86_64/test-1.0.0-1-any.pkg.tar.zst")
respPkg := MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", rootURL+"/other/x86_64/test-1.0.0-1-any.pkg.tar.zst.sig")
respSig := MakeRequest(t, req, http.StatusOK)
if err := gpgVerify(respPub.Body.Bytes(), respSig.Body.Bytes(), respPkg.Body.Bytes()); err != nil {
t.Fatal(err)
}
})
t.Run("Repository", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", rootURL+"/repository.key")
respPub := MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", rootURL+"/base/x86_64/base.db")
respPkg := MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", rootURL+"/base/x86_64/base.db.sig")
respSig := MakeRequest(t, req, http.StatusOK)
if err := gpgVerify(respPub.Body.Bytes(), respSig.Body.Bytes(), respPkg.Body.Bytes()); err != nil {
t.Fatal(err)
}
files, err := listGzipFiles(respPkg.Body.Bytes())
require.NoError(t, err)
require.Len(t, files, 2)
for s, d := range files {
name := getProperty(string(d.Data), "NAME")
ver := getProperty(string(d.Data), "VERSION")
require.Equal(t, name+"-"+ver+"/desc", s)
fn := getProperty(string(d.Data), "FILENAME")
pgp := getProperty(string(d.Data), "PGPSIG")
req = NewRequest(t, "GET", rootURL+"/base/x86_64/"+fn+".sig")
respSig := MakeRequest(t, req, http.StatusOK)
decodeString, err := base64.StdEncoding.DecodeString(pgp)
require.NoError(t, err)
require.Equal(t, respSig.Body.Bytes(), decodeString)
}
})
t.Run("Delete", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithBody(t, "DELETE", rootURL+"/base/notfound/1.0.0-1", nil).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestWithBody(t, "DELETE", rootURL+"/base/test/1.0.0-1", nil).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", rootURL+"/base/x86_64/base.db")
respPkg := MakeRequest(t, req, http.StatusOK)
files, err := listGzipFiles(respPkg.Body.Bytes())
require.NoError(t, err)
require.Len(t, files, 1)
req = NewRequestWithBody(t, "DELETE", rootURL+"/base/test2/1.0.0-1", nil).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", rootURL+"/base/x86_64/base.db")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", rootURL+"/default/x86_64/base.db")
respPkg = MakeRequest(t, req, http.StatusOK)
files, err = listGzipFiles(respPkg.Body.Bytes())
require.NoError(t, err)
require.Len(t, files, 1)
})
}
func getProperty(data, key string) string {
r := bufio.NewReader(strings.NewReader(data))
for {
line, _, err := r.ReadLine()
if err != nil {
return ""
}
if strings.Contains(string(line), "%"+key+"%") {
readLine, _, _ := r.ReadLine()
return string(readLine)
}
}
}
func listGzipFiles(data []byte) (fstest.MapFS, error) {
reader, err := gzip.NewReader(bytes.NewBuffer(data))
defer reader.Close()
if err != nil {
return nil, err
}
tarRead := tar.NewReader(reader)
files := make(fstest.MapFS)
for {
cur, err := tarRead.Next()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
if cur.Typeflag != tar.TypeReg {
continue
}
data, err := io.ReadAll(tarRead)
if err != nil {
return nil, err
}
files[cur.Name] = &fstest.MapFile{Data: data}
}
return files, nil
}
func gpgVerify(pub, sig, data []byte) error {
sigPack, err := packet.Read(bytes.NewBuffer(sig))
if err != nil {
return err
}
signature, ok := sigPack.(*packet.Signature)
if !ok {
return errors.New("invalid sign key")
}
pubBlock, err := armor.Decode(bytes.NewReader(pub))
if err != nil {
return err
}
pack, err := packet.Read(pubBlock.Body)
if err != nil {
return err
}
publicKey, ok := pack.(*packet.PublicKey)
if !ok {
return errors.New("invalid public key")
}
hash := signature.Hash.New()
_, err = hash.Write(data)
if err != nil {
return err
}
return publicKey.VerifySignature(hash, signature)
}

View file

@ -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>

After

Width:  |  Height:  |  Size: 337 B