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  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…
	
	Add table
		Add a link
		
	
		Reference in a new issue