mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-10 04:05:42 +01:00
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
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:
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"
|
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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
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
|
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")
|
||||||
|
|
|
@ -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
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/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)
|
||||||
|
|
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
|
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])
|
||||||
|
|
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_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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
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">
|
||||||
<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" .}}
|
||||||
|
|
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