From 011b93e49f5d6fd090d0fa2dd901b8c1cd7bfa16 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 15 May 2024 19:23:01 +0000 Subject: [PATCH 1/9] Original implementation. Co-authored-by: dancheg97 --- docs/content/usage/packages/arch.en-us.md | 65 +++ docs/content/usage/packages/overview.en-us.md | 1 + models/packages/descriptor.go | 3 + models/packages/package.go | 6 + modules/packages/arch/metadata.go | 302 ++++++++++++ modules/packages/arch/metadata_test.go | 452 +++++++++++++++++ modules/setting/packages.go | 2 + options/locale/locale_en-US.ini | 10 + public/assets/img/svg/gitea-arch.svg | 1 + routers/api/packages/api.go | 7 + routers/api/packages/arch/arch.go | 135 ++++++ routers/web/user/package.go | 2 +- services/packages/arch/service.go | 133 +++++ services/packages/arch/upload.go | 83 ++++ services/packages/packages.go | 2 + templates/package/content/arch.tmpl | 70 +++ templates/package/metadata/arch.tmpl | 4 + templates/package/view.tmpl | 2 + tests/integration/api_packages_arch_test.go | 454 ++++++++++++++++++ web_src/svg/gitea-arch.svg | 1 + 20 files changed, 1734 insertions(+), 1 deletion(-) create mode 100644 docs/content/usage/packages/arch.en-us.md create mode 100644 modules/packages/arch/metadata.go create mode 100644 modules/packages/arch/metadata_test.go create mode 100644 public/assets/img/svg/gitea-arch.svg create mode 100644 routers/api/packages/arch/arch.go create mode 100644 services/packages/arch/service.go create mode 100644 services/packages/arch/upload.go create mode 100644 templates/package/content/arch.tmpl create mode 100644 templates/package/metadata/arch.tmpl create mode 100644 tests/integration/api_packages_arch_test.go create mode 100644 web_src/svg/gitea-arch.svg diff --git a/docs/content/usage/packages/arch.en-us.md b/docs/content/usage/packages/arch.en-us.md new file mode 100644 index 0000000000000..095d024a3de39 --- /dev/null +++ b/docs/content/usage/packages/arch.en-us.md @@ -0,0 +1,65 @@ +--- +date: "2016-11-08T16:00:00+02:00" +title: "Arch Package Registry" +weight: 10 +toc: true +draft: false +menu: + sidebar: + parent: "packages" + name: "Arch" + weight: 10 + identifier: "arch" +--- + +# Arch package registry + +Gitea has a Arch Linux package registry, which can act as a fully working [Arch linux mirror](https://wiki.archlinux.org/title/mirrors) and connected directly in `/etc/pacman.conf`. Gitea automatically creates pacman database for packages in user/organization space when a new Arch package is uploaded. + +**Table of Contents** + +{{< toc >}} + +## Install packages + +First, you need to update your pacman configuration, adding following lines: + +```conf +[{owner}.{domain}] +SigLevel = Optional TrustAll +Server = https://{domain}/api/packages/{owner}/arch/{distribution}/{architecture} +``` + +Then, you can run pacman sync command (with -y flag to load connected database file), to install your package: + +```sh +pacman -Sy package +``` + +## Upload packages + +When uploading the package to gitea, you have to prepare package file with the `.pkg.tar.zst` extension and its `.pkg.tar.zst.sig` signature. You can use [curl](https://curl.se/) or any other HTTP client, Gitea supports multiple [authentication schemes](https://docs.gitea.com/usage/authentication). The upload command will create 3 files: package, signature and desc file for the pacman database (which will be created automatically on request). + +The following command will upload arch package and related signature to gitea with basic authentification: + +```sh +curl -X PUT \ + https://{domain}/api/packages/{owner}/arch/push/{package-1-1-x86_64.pkg.tar.zst}/{archlinux}/$(xxd -p package-1-1-x86_64.pkg.tar.zst.sig | tr -d '\n') \ + --user your_username:your_token_or_password \ + --header "Content-Type: application/octet-stream" \ + --data-binary '@/path/to/package/file/package-1-1-x86_64.pkg.tar.zst' +``` + +## Delete packages + +The `DELETE` method will remove specific package version, and all package files related to that version: + +```sh +curl -X DELETE \ + https://{domain}/api/packages/{user}/arch/remove/{package}/{version} \ + --user your_username:your_token_or_password +``` + +## Clients + +Any `pacman` compatible package manager or AUR-helper can be used to install packages from gitea ([yay](https://github.com/Jguer/yay), [paru](https://github.com/Morganamilo/paru), [pikaur](https://github.com/actionless/pikaur), [aura](https://github.com/fosskers/aura)). Alternatively, you can try [pack](https://fmnx.su/core/pack) which supports full gitea API (install/push/remove). Also, any HTTP client can be used to execute get/push/remove operations ([curl](https://curl.se/), [postman](https://www.postman.com/), [thunder-client](https://www.thunderclient.com/)). diff --git a/docs/content/usage/packages/overview.en-us.md b/docs/content/usage/packages/overview.en-us.md index 89fc6f286e65f..b01163b8f6e7c 100644 --- a/docs/content/usage/packages/overview.en-us.md +++ b/docs/content/usage/packages/overview.en-us.md @@ -24,6 +24,7 @@ The following package managers are currently supported: | Name | Language | Package client | | ---- | -------- | -------------- | | [Alpine](usage/packages/alpine.md) | - | `apk` | +| [Arch](usage/packages/arch.md) | - | `pacman` | | [Cargo](usage/packages/cargo.md) | Rust | `cargo` | | [Chef](usage/packages/chef.md) | - | `knife` | | [Composer](usage/packages/composer.md) | PHP | `composer` | diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index b8ef698d3822a..803b73c968995 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/modules/packages/arch" "code.gitea.io/gitea/modules/packages/cargo" "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/packages/composer" @@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc switch p.Type { case TypeAlpine: metadata = &alpine.VersionMetadata{} + case TypeArch: + metadata = &arch.VersionMetadata{} case TypeCargo: metadata = &cargo.Metadata{} case TypeChef: diff --git a/models/packages/package.go b/models/packages/package.go index 65a25741509e4..417d62d199310 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -31,6 +31,7 @@ type Type string // List of supported packages const ( TypeAlpine Type = "alpine" + TypeArch Type = "arch" TypeCargo Type = "cargo" TypeChef Type = "chef" TypeComposer Type = "composer" @@ -55,6 +56,7 @@ const ( var TypeList = []Type{ TypeAlpine, + TypeArch, TypeCargo, TypeChef, TypeComposer, @@ -82,6 +84,8 @@ func (pt Type) Name() string { switch pt { case TypeAlpine: return "Alpine" + case TypeArch: + return "Arch" case TypeCargo: return "Cargo" case TypeChef: @@ -131,6 +135,8 @@ func (pt Type) SVGName() string { switch pt { case TypeAlpine: return "gitea-alpine" + case TypeArch: + return "gitea-arch" case TypeCargo: return "gitea-cargo" case TypeChef: diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go new file mode 100644 index 0000000000000..4175cb46e8d2a --- /dev/null +++ b/modules/packages/arch/metadata.go @@ -0,0 +1,302 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "regexp" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "github.com/mholt/archiver/v3" +) + +const ( + PropertyDescription = "arch.description" + PropertySignature = "arch.signature" +) + +var ( + // https://man.archlinux.org/man/PKGBUILD.5 + reName = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$`) + reVer = regexp.MustCompile(`^[a-zA-Z0-9:_.+]+-+[0-9]+$`) + reOptDep = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$|^[a-zA-Z0-9@._+-]+(:.*)`) + rePkgVer = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$|^[a-zA-Z0-9@._+-]+(>.*)|^[a-zA-Z0-9@._+-]+(<.*)|^[a-zA-Z0-9@._+-]+(=.*)`) +) + +type Package struct { + Name string `json:"name"` + Version string `json:"version"` + VersionMetadata VersionMetadata + FileMetadata FileMetadata +} + +// Arch package metadata related to specific version. +// Version metadata the same across different architectures and distributions. +type VersionMetadata struct { + Base string `json:"base"` + Description string `json:"description"` + ProjectURL string `json:"project_url"` + Groups []string `json:"groups,omitempty"` + Provides []string `json:"provides,omitempty"` + License []string `json:"license,omitempty"` + Depends []string `json:"depends,omitempty"` + OptDepends []string `json:"opt_depends,omitempty"` + MakeDepends []string `json:"make_depends,omitempty"` + CheckDepends []string `json:"check_depends,omitempty"` + Backup []string `json:"backup,omitempty"` +} + +// Metadata related to specific pakcage file. +// This metadata might vary for different architecture and distribution. +type FileMetadata struct { + CompressedSize int64 `json:"compressed_size"` + InstalledSize int64 `json:"installed_size"` + MD5 string `json:"md5"` + SHA256 string `json:"sha256"` + BuildDate int64 `json:"build_date"` + Packager string `json:"packager"` + Arch string `json:"arch"` +} + +// Function that receives arch package archive data and returns it's metadata. +func ParsePackage(r io.Reader, md5, sha256 []byte, size int64) (*Package, error) { + zstd := archiver.NewTarZstd() + err := zstd.Open(r, 0) + if err != nil { + return nil, err + } + defer zstd.Close() + + var pkg *Package + var mtree bool + + for { + f, err := zstd.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + defer f.Close() + + switch f.Name() { + case ".PKGINFO": + pkg, err = ParsePackageInfo(f) + if err != nil { + return nil, err + } + case ".MTREE": + mtree = true + } + } + + if pkg == nil { + return nil, util.NewInvalidArgumentErrorf(".PKGINFO file not found") + } + + if !mtree { + return nil, util.NewInvalidArgumentErrorf(".MTREE file not found") + } + + pkg.FileMetadata.CompressedSize = size + pkg.FileMetadata.SHA256 = hex.EncodeToString(sha256) + pkg.FileMetadata.MD5 = hex.EncodeToString(md5) + + return pkg, nil +} + +// Function that accepts reader for .PKGINFO file from package archive, +// validates all field according to PKGBUILD spec and returns package. +func ParsePackageInfo(r io.Reader) (*Package, error) { + p := &Package{} + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, "#") { + continue + } + + i := strings.IndexRune(line, '=') + if i == -1 { + continue + } + + key := strings.TrimSpace(line[:i]) + value := strings.TrimSpace(line[i+1:]) + + switch key { + case "pkgname": + p.Name = value + case "pkgbase": + p.VersionMetadata.Base = value + case "pkgver": + p.Version = value + case "pkgdesc": + p.VersionMetadata.Description = value + case "url": + p.VersionMetadata.ProjectURL = value + case "packager": + p.FileMetadata.Packager = value + case "arch": + p.FileMetadata.Arch = value + case "provides": + p.VersionMetadata.Provides = append(p.VersionMetadata.Provides, value) + case "license": + p.VersionMetadata.License = append(p.VersionMetadata.License, value) + case "depend": + p.VersionMetadata.Depends = append(p.VersionMetadata.Depends, value) + case "optdepend": + p.VersionMetadata.OptDepends = append(p.VersionMetadata.OptDepends, value) + case "makedepend": + p.VersionMetadata.MakeDepends = append(p.VersionMetadata.MakeDepends, value) + case "checkdepend": + p.VersionMetadata.CheckDepends = append(p.VersionMetadata.CheckDepends, value) + case "backup": + p.VersionMetadata.Backup = append(p.VersionMetadata.Backup, value) + case "group": + p.VersionMetadata.Groups = append(p.VersionMetadata.Groups, value) + case "builddate": + bd, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + p.FileMetadata.BuildDate = bd + case "size": + is, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + p.FileMetadata.InstalledSize = is + } + } + + return p, errors.Join(scanner.Err(), ValidatePackageSpec(p)) +} + +// Arch package validation according to PKGBUILD specification. +func ValidatePackageSpec(p *Package) error { + if !reName.MatchString(p.Name) { + return util.NewInvalidArgumentErrorf("invalid package name") + } + if !reName.MatchString(p.VersionMetadata.Base) { + return util.NewInvalidArgumentErrorf("invalid package base") + } + if !reVer.MatchString(p.Version) { + return util.NewInvalidArgumentErrorf("invalid package version") + } + if p.FileMetadata.Arch == "" { + return util.NewInvalidArgumentErrorf("architecture should be specified") + } + if p.VersionMetadata.ProjectURL != "" { + if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { + return util.NewInvalidArgumentErrorf("invalid project URL") + } + } + for _, cd := range p.VersionMetadata.CheckDepends { + if !rePkgVer.MatchString(cd) { + return util.NewInvalidArgumentErrorf("invalid check dependency: " + cd) + } + } + for _, d := range p.VersionMetadata.Depends { + if !rePkgVer.MatchString(d) { + return util.NewInvalidArgumentErrorf("invalid dependency: " + d) + } + } + for _, md := range p.VersionMetadata.MakeDepends { + if !rePkgVer.MatchString(md) { + return util.NewInvalidArgumentErrorf("invalid make dependency: " + md) + } + } + for _, p := range p.VersionMetadata.Provides { + if !rePkgVer.MatchString(p) { + return util.NewInvalidArgumentErrorf("invalid provides: " + p) + } + } + for _, od := range p.VersionMetadata.OptDepends { + if !reOptDep.MatchString(od) { + return util.NewInvalidArgumentErrorf("invalid optional dependency: " + od) + } + } + for _, bf := range p.VersionMetadata.Backup { + if strings.HasPrefix(bf, "/") { + return util.NewInvalidArgumentErrorf("backup file contains leading forward slash") + } + } + return nil +} + +// Create pacman package description file. +func (p *Package) Desc() string { + entries := [40]string{ + "FILENAME", fmt.Sprintf("%s-%s-%s.pkg.tar.zst", p.Name, p.Version, p.FileMetadata.Arch), + "NAME", p.Name, + "BASE", p.VersionMetadata.Base, + "VERSION", p.Version, + "DESC", p.VersionMetadata.Description, + "GROUPS", strings.Join(p.VersionMetadata.Groups, "\n"), + "CSIZE", fmt.Sprintf("%d", p.FileMetadata.CompressedSize), + "ISIZE", fmt.Sprintf("%d", p.FileMetadata.InstalledSize), + "MD5SUM", p.FileMetadata.MD5, + "SHA256SUM", p.FileMetadata.SHA256, + "URL", p.VersionMetadata.ProjectURL, + "LICENSE", strings.Join(p.VersionMetadata.License, "\n"), + "ARCH", p.FileMetadata.Arch, + "BUILDDATE", fmt.Sprintf("%d", p.FileMetadata.BuildDate), + "PACKAGER", p.FileMetadata.Packager, + "PROVIDES", strings.Join(p.VersionMetadata.Provides, "\n"), + "DEPENDS", strings.Join(p.VersionMetadata.Depends, "\n"), + "OPTDEPENDS", strings.Join(p.VersionMetadata.OptDepends, "\n"), + "MAKEDEPENDS", strings.Join(p.VersionMetadata.MakeDepends, "\n"), + "CHECKDEPENDS", strings.Join(p.VersionMetadata.CheckDepends, "\n"), + } + + var buf bytes.Buffer + for i := 0; i < 40; i += 2 { + if entries[i+1] != "" { + fmt.Fprintf(&buf, "%%%s%%\n%s\n\n", entries[i], entries[i+1]) + } + } + return buf.String() +} + +// Create pacman database archive based on provided package metadata structs. +func CreatePacmanDb(entries map[string][]byte) (*bytes.Buffer, error) { + var b bytes.Buffer + + gw := gzip.NewWriter(&b) + tw := tar.NewWriter(gw) + + for name, content := range entries { + header := &tar.Header{ + Name: name, + Size: int64(len(content)), + Mode: int64(os.ModePerm), + } + + if err := tw.WriteHeader(header); err != nil { + return nil, errors.Join(err, tw.Close(), gw.Close()) + } + + if _, err := tw.Write(content); err != nil { + return nil, errors.Join(err, tw.Close(), gw.Close()) + } + } + + return &b, errors.Join(tw.Close(), gw.Close()) +} diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go new file mode 100644 index 0000000000000..0f3cede9ccff5 --- /dev/null +++ b/modules/packages/arch/metadata_test.go @@ -0,0 +1,452 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "bytes" + "encoding/base64" + "errors" + "io" + "os" + "strings" + "testing" + "testing/fstest" + "time" + + "github.com/mholt/archiver/v3" + "github.com/stretchr/testify/assert" +) + +func TestParsePackage(t *testing.T) { + // Minimal PKGINFO contents and test FS + const PKGINFO = `pkgname = a +pkgbase = b +pkgver = 1-2 +arch = x86_64 +` + fs := fstest.MapFS{ + "pkginfo": &fstest.MapFile{ + Data: []byte(PKGINFO), + Mode: os.ModePerm, + ModTime: time.Now(), + }, + "mtree": &fstest.MapFile{ + Data: []byte("data"), + Mode: os.ModePerm, + ModTime: time.Now(), + }, + } + + // Test .PKGINFO file + pinf, err := fs.Stat("pkginfo") + assert.NoError(t, err) + + pfile, err := fs.Open("pkginfo") + assert.NoError(t, err) + + parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO") + assert.NoError(t, err) + + // Test .MTREE file + minf, err := fs.Stat("mtree") + assert.NoError(t, err) + + mfile, err := fs.Open("mtree") + assert.NoError(t, err) + + marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE") + assert.NoError(t, err) + + t.Run("normal archive", func(t *testing.T) { + var buf bytes.Buffer + + archive := archiver.NewTarZstd() + archive.Create(&buf) + + err = archive.Write(archiver.File{ + FileInfo: archiver.FileInfo{ + FileInfo: pinf, + CustomName: parcname, + }, + ReadCloser: pfile, + }) + assert.NoError(t, errors.Join(pfile.Close(), err)) + + err = archive.Write(archiver.File{ + FileInfo: archiver.FileInfo{ + FileInfo: minf, + CustomName: marcname, + }, + ReadCloser: mfile, + }) + assert.NoError(t, errors.Join(mfile.Close(), archive.Close(), err)) + + _, err = ParsePackage(&buf, []byte{}, []byte{}, 0) + + assert.NoError(t, err) + }) + + t.Run("missing .PKGINFO", func(t *testing.T) { + var buf bytes.Buffer + + archive := archiver.NewTarZstd() + archive.Create(&buf) + + assert.NoError(t, archive.Close()) + + _, err = ParsePackage(&buf, []byte{}, []byte{}, 0) + + assert.Error(t, err) + assert.Contains(t, err.Error(), ".PKGINFO file not found") + }) + + t.Run("missing .MTREE", func(t *testing.T) { + var buf bytes.Buffer + + pfile, err := fs.Open("pkginfo") + assert.NoError(t, err) + + archive := archiver.NewTarZstd() + archive.Create(&buf) + + err = archive.Write(archiver.File{ + FileInfo: archiver.FileInfo{ + FileInfo: pinf, + CustomName: parcname, + }, + ReadCloser: pfile, + }) + assert.NoError(t, errors.Join(pfile.Close(), archive.Close(), err)) + + _, err = ParsePackage(&buf, []byte{}, []byte{}, 0) + + assert.Error(t, err) + assert.Contains(t, err.Error(), ".MTREE file not found") + }) +} + +func TestParsePackageInfo(t *testing.T) { + const PKGINFO = `# Generated by makepkg 6.0.2 +# using fakeroot version 1.31 +pkgname = a +pkgbase = b +pkgver = 1-2 +pkgdesc = comment +url = https://example.com/ +group = group +builddate = 3 +packager = Name Surname +size = 5 +arch = x86_64 +license = BSD +provides = pvd +depend = smth +optdepend = hex +checkdepend = ola +makedepend = cmake +backup = usr/bin/paket1 +` + p, err := ParsePackageInfo(strings.NewReader(PKGINFO)) + assert.NoError(t, err) + assert.Equal(t, Package{ + Name: "a", + Version: "1-2", + VersionMetadata: VersionMetadata{ + Base: "b", + Description: "comment", + ProjectURL: "https://example.com/", + Groups: []string{"group"}, + Provides: []string{"pvd"}, + License: []string{"BSD"}, + Depends: []string{"smth"}, + OptDepends: []string{"hex"}, + MakeDepends: []string{"cmake"}, + CheckDepends: []string{"ola"}, + Backup: []string{"usr/bin/paket1"}, + }, + FileMetadata: FileMetadata{ + InstalledSize: 5, + BuildDate: 3, + Packager: "Name Surname ", + Arch: "x86_64", + }, + }, *p) +} + +func TestValidatePackageSpec(t *testing.T) { + newpkg := func() Package { + return Package{ + Name: "abc", + Version: "1-1", + VersionMetadata: VersionMetadata{ + Base: "ghx", + Description: "whoami", + ProjectURL: "https://example.com/", + Groups: []string{"gnome"}, + Provides: []string{"abc", "def"}, + License: []string{"GPL"}, + Depends: []string{"go", "gpg=1", "curl>=3", "git<=7"}, + OptDepends: []string{"git: something", "make"}, + MakeDepends: []string{"chrom"}, + CheckDepends: []string{"bariy"}, + Backup: []string{"etc/pacman.d/filo"}, + }, + FileMetadata: FileMetadata{ + CompressedSize: 1, + InstalledSize: 2, + MD5: "abc", + SHA256: "def", + BuildDate: 3, + Packager: "smon", + Arch: "x86_64", + }, + } + } + + t.Run("valid package", func(t *testing.T) { + p := newpkg() + + err := ValidatePackageSpec(&p) + + assert.NoError(t, err) + }) + + t.Run("invalid package name", func(t *testing.T) { + p := newpkg() + p.Name = "!$%@^!*&()" + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid package name") + }) + + t.Run("invalid package base", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.Base = "!$%@^!*&()" + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid package base") + }) + + t.Run("invalid package version", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.Base = "una-luna?" + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid package base") + }) + + t.Run("invalid package version", func(t *testing.T) { + p := newpkg() + p.Version = "una-luna" + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid package version") + }) + + t.Run("missing architecture", func(t *testing.T) { + p := newpkg() + p.FileMetadata.Arch = "" + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "architecture should be specified") + }) + + t.Run("invalid URL", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.ProjectURL = "http%%$#" + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid project URL") + }) + + t.Run("invalid check dependency", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.CheckDepends = []string{"Err^_^"} + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid check dependency") + }) + + t.Run("invalid dependency", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.Depends = []string{"^^abc"} + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid dependency") + }) + + t.Run("invalid make dependency", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.MakeDepends = []string{"^m^"} + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid make dependency") + }) + + t.Run("invalid provides", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.Provides = []string{"^m^"} + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid provides") + }) + + t.Run("invalid optional dependency", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.OptDepends = []string{"^m^:MM"} + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid optional dependency") + }) + + t.Run("invalid optional dependency", func(t *testing.T) { + p := newpkg() + p.VersionMetadata.Backup = []string{"/ola/cola"} + + err := ValidatePackageSpec(&p) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "backup file contains leading forward slash") + }) +} + +func TestDescString(t *testing.T) { + const pkgdesc = `%FILENAME% +zstd-1.5.5-1-x86_64.pkg.tar.zst + +%NAME% +zstd + +%BASE% +zstd + +%VERSION% +1.5.5-1 + +%DESC% +Zstandard - Fast real-time compression algorithm + +%GROUPS% +dummy1 +dummy2 + +%CSIZE% +401 + +%ISIZE% +1500453 + +%MD5SUM% +5016660ef3d9aa148a7b72a08d3df1b2 + +%SHA256SUM% +9fa4ede47e35f5971e4f26ecadcbfb66ab79f1d638317ac80334a3362dedbabd + +%URL% +https://facebook.github.io/zstd/ + +%LICENSE% +BSD +GPL2 + +%ARCH% +x86_64 + +%BUILDDATE% +1681646714 + +%PACKAGER% +Jelle van der Waa + +%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 ", + Arch: "x86_64", + }, + } + assert.Equal(t, pkgdesc, md.Desc()) +} + +func TestCreatePacmanDb(t *testing.T) { + const dbarchive = "H4sIAAAAAAAA/0rLzEnVS60oYaAhMDAwMDA3NwfTBgYG6LSBgYEpEtuAwcDQwMzUgEHBgJaOgoHS4pLEIgYDiu1C99wQASmlubmVA+2IUTAKRsEoGAV0B4AAAAD//2VF3KIACAAA" + + db, err := CreatePacmanDb(map[string][]byte{ + "file.ext": []byte("dummy"), + }) + assert.NoError(t, err) + + actual, err := io.ReadAll(db) + assert.NoError(t, err) + + expected, err := base64.RawStdEncoding.DecodeString(dbarchive) + assert.NoError(t, err) + + assert.Equal(t, expected, actual) +} diff --git a/modules/setting/packages.go b/modules/setting/packages.go index b225615a24012..c4a4b96959515 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -24,6 +24,7 @@ var ( LimitTotalOwnerCount int64 LimitTotalOwnerSize int64 LimitSizeAlpine int64 + LimitSizeArch int64 LimitSizeCargo int64 LimitSizeChef int64 LimitSizeComposer int64 @@ -82,6 +83,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) { Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE") + Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH") Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6a08041a7c8b6..ec328510e3f62 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3449,6 +3449,16 @@ alpine.repository = Repository Info alpine.repository.branches = Branches alpine.repository.repositories = Repositories alpine.repository.architectures = Architectures +arch.pacmanconf = Add server with related distribution and architecture to /etc/pacman.conf: +arch.pacmansync = Sync package with pacman: +arch.documentation = For more information on the arch mirrors, see %sthe documentation%s. +arch.properties = Package properties +arch.description = Description +arch.provides = Provides +arch.depends = Depends +arch.optdepends = Optional depends +arch.makedepends = Make depends +arch.checkdepends = Check depends cargo.registry = Setup this registry in the Cargo configuration file (for example ~/.cargo/config.toml): cargo.install = To install the package using Cargo, run the following command: chef.registry = Setup this registry in your ~/.chef/config.rb file: diff --git a/public/assets/img/svg/gitea-arch.svg b/public/assets/img/svg/gitea-arch.svg new file mode 100644 index 0000000000000..943a92c579468 --- /dev/null +++ b/public/assets/img/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 5e3cbac8f9cb1..872e0827bc945 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/packages/alpine" + "code.gitea.io/gitea/routers/api/packages/arch" "code.gitea.io/gitea/routers/api/packages/cargo" "code.gitea.io/gitea/routers/api/packages/chef" "code.gitea.io/gitea/routers/api/packages/composer" @@ -121,6 +122,12 @@ func CommonRoutes() *web.Route { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/arch", func() { + r.Put("/push/{filename}/{distro}", reqPackageAccess(perm.AccessModeWrite), arch.Push) + r.Put("/push/{filename}/{distro}/{sign}", reqPackageAccess(perm.AccessModeWrite), arch.Push) + r.Delete("/remove/{package}/{version}", reqPackageAccess(perm.AccessModeWrite), arch.Remove) + r.Get("/{distro}/{arch}/{file}", arch.Get) + }) r.Group("/cargo", func() { r.Group("/api/v1/crates", func() { r.Get("", cargo.SearchPackages) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go new file mode 100644 index 0000000000000..358ca81c6e731 --- /dev/null +++ b/routers/api/packages/arch/arch.go @@ -0,0 +1,135 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "bytes" + "net/http" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/routers/api/packages/helper" + packages_service "code.gitea.io/gitea/services/packages" + arch_service "code.gitea.io/gitea/services/packages/arch" +) + +func apiError(ctx *context.Context, status int, obj any) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.PlainText(status, message) + }) +} + +// Push new package to arch package registry. +func Push(ctx *context.Context) { + var ( + filename = ctx.Params("filename") + distro = ctx.Params("distro") + sign = ctx.Params("sign") + ) + + upload, close, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if close { + defer upload.Close() + } + + _, _, err = arch_service.UploadArchPackage(ctx, upload, filename, distro, sign) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: + apiError(ctx, http.StatusConflict, err) + case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + ctx.Status(http.StatusOK) +} + +// Get file from arch package registry. +func Get(ctx *context.Context) { + var ( + file = ctx.Params("file") + owner = ctx.Params("username") + distro = ctx.Params("distro") + arch = ctx.Params("arch") + ) + + if strings.HasSuffix(file, ".pkg.tar.zst") { + pkg, err := arch_service.GetPackageFile(ctx, distro, file) + if err != nil { + apiError(ctx, http.StatusNotFound, err) + return + } + + ctx.ServeContent(pkg, &context.ServeHeaderOptions{ + Filename: file, + }) + return + } + + if strings.HasSuffix(file, ".pkg.tar.zst.sig") { + sig, err := arch_service.GetPackageSignature(ctx, distro, file) + if err != nil { + apiError(ctx, http.StatusNotFound, err) + return + } + + ctx.ServeContent(sig, &context.ServeHeaderOptions{ + Filename: file, + }) + return + } + + if strings.HasSuffix(file, ".db.tar.gz") || strings.HasSuffix(file, ".db") { + db, err := arch_service.CreatePacmanDb(ctx, owner, arch, distro) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.ServeContent(bytes.NewReader(db.Bytes()), &context.ServeHeaderOptions{ + Filename: file, + }) + return + } + + ctx.Status(http.StatusNotFound) +} + +// Remove specific package version, related files with properties. +func Remove(ctx *context.Context) { + var ( + pkg = ctx.Params("package") + ver = ctx.Params("version") + ) + + version, err := packages_model.GetVersionByNameAndVersion( + ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg, ver, + ) + if err != nil { + switch err { + case packages_model.ErrPackageNotExist: + apiError(ctx, http.StatusNotFound, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + err = packages_service.RemovePackageVersion(ctx, ctx.Package.Owner, version) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusOK) +} diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 2a18796687363..8cef1afba9402 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -177,7 +177,7 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["PackageDescriptor"] = pd switch pd.Package.Type { - case packages_model.TypeContainer: + case packages_model.TypeContainer, packages_model.TypeArch: ctx.Data["RegistryHost"] = setting.Packages.RegistryHost case packages_model.TypeAlpine: branches := make(container.Set[string]) diff --git a/services/packages/arch/service.go b/services/packages/arch/service.go new file mode 100644 index 0000000000000..a5859009c05c0 --- /dev/null +++ b/services/packages/arch/service.go @@ -0,0 +1,133 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "io" + "sort" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + arch_module "code.gitea.io/gitea/modules/packages/arch" + packages_service "code.gitea.io/gitea/services/packages" +) + +// Get data related to provided filename and distribution, for package files +// update download counter. +func GetPackageFile(ctx *context.Context, distro, file string) (io.ReadSeekCloser, error) { + pf, err := getPackageFile(ctx, distro, file) + if err != nil { + return nil, err + } + + filestream, _, _, err := packages_service.GetPackageFileStream(ctx, pf) + return filestream, err +} + +// This function will search for package signature and if present, will load it +// from package file properties, and return its byte reader. +func GetPackageSignature(ctx *context.Context, distro, file string) (*bytes.Reader, error) { + pf, err := getPackageFile(ctx, distro, strings.TrimSuffix(file, ".sig")) + if err != nil { + return nil, err + } + + proprs, err := packages_model.GetProperties(ctx, packages_model.PropertyTypeFile, pf.ID) + if err != nil { + return nil, err + } + + for _, pp := range proprs { + if pp.Name == arch_module.PropertySignature { + b, err := hex.DecodeString(pp.Value) + if err != nil { + return nil, err + } + return bytes.NewReader(b), nil + } + } + + return nil, errors.New("signature for requested package not found") +} + +// Ejects parameters required to get package file property from file name. +func getPackageFile(ctx *context.Context, distro, file string) (*packages_model.PackageFile, error) { + var ( + splt = strings.Split(file, "-") + pkgname = strings.Join(splt[0:len(splt)-3], "-") + vername = splt[len(splt)-3] + "-" + splt[len(splt)-2] + ) + + version, err := packages_model.GetVersionByNameAndVersion( + ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkgname, vername, + ) + if err != nil { + return nil, err + } + + pkgfile, err := packages_model.GetFileForVersionByName(ctx, version.ID, file, distro) + if err != nil { + return nil, err + } + return pkgfile, nil +} + +// Finds all arch packages in user/organization scope, each package version +// starting from latest in descending order is checked to be compatible with +// requested combination of architecture and distribution. When/If the first +// compatible version is found, related desc file will be loaded from package +// properties and added to resulting .db.tar.gz archive. +func CreatePacmanDb(ctx *context.Context, owner, arch, distro string) (*bytes.Buffer, error) { + pkgs, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeArch) + if err != nil { + return nil, err + } + + entries := make(map[string][]byte) + + for _, pkg := range pkgs { + versions, err := packages_model.GetVersionsByPackageName( + ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg.Name, + ) + if err != nil { + return nil, err + } + + sort.Slice(versions, func(i, j int) bool { + return versions[i].CreatedUnix > versions[j].CreatedUnix + }) + + for _, ver := range versions { + file := fmt.Sprintf("%s-%s-%s.pkg.tar.zst", pkg.Name, ver.Version, arch) + + pf, err := packages_model.GetFileForVersionByName(ctx, ver.ID, file, distro) + if err != nil { + file = fmt.Sprintf("%s-%s-any.pkg.tar.zst", pkg.Name, ver.Version) + pf, err = packages_model.GetFileForVersionByName(ctx, ver.ID, file, distro) + if err != nil { + continue + } + } + + pps, err := packages_model.GetPropertiesByName( + ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyDescription, + ) + if err != nil { + return nil, err + } + + if len(pps) >= 1 { + entries[pkg.Name+"-"+ver.Version+"/desc"] = []byte(pps[0].Value) + break + } + } + } + + return arch_module.CreatePacmanDb(entries) +} diff --git a/services/packages/arch/upload.go b/services/packages/arch/upload.go new file mode 100644 index 0000000000000..221801b4f87b8 --- /dev/null +++ b/services/packages/arch/upload.go @@ -0,0 +1,83 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "encoding/hex" + "errors" + "io" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/context" + packages_module "code.gitea.io/gitea/modules/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" + packages_service "code.gitea.io/gitea/services/packages" +) + +// UploadArchPackage adds an Arch Package to the registry. +// The first return value indictaes if the error is a user error. +func UploadArchPackage(ctx *context.Context, upload io.Reader, filename, distro, sign string) (bool, *packages_model.PackageVersion, error) { + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + return false, nil, err + } + defer buf.Close() + + md5, _, sha256, _ := buf.Sums() + + p, err := arch_module.ParsePackage(buf, md5, sha256, buf.Size()) + if err != nil { + return false, nil, err + } + + _, err = buf.Seek(0, io.SeekStart) + if err != nil { + return false, nil, err + } + + properties := map[string]string{ + arch_module.PropertyDescription: p.Desc(), + } + if sign != "" { + _, err := hex.DecodeString(sign) + if err != nil { + return true, nil, errors.New("unable to decode package signature") + } + properties[arch_module.PropertySignature] = sign + } + + ver, _, err := packages_service.CreatePackageOrAddFileToExisting( + ctx, &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeArch, + Name: p.Name, + Version: p.Version, + }, + Creator: ctx.Doer, + Metadata: p.VersionMetadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: filename, + CompositeKey: distro, + }, + OverwriteExisting: true, + IsLead: true, + Creator: ctx.Doer, + Data: buf, + Properties: properties, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile, packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + return true, nil, err + default: + return false, nil, err + } + } + + return false, ver, nil +} diff --git a/services/packages/packages.go b/services/packages/packages.go index 64b1ddd869632..797029f0cb47a 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -355,6 +355,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p switch packageType { case packages_model.TypeAlpine: typeSpecificSize = setting.Packages.LimitSizeAlpine + case packages_model.TypeArch: + typeSpecificSize = setting.Packages.LimitSizeArch case packages_model.TypeCargo: typeSpecificSize = setting.Packages.LimitSizeCargo case packages_model.TypeChef: diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl new file mode 100644 index 0000000000000..b457af031a539 --- /dev/null +++ b/templates/package/content/arch.tmpl @@ -0,0 +1,70 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} +

