mirror of https://github.com/go-gitea/gitea
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 <dancheg97@fmnx.su> Co-authored-by: dragon <ExplodingFKL@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>pull/32711/head^2
parent
5ab7aa700f
commit
0c3c041c88
@ -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, |
||||||
|
}, |
||||||
|
) |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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 <packager@gitea.com>" |
||||||
|
) |
||||||
|
|
||||||
|
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) |
||||||
|
}) |
||||||
|
} |
After Width: | Height: | Size: 402 B |
@ -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) |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
{{if eq .PackageDescriptor.Package.Type "arch"}} |
||||||
|
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.installation"}}</h4> |
||||||
|
<div class="ui attached segment"> |
||||||
|
<div class="ui form"> |
||||||
|
<div class="field"> |
||||||
|
<label>{{svg "octicon-gear"}} {{ctx.Locale.Tr "packages.arch.registry"}}</label> |
||||||
|
<div class="markup"><pre class="code-block"><code>[{{.PackageDescriptor.Owner.LowerName}}.{{.PackageRegistryHost}}] |
||||||
|
SigLevel = Optional TrustAll |
||||||
|
Server = <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/arch/$repo/$arch"></origin-url></code></pre></div> |
||||||
|
</div> |
||||||
|
<div class="field"> |
||||||
|
<label>{{svg "octicon-sync"}} {{ctx.Locale.Tr "packages.arch.install"}}</label> |
||||||
|
<div class="markup"><pre class="code-block"><code>pacman -Sy {{.PackageDescriptor.Package.LowerName}}</code></pre></div> |
||||||
|
</div> |
||||||
|
<div class="field"> |
||||||
|
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Arch" "https://docs.gitea.com/usage/packages/arch/"}}</label> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.arch.repository"}}</h4> |
||||||
|
<div class="ui attached segment"> |
||||||
|
<table class="ui single line very basic table"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.repository.repositories"}}</h5></td> |
||||||
|
<td>{{StringUtils.Join .Repositories ", "}}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.repository.architectures"}}</h5></td> |
||||||
|
<td>{{StringUtils.Join .Architectures ", "}}</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
{{if .PackageDescriptor.Metadata.Description}} |
||||||
|
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4> |
||||||
|
<div class="ui attached segment">{{.PackageDescriptor.Metadata.Description}}</div> |
||||||
|
{{end}} |
||||||
|
{{end}} |
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "alpine"}} |
{{if eq .PackageDescriptor.Package.Type "alpine"}} |
||||||
{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -0,0 +1,4 @@ |
|||||||
|
{{if eq .PackageDescriptor.Package.Type "arch"}} |
||||||
|
{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}} |
||||||
|
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
|
{{end}} |
@ -1,7 +1,7 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "cargo"}} |
{{if eq .PackageDescriptor.Package.Type "cargo"}} |
||||||
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "chef"}} |
{{if eq .PackageDescriptor.Package.Type "chef"}} |
||||||
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "composer"}} |
{{if eq .PackageDescriptor.Package.Type "composer"}} |
||||||
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.Name}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,6 +1,6 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "conan"}} |
{{if eq .PackageDescriptor.Package.Type "conan"}} |
||||||
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,6 +1,6 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "conda"}} |
{{if eq .PackageDescriptor.Package.Type "conda"}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,9 +1,9 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "container"}} |
{{if eq .PackageDescriptor.Package.Type "container"}} |
||||||
<div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Type.Name}}</div> |
<div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package"}} {{.PackageDescriptor.Metadata.Type.Name}}</div> |
||||||
{{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}} |
||||||
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "cran"}} |
{{if eq .PackageDescriptor.Package.Type "cran"}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.}}</div>{{end}} |
||||||
{{range .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{range .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,4 +1,4 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "debian"}} |
{{if eq .PackageDescriptor.Package.Type "debian"}} |
||||||
{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,4 +1,4 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "helm"}} |
{{if eq .PackageDescriptor.Package.Type "helm"}} |
||||||
{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.Name}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,8 +1,8 @@ |
|||||||
{{if and (eq .PackageDescriptor.Package.Type "maven") (not .PackageDescriptor.Metadata)}} |
{{if and (eq .PackageDescriptor.Package.Type "maven") (not .PackageDescriptor.Metadata)}} |
||||||
<div class="item">{{svg "octicon-note" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.no_metadata"}}</div> |
<div class="item">{{svg "octicon-note"}} {{ctx.Locale.Tr "packages.no_metadata"}}</div> |
||||||
{{end}} |
{{end}} |
||||||
{{if and (eq .PackageDescriptor.Package.Type "maven") .PackageDescriptor.Metadata}} |
{{if and (eq .PackageDescriptor.Package.Type "maven") .PackageDescriptor.Metadata}} |
||||||
{{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,8 +1,8 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "npm"}} |
{{if eq .PackageDescriptor.Package.Type "npm"}} |
||||||
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{range .PackageDescriptor.VersionProperties}} |
{{range .PackageDescriptor.VersionProperties}} |
||||||
{{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "tw-mr-2"}} {{.Value}}</div>{{end}} |
{{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions"}} {{.Value}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "nuget"}} |
{{if eq .PackageDescriptor.Package.Type "nuget"}} |
||||||
{{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "pub"}} |
{{if eq .PackageDescriptor.Package.Type "pub"}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "pypi"}} |
{{if eq .PackageDescriptor.Package.Type "pypi"}} |
||||||
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,4 +1,4 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "rpm"}} |
{{if eq .PackageDescriptor.Package.Type "rpm"}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "rubygems"}} |
{{if eq .PackageDescriptor.Package.Type "rubygems"}} |
||||||
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div> {{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div> {{end}} |
||||||
{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}} |
{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,4 +1,4 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "swift"}} |
{{if eq .PackageDescriptor.Package.Type "swift"}} |
||||||
{{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -1,5 +1,5 @@ |
|||||||
{{if eq .PackageDescriptor.Package.Type "vagrant"}} |
{{if eq .PackageDescriptor.Package.Type "vagrant"}} |
||||||
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} |
||||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} |
||||||
{{end}} |
{{end}} |
||||||
|
@ -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) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 337 B |
Loading…
Reference in new issue