mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-23 06:43:34 +01:00
Arch packages implementation (#4785)
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:
parent
22d3659803
commit
f17194ca91
18 changed files with 1896 additions and 0 deletions
|
@ -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:
|
||||
|
|
|
@ -33,6 +33,7 @@ type Type string
|
|||
// List of supported packages
|
||||
const (
|
||||
TypeAlpine Type = "alpine"
|
||||
TypeArch Type = "arch"
|
||||
TypeCargo Type = "cargo"
|
||||
TypeChef Type = "chef"
|
||||
TypeComposer Type = "composer"
|
||||
|
@ -57,6 +58,7 @@ const (
|
|||
|
||||
var TypeList = []Type{
|
||||
TypeAlpine,
|
||||
TypeArch,
|
||||
TypeCargo,
|
||||
TypeChef,
|
||||
TypeComposer,
|
||||
|
@ -84,6 +86,8 @@ func (pt Type) Name() string {
|
|||
switch pt {
|
||||
case TypeAlpine:
|
||||
return "Alpine"
|
||||
case TypeArch:
|
||||
return "Arch"
|
||||
case TypeCargo:
|
||||
return "Cargo"
|
||||
case TypeChef:
|
||||
|
@ -133,6 +137,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:
|
||||
|
|
316
modules/packages/arch/metadata.go
Normal file
316
modules/packages/arch/metadata.go
Normal 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()
|
||||
}
|
445
modules/packages/arch/metadata_test.go
Normal file
445
modules/packages/arch/metadata_test.go
Normal 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())
|
||||
}
|
|
@ -24,6 +24,7 @@ var (
|
|||
LimitTotalOwnerCount int64
|
||||
LimitTotalOwnerSize int64
|
||||
LimitSizeAlpine int64
|
||||
LimitSizeArch int64
|
||||
LimitSizeCargo int64
|
||||
LimitSizeChef int64
|
||||
LimitSizeComposer int64
|
||||
|
@ -83,6 +84,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")
|
||||
|
|
|
@ -3611,6 +3611,22 @@ alpine.repository = Repository Info
|
|||
alpine.repository.branches = Branches
|
||||
alpine.repository.repositories = Repositories
|
||||
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.install = To install the package using Cargo, run the following command:
|
||||
chef.registry = Setup this registry in your <code>~/.chef/config.rb</code> file:
|
||||
|
|
1
public/assets/img/svg/gitea-arch.svg
generated
Normal file
1
public/assets/img/svg/gitea-arch.svg
generated
Normal 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 |
|
@ -15,6 +15,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"
|
||||
|
@ -137,6 +138,17 @@ func CommonRoutes() *web.Route {
|
|||
})
|
||||
})
|
||||
}, 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("/api/v1/crates", func() {
|
||||
r.Get("", cargo.SearchPackages)
|
||||
|
|
248
routers/api/packages/arch/arch.go
Normal file
248
routers/api/packages/arch/arch.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
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"
|
||||
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -200,6 +202,19 @@ func ViewPackageVersion(ctx *context.Context) {
|
|||
ctx.Data["Branches"] = util.Sorted(branches.Values())
|
||||
ctx.Data["Repositories"] = util.Sorted(repositories.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:
|
||||
distributions := make(container.Set[string])
|
||||
components := make(container.Set[string])
|
||||
|
|
348
services/packages/arch/repository.go
Normal file
348
services/packages/arch/repository.go
Normal 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
|
||||
}
|
|
@ -16,6 +16,7 @@ import (
|
|||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
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"
|
||||
container_service "code.gitea.io/gitea/services/packages/container"
|
||||
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 {
|
||||
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
|
||||
|
|
|
@ -359,6 +359,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:
|
||||
|
|
143
templates/package/content/arch.tmpl
Normal file
143
templates/package/content/arch.tmpl
Normal 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}}
|
4
templates/package/metadata/arch.tmpl
Normal file
4
templates/package/metadata/arch.tmpl
Normal 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}}
|
|
@ -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" .}}
|
||||
|
|
327
tests/integration/api_packages_arch_test.go
Normal file
327
tests/integration/api_packages_arch_test.go
Normal 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)
|
||||
}
|
1
web_src/svg/gitea-arch.svg
Normal file
1
web_src/svg/gitea-arch.svg
Normal 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 |
Loading…
Reference in a new issue