{{ctx.Locale.Tr "packages.installation"}}

+
+
+ +
+ +
[{{.PackageDescriptor.Owner.LowerName}}.{{.RegistryHost}}]
+SigLevel = Optional TrustAll
+Server = 
+
+ +
+ +
pacman -Sy {{.PackageDescriptor.Package.LowerName}}
+
+ +
+ {{ctx.Locale.Tr "packages.arch.documentation" (printf ``) (printf ``) | Safe}} +
+
+
+ +

{{ctx.Locale.Tr "packages.arch.properties"}}

+
+ + + + + + + + {{if .PackageDescriptor.Metadata.Provides}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.Depends}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.OptDepends}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.MakeDepends}} + + + + + {{end}} + + {{if .PackageDescriptor.Metadata.CheckDepends}} + + + + + {{end}} + +
{{ctx.Locale.Tr "packages.arch.description"}}
{{.PackageDescriptor.Metadata.Description}}
{{ctx.Locale.Tr "packages.arch.provides"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.Provides ", "}}
{{ctx.Locale.Tr "packages.arch.depends"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.Depends ", "}}
{{ctx.Locale.Tr "packages.arch.optdepends"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.OptDepends ", "}}
{{ctx.Locale.Tr "packages.arch.makedepends"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.MakeDepends ", "}}
{{ctx.Locale.Tr "packages.arch.checkdepends"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.CheckDepends ", "}}
+
+{{end}} diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl new file mode 100644 index 0000000000000..822973eb7d984 --- /dev/null +++ b/templates/package/metadata/arch.tmpl @@ -0,0 +1,4 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} + {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} +{{end}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 6beb249a7ffc2..2c80dd494d471 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -19,6 +19,7 @@
{{template "package/content/alpine" .}} + {{template "package/content/arch" .}} {{template "package/content/cargo" .}} {{template "package/content/chef" .}} {{template "package/content/composer" .}} @@ -50,6 +51,7 @@
{{svg "octicon-calendar" 16 "tw-mr-2"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}
{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}
{{template "package/metadata/alpine" .}} + {{template "package/metadata/arch" .}} {{template "package/metadata/cargo" .}} {{template "package/metadata/chef" .}} {{template "package/metadata/composer" .}} diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go new file mode 100644 index 0000000000000..3a91d797dd67c --- /dev/null +++ b/tests/integration/api_packages_arch_test.go @@ -0,0 +1,454 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "os" + "testing" + "testing/fstest" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/packages/arch" + "code.gitea.io/gitea/tests" + + "github.com/mholt/archiver/v3" + "github.com/minio/sha256-simd" + "github.com/stretchr/testify/assert" +) + +func TestPackageArch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + var ( + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + pushBatch = []*TestArchPackage{ + BuildArchPackage(t, "git", "1-1", "x86_64"), + BuildArchPackage(t, "git", "2-1", "x86_64"), + BuildArchPackage(t, "git", "1-1", "i686"), + BuildArchPackage(t, "adwaita", "1-1", "any"), + BuildArchPackage(t, "adwaita", "2-1", "any"), + } + + removeBatch = []*TestArchPackage{ + BuildArchPackage(t, "curl", "1-1", "x86_64"), + BuildArchPackage(t, "curl", "2-1", "x86_64"), + BuildArchPackage(t, "dock", "1-1", "any"), + BuildArchPackage(t, "dock", "2-1", "any"), + } + + firstDatabaseBatch = []*TestArchPackage{ + BuildArchPackage(t, "pacman", "1-1", "x86_64"), + BuildArchPackage(t, "pacman", "1-1", "i686"), + BuildArchPackage(t, "htop", "1-1", "x86_64"), + BuildArchPackage(t, "htop", "1-1", "i686"), + BuildArchPackage(t, "dash", "1-1", "any"), + } + + secondDatabaseBatch = []*TestArchPackage{ + BuildArchPackage(t, "pacman", "2-1", "x86_64"), + BuildArchPackage(t, "htop", "2-1", "i686"), + BuildArchPackage(t, "dash", "2-1", "any"), + } + + PacmanDBx86 = BuildPacmanDb(t, + secondDatabaseBatch[0].Pkg, + firstDatabaseBatch[2].Pkg, + secondDatabaseBatch[2].Pkg, + ) + + PacmanDBi686 = BuildPacmanDb(t, + firstDatabaseBatch[0].Pkg, + secondDatabaseBatch[1].Pkg, + secondDatabaseBatch[2].Pkg, + ) + + signdata = []byte{1, 2, 3, 4} + ) + + t.Run("PushWithSignature", func(t *testing.T) { + for _, p := range pushBatch { + t.Run(p.File, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + url := fmt.Sprintf( + "/api/packages/%s/arch/push/%s/archlinux/%s", + user.Name, p.File, hex.EncodeToString(signdata), + ) + + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + + pv, err := packages.GetVersionByNameAndVersion( + db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, + ) + assert.NoError(t, err) + + pf, err := packages.GetFileForVersionByName( + db.DefaultContext, pv.ID, p.File, "archlinux", + ) + assert.NoError(t, err) + assert.NotNil(t, pf) + + pps, err := packages.GetPropertiesByName( + db.DefaultContext, packages.PropertyTypeFile, + pf.ID, arch.PropertySignature, + ) + assert.NoError(t, err) + assert.Len(t, pps, 1) + }) + } + }) + + t.Run("PushWithoutSignature", func(t *testing.T) { + for _, p := range pushBatch { + t.Run(p.File, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + url := fmt.Sprintf( + "/api/packages/%s/arch/push/%s/parabola", + user.Name, p.File, + ) + + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + + pv, err := packages.GetVersionByNameAndVersion( + db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, + ) + assert.NoError(t, err) + + pf, err := packages.GetFileForVersionByName( + db.DefaultContext, pv.ID, p.File, "parabola", + ) + assert.NoError(t, err) + assert.NotNil(t, pf) + }) + } + }) + + t.Run("GetPackage", func(t *testing.T) { + for _, p := range pushBatch { + t.Run(p.File, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + url := fmt.Sprintf( + "/api/packages/%s/arch/push/%s/artix/%s", + user.Name, p.File, hex.EncodeToString(signdata), + ) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + + url = fmt.Sprintf( + "/api/packages/%s/arch/artix/%s/%s", + user.Name, p.Arch, p.File, + ) + req = NewRequest(t, "GET", url) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, p.Data, resp.Body.Bytes()) + }) + } + }) + + t.Run("GetSignature", func(t *testing.T) { + for _, p := range pushBatch { + t.Run(p.File, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + url := fmt.Sprintf( + "/api/packages/%s/arch/push/%s/arco/%s", + user.Name, p.File, hex.EncodeToString(signdata), + ) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + + url = fmt.Sprintf( + "/api/packages/%s/arch/arco/%s/%s.sig", + user.Name, p.Arch, p.File, + ) + req = NewRequest(t, "GET", url) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, signdata, resp.Body.Bytes()) + }) + } + }) + + t.Run("Remove", func(t *testing.T) { + for _, p := range removeBatch { + t.Run(p.File, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + url := fmt.Sprintf( + "/api/packages/%s/arch/push/%s/manjaro/%s", + user.Name, p.File, hex.EncodeToString(signdata), + ) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + + url = fmt.Sprintf( + "/api/packages/%s/arch/remove/%s/%s", + user.Name, p.Name, p.Ver, + ) + req = NewRequest(t, "DELETE", url) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + + _, err := packages.GetVersionByNameAndVersion( + db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, + ) + assert.ErrorIs(t, err, packages.ErrPackageNotExist) + }) + } + }) + + t.Run("PacmanDatabase", func(t *testing.T) { + prepareDatabasePackages := func(t *testing.T) { + for _, p := range firstDatabaseBatch { + url := fmt.Sprintf( + "/api/packages/%s/arch/push/%s/ion/%s", + user.Name, p.File, hex.EncodeToString(signdata), + ) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + } + + // While creating pacman database, package versions are sorted by + // UnixTime, second delay is required to ensure that newer package + // version creation time differs from older packages. + time.Sleep(time.Second) + + for _, p := range secondDatabaseBatch { + url := fmt.Sprintf( + "/api/packages/%s/arch/push/%s/ion/%s", + user.Name, p.File, hex.EncodeToString(signdata), + ) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + } + } + + t.Run("x86_64", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + prepareDatabasePackages(t) + + url := fmt.Sprintf( + "/api/packages/%s/arch/ion/x86_64/user.db.tar.gz", user.Name, + ) + req := NewRequest(t, "GET", url) + resp := MakeRequest(t, req, http.StatusOK) + + CompareTarGzEntries(t, PacmanDBx86, resp.Body.Bytes()) + }) + + t.Run("i686", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + prepareDatabasePackages(t) + + url := fmt.Sprintf( + "/api/packages/%s/arch/ion/i686/user.db", user.Name, + ) + req := NewRequest(t, "GET", url) + resp := MakeRequest(t, req, http.StatusOK) + + CompareTarGzEntries(t, PacmanDBi686, resp.Body.Bytes()) + }) + }) +} + +type TestArchPackage struct { + Pkg arch.Package + Data []byte + File string + Name string + Ver string + Arch string +} + +func BuildArchPackage(t *testing.T, name, ver, architecture string) *TestArchPackage { + fs := fstest.MapFS{ + "pkginfo": &fstest.MapFile{ + Data: []byte(fmt.Sprintf( + "pkgname = %s\npkgbase = %s\npkgver = %s\narch = %s\n", + name, name, ver, architecture, + )), + Mode: os.ModePerm, + ModTime: time.Now(), + }, + "mtree": &fstest.MapFile{ + Data: []byte("test"), + Mode: os.ModePerm, + ModTime: time.Now(), + }, + } + + pinf, err := fs.Stat("pkginfo") + assert.NoError(t, err) + + pfile, err := fs.Open("pkginfo") + assert.NoError(t, err) + + parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO") + assert.NoError(t, err) + + minf, err := fs.Stat("mtree") + assert.NoError(t, err) + + mfile, err := fs.Open("mtree") + assert.NoError(t, err) + + marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE") + assert.NoError(t, err) + + var buf bytes.Buffer + + archive := archiver.NewTarZstd() + archive.Create(&buf) + + err = archive.Write(archiver.File{ + FileInfo: archiver.FileInfo{ + FileInfo: pinf, + CustomName: parcname, + }, + ReadCloser: pfile, + }) + assert.NoError(t, errors.Join(pfile.Close(), err)) + + err = archive.Write(archiver.File{ + FileInfo: archiver.FileInfo{ + FileInfo: minf, + CustomName: marcname, + }, + ReadCloser: mfile, + }) + assert.NoError(t, errors.Join(mfile.Close(), archive.Close(), err)) + + md5, sha256, size := archPkgParams(buf.Bytes()) + + return &TestArchPackage{ + Data: buf.Bytes(), + Name: name, + Ver: ver, + Arch: architecture, + File: fmt.Sprintf("%s-%s-%s.pkg.tar.zst", name, ver, architecture), + Pkg: arch.Package{ + Name: name, + Version: ver, + VersionMetadata: arch.VersionMetadata{ + Base: name, + }, + FileMetadata: arch.FileMetadata{ + CompressedSize: size, + MD5: hex.EncodeToString(md5), + SHA256: hex.EncodeToString(sha256), + Arch: architecture, + }, + }, + } +} + +func archPkgParams(b []byte) ([]byte, []byte, int64) { + md5 := md5.New() + sha256 := sha256.New() + c := counter{bytes.NewReader(b), 0} + + br := bufio.NewReader(io.TeeReader(&c, io.MultiWriter(md5, sha256))) + + io.ReadAll(br) + return md5.Sum(nil), sha256.Sum(nil), int64(c.n) +} + +type counter struct { + io.Reader + n int +} + +func (w *counter) Read(p []byte) (int, error) { + n, err := w.Reader.Read(p) + w.n += n + return n, err +} + +func BuildPacmanDb(t *testing.T, pkgs ...arch.Package) []byte { + entries := map[string][]byte{} + for _, p := range pkgs { + entries[fmt.Sprintf("%s-%s/desc", p.Name, p.Version)] = []byte(p.Desc()) + } + b, err := arch.CreatePacmanDb(entries) + if err != nil { + assert.NoError(t, err) + return nil + } + return b.Bytes() +} + +func CompareTarGzEntries(t *testing.T, expected, actual []byte) { + fgz, err := gzip.NewReader(bytes.NewReader(expected)) + if err != nil { + assert.NoError(t, err) + return + } + ftar := tar.NewReader(fgz) + + validatemap := map[string]struct{}{} + + for { + h, err := ftar.Next() + if err != nil { + break + } + + validatemap[h.Name] = struct{}{} + } + + sgz, err := gzip.NewReader(bytes.NewReader(actual)) + if err != nil { + assert.NoError(t, err) + return + } + star := tar.NewReader(sgz) + + for { + h, err := star.Next() + if err != nil { + break + } + + _, ok := validatemap[h.Name] + if !ok { + assert.Fail(t, "Unexpected entry in archive: "+h.Name) + } + delete(validatemap, h.Name) + } + + if len(validatemap) == 0 { + return + } + + for e := range validatemap { + assert.Fail(t, "Entry not found in archive: "+e) + } +} diff --git a/web_src/svg/gitea-arch.svg b/web_src/svg/gitea-arch.svg new file mode 100644 index 0000000000000..ba8254d8049e7 --- /dev/null +++ b/web_src/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file From 4fac06b0044af524e2d081781221922d24782596 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 20 May 2024 19:29:56 +0000 Subject: [PATCH 2/9] Reimplement arch package registry. --- models/packages/arch/search.go | 38 +++ modules/packages/arch/metadata.go | 279 ++++++------------ options/locale/locale_en-US.ini | 15 +- routers/api/packages/api.go | 12 +- routers/api/packages/arch/arch.go | 238 +++++++++++---- routers/web/user/package.go | 20 +- services/packages/alpine/repository.go | 2 +- services/packages/arch/repository.go | 391 +++++++++++++++++++++++++ services/packages/arch/service.go | 133 --------- services/packages/arch/upload.go | 83 ------ templates/package/content/arch.tmpl | 75 ++--- templates/package/metadata/arch.tmpl | 2 +- 12 files changed, 763 insertions(+), 525 deletions(-) create mode 100644 models/packages/arch/search.go create mode 100644 services/packages/arch/repository.go delete mode 100644 services/packages/arch/service.go delete mode 100644 services/packages/arch/upload.go diff --git a/models/packages/arch/search.go b/models/packages/arch/search.go new file mode 100644 index 0000000000000..f35c037b23a58 --- /dev/null +++ b/models/packages/arch/search.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "context" + + packages_model "code.gitea.io/gitea/models/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" +) + +// GetRepositories gets all available repositories +func GetRepositories(ctx context.Context, ownerID int64) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeArch, + ownerID, + packages_model.PropertyTypeFile, + arch_module.PropertyRepository, + nil, + ) +} + +// GetArchitectures gets all available architectures for the given repository +func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeArch, + ownerID, + packages_model.PropertyTypeFile, + arch_module.PropertyArchitecture, + &packages_model.DistinctPropertyDependency{ + Name: arch_module.PropertyRepository, + Value: repository, + }, + ) +} diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go index 4175cb46e8d2a..5236ea13af47d 100644 --- a/modules/packages/arch/metadata.go +++ b/modules/packages/arch/metadata.go @@ -6,13 +6,7 @@ package arch import ( "archive/tar" "bufio" - "bytes" - "compress/gzip" - "encoding/hex" - "errors" - "fmt" "io" - "os" "regexp" "strconv" "strings" @@ -20,20 +14,33 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" - "github.com/mholt/archiver/v3" + "github.com/klauspost/compress/zstd" ) const ( - PropertyDescription = "arch.description" - PropertySignature = "arch.signature" + PropertyRepository = "arch.repository" + PropertyArchitecture = "arch.architecture" + PropertySignature = "arch.signature" + PropertyMetadata = "arch.metadata" + + SettingKeyPrivate = "arch.key.private" + SettingKeyPublic = "arch.key.public" + + RepositoryPackage = "_arch" + RepositoryVersion = "_repository" + + AnyArch = "any" ) var ( + ErrMissingPkgInfoFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing") + ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") + ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") + // https://man.archlinux.org/man/PKGBUILD.5 - reName = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$`) - reVer = regexp.MustCompile(`^[a-zA-Z0-9:_.+]+-+[0-9]+$`) - reOptDep = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$|^[a-zA-Z0-9@._+-]+(:.*)`) - rePkgVer = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$|^[a-zA-Z0-9@._+-]+(>.*)|^[a-zA-Z0-9@._+-]+(<.*)|^[a-zA-Z0-9@._+-]+(=.*)`) + namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`) + versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) ) type Package struct { @@ -43,90 +50,83 @@ type Package struct { FileMetadata FileMetadata } -// Arch package metadata related to specific version. -// Version metadata the same across different architectures and distributions. type VersionMetadata struct { - Base string `json:"base"` - Description string `json:"description"` - ProjectURL string `json:"project_url"` - Groups []string `json:"groups,omitempty"` - Provides []string `json:"provides,omitempty"` - License []string `json:"license,omitempty"` - Depends []string `json:"depends,omitempty"` - OptDepends []string `json:"opt_depends,omitempty"` - MakeDepends []string `json:"make_depends,omitempty"` - CheckDepends []string `json:"check_depends,omitempty"` - Backup []string `json:"backup,omitempty"` + Description string `json:"description,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Licenses []string `json:"licenses,omitempty"` } -// Metadata related to specific pakcage file. -// This metadata might vary for different architecture and distribution. type FileMetadata struct { - CompressedSize int64 `json:"compressed_size"` - InstalledSize int64 `json:"installed_size"` - MD5 string `json:"md5"` - SHA256 string `json:"sha256"` - BuildDate int64 `json:"build_date"` - Packager string `json:"packager"` - Arch string `json:"arch"` + Architecture string `json:"architecture"` + Base string `json:"base,omitempty"` + InstalledSize int64 `json:"installed_size,omitempty"` + BuildDate int64 `json:"build_date,omitempty"` + Packager string `json:"packager,omitempty"` + Groups []string `json:"groups,omitempty"` + Provides []string `json:"provides,omitempty"` + Depends []string `json:"depends,omitempty"` + OptDepends []string `json:"opt_depends,omitempty"` + MakeDepends []string `json:"make_depends,omitempty"` + CheckDepends []string `json:"check_depends,omitempty"` + XData []string `json:"xdata,omitempty"` + Backup []string `json:"backup,omitempty"` + Files []string `json:"files,omitempty"` } -// Function that receives arch package archive data and returns it's metadata. -func ParsePackage(r io.Reader, md5, sha256 []byte, size int64) (*Package, error) { - zstd := archiver.NewTarZstd() - err := zstd.Open(r, 0) +// ParsePackage parses an Arch package file +func ParsePackage(r io.Reader) (*Package, error) { + zr, err := zstd.NewReader(r) if err != nil { return nil, err } - defer zstd.Close() + defer zr.Close() - var pkg *Package - var mtree bool + var p *Package + files := make([]string, 0, 10) + tr := tar.NewReader(zr) for { - f, err := zstd.Read() + hd, err := tr.Next() if err == io.EOF { break } if err != nil { return nil, err } - defer f.Close() - switch f.Name() { - case ".PKGINFO": - pkg, err = ParsePackageInfo(f) + if hd.Typeflag != tar.TypeReg { + continue + } + + filename := hd.FileInfo().Name() + if filename == ".PKGINFO" { + p, err = ParsePackageInfo(tr) if err != nil { return nil, err } - case ".MTREE": - mtree = true + } else if !strings.HasPrefix(filename, ".") { + files = append(files, hd.Name) } } - if pkg == nil { - return nil, util.NewInvalidArgumentErrorf(".PKGINFO file not found") - } - - if !mtree { - return nil, util.NewInvalidArgumentErrorf(".MTREE file not found") + if p == nil { + return nil, ErrMissingPkgInfoFile } - pkg.FileMetadata.CompressedSize = size - pkg.FileMetadata.SHA256 = hex.EncodeToString(sha256) - pkg.FileMetadata.MD5 = hex.EncodeToString(md5) + p.FileMetadata.Files = files - return pkg, nil + return p, nil } -// Function that accepts reader for .PKGINFO file from package archive, -// validates all field according to PKGBUILD spec and returns package. +// ParsePackageInfo parses a .PKGINFO file to retrieve the metadata +// https://man.archlinux.org/man/PKGBUILD.5 +// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_package.c#L161 func ParsePackageInfo(r io.Reader) (*Package, error) { p := &Package{} - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := scanner.Text() + s := bufio.NewScanner(r) + for s.Scan() { + line := s.Text() if strings.HasPrefix(line, "#") { continue @@ -144,7 +144,7 @@ func ParsePackageInfo(r io.Reader) (*Package, error) { case "pkgname": p.Name = value case "pkgbase": - p.VersionMetadata.Base = value + p.FileMetadata.Base = value case "pkgver": p.Version = value case "pkgdesc": @@ -154,149 +154,56 @@ func ParsePackageInfo(r io.Reader) (*Package, error) { case "packager": p.FileMetadata.Packager = value case "arch": - p.FileMetadata.Arch = value - case "provides": - p.VersionMetadata.Provides = append(p.VersionMetadata.Provides, value) + p.FileMetadata.Architecture = value case "license": - p.VersionMetadata.License = append(p.VersionMetadata.License, value) + p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value) + case "provides": + p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) case "depend": - p.VersionMetadata.Depends = append(p.VersionMetadata.Depends, value) + p.FileMetadata.Depends = append(p.FileMetadata.Depends, value) case "optdepend": - p.VersionMetadata.OptDepends = append(p.VersionMetadata.OptDepends, value) + p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value) case "makedepend": - p.VersionMetadata.MakeDepends = append(p.VersionMetadata.MakeDepends, value) + p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value) case "checkdepend": - p.VersionMetadata.CheckDepends = append(p.VersionMetadata.CheckDepends, value) + p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value) case "backup": - p.VersionMetadata.Backup = append(p.VersionMetadata.Backup, value) + p.FileMetadata.Backup = append(p.FileMetadata.Backup, value) case "group": - p.VersionMetadata.Groups = append(p.VersionMetadata.Groups, value) + p.FileMetadata.Groups = append(p.FileMetadata.Groups, value) case "builddate": - bd, err := strconv.ParseInt(value, 10, 64) + date, err := strconv.ParseInt(value, 10, 64) if err != nil { return nil, err } - p.FileMetadata.BuildDate = bd + p.FileMetadata.BuildDate = date case "size": - is, err := strconv.ParseInt(value, 10, 64) + size, err := strconv.ParseInt(value, 10, 64) if err != nil { return nil, err } - p.FileMetadata.InstalledSize = is - } - } - - return p, errors.Join(scanner.Err(), ValidatePackageSpec(p)) -} - -// Arch package validation according to PKGBUILD specification. -func ValidatePackageSpec(p *Package) error { - if !reName.MatchString(p.Name) { - return util.NewInvalidArgumentErrorf("invalid package name") - } - if !reName.MatchString(p.VersionMetadata.Base) { - return util.NewInvalidArgumentErrorf("invalid package base") - } - if !reVer.MatchString(p.Version) { - return util.NewInvalidArgumentErrorf("invalid package version") - } - if p.FileMetadata.Arch == "" { - return util.NewInvalidArgumentErrorf("architecture should be specified") - } - if p.VersionMetadata.ProjectURL != "" { - if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { - return util.NewInvalidArgumentErrorf("invalid project URL") - } - } - for _, cd := range p.VersionMetadata.CheckDepends { - if !rePkgVer.MatchString(cd) { - return util.NewInvalidArgumentErrorf("invalid check dependency: " + cd) + p.FileMetadata.InstalledSize = size + case "xdata": + p.FileMetadata.XData = append(p.FileMetadata.XData, value) } } - for _, d := range p.VersionMetadata.Depends { - if !rePkgVer.MatchString(d) { - return util.NewInvalidArgumentErrorf("invalid dependency: " + d) - } - } - for _, md := range p.VersionMetadata.MakeDepends { - if !rePkgVer.MatchString(md) { - return util.NewInvalidArgumentErrorf("invalid make dependency: " + md) - } - } - for _, p := range p.VersionMetadata.Provides { - if !rePkgVer.MatchString(p) { - return util.NewInvalidArgumentErrorf("invalid provides: " + p) - } - } - for _, od := range p.VersionMetadata.OptDepends { - if !reOptDep.MatchString(od) { - return util.NewInvalidArgumentErrorf("invalid optional dependency: " + od) - } - } - for _, bf := range p.VersionMetadata.Backup { - if strings.HasPrefix(bf, "/") { - return util.NewInvalidArgumentErrorf("backup file contains leading forward slash") - } + if err := s.Err(); err != nil { + return nil, err } - return nil -} -// Create pacman package description file. -func (p *Package) Desc() string { - entries := [40]string{ - "FILENAME", fmt.Sprintf("%s-%s-%s.pkg.tar.zst", p.Name, p.Version, p.FileMetadata.Arch), - "NAME", p.Name, - "BASE", p.VersionMetadata.Base, - "VERSION", p.Version, - "DESC", p.VersionMetadata.Description, - "GROUPS", strings.Join(p.VersionMetadata.Groups, "\n"), - "CSIZE", fmt.Sprintf("%d", p.FileMetadata.CompressedSize), - "ISIZE", fmt.Sprintf("%d", p.FileMetadata.InstalledSize), - "MD5SUM", p.FileMetadata.MD5, - "SHA256SUM", p.FileMetadata.SHA256, - "URL", p.VersionMetadata.ProjectURL, - "LICENSE", strings.Join(p.VersionMetadata.License, "\n"), - "ARCH", p.FileMetadata.Arch, - "BUILDDATE", fmt.Sprintf("%d", p.FileMetadata.BuildDate), - "PACKAGER", p.FileMetadata.Packager, - "PROVIDES", strings.Join(p.VersionMetadata.Provides, "\n"), - "DEPENDS", strings.Join(p.VersionMetadata.Depends, "\n"), - "OPTDEPENDS", strings.Join(p.VersionMetadata.OptDepends, "\n"), - "MAKEDEPENDS", strings.Join(p.VersionMetadata.MakeDepends, "\n"), - "CHECKDEPENDS", strings.Join(p.VersionMetadata.CheckDepends, "\n"), + if !namePattern.MatchString(p.Name) { + return nil, ErrInvalidName } - - var buf bytes.Buffer - for i := 0; i < 40; i += 2 { - if entries[i+1] != "" { - fmt.Fprintf(&buf, "%%%s%%\n%s\n\n", entries[i], entries[i+1]) - } + if !versionPattern.MatchString(p.Version) { + return nil, ErrInvalidVersion + } + if p.FileMetadata.Architecture == "" { + return nil, ErrInvalidArchitecture } - return buf.String() -} - -// Create pacman database archive based on provided package metadata structs. -func CreatePacmanDb(entries map[string][]byte) (*bytes.Buffer, error) { - var b bytes.Buffer - - gw := gzip.NewWriter(&b) - tw := tar.NewWriter(gw) - - for name, content := range entries { - header := &tar.Header{ - Name: name, - Size: int64(len(content)), - Mode: int64(os.ModePerm), - } - - if err := tw.WriteHeader(header); err != nil { - return nil, errors.Join(err, tw.Close(), gw.Close()) - } - if _, err := tw.Write(content); err != nil { - return nil, errors.Join(err, tw.Close(), gw.Close()) - } + if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { + p.VersionMetadata.ProjectURL = "" } - return &b, errors.Join(tw.Close(), gw.Close()) + return p, nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ec328510e3f62..72d2d51e08d46 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3449,16 +3449,11 @@ alpine.repository = Repository Info alpine.repository.branches = Branches alpine.repository.repositories = Repositories alpine.repository.architectures = Architectures -arch.pacmanconf = Add server with related distribution and architecture to /etc/pacman.conf: -arch.pacmansync = Sync package with pacman: -arch.documentation = For more information on the arch mirrors, see %sthe documentation%s. -arch.properties = Package properties -arch.description = Description -arch.provides = Provides -arch.depends = Depends -arch.optdepends = Optional depends -arch.makedepends = Make depends -arch.checkdepends = Check depends +arch.registry = Add server with related repository and architecture to /etc/pacman.conf: +arch.install = Sync package with pacman: +arch.repository = Repository Info +arch.repository.repositories = Repositories +arch.repository.architectures = Architectures cargo.registry = Setup this registry in the Cargo configuration file (for example ~/.cargo/config.toml): cargo.install = To install the package using Cargo, run the following command: chef.registry = Setup this registry in your ~/.chef/config.rb file: diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 872e0827bc945..c91ddc9c76ea1 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -123,10 +123,14 @@ func CommonRoutes() *web.Route { }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { - r.Put("/push/{filename}/{distro}", reqPackageAccess(perm.AccessModeWrite), arch.Push) - r.Put("/push/{filename}/{distro}/{sign}", reqPackageAccess(perm.AccessModeWrite), arch.Push) - r.Delete("/remove/{package}/{version}", reqPackageAccess(perm.AccessModeWrite), arch.Remove) - r.Get("/{distro}/{arch}/{file}", arch.Get) + r.Get("/key", arch.GetRepositoryKey) + r.Group("/{repository}", func() { + r.Put("", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + r.Group("/{architecture}/{filename}", func() { + r.Get("", arch.DownloadPackageOrRepositoryFile) + r.Delete("", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageFile) + }) + }) }) r.Group("/cargo", func() { r.Group("/api/v1/crates", func() { diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 358ca81c6e731..32ace1c900483 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -5,12 +5,20 @@ package arch import ( "bytes" + "encoding/base64" + "errors" + "fmt" + "io" "net/http" "strings" packages_model "code.gitea.io/gitea/models/packages" - "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + 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" ) @@ -21,24 +29,101 @@ func apiError(ctx *context.Context, status int, obj any) { }) } -// Push new package to arch package registry. -func Push(ctx *context.Context) { - var ( - filename = ctx.Params("filename") - distro = ctx.Params("distro") - sign = ctx.Params("sign") - ) +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", + }) +} + +func UploadPackageFile(ctx *context.Context) { + repository := strings.TrimSpace(ctx.Params("repository")) + if repository == "" { + apiError(ctx, http.StatusBadRequest, "invalid repository") + return + } - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - if close { + if needToClose { defer upload.Close() } - _, _, err = arch_service.UploadArchPackage(ctx, upload, filename, distro, sign) + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + pck, err := arch_module.ParsePackage(buf) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + apiError(ctx, http.StatusBadRequest, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + fileMetadataRaw, err := json.Marshal(pck.FileMetadata) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + signature, err := arch_service.SignData(ctx, ctx.Package.Owner.ID, buf) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeArch, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + Metadata: pck.VersionMetadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s-%s.pck.tar.zst", pck.Name, pck.Version, pck.FileMetadata.Architecture), + CompositeKey: fmt.Sprintf("%s|%s", repository, pck.FileMetadata.Architecture), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: pck.FileMetadata.Architecture, + arch_module.PropertyMetadata: string(fileMetadataRaw), + arch_module.PropertySignature: base64.StdEncoding.EncodeToString(signature), + }, + }, + ) if err != nil { switch err { case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: @@ -51,85 +136,130 @@ func Push(ctx *context.Context) { return } - ctx.Status(http.StatusOK) + if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, pck.FileMetadata.Architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusCreated) } -// Get file from arch package registry. -func Get(ctx *context.Context) { - var ( - file = ctx.Params("file") - owner = ctx.Params("username") - distro = ctx.Params("distro") - arch = ctx.Params("arch") - ) +func DownloadPackageOrRepositoryFile(ctx *context.Context) { + repository := ctx.Params("repository") + architecture := ctx.Params("architecture") + filename := ctx.Params("filename") + filenameOrig := filename + + isSignature := strings.HasSuffix(filename, ".sig") + if isSignature { + filename = filename[:len(filename)-len(".sig")] + } + + opts := &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeArch, + Query: filename, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + } - if strings.HasSuffix(file, ".pkg.tar.zst") { - pkg, err := arch_service.GetPackageFile(ctx, distro, file) + if strings.HasSuffix(filename, ".db.tar.gz") || strings.HasSuffix(filename, ".files.tar.gz") || strings.HasSuffix(filename, ".files") || strings.HasSuffix(filename, ".db") { + // normalize to packages.db + opts.Query = arch_service.IndexArchiveFilename + + pv, err := arch_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID) if err != nil { - apiError(ctx, http.StatusNotFound, err) + apiError(ctx, http.StatusInternalServerError, err) return } + opts.VersionID = pv.ID + } - ctx.ServeContent(pkg, &context.ServeHeaderOptions{ - Filename: file, - }) + pfs, _, err := packages_model.SearchFiles(ctx, opts) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) return } - - if strings.HasSuffix(file, ".pkg.tar.zst.sig") { - sig, err := arch_service.GetPackageSignature(ctx, distro, file) - if err != nil { - apiError(ctx, http.StatusNotFound, err) + if len(pfs) == 0 { + // Try again with architecture 'any' + if architecture == arch_module.AnyArch { + apiError(ctx, http.StatusNotFound, nil) return } - ctx.ServeContent(sig, &context.ServeHeaderOptions{ - Filename: file, - }) + opts.CompositeKey = fmt.Sprintf("%s|%s", repository, arch_module.AnyArch) + if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) return } - if strings.HasSuffix(file, ".db.tar.gz") || strings.HasSuffix(file, ".db") { - db, err := arch_service.CreatePacmanDb(ctx, owner, arch, distro) + if isSignature { + pfps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pfs[0].ID, arch_module.PropertySignature) + if err != nil || len(pfps) == 0 { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + data, err := base64.StdEncoding.DecodeString(pfps[0].Value) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - ctx.ServeContent(bytes.NewReader(db.Bytes()), &context.ServeHeaderOptions{ - Filename: file, + ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{ + Filename: filenameOrig, }) return } - ctx.Status(http.StatusNotFound) + s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + helper.ServePackageFile(ctx, s, u, pf) } -// Remove specific package version, related files with properties. -func Remove(ctx *context.Context) { - var ( - pkg = ctx.Params("package") - ver = ctx.Params("version") - ) +func DeletePackageFile(ctx *context.Context) { + repository, architecture := ctx.Params("repository"), ctx.Params("architecture") - version, err := packages_model.GetVersionByNameAndVersion( - ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg, ver, - ) + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeArch, + Query: ctx.Params("filename"), + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + }) if err != nil { - switch err { - case packages_model.ErrPackageNotExist: + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + if err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx, ctx.Doer, pfs[0]); err != nil { + if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) - default: + } else { apiError(ctx, http.StatusInternalServerError, err) } return } - err = packages_service.RemovePackageVersion(ctx, ctx.Package.Owner, version) - if err != nil { + if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, architecture); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - ctx.Status(http.StatusOK) + ctx.Status(http.StatusNoContent) } diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 8cef1afba9402..af69e2e242318 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" alpine_module "code.gitea.io/gitea/modules/packages/alpine" + arch_module "code.gitea.io/gitea/modules/packages/arch" debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" "code.gitea.io/gitea/modules/setting" @@ -177,7 +178,7 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["PackageDescriptor"] = pd switch pd.Package.Type { - case packages_model.TypeContainer, packages_model.TypeArch: + case packages_model.TypeContainer: ctx.Data["RegistryHost"] = setting.Packages.RegistryHost case packages_model.TypeAlpine: branches := make(container.Set[string]) @@ -198,6 +199,23 @@ func ViewPackageVersion(ctx *context.Context) { } ctx.Data["Branches"] = util.Sorted(branches.Values()) + ctx.Data["Repositories"] = util.Sorted(repositories.Values()) + ctx.Data["Architectures"] = util.Sorted(architectures.Values()) + case packages_model.TypeArch: + repositories := make(container.Set[string]) + architectures := make(container.Set[string]) + + for _, f := range pd.Files { + for _, pp := range f.Properties { + switch pp.Name { + case arch_module.PropertyRepository: + repositories.Add(pp.Value) + case arch_module.PropertyArchitecture: + architectures.Add(pp.Value) + } + } + } + ctx.Data["Repositories"] = util.Sorted(repositories.Values()) ctx.Data["Architectures"] = util.Sorted(architectures.Values()) case packages_model.TypeDebian: diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go index 664ab3455980d..27e63919803eb 100644 --- a/services/packages/alpine/repository.go +++ b/services/packages/alpine/repository.go @@ -72,7 +72,7 @@ func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, err return priv, pub, nil } -// BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures +// BuildAllRepositoryFiles (re)builds all repository files for every available branches, repositories and architectures func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) if err != nil { diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go new file mode 100644 index 0000000000000..5e0c980415d6a --- /dev/null +++ b/services/packages/arch/repository.go @@ -0,0 +1,391 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "os" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + arch_model "code.gitea.io/gitea/models/packages/arch" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" + "code.gitea.io/gitea/modules/util" + packages_service "code.gitea.io/gitea/services/packages" + + "github.com/keybase/go-crypto/openpgp" + "github.com/keybase/go-crypto/openpgp/armor" + "github.com/keybase/go-crypto/openpgp/packet" +) + +const ( + IndexArchiveFilename = "packages.db" +) + +// GetOrCreateRepositoryVersion gets or creates the internal repository package +// The Arch registry needs multiple index files which are stored in this package. +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) +} + +// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository 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 == "" { + priv, pub, err = generateKeypair() + 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() (string, string, error) { + e, err := openpgp.NewEntity("", "Arch Registry", "", nil) + 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 +} + +func SignData(ctx context.Context, ownerID int64, r io.Reader) ([]byte, error) { + 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 + } + + buf := &bytes.Buffer{} + if err := openpgp.DetachSign(buf, e, r, nil); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// BuildAllRepositoryFiles (re)builds all repository files for every available repositories and architectures +func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { + pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) + if err != nil { + return err + } + + // 1. Delete all existing repository files + pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) + if err != nil { + return err + } + + for _, pf := range pfs { + if err := packages_service.DeletePackageFile(ctx, pf); err != nil { + return err + } + } + + // 2. (Re)Build repository files for existing packages + repositories, err := arch_model.GetRepositories(ctx, ownerID) + if err != nil { + return err + } + for _, repository := range repositories { + architectures, err := arch_model.GetArchitectures(ctx, ownerID, repository) + if err != nil { + return err + } + for _, architecture := range architectures { + if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil { + return fmt.Errorf("failed to build repository files [%s/%s]: %w", repository, architecture, err) + } + } + } + + return nil +} + +// BuildSpecificRepositoryFiles builds index files for the repository +func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, repository, architecture string) error { + pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) + if err != nil { + return err + } + + architectures := container.SetOf(architecture) + if architecture == arch_module.AnyArch { + // Update all other architectures too when updating the any index + additionalArchitectures, err := arch_model.GetArchitectures(ctx, ownerID, repository) + if err != nil { + return err + } + architectures.AddMultiple(additionalArchitectures...) + } + + for architecture := range architectures { + if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil { + return err + } + } + return nil +} + +func searchPackageFiles(ctx context.Context, ownerID int64, repository, architecture string) ([]*packages_model.PackageFile, error) { + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ownerID, + PackageType: packages_model.TypeArch, + Query: "%.pck.tar.zst", + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: architecture, + }, + }) + if err != nil { + return nil, err + } + return pfs, nil +} + +func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, repository, architecture string) error { + pfs, err := searchPackageFiles(ctx, ownerID, repository, architecture) + if err != nil { + return err + } + if architecture != arch_module.AnyArch { + // Add all any packages too + anyarchFiles, err := searchPackageFiles(ctx, ownerID, repository, arch_module.AnyArch) + if err != nil { + return err + } + pfs = append(pfs, anyarchFiles...) + } + + // Delete the package indices if there are no packages + if len(pfs) == 0 { + pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s", repository, architecture)) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } else if pf == nil { + return nil + } + + return packages_service.DeletePackageFile(ctx, pf) + } + + indexContent, _ := packages_module.NewHashedBuffer() + defer indexContent.Close() + + gw := gzip.NewWriter(indexContent) + tw := tar.NewWriter(gw) + + cache := make(map[int64]*packages_model.Package) + + for _, pf := range pfs { + opts := &entryOptions{ + File: pf, + } + + opts.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID) + if err != nil { + return err + } + if err := json.Unmarshal([]byte(opts.Version.MetadataJSON), &opts.VersionMetadata); err != nil { + return err + } + opts.Package = cache[opts.Version.PackageID] + if opts.Package == nil { + opts.Package, err = packages_model.GetPackageByID(ctx, opts.Version.PackageID) + if err != nil { + return err + } + cache[opts.Package.ID] = opts.Package + } + opts.Blob, err = packages_model.GetBlobByID(ctx, pf.BlobID) + if err != nil { + return err + } + + sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertySignature) + if err != nil { + return err + } + if len(sig) == 0 { + return util.ErrNotExist + } + opts.Signature = sig[0].Value + + meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyMetadata) + if err != nil { + return err + } + if len(meta) == 0 { + return util.ErrNotExist + } + if err := json.Unmarshal([]byte(meta[0].Value), &opts.FileMetadata); err != nil { + return err + } + + if err := writeFiles(tw, opts); err != nil { + return err + } + if err := writeDescription(tw, opts); err != nil { + return err + } + } + + tw.Close() + gw.Close() + + signature, err := SignData(ctx, ownerID, indexContent) + if err != nil { + return err + } + + if _, err := indexContent.Seek(0, io.SeekStart); err != nil { + return err + } + + _, err = packages_service.AddFileToPackageVersionInternal( + ctx, + repoVersion, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: IndexArchiveFilename, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + }, + Creator: user_model.NewGhostUser(), + Data: indexContent, + IsLead: false, + OverwriteExisting: true, + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: architecture, + arch_module.PropertySignature: base64.StdEncoding.EncodeToString(signature), + }, + }, + ) + return err +} + +type entryOptions struct { + Package *packages_model.Package + Version *packages_model.PackageVersion + VersionMetadata *arch_module.VersionMetadata + File *packages_model.PackageFile + FileMetadata *arch_module.FileMetadata + Blob *packages_model.PackageBlob + Signature string +} + +func writeFiles(tw *tar.Writer, opts *entryOptions) error { + return writeFields(tw, fmt.Sprintf("%s-%s/files", opts.Package.Name, opts.Version.Version), map[string]string{ + "FILES": strings.Join(opts.FileMetadata.Files, "\n"), + }) +} + +// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_sync.c#L562 +func writeDescription(tw *tar.Writer, opts *entryOptions) error { + return writeFields(tw, fmt.Sprintf("%s-%s/desc", opts.Package.Name, opts.Version.Version), map[string]string{ + "FILENAME": opts.File.Name, + "MD5SUM": opts.Blob.HashMD5, + "SHA256SUM": opts.Blob.HashSHA256, + "PGPSIG": opts.Signature, + "CSIZE": fmt.Sprintf("%d", opts.Blob.Size), + "ISIZE": fmt.Sprintf("%d", opts.FileMetadata.InstalledSize), + "NAME": opts.Package.Name, + "BASE": opts.FileMetadata.Base, + "ARCH": opts.FileMetadata.Architecture, + "VERSION": opts.Version.Version, + "DESC": opts.VersionMetadata.Description, + "URL": opts.VersionMetadata.ProjectURL, + "LICENSE": strings.Join(opts.VersionMetadata.Licenses, "\n"), + "GROUPS": strings.Join(opts.FileMetadata.Groups, "\n"), + "BUILDDATE": fmt.Sprintf("%d", opts.FileMetadata.BuildDate), + "PACKAGER": opts.FileMetadata.Packager, + "PROVIDES": strings.Join(opts.FileMetadata.Provides, "\n"), + "DEPENDS": strings.Join(opts.FileMetadata.Depends, "\n"), + "OPTDEPENDS": strings.Join(opts.FileMetadata.OptDepends, "\n"), + "MAKEDEPENDS": strings.Join(opts.FileMetadata.MakeDepends, "\n"), + "CHECKDEPENDS": strings.Join(opts.FileMetadata.CheckDepends, "\n"), + "XDATA": strings.Join(opts.FileMetadata.XData, "\n"), + }) +} + +func writeFields(tw *tar.Writer, filename string, fields map[string]string) error { + buf := &bytes.Buffer{} + for key, value := range fields { + if value == "" { + continue + } + fmt.Fprintf(buf, "%%%s%%\n%s\n\n", key, value) + } + + if err := tw.WriteHeader(&tar.Header{ + Name: filename, + Size: int64(buf.Len()), + Mode: int64(os.ModePerm), + }); err != nil { + return err + } + + _, err := io.Copy(tw, buf) + return err +} diff --git a/services/packages/arch/service.go b/services/packages/arch/service.go deleted file mode 100644 index a5859009c05c0..0000000000000 --- a/services/packages/arch/service.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package arch - -import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "io" - "sort" - "strings" - - packages_model "code.gitea.io/gitea/models/packages" - "code.gitea.io/gitea/modules/context" - arch_module "code.gitea.io/gitea/modules/packages/arch" - packages_service "code.gitea.io/gitea/services/packages" -) - -// Get data related to provided filename and distribution, for package files -// update download counter. -func GetPackageFile(ctx *context.Context, distro, file string) (io.ReadSeekCloser, error) { - pf, err := getPackageFile(ctx, distro, file) - if err != nil { - return nil, err - } - - filestream, _, _, err := packages_service.GetPackageFileStream(ctx, pf) - return filestream, err -} - -// This function will search for package signature and if present, will load it -// from package file properties, and return its byte reader. -func GetPackageSignature(ctx *context.Context, distro, file string) (*bytes.Reader, error) { - pf, err := getPackageFile(ctx, distro, strings.TrimSuffix(file, ".sig")) - if err != nil { - return nil, err - } - - proprs, err := packages_model.GetProperties(ctx, packages_model.PropertyTypeFile, pf.ID) - if err != nil { - return nil, err - } - - for _, pp := range proprs { - if pp.Name == arch_module.PropertySignature { - b, err := hex.DecodeString(pp.Value) - if err != nil { - return nil, err - } - return bytes.NewReader(b), nil - } - } - - return nil, errors.New("signature for requested package not found") -} - -// Ejects parameters required to get package file property from file name. -func getPackageFile(ctx *context.Context, distro, file string) (*packages_model.PackageFile, error) { - var ( - splt = strings.Split(file, "-") - pkgname = strings.Join(splt[0:len(splt)-3], "-") - vername = splt[len(splt)-3] + "-" + splt[len(splt)-2] - ) - - version, err := packages_model.GetVersionByNameAndVersion( - ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkgname, vername, - ) - if err != nil { - return nil, err - } - - pkgfile, err := packages_model.GetFileForVersionByName(ctx, version.ID, file, distro) - if err != nil { - return nil, err - } - return pkgfile, nil -} - -// Finds all arch packages in user/organization scope, each package version -// starting from latest in descending order is checked to be compatible with -// requested combination of architecture and distribution. When/If the first -// compatible version is found, related desc file will be loaded from package -// properties and added to resulting .db.tar.gz archive. -func CreatePacmanDb(ctx *context.Context, owner, arch, distro string) (*bytes.Buffer, error) { - pkgs, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeArch) - if err != nil { - return nil, err - } - - entries := make(map[string][]byte) - - for _, pkg := range pkgs { - versions, err := packages_model.GetVersionsByPackageName( - ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg.Name, - ) - if err != nil { - return nil, err - } - - sort.Slice(versions, func(i, j int) bool { - return versions[i].CreatedUnix > versions[j].CreatedUnix - }) - - for _, ver := range versions { - file := fmt.Sprintf("%s-%s-%s.pkg.tar.zst", pkg.Name, ver.Version, arch) - - pf, err := packages_model.GetFileForVersionByName(ctx, ver.ID, file, distro) - if err != nil { - file = fmt.Sprintf("%s-%s-any.pkg.tar.zst", pkg.Name, ver.Version) - pf, err = packages_model.GetFileForVersionByName(ctx, ver.ID, file, distro) - if err != nil { - continue - } - } - - pps, err := packages_model.GetPropertiesByName( - ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyDescription, - ) - if err != nil { - return nil, err - } - - if len(pps) >= 1 { - entries[pkg.Name+"-"+ver.Version+"/desc"] = []byte(pps[0].Value) - break - } - } - } - - return arch_module.CreatePacmanDb(entries) -} diff --git a/services/packages/arch/upload.go b/services/packages/arch/upload.go deleted file mode 100644 index 221801b4f87b8..0000000000000 --- a/services/packages/arch/upload.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package arch - -import ( - "encoding/hex" - "errors" - "io" - - packages_model "code.gitea.io/gitea/models/packages" - "code.gitea.io/gitea/modules/context" - packages_module "code.gitea.io/gitea/modules/packages" - arch_module "code.gitea.io/gitea/modules/packages/arch" - packages_service "code.gitea.io/gitea/services/packages" -) - -// UploadArchPackage adds an Arch Package to the registry. -// The first return value indictaes if the error is a user error. -func UploadArchPackage(ctx *context.Context, upload io.Reader, filename, distro, sign string) (bool, *packages_model.PackageVersion, error) { - buf, err := packages_module.CreateHashedBufferFromReader(upload) - if err != nil { - return false, nil, err - } - defer buf.Close() - - md5, _, sha256, _ := buf.Sums() - - p, err := arch_module.ParsePackage(buf, md5, sha256, buf.Size()) - if err != nil { - return false, nil, err - } - - _, err = buf.Seek(0, io.SeekStart) - if err != nil { - return false, nil, err - } - - properties := map[string]string{ - arch_module.PropertyDescription: p.Desc(), - } - if sign != "" { - _, err := hex.DecodeString(sign) - if err != nil { - return true, nil, errors.New("unable to decode package signature") - } - properties[arch_module.PropertySignature] = sign - } - - ver, _, err := packages_service.CreatePackageOrAddFileToExisting( - ctx, &packages_service.PackageCreationInfo{ - PackageInfo: packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeArch, - Name: p.Name, - Version: p.Version, - }, - Creator: ctx.Doer, - Metadata: p.VersionMetadata, - }, - &packages_service.PackageFileCreationInfo{ - PackageFileInfo: packages_service.PackageFileInfo{ - Filename: filename, - CompositeKey: distro, - }, - OverwriteExisting: true, - IsLead: true, - Creator: ctx.Doer, - Data: buf, - Properties: properties, - }, - ) - if err != nil { - switch err { - case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile, packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: - return true, nil, err - default: - return false, nil, err - } - } - - return false, ver, nil -} diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl index b457af031a539..32c59a8022dcd 100644 --- a/templates/package/content/arch.tmpl +++ b/templates/package/content/arch.tmpl @@ -2,69 +2,40 @@

