From 0c3c041c88afc66a5048c1d8cf1b29c8bbbb798f Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 5 Dec 2024 00:09:07 +0100 Subject: [PATCH] Add Arch package registry (#32692) Close #25037 Close #31037 This PR adds a Arch package registry usable with pacman. ![grafik](https://github.com/user-attachments/assets/81cdb0c2-02f9-4733-bee2-e48af6b45224) Rewrite of #25396 and #31037. You can follow [this tutorial](https://wiki.archlinux.org/title/Creating_packages) to build a package for testing. Docs PR: https://gitea.com/gitea/docs/pulls/111 Co-authored-by: [d1nch8g@ion.lc](mailto:d1nch8g@ion.lc) Co-authored-by: @ExplodingDragon --------- Co-authored-by: dancheg97 Co-authored-by: dragon Co-authored-by: wxiaoguang --- models/packages/arch/search.go | 38 ++ models/packages/descriptor.go | 3 + models/packages/package.go | 6 + models/packages/package_file.go | 5 + modules/packages/arch/metadata.go | 249 ++++++++++++ modules/packages/arch/metadata_test.go | 157 ++++++++ modules/setting/packages.go | 2 + options/locale/locale_en-US.ini | 5 + public/assets/img/svg/gitea-arch.svg | 1 + routers/api/packages/api.go | 44 +++ routers/api/packages/arch/arch.go | 306 +++++++++++++++ routers/web/user/package.go | 31 +- services/forms/package_form.go | 2 +- services/packages/alpine/repository.go | 2 +- services/packages/arch/repository.go | 401 ++++++++++++++++++++ services/packages/cleanup/cleanup.go | 18 +- services/packages/packages.go | 2 + templates/package/content/arch.tmpl | 41 ++ templates/package/content/container.tmpl | 4 +- templates/package/metadata/alpine.tmpl | 6 +- 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 | 4 +- templates/package/metadata/vagrant.tmpl | 6 +- templates/package/view.tmpl | 26 +- tests/integration/api_packages_arch_test.go | 302 +++++++++++++++ web_src/css/base.css | 2 +- web_src/svg/gitea-arch.svg | 1 + 43 files changed, 1687 insertions(+), 91 deletions(-) create mode 100644 models/packages/arch/search.go 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/repository.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/models/packages/arch/search.go b/models/packages/arch/search.go new file mode 100644 index 0000000000..f35c037b23 --- /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/models/packages/descriptor.go b/models/packages/descriptor.go index b8ef698d38..803b73c968 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 65a2574150..417d62d199 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/models/packages/package_file.go b/models/packages/package_file.go index 1bb6b57a34..270cb32fdf 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 new file mode 100644 index 0000000000..e1e79c60e0 --- /dev/null +++ b/modules/packages/arch/metadata.go @@ -0,0 +1,249 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "io" + "regexp" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "github.com/klauspost/compress/zstd" + "github.com/ulikunitz/xz" +) + +const ( + 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") + 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") + + // https://man.archlinux.org/man/PKGBUILD.5 + 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 { + Name string + Version string + VersionMetadata VersionMetadata + FileMetadata FileMetadata + FileCompressionExtension string +} + +type VersionMetadata struct { + Description string `json:"description,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Licenses []string `json:"licenses,omitempty"` +} + +type FileMetadata struct { + 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"` +} + +// ParsePackage parses an Arch package file +func ParsePackage(r io.Reader) (*Package, error) { + header := make([]byte, 10) + n, err := util.ReadAtMost(r, header) + if err != nil { + return nil, err + } + + 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(inner) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + filename := hd.FileInfo().Name() + if filename == ".PKGINFO" { + p, err = ParsePackageInfo(tr) + if err != nil { + return nil, err + } + } else if !strings.HasPrefix(filename, ".") { + files = append(files, hd.Name) + } + } + + if p == nil { + return nil, ErrMissingPKGINFOFile + } + + p.FileMetadata.Files = files + p.FileCompressionExtension = compressionType + + return p, nil +} + +// 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{} + + s := bufio.NewScanner(r) + for s.Scan() { + line := s.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.FileMetadata.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.Architecture = value + case "license": + p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value) + case "provides": + p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) + case "depend": + p.FileMetadata.Depends = append(p.FileMetadata.Depends, value) + case "optdepend": + p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value) + case "makedepend": + p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value) + case "checkdepend": + p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value) + case "backup": + p.FileMetadata.Backup = append(p.FileMetadata.Backup, value) + case "group": + p.FileMetadata.Groups = append(p.FileMetadata.Groups, value) + case "builddate": + date, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + p.FileMetadata.BuildDate = date + case "size": + size, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + p.FileMetadata.InstalledSize = size + case "xdata": + p.FileMetadata.XData = append(p.FileMetadata.XData, value) + } + } + if err := s.Err(); err != nil { + return nil, err + } + + if !namePattern.MatchString(p.Name) { + return nil, ErrInvalidName + } + if !versionPattern.MatchString(p.Version) { + return nil, ErrInvalidVersion + } + if p.FileMetadata.Architecture == "" { + return nil, ErrInvalidArchitecture + } + + if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { + p.VersionMetadata.ProjectURL = "" + } + + return p, nil +} diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go new file mode 100644 index 0000000000..f611ef5e84 --- /dev/null +++ b/modules/packages/arch/metadata_test.go @@ -0,0 +1,157 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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 ( + packageName = "gitea" + packageVersion = "1.0.1" + packageDescription = "Package Description" + packageProjectURL = "https://gitea.com" + packagePackager = "KN4CK3R " +) + +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 = MIT +packager = ` + packagePackager + ` +depend = common +xdata = value +depend = gitea +provides = common +provides = gitea +optdepend = hex +checkdepend = common +makedepend = cmake +backup = usr/bin/paket1`) +} + +func TestParsePackage(t *testing.T) { + createPackage := func(compression string, files map[string][]byte) io.Reader { + var buf bytes.Buffer + 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{ + Name: name, + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write(content) + } + + tw.Close() + cw.Close() + + return &buf + } + + 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) + }) + + 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) + }) + + 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) + + assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files) + }) + }) + } +} + +func TestParsePackageInfo(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + data := createPKGINFOContent("", packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("InvalidVersion", func(t *testing.T) { + data := createPKGINFOContent(packageName, "") + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPKGINFOContent(packageName, packageVersion) + + 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) + }) +} diff --git a/modules/setting/packages.go b/modules/setting/packages.go index bc093e7ea6..3f618cfd64 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -22,6 +22,7 @@ var ( LimitTotalOwnerCount int64 LimitTotalOwnerSize int64 LimitSizeAlpine int64 + LimitSizeArch int64 LimitSizeCargo int64 LimitSizeChef int64 LimitSizeComposer int64 @@ -79,6 +80,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 57d2d89c5a..395063faf8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3524,6 +3524,11 @@ alpine.repository = Repository Info alpine.repository.branches = Branches alpine.repository.repositories = Repositories alpine.repository.architectures = Architectures +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/public/assets/img/svg/gitea-arch.svg b/public/assets/img/svg/gitea-arch.svg new file mode 100644 index 0000000000..943a92c579 --- /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 c3da5a7513..4e194f65fa 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" @@ -135,6 +136,49 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/arch", func() { + 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 (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 == "DELETE" && pathFieldsLen >= 3 { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/")) + ctx.SetPathParam("name", pathFields[pathFieldsLen-3]) + ctx.SetPathParam("version", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-1]) + arch.DeletePackageVersion(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 new file mode 100644 index 0000000000..573e93cfb0 --- /dev/null +++ b/routers/api/packages/arch/arch.go @@ -0,0 +1,306 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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/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" +) + +func apiError(ctx *context.Context, status int, obj any) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.PlainText(status, message) + }) +} + +func GetRepositoryKey(ctx *context.Context) { + _, pub, err := arch_service.GetOrCreateKeyPair(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ContentType: "application/pgp-keys", + }) +} + +func UploadPackageFile(ctx *context.Context) { + repository := strings.TrimSpace(ctx.PathParam("repository")) + + upload, needToClose, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if needToClose { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + 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 + } + + 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{ + 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.pkg.tar.%s", pck.Name, pck.Version, pck.FileMetadata.Architecture, pck.FileCompressionExtension), + 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: + 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 + } + + 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) +} + +func GetPackageOrRepositoryFile(ctx *context.Context) { + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + filename := ctx.PathParam("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(filename, ".db.tar.gz") || strings.HasSuffix(filename, ".files.tar.gz") || strings.HasSuffix(filename, ".files") || strings.HasSuffix(filename, ".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) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + opts.VersionID = pv.ID + } + + pfs, _, err := packages_model.SearchFiles(ctx, opts) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pfs) == 0 { + // Try again with architecture 'any' + if architecture == arch_module.AnyArch { + apiError(ctx, http.StatusNotFound, nil) + return + } + + 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 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(data), &context.ServeHeaderOptions{ + Filename: filenameOrig, + }) + return + } + + 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) +} + +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 { + apiError(ctx, http.StatusInternalServerError, err) + return + } + 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{ + VersionID: pv.ID, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + }) + if err != nil { + 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) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 4e39eabc0f..c6f85ac734 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -20,6 +20,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" @@ -178,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]) @@ -204,6 +205,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: @@ -249,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 cc940d42d3..9b6f907164 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/alpine/repository.go b/services/packages/alpine/repository.go index 664ab34559..27e6391980 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 0000000000..ab1b85ae95 --- /dev/null +++ b/services/packages/arch/repository.go @@ -0,0 +1,401 @@ +// 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/globallock" + "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" +) + +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) { + 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: "%.pkg.tar.%", + 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 +} + +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), []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), []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 []keyValue) error { + buf := &bytes.Buffer{} + for _, kv := range fields { + if kv.Value == "" { + continue + } + fmt.Fprintf(buf, "%%%s%%\n%s\n\n", kv.Key, kv.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/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go index d7c9355da5..b7ba2b6ac4 100644 --- a/services/packages/cleanup/cleanup.go +++ b/services/packages/cleanup/cleanup.go @@ -16,6 +16,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" packages_service "code.gitea.io/gitea/services/packages" alpine_service "code.gitea.io/gitea/services/packages/alpine" + arch_service "code.gitea.io/gitea/services/packages/arch" cargo_service "code.gitea.io/gitea/services/packages/cargo" container_service "code.gitea.io/gitea/services/packages/container" debian_service "code.gitea.io/gitea/services/packages/debian" @@ -120,18 +121,29 @@ func ExecuteCleanupRules(outerCtx context.Context) error { } if anyVersionDeleted { - if pcr.Type == packages_model.TypeDebian { + switch pcr.Type { + case packages_model.TypeDebian: if err := debian_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: debian.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } - } else if pcr.Type == packages_model.TypeAlpine { + case packages_model.TypeAlpine: if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } - } else if pcr.Type == packages_model.TypeRpm { + case packages_model.TypeRpm: if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { 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) + } } } return nil diff --git a/services/packages/packages.go b/services/packages/packages.go index 95579be34b..55351afce2 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 0000000000..1c568cbb78 --- /dev/null +++ b/templates/package/content/arch.tmpl @@ -0,0 +1,41 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} +

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

+
+
+
+ +
[{{.PackageDescriptor.Owner.LowerName}}.{{.PackageRegistryHost}}]
+SigLevel = Optional TrustAll
+Server = 
+
+
+ +
pacman -Sy {{.PackageDescriptor.Package.LowerName}}
+
+
+ +
+
+
+ +

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

+
+ + + + + + + + + + + +
{{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/content/container.tmpl b/templates/package/content/container.tmpl index 138fedecb3..aaed25bfbd 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/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl index 3e7f10f66a..c9174948b1 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.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/arch.tmpl b/templates/package/metadata/arch.tmpl new file mode 100644 index 0000000000..2aea036ec2 --- /dev/null +++ b/templates/package/metadata/arch.tmpl @@ -0,0 +1,4 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} + {{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 5ad3c20a93..f7dd887a24 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 23a9ce3ec0..6bf606ca48 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 0f6ff9d6f2..e69e91745f 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 4e05ec2587..8b15375553 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 3628686e13..4add9453fa 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 f5abb7ef6e..ecc17964d7 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 1d5a11e196..3ada7ac743 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 3cd845c9fe..d35e8b00da 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 50ea484999..b3b3f348cf 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 36412723d2..33662be7ca 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 df37504e37..ff245f2b03 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 5534577bd2..2d18528f85 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 16f7cec370..e54207c4c6 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 3d9b213907..9dfac07cbf 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 eda8a489f3..65093933a9 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 9b11287691..04fc3695ab 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 fdffb6dede..fe28759de3 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.Author.String}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl index 4628a2dcbb..795ab33da9 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 0f06d7afbd..9e92207466 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}} @@ -19,6 +17,7 @@
{{template "package/content/alpine" .}} + {{template "package/content/arch" .}} {{template "package/content/cargo" .}} {{template "package/content/chef" .}} {{template "package/content/composer" .}} @@ -42,14 +41,15 @@
{{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" .}} {{template "package/metadata/chef" .}} {{template "package/metadata/composer" .}} @@ -70,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")}} @@ -98,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/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go new file mode 100644 index 0000000000..9c7a9dd19d --- /dev/null +++ b/tests/integration/api_packages_arch_test.go @@ -0,0 +1,302 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + arch_module "code.gitea.io/gitea/modules/packages/arch" + arch_service "code.gitea.io/gitea/services/packages/arch" + "code.gitea.io/gitea/tests" + + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" +) + +func TestPackageArch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + packageName := "gitea-test" + packageVersion := "1.4.1-r3" + + createPackage := func(compression, name, version, architecture string) []byte { + var buf bytes.Buffer + 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 + ` +pkgver = ` + version + ` +pkgdesc = Description +# comment +builddate = 1678834800 +size = 8 +arch = ` + architecture + ` +license = MIT`) + + hdr := &tar.Header{ + Name: ".PKGINFO", + Mode: 0o600, + Size: int64(len(info)), + } + 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")) + } + + tw.Close() + cw.Close() + + return buf.Bytes() + } + + 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+"/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-----") + }) + + 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) + + 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) + + // 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) + }) + + 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 + } + + 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) + } + }) + + t.Run("Download", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + 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) + + 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) + }) + + t.Run("Any", 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) + + 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, "%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.pkg.tar.%s", rootURL, repository, arch, packageName+"_"+arch_module.AnyArch, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + } + + 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) + }) + + t.Run("Delete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + 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/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)). + 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) + }) + }) + } + } +} diff --git a/web_src/css/base.css b/web_src/css/base.css index babbf4c89d..8f5ef51c4a 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; } diff --git a/web_src/svg/gitea-arch.svg b/web_src/svg/gitea-arch.svg new file mode 100644 index 0000000000..ba8254d804 --- /dev/null +++ b/web_src/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file