{{ctx.Locale.Tr "packages.installation"}}

-
- -
[{{.PackageDescriptor.Owner.LowerName}}.{{.RegistryHost}}]
+				
+				
[{{.PackageDescriptor.Owner.LowerName}}]
 SigLevel = Optional TrustAll
-Server = 
+Server =
-
- +
pacman -Sy {{.PackageDescriptor.Package.LowerName}}
-
- {{ctx.Locale.Tr "packages.arch.documentation" (printf ``) (printf ``) | Safe}} +
-

{{ctx.Locale.Tr "packages.arch.properties"}}

+

{{ctx.Locale.Tr "packages.arch.repository"}}

- - - - - - - - {{if .PackageDescriptor.Metadata.Provides}} - - - - - {{end}} - - {{if .PackageDescriptor.Metadata.Depends}} - - - - - {{end}} - - {{if .PackageDescriptor.Metadata.OptDepends}} - - - - - {{end}} - - {{if .PackageDescriptor.Metadata.MakeDepends}} - - - - - {{end}} - - {{if .PackageDescriptor.Metadata.CheckDepends}} - - - - - {{end}} - -
{{ctx.Locale.Tr "packages.arch.description"}}
{{.PackageDescriptor.Metadata.Description}}
{{ctx.Locale.Tr "packages.arch.provides"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.Provides ", "}}
{{ctx.Locale.Tr "packages.arch.depends"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.Depends ", "}}
{{ctx.Locale.Tr "packages.arch.optdepends"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.OptDepends ", "}}
{{ctx.Locale.Tr "packages.arch.makedepends"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.MakeDepends ", "}}
{{ctx.Locale.Tr "packages.arch.checkdepends"}}
{{StringUtils.Join $.PackageDescriptor.Metadata.CheckDepends ", "}}
+ + + + + + + + + + + +
{{ctx.Locale.Tr "packages.arch.repository.repositories"}}
{{StringUtils.Join .Repositories ", "}}
{{ctx.Locale.Tr "packages.arch.repository.architectures"}}
{{StringUtils.Join .Architectures ", "}}
+ + {{if .PackageDescriptor.Metadata.Description}} +

{{ctx.Locale.Tr "packages.about"}}

+
{{.PackageDescriptor.Metadata.Description}}
+ {{end}} {{end}} diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl index 822973eb7d984..a461d697a4564 100644 --- a/templates/package/metadata/arch.tmpl +++ b/templates/package/metadata/arch.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "arch"}} - {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}
{{end}} {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} {{end}} From f5fad32fd00f7a9c4a07506a5685e0a85e8e0748 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 5 Jun 2024 14:30:03 +0000 Subject: [PATCH 3/9] Add tests. --- modules/packages/arch/metadata.go | 4 +- modules/packages/arch/metadata_test.go | 511 ++++------------- routers/api/packages/arch/arch.go | 3 +- tests/integration/api_packages_arch_test.go | 596 +++++++------------- 4 files changed, 319 insertions(+), 795 deletions(-) diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go index 5236ea13af47d..dbd947761dddf 100644 --- a/modules/packages/arch/metadata.go +++ b/modules/packages/arch/metadata.go @@ -33,7 +33,7 @@ const ( ) var ( - ErrMissingPkgInfoFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing") + ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing") ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") @@ -110,7 +110,7 @@ func ParsePackage(r io.Reader) (*Package, error) { } if p == nil { - return nil, ErrMissingPkgInfoFile + return nil, ErrMissingPKGINFOFile } p.FileMetadata.Files = files diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go index 0f3cede9ccff5..f38ade2e68e1d 100644 --- a/modules/packages/arch/metadata_test.go +++ b/modules/packages/arch/metadata_test.go @@ -4,449 +4,140 @@ package arch import ( + "archive/tar" "bytes" - "encoding/base64" - "errors" "io" - "os" - "strings" "testing" - "testing/fstest" - "time" - "github.com/mholt/archiver/v3" + "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" ) -func TestParsePackage(t *testing.T) { - // Minimal PKGINFO contents and test FS - const PKGINFO = `pkgname = a -pkgbase = b -pkgver = 1-2 -arch = x86_64 -` - fs := fstest.MapFS{ - "pkginfo": &fstest.MapFile{ - Data: []byte(PKGINFO), - Mode: os.ModePerm, - ModTime: time.Now(), - }, - "mtree": &fstest.MapFile{ - Data: []byte("data"), - Mode: os.ModePerm, - ModTime: time.Now(), - }, - } - - // Test .PKGINFO file - pinf, err := fs.Stat("pkginfo") - assert.NoError(t, err) - - pfile, err := fs.Open("pkginfo") - assert.NoError(t, err) - - parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO") - assert.NoError(t, err) - - // Test .MTREE file - minf, err := fs.Stat("mtree") - assert.NoError(t, err) - - mfile, err := fs.Open("mtree") - assert.NoError(t, err) - - marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE") - assert.NoError(t, err) - - t.Run("normal archive", func(t *testing.T) { - var buf bytes.Buffer - - archive := archiver.NewTarZstd() - archive.Create(&buf) - - err = archive.Write(archiver.File{ - FileInfo: archiver.FileInfo{ - FileInfo: pinf, - CustomName: parcname, - }, - ReadCloser: pfile, - }) - assert.NoError(t, errors.Join(pfile.Close(), err)) - - err = archive.Write(archiver.File{ - FileInfo: archiver.FileInfo{ - FileInfo: minf, - CustomName: marcname, - }, - ReadCloser: mfile, - }) - assert.NoError(t, errors.Join(mfile.Close(), archive.Close(), err)) - - _, err = ParsePackage(&buf, []byte{}, []byte{}, 0) - - assert.NoError(t, err) - }) - - t.Run("missing .PKGINFO", func(t *testing.T) { - var buf bytes.Buffer - - archive := archiver.NewTarZstd() - archive.Create(&buf) - - assert.NoError(t, archive.Close()) - - _, err = ParsePackage(&buf, []byte{}, []byte{}, 0) - - assert.Error(t, err) - assert.Contains(t, err.Error(), ".PKGINFO file not found") - }) - - t.Run("missing .MTREE", func(t *testing.T) { - var buf bytes.Buffer - - pfile, err := fs.Open("pkginfo") - assert.NoError(t, err) - - archive := archiver.NewTarZstd() - archive.Create(&buf) - - err = archive.Write(archiver.File{ - FileInfo: archiver.FileInfo{ - FileInfo: pinf, - CustomName: parcname, - }, - ReadCloser: pfile, - }) - assert.NoError(t, errors.Join(pfile.Close(), archive.Close(), err)) - - _, err = ParsePackage(&buf, []byte{}, []byte{}, 0) - - assert.Error(t, err) - assert.Contains(t, err.Error(), ".MTREE file not found") - }) -} +const ( + packageName = "gitea" + packageVersion = "1.0.1" + packageDescription = "Package Description" + packageProjectURL = "https://gitea.com" + packagePackager = "KN4CK3R " +) -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 -size = 5 +func createPKGINFOContent(name, version string) []byte { + return []byte(`pkgname = ` + name + ` +pkgbase = ` + name + ` +pkgver = ` + version + ` +pkgdesc = ` + packageDescription + ` +url = ` + packageProjectURL + ` +# comment +group=group +builddate = 1678834800 +size = 123456 arch = x86_64 -license = BSD -provides = pvd -depend = smth +license = MIT +packager = ` + packagePackager + ` +depend = common +xdata = value +depend = gitea +provides = common +provides = gitea optdepend = hex -checkdepend = ola +checkdepend = common makedepend = cmake -backup = usr/bin/paket1 -` - p, err := ParsePackageInfo(strings.NewReader(PKGINFO)) - assert.NoError(t, err) - assert.Equal(t, Package{ - Name: "a", - Version: "1-2", - VersionMetadata: VersionMetadata{ - Base: "b", - Description: "comment", - ProjectURL: "https://example.com/", - Groups: []string{"group"}, - Provides: []string{"pvd"}, - License: []string{"BSD"}, - Depends: []string{"smth"}, - OptDepends: []string{"hex"}, - MakeDepends: []string{"cmake"}, - CheckDepends: []string{"ola"}, - Backup: []string{"usr/bin/paket1"}, - }, - FileMetadata: FileMetadata{ - InstalledSize: 5, - BuildDate: 3, - Packager: "Name Surname ", - Arch: "x86_64", - }, - }, *p) +backup = usr/bin/paket1`) } -func TestValidatePackageSpec(t *testing.T) { - newpkg := func() Package { - return Package{ - Name: "abc", - Version: "1-1", - VersionMetadata: VersionMetadata{ - Base: "ghx", - Description: "whoami", - ProjectURL: "https://example.com/", - Groups: []string{"gnome"}, - Provides: []string{"abc", "def"}, - License: []string{"GPL"}, - Depends: []string{"go", "gpg=1", "curl>=3", "git<=7"}, - OptDepends: []string{"git: something", "make"}, - MakeDepends: []string{"chrom"}, - CheckDepends: []string{"bariy"}, - Backup: []string{"etc/pacman.d/filo"}, - }, - FileMetadata: FileMetadata{ - CompressedSize: 1, - InstalledSize: 2, - MD5: "abc", - SHA256: "def", - BuildDate: 3, - Packager: "smon", - Arch: "x86_64", - }, +func TestParsePackage(t *testing.T) { + createPackage := func(files map[string][]byte) io.Reader { + var buf bytes.Buffer + zw, _ := zstd.NewWriter(&buf) + tw := tar.NewWriter(zw) + + for name, content := range files { + hdr := &tar.Header{ + Name: name, + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write(content) } - } - - t.Run("valid package", func(t *testing.T) { - p := newpkg() - - err := ValidatePackageSpec(&p) - - assert.NoError(t, err) - }) - - t.Run("invalid package name", func(t *testing.T) { - p := newpkg() - p.Name = "!$%@^!*&()" - - err := ValidatePackageSpec(&p) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid package name") - }) - - t.Run("invalid package base", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.Base = "!$%@^!*&()" - - err := ValidatePackageSpec(&p) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid package base") - }) - t.Run("invalid package version", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.Base = "una-luna?" + tw.Close() + zw.Close() - err := ValidatePackageSpec(&p) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid package base") - }) - - t.Run("invalid package version", func(t *testing.T) { - p := newpkg() - p.Version = "una-luna" - - err := ValidatePackageSpec(&p) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid package version") - }) - - t.Run("missing architecture", func(t *testing.T) { - p := newpkg() - p.FileMetadata.Arch = "" - - err := ValidatePackageSpec(&p) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "architecture should be specified") - }) - - t.Run("invalid URL", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.ProjectURL = "http%%$#" - - err := ValidatePackageSpec(&p) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid project URL") - }) - - t.Run("invalid check dependency", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.CheckDepends = []string{"Err^_^"} - - err := ValidatePackageSpec(&p) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid check dependency") - }) - - t.Run("invalid dependency", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.Depends = []string{"^^abc"} + return &buf + } - err := ValidatePackageSpec(&p) + t.Run("MissingPKGINFOFile", func(t *testing.T) { + data := createPackage(map[string][]byte{"dummy.txt": {}}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid dependency") + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrMissingPKGINFOFile) }) - t.Run("invalid make dependency", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.MakeDepends = []string{"^m^"} - - err := ValidatePackageSpec(&p) + t.Run("InvalidPKGINFOFile", func(t *testing.T) { + data := createPackage(map[string][]byte{".PKGINFO": {}}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid make dependency") + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrInvalidName) }) - t.Run("invalid provides", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.Provides = []string{"^m^"} + t.Run("Valid", func(t *testing.T) { + data := createPackage(map[string][]byte{ + ".PKGINFO": createPKGINFOContent(packageName, packageVersion), + "/test/dummy.txt": {}, + }) - err := ValidatePackageSpec(&p) + p, err := ParsePackage(data) + assert.NoError(t, err) + assert.NotNil(t, p) - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid provides") + assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files) }) +} - t.Run("invalid optional dependency", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.OptDepends = []string{"^m^:MM"} - - err := ValidatePackageSpec(&p) +func TestParsePackageInfo(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + data := createPKGINFOContent("", packageVersion) - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid optional dependency") + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) }) - t.Run("invalid optional dependency", func(t *testing.T) { - p := newpkg() - p.VersionMetadata.Backup = []string{"/ola/cola"} - - err := ValidatePackageSpec(&p) + t.Run("InvalidVersion", func(t *testing.T) { + data := createPKGINFOContent(packageName, "") - assert.Error(t, err) - assert.Contains(t, err.Error(), "backup file contains leading forward slash") + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) }) -} -func TestDescString(t *testing.T) { - const pkgdesc = `%FILENAME% -zstd-1.5.5-1-x86_64.pkg.tar.zst + t.Run("Valid", func(t *testing.T) { + data := createPKGINFOContent(packageName, packageVersion) -%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 - -%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 ", - Arch: "x86_64", - }, - } - assert.Equal(t, pkgdesc, md.Desc()) -} - -func TestCreatePacmanDb(t *testing.T) { - const dbarchive = "H4sIAAAAAAAA/0rLzEnVS60oYaAhMDAwMDA3NwfTBgYG6LSBgYEpEtuAwcDQwMzUgEHBgJaOgoHS4pLEIgYDiu1C99wQASmlubmVA+2IUTAKRsEoGAV0B4AAAAD//2VF3KIACAAA" - - db, err := CreatePacmanDb(map[string][]byte{ - "file.ext": []byte("dummy"), + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.NoError(t, err) + assert.NotNil(t, p) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageName, p.FileMetadata.Base) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, packageDescription, p.VersionMetadata.Description) + assert.Equal(t, packagePackager, p.FileMetadata.Packager) + assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL) + assert.ElementsMatch(t, []string{"MIT"}, p.VersionMetadata.Licenses) + assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate) + assert.EqualValues(t, 123456, p.FileMetadata.InstalledSize) + assert.Equal(t, "x86_64", p.FileMetadata.Architecture) + assert.ElementsMatch(t, []string{"value"}, p.FileMetadata.XData) + assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends) + assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends) + assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends) + assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends) + assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup) }) - assert.NoError(t, err) - - actual, err := io.ReadAll(db) - assert.NoError(t, err) - - expected, err := base64.RawStdEncoding.DecodeString(dbarchive) - assert.NoError(t, err) - - assert.Equal(t, expected, actual) } diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 32ace1c900483..625c06e7ccc9e 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -163,7 +163,8 @@ func DownloadPackageOrRepositoryFile(ctx *context.Context) { } if strings.HasSuffix(filename, ".db.tar.gz") || strings.HasSuffix(filename, ".files.tar.gz") || strings.HasSuffix(filename, ".files") || strings.HasSuffix(filename, ".db") { - // normalize to packages.db + // The requested filename is based on the user-defined repository name. + // Normalize everything to "packages.db". opts.Query = arch_service.IndexArchiveFilename pv, err := arch_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID) diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go index 3a91d797dd67c..b96ade3e1843a 100644 --- a/tests/integration/api_packages_arch_test.go +++ b/tests/integration/api_packages_arch_test.go @@ -5,450 +5,282 @@ package integration import ( "archive/tar" - "bufio" "bytes" "compress/gzip" - "crypto/md5" - "encoding/hex" - "errors" "fmt" "io" "net/http" - "os" "testing" - "testing/fstest" - "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/packages/arch" + arch_module "code.gitea.io/gitea/modules/packages/arch" + arch_service "code.gitea.io/gitea/services/packages/arch" "code.gitea.io/gitea/tests" - "github.com/mholt/archiver/v3" - "github.com/minio/sha256-simd" + "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" ) func TestPackageArch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - var ( - user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - - pushBatch = []*TestArchPackage{ - BuildArchPackage(t, "git", "1-1", "x86_64"), - BuildArchPackage(t, "git", "2-1", "x86_64"), - BuildArchPackage(t, "git", "1-1", "i686"), - BuildArchPackage(t, "adwaita", "1-1", "any"), - BuildArchPackage(t, "adwaita", "2-1", "any"), + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + packageName := "gitea-test" + packageVersion := "1.4.1-r3" + + createPackage := func(name, version, architecture string) []byte { + var buf bytes.Buffer + zw, _ := zstd.NewWriter(&buf) + tw := tar.NewWriter(zw) + + info := []byte(`pkgname = ` + name + ` +pkgbase = ` + name + ` +pkgver = ` + version + ` +pkgdesc = Description +# comment +builddate = 1678834800 +size = 8 +arch = ` + architecture + ` +license = MIT`) + + hdr := &tar.Header{ + Name: ".PKGINFO", + Mode: 0o600, + Size: int64(len(info)), } - - removeBatch = []*TestArchPackage{ - BuildArchPackage(t, "curl", "1-1", "x86_64"), - BuildArchPackage(t, "curl", "2-1", "x86_64"), - BuildArchPackage(t, "dock", "1-1", "any"), - BuildArchPackage(t, "dock", "2-1", "any"), + tw.WriteHeader(hdr) + tw.Write(info) + + for _, file := range []string{"etc/dummy", "opt/file/bin"} { + hdr := &tar.Header{ + Name: file, + Mode: 0o600, + Size: 4, + } + tw.WriteHeader(hdr) + tw.Write([]byte("test")) } - firstDatabaseBatch = []*TestArchPackage{ - BuildArchPackage(t, "pacman", "1-1", "x86_64"), - BuildArchPackage(t, "pacman", "1-1", "i686"), - BuildArchPackage(t, "htop", "1-1", "x86_64"), - BuildArchPackage(t, "htop", "1-1", "i686"), - BuildArchPackage(t, "dash", "1-1", "any"), - } + tw.Close() + zw.Close() - secondDatabaseBatch = []*TestArchPackage{ - BuildArchPackage(t, "pacman", "2-1", "x86_64"), - BuildArchPackage(t, "htop", "2-1", "i686"), - BuildArchPackage(t, "dash", "2-1", "any"), - } + return buf.Bytes() + } - PacmanDBx86 = BuildPacmanDb(t, - secondDatabaseBatch[0].Pkg, - firstDatabaseBatch[2].Pkg, - secondDatabaseBatch[2].Pkg, - ) + contentAarch64 := createPackage(packageName, packageVersion, "aarch64") + contentAny := createPackage(packageName+"_"+arch_module.AnyArch, packageVersion, arch_module.AnyArch) - PacmanDBi686 = BuildPacmanDb(t, - firstDatabaseBatch[0].Pkg, - secondDatabaseBatch[1].Pkg, - secondDatabaseBatch[2].Pkg, - ) + repositories := []string{"main", "testing"} - signdata = []byte{1, 2, 3, 4} - ) + rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name) - t.Run("PushWithSignature", func(t *testing.T) { - for _, p := range pushBatch { - t.Run(p.File, func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + t.Run("RepositoryKey", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - url := fmt.Sprintf( - "/api/packages/%s/arch/push/%s/archlinux/%s", - user.Name, p.File, hex.EncodeToString(signdata), - ) + req := NewRequest(t, "GET", rootURL+"/key") + resp := MakeRequest(t, req, http.StatusOK) - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) - req = AddBasicAuthHeader(req, user.Name) - MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----") + }) - pv, err := packages.GetVersionByNameAndVersion( - db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, - ) - assert.NoError(t, err) + for _, repository := range repositories { + t.Run(fmt.Sprintf("[Repository:%s]", repository), func(t *testing.T) { + t.Run("Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - pf, err := packages.GetFileForVersionByName( - db.DefaultContext, pv.ID, p.File, "archlinux", - ) - assert.NoError(t, err) - assert.NotNil(t, pf) + uploadURL := fmt.Sprintf("%s/%s", rootURL, repository) - pps, err := packages.GetPropertiesByName( - db.DefaultContext, packages.PropertyTypeFile, - pf.ID, arch.PropertySignature, - ) - assert.NoError(t, err) - assert.Len(t, pps, 1) - }) - } - }) + req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) + MakeRequest(t, req, http.StatusUnauthorized) - t.Run("PushWithoutSignature", func(t *testing.T) { - for _, p := range pushBatch { - t.Run(p.File, func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusBadRequest) - url := fmt.Sprintf( - "/api/packages/%s/arch/push/%s/parabola", - user.Name, p.File, - ) + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) - req = AddBasicAuthHeader(req, user.Name) - MakeRequest(t, req, http.StatusOK) + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch) + assert.NoError(t, err) + assert.Len(t, pvs, 1) - pv, err := packages.GetVersionByNameAndVersion( - db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, - ) + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) assert.NoError(t, err) + assert.Nil(t, pd.SemVer) + assert.IsType(t, &arch_module.VersionMetadata{}, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) - pf, err := packages.GetFileForVersionByName( - db.DefaultContext, pv.ID, p.File, "parabola", - ) + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) assert.NoError(t, err) - assert.NotNil(t, pf) + assert.NotEmpty(t, pfs) + assert.Condition(t, func() bool { + seen := false + expectedFilename := fmt.Sprintf("%s-%s-aarch64.pck.tar.zst", packageName, packageVersion) + expectedCompositeKey := fmt.Sprintf("%s|aarch64", repository) + for _, pf := range pfs { + if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey { + if seen { + return false + } + seen = true + + assert.True(t, pf.IsLead) + + pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID) + assert.NoError(t, err) + + for _, pfp := range pfps { + switch pfp.Name { + case arch_module.PropertyRepository: + assert.Equal(t, repository, pfp.Value) + case arch_module.PropertyArchitecture: + assert.Equal(t, "aarch64", pfp.Value) + } + } + } + } + return seen + }) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusConflict) }) - } - }) - t.Run("GetPackage", func(t *testing.T) { - for _, p := range pushBatch { - t.Run(p.File, func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + readIndexContent := func(r io.Reader) (map[string]string, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + + content := make(map[string]string) + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + buf, err := io.ReadAll(tr) + if err != nil { + return nil, err + } + + content[hd.Name] = string(buf) + } + + return content, nil + } - url := fmt.Sprintf( - "/api/packages/%s/arch/push/%s/artix/%s", - user.Name, p.File, hex.EncodeToString(signdata), - ) - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) - req = AddBasicAuthHeader(req, user.Name) - MakeRequest(t, req, http.StatusOK) + t.Run("Index", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - url = fmt.Sprintf( - "/api/packages/%s/arch/artix/%s/%s", - user.Name, p.Arch, p.File, - ) - req = NewRequest(t, "GET", url) + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) resp := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, p.Data, resp.Body.Bytes()) - }) - } - }) - - t.Run("GetSignature", func(t *testing.T) { - for _, p := range pushBatch { - t.Run(p.File, func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - url := fmt.Sprintf( - "/api/packages/%s/arch/push/%s/arco/%s", - user.Name, p.File, hex.EncodeToString(signdata), - ) - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) - req = AddBasicAuthHeader(req, user.Name) - MakeRequest(t, req, http.StatusOK) + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) - url = fmt.Sprintf( - "/api/packages/%s/arch/arco/%s/%s.sig", - user.Name, p.Arch, p.File, - ) - req = NewRequest(t, "GET", url) - resp := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, signdata, resp.Body.Bytes()) + desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%FILENAME%\n"+fmt.Sprintf("%s-%s-aarch64.pck.tar.zst", packageName, packageVersion)+"\n\n") + assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") + assert.Contains(t, desc, "%VERSION%\n"+packageVersion+"\n\n") + assert.Contains(t, desc, "%ARCH%\naarch64\n") + assert.NotContains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") + assert.Contains(t, desc, "%LICENSE%\nMIT\n") + + files, has := content[fmt.Sprintf("%s-%s/files", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, files, "%FILES%\netc/dummy\nopt/file/bin\n\n") + + for _, indexFile := range []string{ + arch_service.IndexArchiveFilename, + arch_service.IndexArchiveFilename + ".tar.gz", + "index.db", + "index.db.tar.gz", + "index.files", + "index.files.tar.gz", + } { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, indexFile)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s.sig", rootURL, repository, indexFile)) + MakeRequest(t, req, http.StatusOK) + } }) - } - }) - t.Run("Remove", func(t *testing.T) { - for _, p := range removeBatch { - t.Run(p.File, func(t *testing.T) { + t.Run("Download", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - url := fmt.Sprintf( - "/api/packages/%s/arch/push/%s/manjaro/%s", - user.Name, p.File, hex.EncodeToString(signdata), - ) - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) - req = AddBasicAuthHeader(req, user.Name) + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pck.tar.zst", rootURL, repository, packageName, packageVersion)) MakeRequest(t, req, http.StatusOK) - url = fmt.Sprintf( - "/api/packages/%s/arch/remove/%s/%s", - user.Name, p.Name, p.Ver, - ) - req = NewRequest(t, "DELETE", url) - req = AddBasicAuthHeader(req, user.Name) + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pck.tar.zst.sig", rootURL, repository, packageName, packageVersion)) MakeRequest(t, req, http.StatusOK) - - _, err := packages.GetVersionByNameAndVersion( - db.DefaultContext, user.ID, packages.TypeArch, p.Name, p.Ver, - ) - assert.ErrorIs(t, err, packages.ErrPackageNotExist) }) - } - }) - - t.Run("PacmanDatabase", func(t *testing.T) { - prepareDatabasePackages := func(t *testing.T) { - for _, p := range firstDatabaseBatch { - url := fmt.Sprintf( - "/api/packages/%s/arch/push/%s/ion/%s", - user.Name, p.File, hex.EncodeToString(signdata), - ) - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) - req = AddBasicAuthHeader(req, user.Name) - MakeRequest(t, req, http.StatusOK) - } - - // While creating pacman database, package versions are sorted by - // UnixTime, second delay is required to ensure that newer package - // version creation time differs from older packages. - time.Sleep(time.Second) - - for _, p := range secondDatabaseBatch { - url := fmt.Sprintf( - "/api/packages/%s/arch/push/%s/ion/%s", - user.Name, p.File, hex.EncodeToString(signdata), - ) - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(p.Data)) - req = AddBasicAuthHeader(req, user.Name) - MakeRequest(t, req, http.StatusOK) - } - } - - t.Run("x86_64", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - prepareDatabasePackages(t) - - url := fmt.Sprintf( - "/api/packages/%s/arch/ion/x86_64/user.db.tar.gz", user.Name, - ) - req := NewRequest(t, "GET", url) - resp := MakeRequest(t, req, http.StatusOK) - CompareTarGzEntries(t, PacmanDBx86, resp.Body.Bytes()) - }) + t.Run("Any", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("i686", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", rootURL, repository), bytes.NewReader(contentAny)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) - prepareDatabasePackages(t) + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + resp := MakeRequest(t, req, http.StatusOK) - url := fmt.Sprintf( - "/api/packages/%s/arch/ion/i686/user.db", user.Name, - ) - req := NewRequest(t, "GET", url) - resp := MakeRequest(t, req, http.StatusOK) + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) - CompareTarGzEntries(t, PacmanDBi686, resp.Body.Bytes()) + desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") + assert.Contains(t, desc, "%ARCH%\naarch64\n") + + desc, has = content[fmt.Sprintf("%s-%s/desc", packageName+"_"+arch_module.AnyArch, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%NAME%\n"+packageName+"_any\n\n") + assert.Contains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") + + // "any" architecture package should be available with every architecture requested + for _, arch := range []string{arch_module.AnyArch, "aarch64", "myarch"} { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s-%s-any.pck.tar.zst", rootURL, repository, arch, packageName+"_"+arch_module.AnyArch, packageVersion)) + MakeRequest(t, req, http.StatusOK) + } + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/any/%s-%s-any.pck.tar.zst", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + }) }) - }) -} - -type TestArchPackage struct { - Pkg arch.Package - Data []byte - File string - Name string - Ver string - Arch string -} - -func BuildArchPackage(t *testing.T, name, ver, architecture string) *TestArchPackage { - fs := fstest.MapFS{ - "pkginfo": &fstest.MapFile{ - Data: []byte(fmt.Sprintf( - "pkgname = %s\npkgbase = %s\npkgver = %s\narch = %s\n", - name, name, ver, architecture, - )), - Mode: os.ModePerm, - ModTime: time.Now(), - }, - "mtree": &fstest.MapFile{ - Data: []byte("test"), - Mode: os.ModePerm, - ModTime: time.Now(), - }, } - pinf, err := fs.Stat("pkginfo") - assert.NoError(t, err) - - pfile, err := fs.Open("pkginfo") - assert.NoError(t, err) - - parcname, err := archiver.NameInArchive(pinf, ".PKGINFO", ".PKGINFO") - assert.NoError(t, err) - - minf, err := fs.Stat("mtree") - assert.NoError(t, err) + t.Run("Delete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - mfile, err := fs.Open("mtree") - assert.NoError(t, err) + for _, repository := range repositories { + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pck.tar.zst", rootURL, repository, packageName, packageVersion)) + MakeRequest(t, req, http.StatusUnauthorized) - marcname, err := archiver.NameInArchive(minf, ".MTREE", ".MTREE") - assert.NoError(t, err) + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pck.tar.zst", rootURL, repository, packageName, packageVersion)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) - var buf bytes.Buffer - - archive := archiver.NewTarZstd() - archive.Create(&buf) - - err = archive.Write(archiver.File{ - FileInfo: archiver.FileInfo{ - FileInfo: pinf, - CustomName: parcname, - }, - ReadCloser: pfile, - }) - assert.NoError(t, errors.Join(pfile.Close(), err)) - - err = archive.Write(archiver.File{ - FileInfo: archiver.FileInfo{ - FileInfo: minf, - CustomName: marcname, - }, - ReadCloser: mfile, - }) - assert.NoError(t, errors.Join(mfile.Close(), archive.Close(), err)) - - md5, sha256, size := archPkgParams(buf.Bytes()) - - return &TestArchPackage{ - Data: buf.Bytes(), - Name: name, - Ver: ver, - Arch: architecture, - File: fmt.Sprintf("%s-%s-%s.pkg.tar.zst", name, ver, architecture), - Pkg: arch.Package{ - Name: name, - Version: ver, - VersionMetadata: arch.VersionMetadata{ - Base: name, - }, - FileMetadata: arch.FileMetadata{ - CompressedSize: size, - MD5: hex.EncodeToString(md5), - SHA256: hex.EncodeToString(sha256), - Arch: architecture, - }, - }, - } -} - -func archPkgParams(b []byte) ([]byte, []byte, int64) { - md5 := md5.New() - sha256 := sha256.New() - c := counter{bytes.NewReader(b), 0} - - br := bufio.NewReader(io.TeeReader(&c, io.MultiWriter(md5, sha256))) - - io.ReadAll(br) - return md5.Sum(nil), sha256.Sum(nil), int64(c.n) -} - -type counter struct { - io.Reader - n int -} - -func (w *counter) Read(p []byte) (int, error) { - n, err := w.Reader.Read(p) - w.n += n - return n, err -} - -func BuildPacmanDb(t *testing.T, pkgs ...arch.Package) []byte { - entries := map[string][]byte{} - for _, p := range pkgs { - entries[fmt.Sprintf("%s-%s/desc", p.Name, p.Version)] = []byte(p.Desc()) - } - b, err := arch.CreatePacmanDb(entries) - if err != nil { - assert.NoError(t, err) - return nil - } - return b.Bytes() -} - -func CompareTarGzEntries(t *testing.T, expected, actual []byte) { - fgz, err := gzip.NewReader(bytes.NewReader(expected)) - if err != nil { - assert.NoError(t, err) - return - } - ftar := tar.NewReader(fgz) - - validatemap := map[string]struct{}{} - - for { - h, err := ftar.Next() - if err != nil { - break - } - - validatemap[h.Name] = struct{}{} - } - - sgz, err := gzip.NewReader(bytes.NewReader(actual)) - if err != nil { - assert.NoError(t, err) - return - } - star := tar.NewReader(sgz) - - for { - h, err := star.Next() - if err != nil { - break - } - - _, ok := validatemap[h.Name] - if !ok { - assert.Fail(t, "Unexpected entry in archive: "+h.Name) + // Deleting the last file of an architecture should remove that index + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + MakeRequest(t, req, http.StatusNotFound) } - delete(validatemap, h.Name) - } - - if len(validatemap) == 0 { - return - } - - for e := range validatemap { - assert.Fail(t, "Entry not found in archive: "+e) - } + }) } From f9688dac080784ec014e2980159526be9df0ac6a Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 24 Nov 2024 20:33:00 +0200 Subject: [PATCH 4/9] Add support for repository names containing slashes. Add support for different file compressions. Prevent duplicates caused by different file compressions. Co-authored-by: dragon --- models/packages/package_file.go | 5 + modules/packages/arch/metadata.go | 54 ++- modules/packages/arch/metadata_test.go | 66 ++-- routers/api/packages/api.go | 48 ++- routers/api/packages/arch/arch.go | 49 ++- routers/web/user/package.go | 13 +- services/forms/package_form.go | 2 +- services/packages/arch/repository.go | 70 ++-- services/packages/cleanup/cleanup.go | 6 + templates/package/content/arch.tmpl | 2 +- templates/package/content/container.tmpl | 4 +- tests/integration/api_packages_arch_test.go | 356 ++++++++++---------- 12 files changed, 415 insertions(+), 260 deletions(-) diff --git a/models/packages/package_file.go b/models/packages/package_file.go index 1bb6b57a34e8e..270cb32fdf6b5 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -221,6 +221,11 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag return pfs, count, err } +// HasFiles tests if there are files of packages matching the search options +func HasFiles(ctx context.Context, opts *PackageFileSearchOptions) (bool, error) { + return db.Exist[PackageFile](ctx, opts.toConds()) +} + // CalculateFileSize sums up all blob sizes matching the search options. // It does NOT respect the deduplication of blobs. func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) { diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go index dbd947761dddf..e1e79c60e0dfe 100644 --- a/modules/packages/arch/metadata.go +++ b/modules/packages/arch/metadata.go @@ -6,6 +6,8 @@ package arch import ( "archive/tar" "bufio" + "bytes" + "compress/gzip" "io" "regexp" "strconv" @@ -15,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/validation" "github.com/klauspost/compress/zstd" + "github.com/ulikunitz/xz" ) const ( @@ -34,6 +37,7 @@ const ( var ( ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing") + ErrUnsupportedFormat = util.NewInvalidArgumentErrorf("unsupported package container format") ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") @@ -44,10 +48,11 @@ var ( ) type Package struct { - Name string `json:"name"` - Version string `json:"version"` - VersionMetadata VersionMetadata - FileMetadata FileMetadata + Name string + Version string + VersionMetadata VersionMetadata + FileMetadata FileMetadata + FileCompressionExtension string } type VersionMetadata struct { @@ -75,16 +80,50 @@ type FileMetadata struct { // ParsePackage parses an Arch package file func ParsePackage(r io.Reader) (*Package, error) { - zr, err := zstd.NewReader(r) + header := make([]byte, 10) + n, err := util.ReadAtMost(r, header) if err != nil { return nil, err } - defer zr.Close() + + r = io.MultiReader(bytes.NewReader(header[:n]), r) + + var inner io.Reader + var compressionType string + if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst + zr, err := zstd.NewReader(r) + if err != nil { + return nil, err + } + defer zr.Close() + + inner = zr + compressionType = "zst" + } else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz + xzr, err := xz.NewReader(r) + if err != nil { + return nil, err + } + + inner = xzr + compressionType = "xz" + } else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + inner = gzr + compressionType = "gz" + } else { + return nil, ErrUnsupportedFormat + } var p *Package files := make([]string, 0, 10) - tr := tar.NewReader(zr) + tr := tar.NewReader(inner) for { hd, err := tr.Next() if err == io.EOF { @@ -114,6 +153,7 @@ func ParsePackage(r io.Reader) (*Package, error) { } p.FileMetadata.Files = files + p.FileCompressionExtension = compressionType return p, nil } diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go index f38ade2e68e1d..f611ef5e84586 100644 --- a/modules/packages/arch/metadata_test.go +++ b/modules/packages/arch/metadata_test.go @@ -6,11 +6,13 @@ package arch import ( "archive/tar" "bytes" + "compress/gzip" "io" "testing" "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" ) const ( @@ -46,10 +48,18 @@ backup = usr/bin/paket1`) } func TestParsePackage(t *testing.T) { - createPackage := func(files map[string][]byte) io.Reader { + createPackage := func(compression string, files map[string][]byte) io.Reader { var buf bytes.Buffer - zw, _ := zstd.NewWriter(&buf) - tw := tar.NewWriter(zw) + var cw io.WriteCloser + switch compression { + case "zst": + cw, _ = zstd.NewWriter(&buf) + case "xz": + cw, _ = xz.NewWriter(&buf) + case "gz": + cw = gzip.NewWriter(&buf) + } + tw := tar.NewWriter(cw) for name, content := range files { hdr := &tar.Header{ @@ -62,39 +72,43 @@ func TestParsePackage(t *testing.T) { } tw.Close() - zw.Close() + cw.Close() return &buf } - t.Run("MissingPKGINFOFile", func(t *testing.T) { - data := createPackage(map[string][]byte{"dummy.txt": {}}) + for _, c := range []string{"gz", "xz", "zst"} { + t.Run(c, func(t *testing.T) { + t.Run("MissingPKGINFOFile", func(t *testing.T) { + data := createPackage(c, map[string][]byte{"dummy.txt": {}}) - pp, err := ParsePackage(data) - assert.Nil(t, pp) - assert.ErrorIs(t, err, ErrMissingPKGINFOFile) - }) + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrMissingPKGINFOFile) + }) - t.Run("InvalidPKGINFOFile", func(t *testing.T) { - data := createPackage(map[string][]byte{".PKGINFO": {}}) + t.Run("InvalidPKGINFOFile", func(t *testing.T) { + data := createPackage(c, map[string][]byte{".PKGINFO": {}}) - pp, err := ParsePackage(data) - assert.Nil(t, pp) - assert.ErrorIs(t, err, ErrInvalidName) - }) + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrInvalidName) + }) - t.Run("Valid", func(t *testing.T) { - data := createPackage(map[string][]byte{ - ".PKGINFO": createPKGINFOContent(packageName, packageVersion), - "/test/dummy.txt": {}, - }) + t.Run("Valid", func(t *testing.T) { + data := createPackage(c, map[string][]byte{ + ".PKGINFO": createPKGINFOContent(packageName, packageVersion), + "/test/dummy.txt": {}, + }) - p, err := ParsePackage(data) - assert.NoError(t, err) - assert.NotNil(t, p) + p, err := ParsePackage(data) + assert.NoError(t, err) + assert.NotNil(t, p) - assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files) - }) + assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files) + }) + }) + } } func TestParsePackageInfo(t *testing.T) { diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 5adc2520f6ba0..9707501409008 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -137,15 +137,47 @@ func CommonRoutes() *web.Router { }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { - r.Get("/key", arch.GetRepositoryKey) - r.Group("/{repository}", func() { - r.Put("", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) - r.Group("/{architecture}/{filename}", func() { - r.Get("", arch.DownloadPackageOrRepositoryFile) - r.Delete("", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageFile) - }) + r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) + + r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { + path := strings.Trim(ctx.PathParam("*"), "/") + + if ctx.Req.Method == "PUT" { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetPathParam("repository", path) + arch.UploadPackageFile(ctx) + return + } + + pathFields := strings.Split(path, "/") + pathFieldsLen := len(pathFields) + + if pathFieldsLen >= 2 { + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/")) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("filename", pathFields[pathFieldsLen-1]) + + if ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET" { + arch.GetPackageOrRepositoryFile(ctx) + return + } + + if ctx.Req.Method == "DELETE" { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + arch.DeletePackageFile(ctx) + return + } + } + + ctx.Status(http.StatusNotFound) }) - }) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { r.Group("/api/v1/crates", func() { r.Get("", cargo.SearchPackages) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 625c06e7ccc9e..70671cdfc9f18 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -42,7 +42,7 @@ func GetRepositoryKey(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { - repository := strings.TrimSpace(ctx.Params("repository")) + repository := strings.TrimSpace(ctx.PathParam("repository")) if repository == "" { apiError(ctx, http.StatusBadRequest, "invalid repository") return @@ -96,6 +96,32 @@ func UploadPackageFile(ctx *context.Context) { return } + release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + // Search for duplicates with different file compression + has, err := packages_model.HasFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeArch, + Query: fmt.Sprintf("%s-%s-%s.pkg.tar.%%", pck.Name, pck.Version, pck.FileMetadata.Architecture), + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: pck.FileMetadata.Architecture, + }, + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if has { + apiError(ctx, http.StatusConflict, packages_model.ErrDuplicatePackageFile) + return + } + _, _, err = packages_service.CreatePackageOrAddFileToExisting( ctx, &packages_service.PackageCreationInfo{ @@ -110,7 +136,7 @@ func UploadPackageFile(ctx *context.Context) { }, &packages_service.PackageFileCreationInfo{ PackageFileInfo: packages_service.PackageFileInfo{ - Filename: fmt.Sprintf("%s-%s-%s.pck.tar.zst", pck.Name, pck.Version, pck.FileMetadata.Architecture), + Filename: fmt.Sprintf("%s-%s-%s.pkg.tar.%s", pck.Name, pck.Version, pck.FileMetadata.Architecture, pck.FileCompressionExtension), CompositeKey: fmt.Sprintf("%s|%s", repository, pck.FileMetadata.Architecture), }, Creator: ctx.Doer, @@ -144,10 +170,10 @@ func UploadPackageFile(ctx *context.Context) { ctx.Status(http.StatusCreated) } -func DownloadPackageOrRepositoryFile(ctx *context.Context) { - repository := ctx.Params("repository") - architecture := ctx.Params("architecture") - filename := ctx.Params("filename") +func GetPackageOrRepositoryFile(ctx *context.Context) { + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + filename := ctx.PathParam("filename") filenameOrig := filename isSignature := strings.HasSuffix(filename, ".sig") @@ -231,12 +257,19 @@ func DownloadPackageOrRepositoryFile(ctx *context.Context) { } func DeletePackageFile(ctx *context.Context) { - repository, architecture := ctx.Params("repository"), ctx.Params("architecture") + repository, architecture := ctx.PathParam("repository"), ctx.PathParam("architecture") + + release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ OwnerID: ctx.Package.Owner.ID, PackageType: packages_model.TypeArch, - Query: ctx.Params("filename"), + Query: ctx.PathParam("filename"), CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), }) if err != nil { diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 933c73e1eed67..c6f85ac734eb9 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -179,13 +179,13 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["IsPackagesPage"] = true ctx.Data["PackageDescriptor"] = pd + registryHostURL, err := url.Parse(httplib.GuessCurrentHostURL(ctx)) + if err != nil { + registryHostURL, _ = url.Parse(setting.AppURL) + } + ctx.Data["PackageRegistryHost"] = registryHostURL.Host + switch pd.Package.Type { - case packages_model.TypeContainer: - registryAppURL, err := url.Parse(httplib.GuessCurrentAppURL(ctx)) - if err != nil { - registryAppURL, _ = url.Parse(setting.AppURL) - } - ctx.Data["RegistryHost"] = registryAppURL.Host case packages_model.TypeAlpine: branches := make(container.Set[string]) repositories := make(container.Set[string]) @@ -267,7 +267,6 @@ func ViewPackageVersion(ctx *context.Context) { var ( total int64 pvs []*packages_model.PackageVersion - err error ) switch pd.Package.Type { case packages_model.TypeContainer: diff --git a/services/forms/package_form.go b/services/forms/package_form.go index cc940d42d34df..9b6f9071647bc 100644 --- a/services/forms/package_form.go +++ b/services/forms/package_form.go @@ -15,7 +15,7 @@ import ( type PackageCleanupRuleForm struct { ID int64 Enabled bool - Type string `binding:"Required;In(alpine,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` + Type string `binding:"Required;In(alpine,arch,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` KeepCount int `binding:"In(0,1,5,10,25,50,100)"` KeepPattern string `binding:"RegexPattern"` RemoveDays int `binding:"In(0,7,14,30,60,90,180)"` diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go index 5e0c980415d6a..ab1b85ae95870 100644 --- a/services/packages/arch/repository.go +++ b/services/packages/arch/repository.go @@ -19,6 +19,7 @@ import ( arch_model "code.gitea.io/gitea/models/packages/arch" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/json" packages_module "code.gitea.io/gitea/modules/packages" arch_module "code.gitea.io/gitea/modules/packages/arch" @@ -34,6 +35,10 @@ const ( IndexArchiveFilename = "packages.db" ) +func AquireRegistryLock(ctx context.Context, ownerID int64) (globallock.ReleaseFunc, error) { + return globallock.Lock(ctx, fmt.Sprintf("packages_arch_%d", ownerID)) +} + // GetOrCreateRepositoryVersion gets or creates the internal repository package // The Arch registry needs multiple index files which are stored in this package. func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) { @@ -192,7 +197,7 @@ func searchPackageFiles(ctx context.Context, ownerID int64, repository, architec pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ OwnerID: ownerID, PackageType: packages_model.TypeArch, - Query: "%.pck.tar.zst", + Query: "%.pkg.tar.%", Properties: map[string]string{ arch_module.PropertyRepository: repository, arch_module.PropertyArchitecture: architecture, @@ -335,47 +340,52 @@ type entryOptions struct { Signature string } +type keyValue struct { + Key string + Value string +} + func writeFiles(tw *tar.Writer, opts *entryOptions) error { - return writeFields(tw, fmt.Sprintf("%s-%s/files", opts.Package.Name, opts.Version.Version), map[string]string{ - "FILES": strings.Join(opts.FileMetadata.Files, "\n"), + return writeFields(tw, fmt.Sprintf("%s-%s/files", opts.Package.Name, opts.Version.Version), []keyValue{ + {"FILES", strings.Join(opts.FileMetadata.Files, "\n")}, }) } // https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_sync.c#L562 func writeDescription(tw *tar.Writer, opts *entryOptions) error { - return writeFields(tw, fmt.Sprintf("%s-%s/desc", opts.Package.Name, opts.Version.Version), map[string]string{ - "FILENAME": opts.File.Name, - "MD5SUM": opts.Blob.HashMD5, - "SHA256SUM": opts.Blob.HashSHA256, - "PGPSIG": opts.Signature, - "CSIZE": fmt.Sprintf("%d", opts.Blob.Size), - "ISIZE": fmt.Sprintf("%d", opts.FileMetadata.InstalledSize), - "NAME": opts.Package.Name, - "BASE": opts.FileMetadata.Base, - "ARCH": opts.FileMetadata.Architecture, - "VERSION": opts.Version.Version, - "DESC": opts.VersionMetadata.Description, - "URL": opts.VersionMetadata.ProjectURL, - "LICENSE": strings.Join(opts.VersionMetadata.Licenses, "\n"), - "GROUPS": strings.Join(opts.FileMetadata.Groups, "\n"), - "BUILDDATE": fmt.Sprintf("%d", opts.FileMetadata.BuildDate), - "PACKAGER": opts.FileMetadata.Packager, - "PROVIDES": strings.Join(opts.FileMetadata.Provides, "\n"), - "DEPENDS": strings.Join(opts.FileMetadata.Depends, "\n"), - "OPTDEPENDS": strings.Join(opts.FileMetadata.OptDepends, "\n"), - "MAKEDEPENDS": strings.Join(opts.FileMetadata.MakeDepends, "\n"), - "CHECKDEPENDS": strings.Join(opts.FileMetadata.CheckDepends, "\n"), - "XDATA": strings.Join(opts.FileMetadata.XData, "\n"), + return writeFields(tw, fmt.Sprintf("%s-%s/desc", opts.Package.Name, opts.Version.Version), []keyValue{ + {"FILENAME", opts.File.Name}, + {"MD5SUM", opts.Blob.HashMD5}, + {"SHA256SUM", opts.Blob.HashSHA256}, + {"PGPSIG", opts.Signature}, + {"CSIZE", fmt.Sprintf("%d", opts.Blob.Size)}, + {"ISIZE", fmt.Sprintf("%d", opts.FileMetadata.InstalledSize)}, + {"NAME", opts.Package.Name}, + {"BASE", opts.FileMetadata.Base}, + {"ARCH", opts.FileMetadata.Architecture}, + {"VERSION", opts.Version.Version}, + {"DESC", opts.VersionMetadata.Description}, + {"URL", opts.VersionMetadata.ProjectURL}, + {"LICENSE", strings.Join(opts.VersionMetadata.Licenses, "\n")}, + {"GROUPS", strings.Join(opts.FileMetadata.Groups, "\n")}, + {"BUILDDATE", fmt.Sprintf("%d", opts.FileMetadata.BuildDate)}, + {"PACKAGER", opts.FileMetadata.Packager}, + {"PROVIDES", strings.Join(opts.FileMetadata.Provides, "\n")}, + {"DEPENDS", strings.Join(opts.FileMetadata.Depends, "\n")}, + {"OPTDEPENDS", strings.Join(opts.FileMetadata.OptDepends, "\n")}, + {"MAKEDEPENDS", strings.Join(opts.FileMetadata.MakeDepends, "\n")}, + {"CHECKDEPENDS", strings.Join(opts.FileMetadata.CheckDepends, "\n")}, + {"XDATA", strings.Join(opts.FileMetadata.XData, "\n")}, }) } -func writeFields(tw *tar.Writer, filename string, fields map[string]string) error { +func writeFields(tw *tar.Writer, filename string, fields []keyValue) error { buf := &bytes.Buffer{} - for key, value := range fields { - if value == "" { + for _, kv := range fields { + if kv.Value == "" { continue } - fmt.Fprintf(buf, "%%%s%%\n%s\n\n", key, value) + fmt.Fprintf(buf, "%%%s%%\n%s\n\n", kv.Key, kv.Value) } if err := tw.WriteHeader(&tar.Header{ diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go index 7ed3c25f04e2b..b7ba2b6ac4afc 100644 --- a/services/packages/cleanup/cleanup.go +++ b/services/packages/cleanup/cleanup.go @@ -135,6 +135,12 @@ func ExecuteCleanupRules(outerCtx context.Context) error { return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } case packages_model.TypeArch: + release, err := arch_service.AquireRegistryLock(ctx, pcr.OwnerID) + if err != nil { + return err + } + defer release() + if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl index 32c59a8022dcd..1c568cbb78339 100644 --- a/templates/package/content/arch.tmpl +++ b/templates/package/content/arch.tmpl @@ -4,7 +4,7 @@
-
[{{.PackageDescriptor.Owner.LowerName}}]
+				
[{{.PackageDescriptor.Owner.LowerName}}.{{.PackageRegistryHost}}]
 SigLevel = Optional TrustAll
 Server = 
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index 138fedecb3fc3..aaed25bfbd61d 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -5,13 +5,13 @@
{{if eq .PackageDescriptor.Metadata.Type "helm"}} -
helm pull oci://{{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}} --version {{.PackageDescriptor.Version.LowerVersion}}
+
helm pull oci://{{.PackageRegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}} --version {{.PackageDescriptor.Version.LowerVersion}}
{{else}} {{$separator := ":"}} {{if not .PackageDescriptor.Metadata.IsTagged}} {{$separator = "@"}} {{end}} -
docker pull {{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}
+
docker pull {{.PackageRegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}
{{end}}
diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go index b96ade3e1843a..00a36301f58c6 100644 --- a/tests/integration/api_packages_arch_test.go +++ b/tests/integration/api_packages_arch_test.go @@ -22,6 +22,7 @@ import ( "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" ) func TestPackageArch(t *testing.T) { @@ -32,10 +33,18 @@ func TestPackageArch(t *testing.T) { packageName := "gitea-test" packageVersion := "1.4.1-r3" - createPackage := func(name, version, architecture string) []byte { + createPackage := func(compression, name, version, architecture string) []byte { var buf bytes.Buffer - zw, _ := zstd.NewWriter(&buf) - tw := tar.NewWriter(zw) + var cw io.WriteCloser + switch compression { + case "zst": + cw, _ = zstd.NewWriter(&buf) + case "xz": + cw, _ = xz.NewWriter(&buf) + case "gz": + cw = gzip.NewWriter(&buf) + } + tw := tar.NewWriter(cw) info := []byte(`pkgname = ` + name + ` pkgbase = ` + name + ` @@ -66,221 +75,228 @@ license = MIT`) } tw.Close() - zw.Close() + cw.Close() return buf.Bytes() } - contentAarch64 := createPackage(packageName, packageVersion, "aarch64") - contentAny := createPackage(packageName+"_"+arch_module.AnyArch, packageVersion, arch_module.AnyArch) - - repositories := []string{"main", "testing"} + compressions := []string{"gz", "xz", "zst"} + repositories := []string{"main", "testing", "with/slash"} rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name) t.Run("RepositoryKey", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", rootURL+"/key") + req := NewRequest(t, "GET", rootURL+"/repository.key") resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type")) assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----") }) - for _, repository := range repositories { - t.Run(fmt.Sprintf("[Repository:%s]", repository), func(t *testing.T) { - t.Run("Upload", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - uploadURL := fmt.Sprintf("%s/%s", rootURL, repository) - - req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) - MakeRequest(t, req, http.StatusUnauthorized) - - req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})). - AddBasicAuth(user.Name) - MakeRequest(t, req, http.StatusBadRequest) - - req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). - AddBasicAuth(user.Name) - MakeRequest(t, req, http.StatusCreated) - - pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch) - assert.NoError(t, err) - assert.Len(t, pvs, 1) - - pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) - assert.NoError(t, err) - assert.Nil(t, pd.SemVer) - assert.IsType(t, &arch_module.VersionMetadata{}, pd.Metadata) - assert.Equal(t, packageName, pd.Package.Name) - assert.Equal(t, packageVersion, pd.Version.Version) - - pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) - assert.NoError(t, err) - assert.NotEmpty(t, pfs) - assert.Condition(t, func() bool { - seen := false - expectedFilename := fmt.Sprintf("%s-%s-aarch64.pck.tar.zst", packageName, packageVersion) - expectedCompositeKey := fmt.Sprintf("%s|aarch64", repository) - for _, pf := range pfs { - if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey { - if seen { - return false - } - seen = true + contentAarch64Gz := createPackage("gz", packageName, packageVersion, "aarch64") + for _, compression := range compressions { + contentAarch64 := createPackage(compression, packageName, packageVersion, "aarch64") + contentAny := createPackage(compression, packageName+"_"+arch_module.AnyArch, packageVersion, arch_module.AnyArch) + + for _, repository := range repositories { + t.Run(fmt.Sprintf("[%s,%s]", repository, compression), func(t *testing.T) { + t.Run("Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + uploadURL := fmt.Sprintf("%s/%s", rootURL, repository) + + req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.Nil(t, pd.SemVer) + assert.IsType(t, &arch_module.VersionMetadata{}, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.NotEmpty(t, pfs) + assert.Condition(t, func() bool { + seen := false + expectedFilename := fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression) + expectedCompositeKey := fmt.Sprintf("%s|aarch64", repository) + for _, pf := range pfs { + if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey { + if seen { + return false + } + seen = true - assert.True(t, pf.IsLead) + assert.True(t, pf.IsLead) - pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID) - assert.NoError(t, err) + pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID) + assert.NoError(t, err) - for _, pfp := range pfps { - switch pfp.Name { - case arch_module.PropertyRepository: - assert.Equal(t, repository, pfp.Value) - case arch_module.PropertyArchitecture: - assert.Equal(t, "aarch64", pfp.Value) + for _, pfp := range pfps { + switch pfp.Name { + case arch_module.PropertyRepository: + assert.Equal(t, repository, pfp.Value) + case arch_module.PropertyArchitecture: + assert.Equal(t, "aarch64", pfp.Value) + } } } } - } - return seen - }) + return seen + }) - req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). - AddBasicAuth(user.Name) - MakeRequest(t, req, http.StatusConflict) - }) - - readIndexContent := func(r io.Reader) (map[string]string, error) { - gzr, err := gzip.NewReader(r) - if err != nil { - return nil, err - } + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusConflict) - content := make(map[string]string) + // Add same package with different compression leads to conflict + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64Gz)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusConflict) + }) - tr := tar.NewReader(gzr) - for { - hd, err := tr.Next() - if err == io.EOF { - break - } + readIndexContent := func(r io.Reader) (map[string]string, error) { + gzr, err := gzip.NewReader(r) if err != nil { return nil, err } - buf, err := io.ReadAll(tr) - if err != nil { - return nil, err - } + content := make(map[string]string) - content[hd.Name] = string(buf) - } + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } - return content, nil - } + buf, err := io.ReadAll(tr) + if err != nil { + return nil, err + } - t.Run("Index", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) - resp := MakeRequest(t, req, http.StatusOK) - - content, err := readIndexContent(resp.Body) - assert.NoError(t, err) - - desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] - assert.True(t, has) - assert.Contains(t, desc, "%FILENAME%\n"+fmt.Sprintf("%s-%s-aarch64.pck.tar.zst", packageName, packageVersion)+"\n\n") - assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") - assert.Contains(t, desc, "%VERSION%\n"+packageVersion+"\n\n") - assert.Contains(t, desc, "%ARCH%\naarch64\n") - assert.NotContains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") - assert.Contains(t, desc, "%LICENSE%\nMIT\n") - - files, has := content[fmt.Sprintf("%s-%s/files", packageName, packageVersion)] - assert.True(t, has) - assert.Contains(t, files, "%FILES%\netc/dummy\nopt/file/bin\n\n") - - for _, indexFile := range []string{ - arch_service.IndexArchiveFilename, - arch_service.IndexArchiveFilename + ".tar.gz", - "index.db", - "index.db.tar.gz", - "index.files", - "index.files.tar.gz", - } { - req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, indexFile)) - MakeRequest(t, req, http.StatusOK) + content[hd.Name] = string(buf) + } - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s.sig", rootURL, repository, indexFile)) - MakeRequest(t, req, http.StatusOK) + return content, nil } - }) - t.Run("Download", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + t.Run("Index", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + resp := MakeRequest(t, req, http.StatusOK) + + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) + + desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%FILENAME%\n"+fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression)+"\n\n") + assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") + assert.Contains(t, desc, "%VERSION%\n"+packageVersion+"\n\n") + assert.Contains(t, desc, "%ARCH%\naarch64\n") + assert.NotContains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") + assert.Contains(t, desc, "%LICENSE%\nMIT\n") + + files, has := content[fmt.Sprintf("%s-%s/files", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, files, "%FILES%\netc/dummy\nopt/file/bin\n\n") + + for _, indexFile := range []string{ + arch_service.IndexArchiveFilename, + arch_service.IndexArchiveFilename + ".tar.gz", + "index.db", + "index.db.tar.gz", + "index.files", + "index.files.tar.gz", + } { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, indexFile)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s.sig", rootURL, repository, indexFile)) + MakeRequest(t, req, http.StatusOK) + } + }) - req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pck.tar.zst", rootURL, repository, packageName, packageVersion)) - MakeRequest(t, req, http.StatusOK) + t.Run("Download", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pck.tar.zst.sig", rootURL, repository, packageName, packageVersion)) - MakeRequest(t, req, http.StatusOK) - }) + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) - t.Run("Any", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s.sig", rootURL, repository, packageName, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + }) - req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", rootURL, repository), bytes.NewReader(contentAny)). - AddBasicAuth(user.Name) - MakeRequest(t, req, http.StatusCreated) + t.Run("Any", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) - resp := MakeRequest(t, req, http.StatusOK) + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", rootURL, repository), bytes.NewReader(contentAny)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) - content, err := readIndexContent(resp.Body) - assert.NoError(t, err) + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + resp := MakeRequest(t, req, http.StatusOK) - desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] - assert.True(t, has) - assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") - assert.Contains(t, desc, "%ARCH%\naarch64\n") + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) - desc, has = content[fmt.Sprintf("%s-%s/desc", packageName+"_"+arch_module.AnyArch, packageVersion)] - assert.True(t, has) - assert.Contains(t, desc, "%NAME%\n"+packageName+"_any\n\n") - assert.Contains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") + desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") + assert.Contains(t, desc, "%ARCH%\naarch64\n") - // "any" architecture package should be available with every architecture requested - for _, arch := range []string{arch_module.AnyArch, "aarch64", "myarch"} { - req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s-%s-any.pck.tar.zst", rootURL, repository, arch, packageName+"_"+arch_module.AnyArch, packageVersion)) - MakeRequest(t, req, http.StatusOK) - } + desc, has = content[fmt.Sprintf("%s-%s/desc", packageName+"_"+arch_module.AnyArch, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%NAME%\n"+packageName+"_any\n\n") + assert.Contains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") - req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/any/%s-%s-any.pck.tar.zst", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)). - AddBasicAuth(user.Name) - MakeRequest(t, req, http.StatusNoContent) - }) - }) - } + // "any" architecture package should be available with every architecture requested + for _, arch := range []string{arch_module.AnyArch, "aarch64", "myarch"} { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s-%s-any.pkg.tar.%s", rootURL, repository, arch, packageName+"_"+arch_module.AnyArch, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + } - t.Run("Delete", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/any/%s-%s-any.pkg.tar.%s", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion, compression)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + }) - for _, repository := range repositories { - req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pck.tar.zst", rootURL, repository, packageName, packageVersion)) - MakeRequest(t, req, http.StatusUnauthorized) + t.Run("Delete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pck.tar.zst", rootURL, repository, packageName, packageVersion)). - AddBasicAuth(user.Name) - MakeRequest(t, req, http.StatusNoContent) + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression)) + MakeRequest(t, req, http.StatusUnauthorized) - // Deleting the last file of an architecture should remove that index - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) - MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + // Deleting the last file of an architecture should remove that index + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + MakeRequest(t, req, http.StatusNotFound) + }) + }) } - }) + } } From 52ee57189b03de41a0c9aefaa8694a6b9c32b3b4 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 27 Nov 2024 16:01:56 +0000 Subject: [PATCH 5/9] Remove docs. --- docs/content/usage/packages/arch.en-us.md | 65 ----------------------- 1 file changed, 65 deletions(-) delete mode 100644 docs/content/usage/packages/arch.en-us.md diff --git a/docs/content/usage/packages/arch.en-us.md b/docs/content/usage/packages/arch.en-us.md deleted file mode 100644 index 095d024a3de39..0000000000000 --- a/docs/content/usage/packages/arch.en-us.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -date: "2016-11-08T16:00:00+02:00" -title: "Arch Package Registry" -weight: 10 -toc: true -draft: false -menu: - sidebar: - parent: "packages" - name: "Arch" - weight: 10 - identifier: "arch" ---- - -# Arch package registry - -Gitea has a Arch Linux package registry, which can act as a fully working [Arch linux mirror](https://wiki.archlinux.org/title/mirrors) and connected directly in `/etc/pacman.conf`. Gitea automatically creates pacman database for packages in user/organization space when a new Arch package is uploaded. - -**Table of Contents** - -{{< toc >}} - -## Install packages - -First, you need to update your pacman configuration, adding following lines: - -```conf -[{owner}.{domain}] -SigLevel = Optional TrustAll -Server = https://{domain}/api/packages/{owner}/arch/{distribution}/{architecture} -``` - -Then, you can run pacman sync command (with -y flag to load connected database file), to install your package: - -```sh -pacman -Sy package -``` - -## Upload packages - -When uploading the package to gitea, you have to prepare package file with the `.pkg.tar.zst` extension and its `.pkg.tar.zst.sig` signature. You can use [curl](https://curl.se/) or any other HTTP client, Gitea supports multiple [authentication schemes](https://docs.gitea.com/usage/authentication). The upload command will create 3 files: package, signature and desc file for the pacman database (which will be created automatically on request). - -The following command will upload arch package and related signature to gitea with basic authentification: - -```sh -curl -X PUT \ - https://{domain}/api/packages/{owner}/arch/push/{package-1-1-x86_64.pkg.tar.zst}/{archlinux}/$(xxd -p package-1-1-x86_64.pkg.tar.zst.sig | tr -d '\n') \ - --user your_username:your_token_or_password \ - --header "Content-Type: application/octet-stream" \ - --data-binary '@/path/to/package/file/package-1-1-x86_64.pkg.tar.zst' -``` - -## Delete packages - -The `DELETE` method will remove specific package version, and all package files related to that version: - -```sh -curl -X DELETE \ - https://{domain}/api/packages/{user}/arch/remove/{package}/{version} \ - --user your_username:your_token_or_password -``` - -## Clients - -Any `pacman` compatible package manager or AUR-helper can be used to install packages from gitea ([yay](https://github.com/Jguer/yay), [paru](https://github.com/Morganamilo/paru), [pikaur](https://github.com/actionless/pikaur), [aura](https://github.com/fosskers/aura)). Alternatively, you can try [pack](https://fmnx.su/core/pack) which supports full gitea API (install/push/remove). Also, any HTTP client can be used to execute get/push/remove operations ([curl](https://curl.se/), [postman](https://www.postman.com/), [thunder-client](https://www.thunderclient.com/)). From a96de369419c12e16e8f524afd663e514b7be9fa Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 28 Nov 2024 14:25:07 +0000 Subject: [PATCH 6/9] Allow empty repository name. Change delete endpoint. --- routers/api/packages/api.go | 25 +++++++++++---------- routers/api/packages/arch/arch.go | 25 +++++++++++++-------- tests/integration/api_packages_arch_test.go | 8 +++---- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 9707501409008..7164dd494eafa 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -155,24 +155,25 @@ func CommonRoutes() *web.Router { pathFields := strings.Split(path, "/") pathFieldsLen := len(pathFields) - if pathFieldsLen >= 2 { + if (ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET") && pathFieldsLen >= 2 { ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/")) ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2]) ctx.SetPathParam("filename", pathFields[pathFieldsLen-1]) + arch.GetPackageOrRepositoryFile(ctx) + return + } - if ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET" { - arch.GetPackageOrRepositoryFile(ctx) - return - } - - if ctx.Req.Method == "DELETE" { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - arch.DeletePackageFile(ctx) + if ctx.Req.Method == "DELETE" && pathFieldsLen >= 3 { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { return } + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/")) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-3]) + ctx.SetPathParam("name", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("version", pathFields[pathFieldsLen-1]) + arch.DeletePackageVersion(ctx) + return } ctx.Status(http.StatusNotFound) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 70671cdfc9f18..573e93cfb016f 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -43,10 +43,6 @@ func GetRepositoryKey(ctx *context.Context) { func UploadPackageFile(ctx *context.Context) { repository := strings.TrimSpace(ctx.PathParam("repository")) - if repository == "" { - apiError(ctx, http.StatusBadRequest, "invalid repository") - return - } upload, needToClose, err := ctx.UploadStream() if err != nil { @@ -256,8 +252,11 @@ func GetPackageOrRepositoryFile(ctx *context.Context) { helper.ServePackageFile(ctx, s, u, pf) } -func DeletePackageFile(ctx *context.Context) { - repository, architecture := ctx.PathParam("repository"), ctx.PathParam("architecture") +func DeletePackageVersion(ctx *context.Context) { + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + name := ctx.PathParam("name") + version := ctx.PathParam("version") release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID) if err != nil { @@ -266,10 +265,18 @@ func DeletePackageFile(ctx *context.Context) { } defer release() + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeArch, name, version) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - PackageType: packages_model.TypeArch, - Query: ctx.PathParam("filename"), + VersionID: pv.ID, CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), }) if err != nil { diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go index 00a36301f58c6..909d1059b6994 100644 --- a/tests/integration/api_packages_arch_test.go +++ b/tests/integration/api_packages_arch_test.go @@ -81,7 +81,7 @@ license = MIT`) } compressions := []string{"gz", "xz", "zst"} - repositories := []string{"main", "testing", "with/slash"} + repositories := []string{"main", "testing", "with/slash", ""} rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name) @@ -277,7 +277,7 @@ license = MIT`) MakeRequest(t, req, http.StatusOK) } - req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/any/%s-%s-any.pkg.tar.%s", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion, compression)). + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/any/%s/%s", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent) }) @@ -285,10 +285,10 @@ license = MIT`) t.Run("Delete", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression)) + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s/%s", rootURL, repository, packageName, packageVersion)) MakeRequest(t, req, http.StatusUnauthorized) - req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression)). + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s/%s", rootURL, repository, packageName, packageVersion)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent) From 19cb13be91e618450acb18b6cb6c44b0147277b5 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 3 Dec 2024 10:59:41 +0800 Subject: [PATCH 7/9] fix ui --- templates/package/metadata/alpine.tmpl | 2 +- templates/package/metadata/arch.tmpl | 4 ++-- templates/package/metadata/cargo.tmpl | 10 +++++----- templates/package/metadata/chef.tmpl | 6 +++--- templates/package/metadata/composer.tmpl | 6 +++--- templates/package/metadata/conan.tmpl | 8 ++++---- templates/package/metadata/conda.tmpl | 8 ++++---- templates/package/metadata/container.tmpl | 14 ++++++------- templates/package/metadata/cran.tmpl | 6 +++--- templates/package/metadata/debian.tmpl | 4 ++-- templates/package/metadata/helm.tmpl | 4 ++-- templates/package/metadata/maven.tmpl | 8 ++++---- templates/package/metadata/npm.tmpl | 8 ++++---- templates/package/metadata/nuget.tmpl | 6 +++--- templates/package/metadata/pub.tmpl | 6 +++--- templates/package/metadata/pypi.tmpl | 6 +++--- templates/package/metadata/rpm.tmpl | 4 ++-- templates/package/metadata/rubygems.tmpl | 6 +++--- templates/package/metadata/swift.tmpl | 2 +- templates/package/metadata/vagrant.tmpl | 6 +++--- templates/package/view.tmpl | 24 +++++++++++------------ web_src/css/base.css | 2 +- 22 files changed, 74 insertions(+), 76 deletions(-) diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl index 3e7f10f66a6d5..2657f51ae5d48 100644 --- a/templates/package/metadata/alpine.tmpl +++ b/templates/package/metadata/alpine.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "alpine"}} {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl index a461d697a4564..2aea036ec2dd2 100644 --- a/templates/package/metadata/arch.tmpl +++ b/templates/package/metadata/arch.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "arch"}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} {{end}} diff --git a/templates/package/metadata/cargo.tmpl b/templates/package/metadata/cargo.tmpl index 5ad3c20a932fb..f7dd887a24d6e 100644 --- a/templates/package/metadata/cargo.tmpl +++ b/templates/package/metadata/cargo.tmpl @@ -1,7 +1,7 @@ {{if eq .PackageDescriptor.Package.Type "cargo"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/chef.tmpl b/templates/package/metadata/chef.tmpl index 23a9ce3ec0bbe..6bf606ca487f5 100644 --- a/templates/package/metadata/chef.tmpl +++ b/templates/package/metadata/chef.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "chef"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/composer.tmpl b/templates/package/metadata/composer.tmpl index 0f6ff9d6f2d75..e69e91745fc0b 100644 --- a/templates/package/metadata/composer.tmpl +++ b/templates/package/metadata/composer.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "composer"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.Homepage}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.Homepage}}{{end}} + {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/conan.tmpl b/templates/package/metadata/conan.tmpl index 4e05ec2587cb3..8b15375553931 100644 --- a/templates/package/metadata/conan.tmpl +++ b/templates/package/metadata/conan.tmpl @@ -1,6 +1,6 @@ {{if eq .PackageDescriptor.Package.Type "conan"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl index 3628686e13acd..4add9453fa7d5 100644 --- a/templates/package/metadata/conda.tmpl +++ b/templates/package/metadata/conda.tmpl @@ -1,6 +1,6 @@ {{if eq .PackageDescriptor.Package.Type "conda"}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/container.tmpl b/templates/package/metadata/container.tmpl index f5abb7ef6e5f5..ecc17964d781a 100644 --- a/templates/package/metadata/container.tmpl +++ b/templates/package/metadata/container.tmpl @@ -1,9 +1,9 @@ {{if eq .PackageDescriptor.Package.Type "container"}} -
{{svg "octicon-package" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Type.Name}}
- {{if .PackageDescriptor.Metadata.Platform}}
{{svg "octicon-cpu" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Platform}}
{{end}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Licenses}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} +
{{svg "octicon-package"}} {{.PackageDescriptor.Metadata.Type.Name}}
+ {{if .PackageDescriptor.Metadata.Platform}}
{{svg "octicon-cpu"}} {{.PackageDescriptor.Metadata.Platform}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.Licenses}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/cran.tmpl b/templates/package/metadata/cran.tmpl index 1d5a11e196725..3ada7ac743a06 100644 --- a/templates/package/metadata/cran.tmpl +++ b/templates/package/metadata/cran.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "cran"}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "mr-3"}} {{.}}
{{end}} - {{range .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.ProjectURL}}{{end}} {{end}} diff --git a/templates/package/metadata/debian.tmpl b/templates/package/metadata/debian.tmpl index 3cd845c9fe928..d35e8b00daadd 100644 --- a/templates/package/metadata/debian.tmpl +++ b/templates/package/metadata/debian.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "debian"}} - {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} {{end}} diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl index 50ea484999108..b3b3f348cf1f2 100644 --- a/templates/package/metadata/helm.tmpl +++ b/templates/package/metadata/helm.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "helm"}} - {{range .PackageDescriptor.Metadata.Maintainers}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.Home}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{range .PackageDescriptor.Metadata.Maintainers}}
{{svg "octicon-person"}} {{.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.Home}}{{end}} {{end}} diff --git a/templates/package/metadata/maven.tmpl b/templates/package/metadata/maven.tmpl index 36412723d24f8..33662be7caf4e 100644 --- a/templates/package/metadata/maven.tmpl +++ b/templates/package/metadata/maven.tmpl @@ -1,8 +1,8 @@ {{if and (eq .PackageDescriptor.Package.Type "maven") (not .PackageDescriptor.Metadata)}} -
{{svg "octicon-note" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.no_metadata"}}
+
{{svg "octicon-note"}} {{ctx.Locale.Tr "packages.no_metadata"}}
{{end}} {{if and (eq .PackageDescriptor.Package.Type "maven") .PackageDescriptor.Metadata}} - {{if .PackageDescriptor.Metadata.Name}}
{{svg "octicon-note" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.Name}}
{{svg "octicon-note"}} {{.PackageDescriptor.Metadata.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/npm.tmpl b/templates/package/metadata/npm.tmpl index df37504e374d3..ff245f2b03ef2 100644 --- a/templates/package/metadata/npm.tmpl +++ b/templates/package/metadata/npm.tmpl @@ -1,8 +1,8 @@ {{if eq .PackageDescriptor.Package.Type "npm"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{range .PackageDescriptor.VersionProperties}} - {{if eq .Name "npm.tag"}}
{{svg "octicon-versions" 16 "tw-mr-2"}} {{.Value}}
{{end}} + {{if eq .Name "npm.tag"}}
{{svg "octicon-versions"}} {{.Value}}
{{end}} {{end}} {{end}} diff --git a/templates/package/metadata/nuget.tmpl b/templates/package/metadata/nuget.tmpl index 5534577bd26ef..2d18528f857e4 100644 --- a/templates/package/metadata/nuget.tmpl +++ b/templates/package/metadata/nuget.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "nuget"}} - {{if .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Authors}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Authors}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/pub.tmpl b/templates/package/metadata/pub.tmpl index 16f7cec370407..e54207c4c60e6 100644 --- a/templates/package/metadata/pub.tmpl +++ b/templates/package/metadata/pub.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "pub"}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/pypi.tmpl b/templates/package/metadata/pypi.tmpl index 3d9b213907f81..9dfac07cbfe6c 100644 --- a/templates/package/metadata/pypi.tmpl +++ b/templates/package/metadata/pypi.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "pypi"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/rpm.tmpl b/templates/package/metadata/rpm.tmpl index eda8a489f3cdf..65093933a92b2 100644 --- a/templates/package/metadata/rpm.tmpl +++ b/templates/package/metadata/rpm.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "rpm"}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/rubygems.tmpl b/templates/package/metadata/rubygems.tmpl index 9b11287691c34..04fc3695abe08 100644 --- a/templates/package/metadata/rubygems.tmpl +++ b/templates/package/metadata/rubygems.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "rubygems"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}} {{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl index fdffb6dedec3f..f8f74859e6139 100644 --- a/templates/package/metadata/swift.tmpl +++ b/templates/package/metadata/swift.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "swift"}} {{if .PackageDescriptor.Metadata.Author.String}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl index 4628a2dcbb0f6..795ab33da9350 100644 --- a/templates/package/metadata/vagrant.tmpl +++ b/templates/package/metadata/vagrant.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "vagrant"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 93f132f7bd58d..9e92207466d96 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -1,12 +1,10 @@ {{template "base/head" .}} -
+
{{template "shared/user/org_profile_avatar" .}}
{{template "user/overview/header" .}}
-
-

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})

-
+

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})

{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} {{if .HasRepositoryAccess}} @@ -43,13 +41,13 @@
{{ctx.Locale.Tr "packages.details"}} -
-
{{svg .PackageDescriptor.Package.Type.SVGName 16 "tw-mr-2"}} {{.PackageDescriptor.Package.Type.Name}}
+
+
{{svg .PackageDescriptor.Package.Type.SVGName}} {{.PackageDescriptor.Package.Type.Name}}
{{if .HasRepositoryAccess}} -
{{svg "octicon-repo" 16 "tw-mr-2"}} {{.PackageDescriptor.Repository.FullName}}
+ {{end}} -
{{svg "octicon-calendar" 16 "tw-mr-2"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
-
{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}
+
{{svg "octicon-calendar"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
+
{{svg "octicon-download"}} {{.PackageDescriptor.Version.DownloadCount}}
{{template "package/metadata/alpine" .}} {{template "package/metadata/arch" .}} {{template "package/metadata/cargo" .}} @@ -72,7 +70,7 @@ {{template "package/metadata/swift" .}} {{template "package/metadata/vagrant" .}} {{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}} -
{{svg "octicon-database" 16 "tw-mr-2"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
+
{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
{{end}}
{{if not (eq .PackageDescriptor.Package.Type "container")}} @@ -100,12 +98,12 @@
{{if or .CanWritePackages .HasRepositoryAccess}}
-
+
{{if .HasRepositoryAccess}} -
{{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues"}}
+
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}}
{{end}} {{if .CanWritePackages}} -
{{svg "octicon-tools" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.settings"}}
+
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
{{end}}
{{end}} diff --git a/web_src/css/base.css b/web_src/css/base.css index babbf4c89dcab..8f5ef51c4aa8b 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1386,7 +1386,7 @@ table th[data-sortt-desc] .svg { .flex-text-block { display: flex; align-items: center; - gap: .25rem; + gap: .5rem; min-width: 0; } From d341038e411dd62910c9837502386faba6b6f526 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 3 Dec 2024 11:04:25 +0800 Subject: [PATCH 8/9] fix more ui mistakes --- templates/package/metadata/alpine.tmpl | 4 ++-- templates/package/metadata/swift.tmpl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl index 2657f51ae5d48..c9174948b1e3a 100644 --- a/templates/package/metadata/alpine.tmpl +++ b/templates/package/metadata/alpine.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "alpine"}} - {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl index f8f74859e6139..fe28759de3f5b 100644 --- a/templates/package/metadata/swift.tmpl +++ b/templates/package/metadata/swift.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "swift"}} - {{if .PackageDescriptor.Metadata.Author.String}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.Author.String}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} From 0669ca7c74431fe651bacbba19c231d8ecea1807 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 3 Dec 2024 14:58:18 +0000 Subject: [PATCH 9/9] Move architecture parameter to the end. --- routers/api/packages/api.go | 6 +++--- tests/integration/api_packages_arch_test.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 7164dd494eafa..02b5d32bdc278 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -169,9 +169,9 @@ func CommonRoutes() *web.Router { return } ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/")) - ctx.SetPathParam("architecture", pathFields[pathFieldsLen-3]) - ctx.SetPathParam("name", pathFields[pathFieldsLen-2]) - ctx.SetPathParam("version", pathFields[pathFieldsLen-1]) + ctx.SetPathParam("name", pathFields[pathFieldsLen-3]) + ctx.SetPathParam("version", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-1]) arch.DeletePackageVersion(ctx) return } diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go index 909d1059b6994..9c7a9dd19dec1 100644 --- a/tests/integration/api_packages_arch_test.go +++ b/tests/integration/api_packages_arch_test.go @@ -277,7 +277,7 @@ license = MIT`) MakeRequest(t, req, http.StatusOK) } - req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/any/%s/%s", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)). + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/any", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent) }) @@ -285,10 +285,10 @@ license = MIT`) t.Run("Delete", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s/%s", rootURL, repository, packageName, packageVersion)) + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)) MakeRequest(t, req, http.StatusUnauthorized) - req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/aarch64/%s/%s", rootURL, repository, packageName, packageVersion)). + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent)