初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+374
View File
@@ -0,0 +1,374 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package alpine
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"io"
"strings"
packages_model "gitea.dev/models/packages"
alpine_model "gitea.dev/models/packages/alpine"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
alpine_module "gitea.dev/modules/packages/alpine"
"gitea.dev/modules/util"
packages_service "gitea.dev/services/packages"
)
const (
IndexFilename = "APKINDEX"
IndexArchiveFilename = IndexFilename + ".tar.gz"
)
// GetOrCreateRepositoryVersion gets or creates the internal repository package
// The Alpine 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.TypeAlpine, alpine_module.RepositoryPackage, alpine_module.RepositoryVersion)
}
// GetOrCreateKeyPair gets or creates the RSA keys used to sign repository files
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
priv, err := user_model.GetSetting(ctx, ownerID, alpine_module.SettingKeyPrivate)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
pub, err := user_model.GetSetting(ctx, ownerID, alpine_module.SettingKeyPublic)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
if priv == "" || pub == "" {
priv, pub, err = util.GenerateKeyPair(4096)
if err != nil {
return "", "", err
}
if err := user_model.SetUserSetting(ctx, ownerID, alpine_module.SettingKeyPrivate, priv); err != nil {
return "", "", err
}
if err := user_model.SetUserSetting(ctx, ownerID, alpine_module.SettingKeyPublic, pub); err != nil {
return "", "", err
}
}
return priv, pub, nil
}
// 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 {
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
branches, err := alpine_model.GetBranches(ctx, ownerID)
if err != nil {
return err
}
for _, branch := range branches {
repositories, err := alpine_model.GetRepositories(ctx, ownerID, branch)
if err != nil {
return err
}
for _, repository := range repositories {
architectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
if err != nil {
return err
}
for _, architecture := range architectures {
if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", branch, repository, architecture, err)
}
}
}
}
return nil
}
// BuildSpecificRepositoryFiles builds index files for the repository
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) error {
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
if err != nil {
return err
}
architectures := container.SetOf(architecture)
if architecture == alpine_module.NoArch {
// Update all other architectures too when updating the noarch index
additionalArchitectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
if err != nil {
return err
}
architectures.AddMultiple(additionalArchitectures...)
}
for architecture := range architectures {
if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
return err
}
}
return nil
}
type packageData struct {
Package *packages_model.Package
Version *packages_model.PackageVersion
Blob *packages_model.PackageBlob
VersionMetadata *alpine_module.VersionMetadata
FileMetadata *alpine_module.FileMetadata
}
type packageCache = map[*packages_model.PackageFile]*packageData
func searchPackageFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) ([]*packages_model.PackageFile, error) {
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
OwnerID: ownerID,
PackageType: packages_model.TypeAlpine,
Query: "%.apk",
Properties: map[string]string{
alpine_module.PropertyBranch: branch,
alpine_module.PropertyRepository: repository,
alpine_module.PropertyArchitecture: architecture,
},
})
if err != nil {
return nil, err
}
return pfs, nil
}
// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
pfs, err := searchPackageFiles(ctx, ownerID, branch, repository, architecture)
if err != nil {
return err
}
if architecture != alpine_module.NoArch {
// Add all noarch packages too
noarchFiles, err := searchPackageFiles(ctx, ownerID, branch, repository, alpine_module.NoArch)
if err != nil {
return err
}
pfs = append(pfs, noarchFiles...)
}
// 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|%s", branch, repository, architecture))
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
} else if pf == nil {
return nil
}
return packages_service.DeletePackageFile(ctx, pf)
}
// Cache data needed for all repository files
cache := make(packageCache)
for _, pf := range pfs {
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
if err != nil {
return err
}
p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
if err != nil {
return err
}
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
if err != nil {
return err
}
pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, alpine_module.PropertyMetadata)
if err != nil {
return err
}
pd := &packageData{
Package: p,
Version: pv,
Blob: pb,
}
if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
return err
}
if len(pps) > 0 {
if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
return err
}
}
cache[pf] = pd
}
var buf bytes.Buffer
for _, pf := range pfs {
pd := cache[pf]
fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum)
fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name)
fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version)
fmt.Fprintf(&buf, "A:%s\n", architecture)
if pd.VersionMetadata.Description != "" {
fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description)
}
if pd.VersionMetadata.ProjectURL != "" {
fmt.Fprintf(&buf, "U:%s\n", pd.VersionMetadata.ProjectURL)
}
if pd.VersionMetadata.License != "" {
fmt.Fprintf(&buf, "L:%s\n", pd.VersionMetadata.License)
}
fmt.Fprintf(&buf, "S:%d\n", pd.Blob.Size)
fmt.Fprintf(&buf, "I:%d\n", pd.FileMetadata.Size)
fmt.Fprintf(&buf, "o:%s\n", pd.FileMetadata.Origin)
fmt.Fprintf(&buf, "m:%s\n", pd.VersionMetadata.Maintainer)
fmt.Fprintf(&buf, "t:%d\n", pd.FileMetadata.BuildDate)
if pd.FileMetadata.CommitHash != "" {
fmt.Fprintf(&buf, "c:%s\n", pd.FileMetadata.CommitHash)
}
if len(pd.FileMetadata.Dependencies) > 0 {
fmt.Fprintf(&buf, "D:%s\n", strings.Join(pd.FileMetadata.Dependencies, " "))
}
if len(pd.FileMetadata.Provides) > 0 {
fmt.Fprintf(&buf, "p:%s\n", strings.Join(pd.FileMetadata.Provides, " "))
}
if pd.FileMetadata.InstallIf != "" {
fmt.Fprintf(&buf, "i:%s\n", pd.FileMetadata.InstallIf)
}
if pd.FileMetadata.ProviderPriority > 0 {
fmt.Fprintf(&buf, "k:%d\n", pd.FileMetadata.ProviderPriority)
}
fmt.Fprint(&buf, "\n")
}
unsignedIndexContent, _ := packages_module.NewHashedBuffer()
defer unsignedIndexContent.Close()
h := sha1.New()
if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), IndexFilename, buf.Bytes(), true); err != nil {
return err
}
priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
if err != nil {
return err
}
privPem, _ := pem.Decode([]byte(priv))
if privPem == nil {
return errors.New("failed to decode private key pem")
}
privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
if err != nil {
return err
}
sign, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA1, h.Sum(nil))
if err != nil {
return err
}
owner, err := user_model.GetUserByID(ctx, ownerID)
if err != nil {
return err
}
fingerprint, err := util.CreatePublicKeyFingerprint(&privKey.PublicKey)
if err != nil {
return err
}
signedIndexContent, _ := packages_module.NewHashedBuffer()
defer signedIndexContent.Close()
if err := writeGzipStream(
signedIndexContent,
fmt.Sprintf(".SIGN.RSA.%s@%s.rsa.pub", owner.LowerName, hex.EncodeToString(fingerprint)),
sign,
false,
); err != nil {
return err
}
if _, err := io.Copy(signedIndexContent, unsignedIndexContent); err != nil {
return err
}
_, err = packages_service.AddFileToPackageVersionInternal(
ctx,
repoVersion,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: IndexArchiveFilename,
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
},
Creator: user_model.NewGhostUser(),
Data: signedIndexContent,
IsLead: false,
OverwriteExisting: true,
Properties: map[string]string{
alpine_module.PropertyBranch: branch,
alpine_module.PropertyRepository: repository,
alpine_module.PropertyArchitecture: architecture,
},
},
)
return err
}
func writeGzipStream(w io.Writer, filename string, content []byte, addTarEnd bool) error {
zw := gzip.NewWriter(w)
defer zw.Close()
tw := tar.NewWriter(zw)
if addTarEnd {
defer tw.Close()
}
hdr := &tar.Header{
Name: filename,
Mode: 0o600,
Size: int64(len(content)),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write(content); err != nil {
return err
}
return nil
}
+417
View File
@@ -0,0 +1,417 @@
// 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"
"strconv"
"strings"
packages_model "gitea.dev/models/packages"
arch_model "gitea.dev/models/packages/arch"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/globallock"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
arch_module "gitea.dev/modules/packages/arch"
"gitea.dev/modules/util"
packages_service "gitea.dev/services/packages"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
const (
IndexArchiveFilename = "packages.db"
)
func AcquireRegistryLock(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)
}
vpfs := make(map[int64]*entryOptions)
for _, pf := range pfs {
current := &entryOptions{
File: pf,
}
current.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID)
if err != nil {
return err
}
// here we compare the versions but not using SearchLatestVersions because we shouldn't allow "downgrading" to a older version by "latest" one.
// https://wiki.archlinux.org/title/Downgrading_packages : randomly downgrading can mess up dependencies:
// If a downgrade involves a soname change, all dependencies may need downgrading or rebuilding too.
if old, ok := vpfs[current.Version.PackageID]; ok {
if compareVersions(old.Version.Version, current.Version.Version) == -1 {
vpfs[current.Version.PackageID] = current
}
} else {
vpfs[current.Version.PackageID] = current
}
}
indexContent, _ := packages_module.NewHashedBuffer()
defer indexContent.Close()
gw := gzip.NewWriter(indexContent)
tw := tar.NewWriter(gw)
cache := make(map[int64]*packages_model.Package)
for _, opts := range vpfs {
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, opts.File.BlobID)
if err != nil {
return err
}
sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.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, opts.File.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", strconv.FormatInt(opts.Blob.Size, 10)},
{"ISIZE", strconv.FormatInt(opts.FileMetadata.InstalledSize, 10)},
{"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", strconv.FormatInt(opts.FileMetadata.BuildDate, 10)},
{"PACKAGER", opts.FileMetadata.Packager},
{"PROVIDES", strings.Join(opts.FileMetadata.Provides, "\n")},
{"REPLACES", strings.Join(opts.FileMetadata.Replaces, "\n")},
{"CONFLICTS", strings.Join(opts.FileMetadata.Conflicts, "\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")},
})
}
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
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"strconv"
"strings"
"unicode"
)
// https://gitlab.archlinux.org/pacman/pacman/-/blob/d55b47e5512808b67bc944feb20c2bcc6c1a4c45/lib/libalpm/version.c
func parseEVR(evr string) (epoch, version, release string) {
if before, after, f := strings.Cut(evr, ":"); f {
epoch = before
evr = after
} else {
epoch = "0"
}
if before, after, f := strings.Cut(evr, "-"); f {
version = before
release = after
} else {
version = evr
release = "1"
}
return epoch, version, release
}
func compareSegments(a, b []string) int {
lenA, lenB := len(a), len(b)
l := min(lenA, lenB)
for i := range l {
if r := compare(a[i], b[i]); r != 0 {
return r
}
}
if lenA == lenB {
return 0
} else if l == lenA {
return -1
}
return 1
}
func compare(a, b string) int {
if a == b {
return 0
}
aNumeric := isNumeric(a)
bNumeric := isNumeric(b)
if aNumeric && bNumeric {
aInt, _ := strconv.Atoi(a)
bInt, _ := strconv.Atoi(b)
switch {
case aInt < bInt:
return -1
case aInt > bInt:
return 1
default:
return 0
}
}
if aNumeric {
return 1
}
if bNumeric {
return -1
}
return strings.Compare(a, b)
}
func isNumeric(s string) bool {
for _, c := range s {
if !unicode.IsDigit(c) {
return false
}
}
return true
}
func compareVersions(a, b string) int {
if a == b {
return 0
}
epochA, versionA, releaseA := parseEVR(a)
epochB, versionB, releaseB := parseEVR(b)
if res := compareSegments([]string{epochA}, []string{epochB}); res != 0 {
return res
}
if res := compareSegments(strings.Split(versionA, "."), strings.Split(versionB, ".")); res != 0 {
return res
}
return compareSegments([]string{releaseA}, []string{releaseB})
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCompareVersions(t *testing.T) {
// https://man.archlinux.org/man/vercmp.8.en
checks := [][]string{
{"1.0a", "1.0b", "1.0beta", "1.0p", "1.0pre", "1.0rc", "1.0", "1.0.a", "1.0.1"},
{"1", "1.0", "1.1", "1.1.1", "1.2", "2.0", "3.0.0"},
}
for _, check := range checks {
for i := 0; i < len(check)-1; i++ {
require.Equal(t, -1, compareVersions(check[i], check[i+1]))
require.Equal(t, 1, compareVersions(check[i+1], check[i]))
}
}
require.Equal(t, 1, compareVersions("1.0-2", "1.0"))
require.Equal(t, 0, compareVersions("0:1.0-1", "1.0"))
require.Equal(t, 1, compareVersions("1:1.0-1", "2.0"))
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
auth_model "gitea.dev/models/auth"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"github.com/golang-jwt/jwt/v5"
)
type packageClaims struct {
jwt.RegisteredClaims
PackageMeta
}
type PackageMeta struct {
UserID int64
Scope auth_model.AccessTokenScope
ActionsUserTaskID int64
}
func CreateAuthorizationToken(u *user_model.User, packageScope auth_model.AccessTokenScope) (string, error) {
now := time.Now()
actionsUserTaskID, _ := user_model.GetActionsUserTaskID(u)
claims := packageClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
NotBefore: jwt.NewNumericDate(now),
},
PackageMeta: PackageMeta{
UserID: u.ID,
Scope: packageScope,
ActionsUserTaskID: actionsUserTaskID,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
if err != nil {
return "", err
}
return tokenString, nil
}
func ParseAuthorizationRequest(req *http.Request) (*PackageMeta, error) {
h := req.Header.Get("Authorization")
if h == "" {
return nil, nil //nolint:nilnil // the auth method is not applicable
}
parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 {
log.Error("split token failed: %s", h)
return nil, errors.New("split token failed")
}
return ParseAuthorizationToken(parts[1])
}
func ParseAuthorizationToken(tokenStr string) (*PackageMeta, error) {
token, err := jwt.ParseWithClaims(tokenStr, &packageClaims{}, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return setting.GetGeneralTokenSigningSecret(), nil
})
if err != nil {
return nil, err
}
c, ok := token.Claims.(*packageClaims)
if !token.Valid || !ok {
return nil, errors.New("invalid token claim")
}
return &c.PackageMeta, nil
}
+319
View File
@@ -0,0 +1,319 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cargo
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"path"
"strconv"
packages_model "gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/json"
cargo_module "gitea.dev/modules/packages/cargo"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
repo_service "gitea.dev/services/repository"
files_service "gitea.dev/services/repository/files"
)
const (
IndexRepositoryName = "_cargo-index"
ConfigFileName = "config.json"
)
// https://doc.rust-lang.org/cargo/reference/registries.html#index-format
func BuildPackagePath(name string) string {
switch len(name) {
case 0:
panic("Cargo package name can not be empty")
case 1:
return path.Join("1", name)
case 2:
return path.Join("2", name)
case 3:
return path.Join("3", string(name[0]), name)
default:
return path.Join(name[0:2], name[2:4], name)
}
}
func InitializeIndexRepository(ctx context.Context, doer, owner *user_model.User) error {
repo, err := getOrCreateIndexRepository(ctx, doer, owner)
if err != nil {
return err
}
if err := createOrUpdateConfigFile(ctx, repo, doer, owner); err != nil {
return fmt.Errorf("createOrUpdateConfigFile: %w", err)
}
return nil
}
func RebuildIndex(ctx context.Context, doer, owner *user_model.User) error {
repo, err := getOrCreateIndexRepository(ctx, doer, owner)
if err != nil {
return err
}
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeCargo)
if err != nil {
return fmt.Errorf("GetPackagesByType: %w", err)
}
return alterRepositoryContent(
ctx,
doer,
repo,
"Rebuild Cargo Index",
func(t *files_service.TemporaryUploadRepository) error {
// Remove all existing content but the Cargo config
files, err := t.LsFiles(ctx)
if err != nil {
return err
}
for i, file := range files {
if file == ConfigFileName {
files[i] = files[len(files)-1]
files = files[:len(files)-1]
break
}
}
if err := t.RemoveFilesFromIndex(ctx, files...); err != nil {
return err
}
// Add all packages
for _, p := range ps {
if err := addOrUpdatePackageIndex(ctx, t, p); err != nil {
return err
}
}
return nil
},
)
}
func UpdatePackageIndexIfExists(ctx context.Context, doer, owner *user_model.User, packageID int64) error {
// We do not want to force the creation of the repo here
// cargo http index does not rely on the repo itself,
// so if the repo does not exist, we just do nothing.
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
return nil
}
return fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
}
p, err := packages_model.GetPackageByID(ctx, packageID)
if err != nil {
return fmt.Errorf("GetPackageByID[%d]: %w", packageID, err)
}
return alterRepositoryContent(
ctx,
doer,
repo,
"Update "+p.Name,
func(t *files_service.TemporaryUploadRepository) error {
return addOrUpdatePackageIndex(ctx, t, p)
},
)
}
type IndexVersionEntry struct {
Name string `json:"name"`
Version string `json:"vers"`
Dependencies []*cargo_module.Dependency `json:"deps"`
FileChecksum string `json:"cksum"`
Features map[string][]string `json:"features"`
Yanked bool `json:"yanked"`
Links string `json:"links,omitempty"`
}
func BuildPackageIndex(ctx context.Context, p *packages_model.Package) (*bytes.Buffer, error) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
Sort: packages_model.SortVersionAsc,
})
if err != nil {
return nil, fmt.Errorf("SearchVersions[%s]: %w", p.Name, err)
}
if len(pvs) == 0 {
return nil, nil //nolint:nilnil // return nil to indicate that the package has no versions
}
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil {
return nil, fmt.Errorf("GetPackageDescriptors[%s]: %w", p.Name, err)
}
var b bytes.Buffer
for _, pd := range pds {
metadata := pd.Metadata.(*cargo_module.Metadata)
dependencies := metadata.Dependencies
if dependencies == nil {
dependencies = make([]*cargo_module.Dependency, 0)
}
features := metadata.Features
if features == nil {
features = make(map[string][]string)
}
yanked, _ := strconv.ParseBool(pd.VersionProperties.GetByName(cargo_module.PropertyYanked))
entry, err := json.Marshal(&IndexVersionEntry{
Name: pd.Package.Name,
Version: pd.Version.Version,
Dependencies: dependencies,
FileChecksum: pd.Files[0].Blob.HashSHA256,
Features: features,
Yanked: yanked,
Links: metadata.Links,
})
if err != nil {
return nil, err
}
b.Write(entry)
b.WriteString("\n")
}
return &b, nil
}
func addOrUpdatePackageIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, p *packages_model.Package) error {
b, err := BuildPackageIndex(ctx, p)
if err != nil {
return err
}
if b == nil {
return nil
}
return writeObjectToIndex(ctx, t, BuildPackagePath(p.LowerName), b)
}
func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.User) (*repo_model.Repository, error) {
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
repo, err = repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{
Name: IndexRepositoryName,
}, true)
if err != nil {
return nil, fmt.Errorf("CreateRepository: %w", err)
}
} else {
return nil, fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
}
}
return repo, nil
}
type Config struct {
DownloadURL string `json:"dl"`
APIURL string `json:"api"`
AuthRequired bool `json:"auth-required"`
}
func BuildConfig(owner *user_model.User, isPrivate bool) *Config {
return &Config{
DownloadURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo/api/v1/crates",
APIURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo",
AuthRequired: isPrivate,
}
}
func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository, doer, owner *user_model.User) error {
return alterRepositoryContent(
ctx,
doer,
repo,
"Initialize Cargo Config",
func(t *files_service.TemporaryUploadRepository) error {
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
if err != nil {
return err
}
return writeObjectToIndex(ctx, t, ConfigFileName, &b)
},
)
}
// This is a shorter version of CreateOrUpdateRepoFile which allows to perform multiple actions on a git repository
func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitMessage string, fn func(*files_service.TemporaryUploadRepository) error) error {
t, err := files_service.NewTemporaryUploadRepository(repo)
if err != nil {
return err
}
defer t.Close()
var lastCommitID string
if err := t.Clone(ctx, repo.DefaultBranch, true); err != nil {
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
return err
}
if err := t.Init(ctx, repo.ObjectFormatName); err != nil {
return err
}
} else {
if err := t.SetDefaultIndex(ctx); err != nil {
return err
}
commit, err := t.GetBranchCommit(repo.DefaultBranch)
if err != nil {
return err
}
lastCommitID = commit.ID.String()
}
if err := fn(t); err != nil {
return err
}
treeHash, err := t.WriteTree(ctx)
if err != nil {
return err
}
commitOpts := &files_service.CommitTreeUserOptions{
ParentCommitID: lastCommitID,
TreeHash: treeHash,
CommitMessage: commitMessage,
DoerUser: doer,
}
commitHash, err := t.CommitTree(ctx, commitOpts)
if err != nil {
return err
}
return t.Push(ctx, doer, commitHash, repo.DefaultBranch, false)
}
func writeObjectToIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, path string, r io.Reader) error {
hash, err := t.HashObjectAndWrite(ctx, r)
if err != nil {
return err
}
return t.AddObjectToIndex(ctx, "100644", hash, path)
}
+211
View File
@@ -0,0 +1,211 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"context"
"fmt"
"time"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
packages_service "gitea.dev/services/packages"
alpine_service "gitea.dev/services/packages/alpine"
arch_service "gitea.dev/services/packages/arch"
cargo_service "gitea.dev/services/packages/cargo"
container_service "gitea.dev/services/packages/container"
debian_service "gitea.dev/services/packages/debian"
rpm_service "gitea.dev/services/packages/rpm"
)
// CleanupTask executes cleanup rules and cleanup expired package data
func CleanupTask(ctx context.Context, olderThan time.Duration) error {
if err := ExecuteCleanupRules(ctx); err != nil {
return err
}
return CleanupExpiredData(ctx, olderThan)
}
func executeCleanupOneRulePackage(ctx context.Context, pcr *packages_model.PackageCleanupRule, p *packages_model.Package) (versionDeleted bool, err error) {
olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays)
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
})
if err != nil {
return false, fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err)
}
if pcr.KeepCount > 0 {
if pcr.KeepCount < len(pvs) {
pvs = pvs[pcr.KeepCount:]
} else {
pvs = nil
}
}
for _, pv := range pvs {
if pcr.Type == packages_model.TypeContainer {
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
return false, fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err)
} else if skip {
log.Debug("Rule[%d]: keep '%s/%s' (container)", pcr.ID, p.Name, pv.Version)
continue
}
}
toMatch := pv.LowerVersion
if pcr.MatchFullName {
toMatch = p.LowerName + "/" + pv.LowerVersion
}
if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
log.Debug("Rule[%d]: keep '%s/%s' (keep pattern)", pcr.ID, p.Name, pv.Version)
continue
}
if pv.CreatedUnix.AsLocalTime().After(olderThan) {
log.Debug("Rule[%d]: keep '%s/%s' (remove days) %v", pcr.ID, p.Name, pv.Version, pv.CreatedUnix.FormatDate())
continue
}
if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) {
log.Debug("Rule[%d]: keep '%s/%s' (remove pattern)", pcr.ID, p.Name, pv.Version)
continue
}
log.Debug("Rule[%d]: remove '%s/%s'", pcr.ID, p.Name, pv.Version)
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
log.Error("CleanupRule [%d]: DeletePackageVersionAndReferences failed: %v", pcr.ID, err)
continue
}
versionDeleted = true
}
return versionDeleted, nil
}
func executeCleanupOneRule(ctx context.Context, pcr *packages_model.PackageCleanupRule) error {
if err := pcr.CompiledPattern(); err != nil {
return fmt.Errorf("CleanupRule [%d]: CompilePattern failed: %w", pcr.ID, err)
}
packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type)
if err != nil {
return fmt.Errorf("CleanupRule [%d]: GetPackagesByType failed: %w", pcr.ID, err)
}
anyVersionDeleted := false
for _, p := range packages {
versionDeleted := false
err = db.WithTx(ctx, func(ctx context.Context) (err error) {
versionDeleted, err = executeCleanupOneRulePackage(ctx, pcr, p)
return err
})
if err != nil {
log.Error("CleanupRule [%d]: executeCleanupOneRulePackage(%d) failed: %v", pcr.ID, p.ID, err)
continue
}
anyVersionDeleted = anyVersionDeleted || versionDeleted
if versionDeleted {
if pcr.Type == packages_model.TypeCargo {
owner, err := user_model.GetUserByID(ctx, pcr.OwnerID)
if err != nil {
return fmt.Errorf("GetUserByID failed: %w", err)
}
if err := cargo_service.UpdatePackageIndexIfExists(ctx, owner, owner, p.ID); err != nil {
return fmt.Errorf("CleanupRule [%d]: cargo.UpdatePackageIndexIfExists failed: %w", pcr.ID, err)
}
}
}
}
if anyVersionDeleted {
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)
}
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)
}
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.AcquireRegistryLock(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
}
func ExecuteCleanupRules(ctx context.Context) error {
return packages_model.IterateEnabledCleanupRules(ctx, func(ctx context.Context, pcr *packages_model.PackageCleanupRule) error {
select {
case <-ctx.Done():
return db.ErrCancelledf("While processing package cleanup rules")
default:
}
err := executeCleanupOneRule(ctx, pcr)
if err != nil {
log.Error("CleanupRule [%d]: executeCleanupOneRule failed: %v", pcr.ID, err)
}
return nil
})
}
func CleanupExpiredData(ctx context.Context, olderThan time.Duration) error {
pbs := make([]*packages_model.PackageBlob, 0, 100)
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := container_service.Cleanup(ctx, olderThan); err != nil {
return err
}
ps, err := packages_model.FindUnreferencedPackages(ctx)
if err != nil {
return err
}
for _, p := range ps {
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID); err != nil {
return err
}
if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil {
return err
}
}
// HINT: PACKAGE-DEFER-STORAGE-DELETE: Handle blob deletion for package storage
pbs, err = packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan)
if err != nil {
return err
}
for _, pb := range pbs {
if err := packages_model.DeleteBlobByID(ctx, pb.ID); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
contentStore := packages_module.NewContentStore()
for _, pb := range pbs {
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
log.Error("Error deleting package blob [%v]: %v", pb.ID, err)
}
}
return nil
}
@@ -0,0 +1,142 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"context"
"errors"
"io"
"os"
packages_model "gitea.dev/models/packages"
packages_module "gitea.dev/modules/packages"
"gitea.dev/modules/setting"
"gitea.dev/modules/tempdir"
)
var (
// errWriteAfterRead occurs if Write is called after a read operation
errWriteAfterRead = errors.New("write is unsupported after a read operation")
// errOffsetMismatch occurs if the file offset is different than the model
errOffsetMismatch = errors.New("offset mismatch between file and model")
)
// BlobUploader handles chunked blob uploads
type BlobUploader struct {
*packages_model.PackageBlobUpload
*packages_module.MultiHasher
file *os.File
reading bool
}
func uploadPathTempDir() *tempdir.TempDir {
return setting.AppDataTempDir("package-upload")
}
func buildFilePath(uploadPath *tempdir.TempDir, id string) string {
return uploadPath.JoinPath(id)
}
// NewBlobUploader creates a new blob uploader for the given id
func NewBlobUploader(ctx context.Context, id string) (*BlobUploader, error) {
model, err := packages_model.GetBlobUploadByID(ctx, id)
if err != nil {
return nil, err
}
hash := packages_module.NewMultiHasher()
if len(model.HashStateBytes) != 0 {
if err := hash.UnmarshalBinary(model.HashStateBytes); err != nil {
return nil, err
}
}
uploadPath := uploadPathTempDir()
_, err = uploadPath.MkdirAllSub("")
if err != nil {
return nil, err
}
f, err := os.OpenFile(buildFilePath(uploadPath, model.ID), os.O_RDWR|os.O_CREATE, 0o666)
if err != nil {
return nil, err
}
return &BlobUploader{
PackageBlobUpload: model,
MultiHasher: hash,
file: f,
reading: false,
}, nil
}
// Close implements io.Closer
func (u *BlobUploader) Close() error {
return u.file.Close()
}
// Append appends a chunk of data and updates the model
func (u *BlobUploader) Append(ctx context.Context, r io.Reader) error {
if u.reading {
return errWriteAfterRead
}
offset, err := u.file.Seek(0, io.SeekEnd)
if err != nil {
return err
}
if offset != u.BytesReceived {
return errOffsetMismatch
}
n, err := io.Copy(io.MultiWriter(u.file, u.MultiHasher), r)
if err != nil {
return err
}
// fast path if nothing was written
if n == 0 {
return nil
}
u.BytesReceived += n
u.HashStateBytes, err = u.MultiHasher.MarshalBinary()
if err != nil {
return err
}
return packages_model.UpdateBlobUpload(ctx, u.PackageBlobUpload)
}
func (u *BlobUploader) Size() int64 {
return u.BytesReceived
}
// Read implements io.Reader
func (u *BlobUploader) Read(p []byte) (int, error) {
if !u.reading {
_, err := u.file.Seek(0, io.SeekStart)
if err != nil {
return 0, err
}
u.reading = true
}
return u.file.Read(p)
}
// RemoveBlobUploadByID Remove deletes the data and the model of a blob upload
func RemoveBlobUploadByID(ctx context.Context, id string) error {
if err := packages_model.DeleteBlobUploadByID(ctx, id); err != nil {
return err
}
err := os.Remove(buildFilePath(uploadPathTempDir(), id))
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"context"
"time"
packages_model "gitea.dev/models/packages"
container_model "gitea.dev/models/packages/container"
"gitea.dev/modules/optional"
container_module "gitea.dev/modules/packages/container"
packages_service "gitea.dev/services/packages"
"github.com/opencontainers/go-digest"
)
// Cleanup removes expired container data
func Cleanup(ctx context.Context, olderThan time.Duration) error {
if err := cleanupExpiredBlobUploads(ctx, olderThan); err != nil {
return err
}
return cleanupExpiredUploadedBlobs(ctx, olderThan)
}
// cleanupExpiredBlobUploads removes expired blob uploads
func cleanupExpiredBlobUploads(ctx context.Context, olderThan time.Duration) error {
pbus, err := packages_model.FindExpiredBlobUploads(ctx, olderThan)
if err != nil {
return err
}
for _, pbu := range pbus {
if err := RemoveBlobUploadByID(ctx, pbu.ID); err != nil {
return err
}
}
return nil
}
// cleanupExpiredUploadedBlobs removes expired uploaded blobs not referenced by a manifest
func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) error {
pfs, err := container_model.SearchExpiredUploadedBlobs(ctx, olderThan)
if err != nil {
return err
}
for _, pf := range pfs {
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
return err
}
}
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
Type: packages_model.TypeContainer,
Version: packages_model.SearchValue{
ExactMatch: true,
Value: container_module.UploadVersion,
},
IsInternal: optional.Some(true),
HasFiles: optional.Some(false),
})
if err != nil {
return err
}
for _, pv := range pvs {
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil {
return err
}
if err := packages_model.DeleteVersionByID(ctx, pv.ID); err != nil {
return err
}
}
return nil
}
func ShouldBeSkipped(ctx context.Context, pcr *packages_model.PackageCleanupRule, p *packages_model.Package, pv *packages_model.PackageVersion) (bool, error) {
// Always skip the "latest" tag
if pv.LowerVersion == "latest" {
return true, nil
}
// Check if the version is a digest (or untagged)
if digest.Digest(pv.LowerVersion).Validate() == nil {
// Check if there is another manifest referencing this version
has, err := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
Properties: map[string]string{
container_module.PropertyManifestReference: pv.LowerVersion,
},
})
if err != nil {
return false, err
}
// Skip it if the version is referenced
if has {
return true, nil
}
}
return false, nil
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"context"
"io"
"strings"
packages_model "gitea.dev/models/packages"
container_service "gitea.dev/models/packages/container"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
"gitea.dev/modules/packages"
container_module "gitea.dev/modules/packages/container"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// UpdateRepositoryNames updates the repository name property for all packages of the specific owner
func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwnerName string) error {
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeContainer)
if err != nil {
return err
}
newOwnerName = strings.ToLower(newOwnerName)
for _, p := range ps {
if err := packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil {
return err
}
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, newOwnerName+"/"+p.LowerName); err != nil {
return err
}
}
return nil
}
func ParseManifestMetadata(ctx context.Context, rd io.Reader, ownerID int64, imageName string) (*v1.Manifest, *packages_model.PackageFileDescriptor, *container_module.Metadata, error) {
var manifest v1.Manifest
if err := json.NewDecoder(rd).Decode(&manifest); err != nil {
return nil, nil, nil, err
}
configDescriptor, err := container_service.GetContainerBlob(ctx, &container_service.BlobSearchOptions{
OwnerID: ownerID,
Image: imageName,
Digest: manifest.Config.Digest.String(),
})
if err != nil {
return nil, nil, nil, err
}
configReader, err := packages.NewContentStore().OpenBlob(packages.BlobHash256Key(configDescriptor.Blob.HashSHA256))
if err != nil {
return nil, nil, nil, err
}
defer configReader.Close()
metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader)
return &manifest, configDescriptor, metadata, err
}
+422
View File
@@ -0,0 +1,422 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package debian
import (
"bytes"
"compress/gzip"
"context"
"crypto"
"errors"
"fmt"
"io"
"sort"
"strings"
"time"
packages_model "gitea.dev/models/packages"
debian_model "gitea.dev/models/packages/debian"
user_model "gitea.dev/models/user"
packages_module "gitea.dev/modules/packages"
debian_module "gitea.dev/modules/packages/debian"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
packages_service "gitea.dev/services/packages"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ulikunitz/xz"
)
// GetOrCreateRepositoryVersion gets or creates the internal repository package
// The Debian 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.TypeDebian, debian_module.RepositoryPackage, debian_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, debian_module.SettingKeyPrivate)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
pub, err := user_model.GetSetting(ctx, ownerID, debian_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, debian_module.SettingKeyPrivate, priv); err != nil {
return "", "", err
}
if err := user_model.SetUserSetting(ctx, ownerID, debian_module.SettingKeyPublic, pub); err != nil {
return "", "", err
}
}
return priv, pub, nil
}
func generateKeypair() (string, string, error) {
// Repository signing keys are long-lived and there is currently no rotation mechanism, choose stronger algorithms
cfg := &packet.Config{
RSABits: 4096,
DefaultHash: crypto.SHA256,
DefaultCipher: packet.CipherAES256,
}
e, err := openpgp.NewEntity("", "Automatically generated Debian Registry Key; created "+time.Now().UTC().Format(time.RFC3339), "", cfg)
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
}
// BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components 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
distributions, err := debian_model.GetDistributions(ctx, ownerID)
if err != nil {
return err
}
for _, distribution := range distributions {
components, err := debian_model.GetComponents(ctx, ownerID, distribution)
if err != nil {
return err
}
architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution)
if err != nil {
return err
}
for _, component := range components {
for _, architecture := range architectures {
if err := buildRepositoryFiles(ctx, ownerID, pv, distribution, component, architecture); err != nil {
return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", distribution, component, architecture, err)
}
}
}
}
return nil
}
// BuildSpecificRepositoryFiles builds index files for the repository
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, distribution, component, architecture string) error {
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
if err != nil {
return err
}
return buildRepositoryFiles(ctx, ownerID, pv, distribution, component, architecture)
}
func buildRepositoryFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution, component, architecture string) error {
if err := buildPackagesIndices(ctx, ownerID, repoVersion, distribution, component, architecture); err != nil {
return err
}
return buildReleaseFiles(ctx, ownerID, repoVersion, distribution)
}
// https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution, component, architecture string) error {
opts := &debian_model.PackageSearchOptions{
OwnerID: ownerID,
Distribution: distribution,
Component: component,
Architecture: architecture,
}
// Delete the package indices if there are no packages
if has, err := debian_model.ExistPackages(ctx, opts); err != nil {
return err
} else if !has {
key := fmt.Sprintf("%s|%s|%s", distribution, component, architecture)
for _, filename := range []string{"Packages", "Packages.gz", "Packages.xz"} {
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, key)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
} else if pf == nil {
continue
}
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
return err
}
}
return nil
}
packagesContent, _ := packages_module.NewHashedBuffer()
defer packagesContent.Close()
packagesGzipContent, _ := packages_module.NewHashedBuffer()
defer packagesGzipContent.Close()
gzw := gzip.NewWriter(packagesGzipContent)
packagesXzContent, _ := packages_module.NewHashedBuffer()
defer packagesXzContent.Close()
xzw, _ := xz.NewWriter(packagesXzContent)
w := io.MultiWriter(packagesContent, gzw, xzw)
addSeparator := false
pfds, err := debian_model.SearchPackages(ctx, opts)
if err != nil {
return err
}
for _, pfd := range pfds {
if addSeparator {
fmt.Fprintln(w)
}
addSeparator = true
fmt.Fprintf(w, "%s\n", strings.TrimSpace(pfd.Properties.GetByName(debian_module.PropertyControl)))
fmt.Fprintf(w, "Filename: pool/%s/%s/%s\n", distribution, component, pfd.File.Name)
fmt.Fprintf(w, "Size: %d\n", pfd.Blob.Size)
fmt.Fprintf(w, "MD5sum: %s\n", pfd.Blob.HashMD5)
fmt.Fprintf(w, "SHA1: %s\n", pfd.Blob.HashSHA1)
fmt.Fprintf(w, "SHA256: %s\n", pfd.Blob.HashSHA256)
fmt.Fprintf(w, "SHA512: %s\n", pfd.Blob.HashSHA512)
}
gzw.Close()
xzw.Close()
for _, file := range []struct {
Name string
Data packages_module.HashedSizeReader
}{
{"Packages", packagesContent},
{"Packages.gz", packagesGzipContent},
{"Packages.xz", packagesXzContent},
} {
_, err := packages_service.AddFileToPackageVersionInternal(
ctx,
repoVersion,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: file.Name,
CompositeKey: fmt.Sprintf("%s|%s|%s", distribution, component, architecture),
},
Creator: user_model.NewGhostUser(),
Data: file.Data,
IsLead: false,
OverwriteExisting: true,
Properties: map[string]string{
debian_module.PropertyRepositoryIncludeInRelease: "",
debian_module.PropertyDistribution: distribution,
debian_module.PropertyComponent: component,
debian_module.PropertyArchitecture: architecture,
},
},
)
if err != nil {
return err
}
}
return nil
}
// https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution string) error {
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
VersionID: repoVersion.ID,
Properties: map[string]string{
debian_module.PropertyRepositoryIncludeInRelease: "",
debian_module.PropertyDistribution: distribution,
},
})
if err != nil {
return err
}
// Delete the release files if there are no packages
if len(pfs) == 0 {
for _, filename := range []string{"Release", "Release.gpg", "InRelease"} {
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, distribution)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
} else if pf == nil {
continue
}
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
return err
}
}
return nil
}
components, err := debian_model.GetComponents(ctx, ownerID, distribution)
if err != nil {
return err
}
sort.Strings(components)
architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution)
if err != nil {
return err
}
sort.Strings(architectures)
priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
if err != nil {
return err
}
block, err := armor.Decode(strings.NewReader(priv))
if err != nil {
return err
}
e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
if err != nil {
return err
}
inReleaseContent, _ := packages_module.NewHashedBuffer()
defer inReleaseContent.Close()
sw, err := clearsign.Encode(inReleaseContent, e.PrivateKey, nil)
if err != nil {
return err
}
var buf bytes.Buffer
w := io.MultiWriter(sw, &buf)
fmt.Fprintf(w, "Origin: %s\n", setting.AppName)
fmt.Fprintf(w, "Label: %s\n", setting.AppName)
fmt.Fprintf(w, "Suite: %s\n", distribution)
fmt.Fprintf(w, "Codename: %s\n", distribution)
fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " "))
fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " "))
fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123))
fmt.Fprint(w, "Acquire-By-Hash: yes\n")
pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs)
if err != nil {
return err
}
var md5, sha1, sha256, sha512 strings.Builder
for _, pfd := range pfds {
path := fmt.Sprintf("%s/binary-%s/%s", pfd.Properties.GetByName(debian_module.PropertyComponent), pfd.Properties.GetByName(debian_module.PropertyArchitecture), pfd.File.Name)
fmt.Fprintf(&md5, " %s %d %s\n", pfd.Blob.HashMD5, pfd.Blob.Size, path)
fmt.Fprintf(&sha1, " %s %d %s\n", pfd.Blob.HashSHA1, pfd.Blob.Size, path)
fmt.Fprintf(&sha256, " %s %d %s\n", pfd.Blob.HashSHA256, pfd.Blob.Size, path)
fmt.Fprintf(&sha512, " %s %d %s\n", pfd.Blob.HashSHA512, pfd.Blob.Size, path)
}
fmt.Fprintln(w, "MD5Sum:")
fmt.Fprint(w, md5.String())
fmt.Fprintln(w, "SHA1:")
fmt.Fprint(w, sha1.String())
fmt.Fprintln(w, "SHA256:")
fmt.Fprint(w, sha256.String())
fmt.Fprintln(w, "SHA512:")
fmt.Fprint(w, sha512.String())
sw.Close()
releaseGpgContent, _ := packages_module.NewHashedBuffer()
defer releaseGpgContent.Close()
if err := openpgp.ArmoredDetachSign(releaseGpgContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
return err
}
releaseContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
defer releaseContent.Close()
for _, file := range []struct {
Name string
Data packages_module.HashedSizeReader
}{
{"Release", releaseContent},
{"Release.gpg", releaseGpgContent},
{"InRelease", inReleaseContent},
} {
_, err = packages_service.AddFileToPackageVersionInternal(
ctx,
repoVersion,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: file.Name,
CompositeKey: distribution,
},
Creator: user_model.NewGhostUser(),
Data: file.Data,
IsLead: false,
OverwriteExisting: true,
Properties: map[string]string{
debian_module.PropertyDistribution: distribution,
},
},
)
if err != nil {
return err
}
}
return nil
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"context"
"fmt"
org_model "gitea.dev/models/organization"
packages_model "gitea.dev/models/packages"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/util"
)
func LinkToRepository(ctx context.Context, pkg *packages_model.Package, repo *repo_model.Repository, doer *user_model.User) error {
if pkg.OwnerID != repo.OwnerID {
return util.ErrPermissionDenied
}
if pkg.RepoID > 0 {
return util.ErrInvalidArgument
}
perms, err := access_model.GetDoerRepoPermission(ctx, repo, doer)
if err != nil {
return fmt.Errorf("error getting permissions for user %d on repository %d: %w", doer.ID, repo.ID, err)
}
if !perms.CanWrite(unit.TypePackages) {
return util.ErrPermissionDenied
}
if err := packages_model.SetRepositoryLink(ctx, pkg.ID, repo.ID); err != nil {
return fmt.Errorf("error while linking package '%v' to repo '%v' : %w", pkg.Name, repo.FullName(), err)
}
return nil
}
func UnlinkFromRepository(ctx context.Context, pkg *packages_model.Package, doer *user_model.User) error {
if pkg.RepoID == 0 {
return util.ErrInvalidArgument
}
repo, err := repo_model.GetRepositoryByID(ctx, pkg.RepoID)
if err != nil && !repo_model.IsErrRepoNotExist(err) {
return fmt.Errorf("error getting repository %d: %w", pkg.RepoID, err)
}
if err == nil {
perms, err := access_model.GetDoerRepoPermission(ctx, repo, doer)
if err != nil {
return fmt.Errorf("error getting permissions for user %d on repository %d: %w", doer.ID, repo.ID, err)
}
if !perms.CanWrite(unit.TypePackages) {
return util.ErrPermissionDenied
}
}
user, err := user_model.GetUserByID(ctx, pkg.OwnerID)
if err != nil {
return err
}
if !doer.IsAdmin {
if !user.IsOrganization() {
if doer.ID != pkg.OwnerID {
return fmt.Errorf("no permission to unlink package '%v' from its repository, or packages are disabled", pkg.Name)
}
} else {
isOrgAdmin, err := org_model.OrgFromUser(user).IsOrgAdmin(ctx, doer.ID)
if err != nil {
return err
} else if !isOrgAdmin {
return fmt.Errorf("no permission to unlink package '%v' from its repository, or packages are disabled", pkg.Name)
}
}
}
return packages_model.UnlinkRepository(ctx, pkg.ID)
}
+715
View File
@@ -0,0 +1,715 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/globallock"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
notify_service "gitea.dev/services/notify"
)
var (
ErrQuotaTypeSize = errors.New("maximum allowed package type size exceeded")
ErrQuotaTotalSize = errors.New("maximum allowed package storage quota exceeded")
ErrQuotaTotalCount = errors.New("maximum allowed package count exceeded")
)
type Specialization interface {
OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error
OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error
GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error)
}
// PackageInfo describes a package
type PackageInfo struct {
Owner *user_model.User
PackageType packages_model.Type
Name string
Version string
}
// PackageCreationInfo describes a package to create
type PackageCreationInfo struct {
PackageInfo
SemverCompatible bool
Creator *user_model.User
Metadata any
PackageProperties map[string]string
VersionProperties map[string]string
}
// PackageFileInfo describes a package file
type PackageFileInfo struct {
Filename string
CompositeKey string
}
// PackageFileCreationInfo describes a package file to create
type PackageFileCreationInfo struct {
PackageFileInfo
Creator *user_model.User
Data packages_module.HashedSizeReader
IsLead bool
Properties map[string]string
OverwriteExisting bool
}
// CreatePackageAndAddFile creates a package with a file. If the same package exists already, ErrDuplicatePackageVersion is returned
func CreatePackageAndAddFile(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
return createPackageAndAddFile(ctx, pvci, pfci, false)
}
// CreatePackageOrAddFileToExisting creates a package with a file or adds the file if the package exists already
func CreatePackageOrAddFileToExisting(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo) (pv *packages_model.PackageVersion, pf *packages_model.PackageFile, err error) {
lockKey := fmt.Sprintf("pkg-upsert-%v-%v-%v", pvci.PackageType, pvci.Name, pvci.Version)
err = globallock.LockAndDo(ctx, lockKey, func(ctx context.Context) error {
pv, pf, err = createPackageAndAddFile(ctx, pvci, pfci, true)
return err
})
return pv, pf, err
}
func createPackageAndAddFile(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
dbCtx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, nil, err
}
defer committer.Close()
pv, created, err := createPackageAndVersion(dbCtx, pvci, allowDuplicate)
if err != nil {
return nil, nil, err
}
pf, pb, blobCreated, err := addFileToPackageVersion(dbCtx, pv, &pvci.PackageInfo, pfci)
removeBlob := false
defer func() {
if blobCreated && removeBlob {
contentStore := packages_module.NewContentStore()
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
log.Error("Error deleting package blob from content store: %v", err)
}
}
}()
if err != nil {
removeBlob = true
return nil, nil, err
}
if err := committer.Commit(); err != nil {
removeBlob = true
return nil, nil, err
}
if created {
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
return nil, nil, err
}
notify_service.PackageCreate(ctx, pvci.Creator, pd)
}
return pv, pf, nil
}
func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, bool, error) {
log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.PackageProperties, pvci.VersionProperties, allowDuplicate)
packageCreated := true
p := &packages_model.Package{
OwnerID: pvci.Owner.ID,
Type: pvci.PackageType,
Name: pvci.Name,
LowerName: strings.ToLower(pvci.Name),
SemverCompatible: pvci.SemverCompatible,
}
var err error
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
if !errors.Is(err, packages_model.ErrDuplicatePackage) {
log.Error("Error inserting package: %v", err)
return nil, false, err
}
packageCreated = false
}
if packageCreated {
for name, value := range pvci.PackageProperties {
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, name, value); err != nil {
log.Error("Error setting package property: %v", err)
return nil, false, err
}
}
}
metadataJSON, err := json.Marshal(pvci.Metadata)
if err != nil {
return nil, false, err
}
versionCreated := true
pv := &packages_model.PackageVersion{
PackageID: p.ID,
CreatorID: pvci.Creator.ID,
Version: pvci.Version,
LowerVersion: strings.ToLower(pvci.Version),
MetadataJSON: string(metadataJSON),
}
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
if errors.Is(err, packages_model.ErrDuplicatePackageVersion) && allowDuplicate {
versionCreated = false
} else {
log.Error("Error inserting package: %v", err) // other error, or disallowing duplicates
return nil, false, err
}
}
if versionCreated {
if err := CheckCountQuotaExceeded(ctx, pvci.Creator, pvci.Owner); err != nil {
return nil, false, err
}
for name, value := range pvci.VersionProperties {
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, name, value); err != nil {
log.Error("Error setting package version property: %v", err)
return nil, false, err
}
}
}
return pv, versionCreated, nil
}
// AddFileToExistingPackage adds a file to an existing package. If the package does not exist, ErrPackageNotExist is returned
func AddFileToExistingPackage(ctx context.Context, pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, error) {
return addFileToPackageWrapper(ctx, func(ctx context.Context) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
if err != nil {
return nil, nil, false, err
}
return addFileToPackageVersion(ctx, pv, pvi, pfci)
})
}
// AddFileToPackageVersionInternal adds a file to the package
// This method skips quota checks and should only be used for system-managed packages.
func AddFileToPackageVersionInternal(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, error) {
return addFileToPackageWrapper(ctx, func(ctx context.Context) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
return addFileToPackageVersionUnchecked(ctx, pv, pfci)
})
}
func addFileToPackageWrapper(ctx context.Context, fn func(ctx context.Context) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error)) (*packages_model.PackageFile, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, err
}
defer committer.Close()
pf, pb, blobCreated, err := fn(ctx)
removeBlob := false
defer func() {
if removeBlob {
contentStore := packages_module.NewContentStore()
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
log.Error("Error deleting package blob from content store: %v", err)
}
}
}()
if err != nil {
removeBlob = blobCreated
return nil, err
}
if err := committer.Commit(); err != nil {
removeBlob = blobCreated
return nil, err
}
return pf, nil
}
// NewPackageBlob creates a package blob instance
func NewPackageBlob(hsr packages_module.HashedSizeReader) *packages_model.PackageBlob {
hashMD5, hashSHA1, hashSHA256, hashSHA512 := hsr.Sums()
return &packages_model.PackageBlob{
Size: hsr.Size(),
HashMD5: hex.EncodeToString(hashMD5),
HashSHA1: hex.EncodeToString(hashSHA1),
HashSHA256: hex.EncodeToString(hashSHA256),
HashSHA512: hex.EncodeToString(hashSHA512),
}
}
func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
if err := CheckSizeQuotaExceeded(ctx, pfci.Creator, pvi.Owner, pvi.PackageType, pfci.Data.Size()); err != nil {
return nil, nil, false, err
}
return addFileToPackageVersionUnchecked(ctx, pv, pfci)
}
func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
log.Trace("Adding package file: %v, %s", pv.ID, pfci.Filename)
pb, exists, err := packages_model.GetOrInsertBlob(ctx, NewPackageBlob(pfci.Data))
if err != nil {
log.Error("Error inserting package blob: %v", err)
return nil, nil, false, err
}
if !exists {
contentStore := packages_module.NewContentStore()
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), pfci.Data, pfci.Data.Size()); err != nil {
log.Error("Error saving package blob in content store: %v", err)
return nil, nil, false, err
}
}
if pfci.OverwriteExisting {
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, pfci.Filename, pfci.CompositeKey)
if err != nil && err != packages_model.ErrPackageFileNotExist {
return nil, pb, !exists, err
}
if pf != nil {
// Short circuit if blob is the same
if pf.BlobID == pb.ID {
return pf, pb, !exists, nil
}
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
return nil, pb, !exists, err
}
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
return nil, pb, !exists, err
}
}
}
pf := &packages_model.PackageFile{
VersionID: pv.ID,
BlobID: pb.ID,
Name: pfci.Filename,
LowerName: strings.ToLower(pfci.Filename),
CompositeKey: pfci.CompositeKey,
IsLead: pfci.IsLead,
}
if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
if err != packages_model.ErrDuplicatePackageFile {
log.Error("Error inserting package file: %v", err)
}
return nil, pb, !exists, err
}
for name, value := range pfci.Properties {
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil {
log.Error("Error setting package file property: %v", err)
return pf, pb, !exists, err
}
}
return pf, pb, !exists, nil
}
// CheckCountQuotaExceeded checks if the owner has more than the allowed packages
// The check is skipped if the doer is an admin.
func CheckCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User) error {
if doer.IsAdmin {
return nil
}
if setting.Packages.LimitTotalOwnerCount > -1 {
totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: owner.ID,
IsInternal: optional.Some(false),
})
if err != nil {
log.Error("CountVersions failed: %v", err)
return err
}
if totalCount > setting.Packages.LimitTotalOwnerCount {
return ErrQuotaTotalCount
}
}
return nil
}
// CheckSizeQuotaExceeded checks if the upload size is bigger than the allowed size
// The check is skipped if the doer is an admin.
func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, packageType packages_model.Type, uploadSize int64) error {
if doer.IsAdmin {
return nil
}
var typeSpecificSize int64
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:
typeSpecificSize = setting.Packages.LimitSizeChef
case packages_model.TypeComposer:
typeSpecificSize = setting.Packages.LimitSizeComposer
case packages_model.TypeConan:
typeSpecificSize = setting.Packages.LimitSizeConan
case packages_model.TypeConda:
typeSpecificSize = setting.Packages.LimitSizeConda
case packages_model.TypeContainer:
typeSpecificSize = setting.Packages.LimitSizeContainer
case packages_model.TypeCran:
typeSpecificSize = setting.Packages.LimitSizeCran
case packages_model.TypeDebian:
typeSpecificSize = setting.Packages.LimitSizeDebian
case packages_model.TypeGeneric:
typeSpecificSize = setting.Packages.LimitSizeGeneric
case packages_model.TypeGo:
typeSpecificSize = setting.Packages.LimitSizeGo
case packages_model.TypeHelm:
typeSpecificSize = setting.Packages.LimitSizeHelm
case packages_model.TypeMaven:
typeSpecificSize = setting.Packages.LimitSizeMaven
case packages_model.TypeNpm:
typeSpecificSize = setting.Packages.LimitSizeNpm
case packages_model.TypeNuGet:
typeSpecificSize = setting.Packages.LimitSizeNuGet
case packages_model.TypePub:
typeSpecificSize = setting.Packages.LimitSizePub
case packages_model.TypePyPI:
typeSpecificSize = setting.Packages.LimitSizePyPI
case packages_model.TypeRpm:
typeSpecificSize = setting.Packages.LimitSizeRpm
case packages_model.TypeRubyGems:
typeSpecificSize = setting.Packages.LimitSizeRubyGems
case packages_model.TypeSwift:
typeSpecificSize = setting.Packages.LimitSizeSwift
case packages_model.TypeTerraformState:
typeSpecificSize = setting.Packages.LimitSizeTerraformState
case packages_model.TypeVagrant:
typeSpecificSize = setting.Packages.LimitSizeVagrant
}
if typeSpecificSize > -1 && typeSpecificSize < uploadSize {
return ErrQuotaTypeSize
}
if setting.Packages.LimitTotalOwnerSize > -1 {
totalSize, err := packages_model.CalculateFileSize(ctx, &packages_model.PackageFileSearchOptions{
OwnerID: owner.ID,
})
if err != nil {
log.Error("CalculateFileSize failed: %v", err)
return err
}
if totalSize+uploadSize > setting.Packages.LimitTotalOwnerSize {
return ErrQuotaTotalSize
}
}
return nil
}
// GetOrCreateInternalPackageVersion gets or creates an internal package
// Some package types need such internal packages for housekeeping.
func GetOrCreateInternalPackageVersion(ctx context.Context, ownerID int64, packageType packages_model.Type, name, version string) (*packages_model.PackageVersion, error) {
var pv *packages_model.PackageVersion
return pv, db.WithTx(ctx, func(ctx context.Context) error {
p := &packages_model.Package{
OwnerID: ownerID,
Type: packageType,
Name: name,
LowerName: name,
IsInternal: true,
}
var err error
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
if !errors.Is(err, packages_model.ErrDuplicatePackage) {
log.Error("Error inserting package: %v", err)
return err
}
}
pv = &packages_model.PackageVersion{
PackageID: p.ID,
CreatorID: ownerID,
Version: version,
LowerVersion: version,
IsInternal: true,
MetadataJSON: "null",
}
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
if err != packages_model.ErrDuplicatePackageVersion {
log.Error("Error inserting package version: %v", err)
return err
}
}
return nil
})
}
// RemovePackageVersionByNameAndVersion deletes a package version and all associated files
func RemovePackageVersionByNameAndVersion(ctx context.Context, doer *user_model.User, pvi *PackageInfo) error {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
if err != nil {
return err
}
return RemovePackageVersion(ctx, doer, pv)
}
// RemovePackageVersion deletes the package version and all associated files
func RemovePackageVersion(ctx context.Context, doer *user_model.User, pv *packages_model.PackageVersion) error {
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
return err
}
if err := GetSpecManager().Get(pd.Package.Type).OnBeforeRemovePackageVersion(ctx, doer, pd); err != nil {
return err
}
// HINT: PACKAGE-DEFER-STORAGE-DELETE: Blobs are not deleted immediately, instead they are deleted by the cleanup_packages cron task.
// If there are no more versions for the package, the same task removes that as well.
if err := db.WithTx(ctx, func(ctx context.Context) error {
log.Trace("Deleting package: %v", pv.ID)
return DeletePackageVersionAndReferences(ctx, pv)
}); err != nil {
return err
}
notify_service.PackageDelete(ctx, doer, pd)
return nil
}
// RemovePackageFileAndVersionIfUnreferenced deletes the package file and the version if there are no referenced files afterwards
func RemovePackageFileAndVersionIfUnreferenced(ctx context.Context, doer *user_model.User, pf *packages_model.PackageFile) error {
var pd *packages_model.PackageDescriptor
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := DeletePackageFile(ctx, pf); err != nil {
return err
}
has, err := packages_model.HasVersionFileReferences(ctx, pf.VersionID)
if err != nil {
return err
}
if !has {
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
if err != nil {
return err
}
pd, err = packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
return err
}
if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
if pd != nil {
notify_service.PackageDelete(ctx, doer, pd)
}
return nil
}
// DeletePackageVersionAndReferences deletes the package version and its properties and files
func DeletePackageVersionAndReferences(ctx context.Context, pv *packages_model.PackageVersion) error {
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil {
return err
}
if err := packages_model.DeleteFilePropertiesByVersionID(ctx, pv.ID); err != nil {
return err
}
if err := packages_model.DeleteFilesByVersionID(ctx, pv.ID); err != nil {
return err
}
return packages_model.DeleteVersionByID(ctx, pv.ID)
}
// DeletePackageFile deletes the package file and its properties
func DeletePackageFile(ctx context.Context, pf *packages_model.PackageFile) error {
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
return err
}
return packages_model.DeleteFileByID(ctx, pf.ID)
}
// OpenFileForDownloadByPackageNameAndVersion returns the content of the specific package file and increases the download counter.
func OpenFileForDownloadByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo, pfi *PackageFileInfo, method string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
log.Trace("Getting package file stream: %v, %v, %s, %s, %s, %s", pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version, pfi.Filename, pfi.CompositeKey)
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
if err != nil {
if err == packages_model.ErrPackageNotExist {
return nil, nil, nil, err
}
log.Error("Error getting package: %v", err)
return nil, nil, nil, err
}
return OpenFileForDownloadByPackageVersion(ctx, pv, pfi, method)
}
// OpenFileForDownloadByPackageVersion returns the content of the specific package file and increases the download counter.
func OpenFileForDownloadByPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfi *PackageFileInfo, method string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, pfi.Filename, pfi.CompositeKey)
if err != nil {
return nil, nil, nil, err
}
return OpenFileForDownload(ctx, pf, method)
}
// OpenFileForDownload returns the content of the specific package file and increases the download counter.
func OpenFileForDownload(ctx context.Context, pf *packages_model.PackageFile, method string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
if err != nil {
return nil, nil, nil, err
}
return OpenBlobForDownload(ctx, pf, pb, method, nil)
}
func OpenBlobStream(pb *packages_model.PackageBlob) (io.ReadSeekCloser, error) {
cs := packages_module.NewContentStore()
key := packages_module.BlobHash256Key(pb.HashSHA256)
return cs.OpenBlob(key)
}
// OpenBlobForDownload returns the content of the specific package blob and increases the download counter.
// If the storage supports direct serving and it's enabled, only the direct serving url is returned.
func OpenBlobForDownload(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, method string, serveDirectReqParams *storage.ServeDirectOptions) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
key := packages_module.BlobHash256Key(pb.HashSHA256)
cs := packages_module.NewContentStore()
var s io.ReadSeekCloser
var u *url.URL
var err error
if cs.ShouldServeDirect() {
u, err = cs.GetServeDirectURL(key, pf.Name, method, serveDirectReqParams)
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
log.Error("Error getting serve direct url (fallback to local reader): %v", err)
}
}
if u == nil {
s, err = cs.OpenBlob(key)
}
if err != nil {
return nil, nil, nil, err
}
if pf.IsLead && method == http.MethodGet {
if err := packages_model.IncrementDownloadCounter(ctx, pf.VersionID); err != nil {
log.Error("Error incrementing download counter: %v", err)
}
}
return s, u, pf, nil
}
// RemovePackage deletes the package and all its versions
func RemovePackage(ctx context.Context, doer *user_model.User, p *packages_model.Package) error {
pds, err := packages_model.GetAllPackageDescriptors(ctx, p)
if err != nil {
return err
}
if err := GetSpecManager().Get(p.Type).OnBeforeRemovePackageAll(ctx, doer, p, pds); err != nil {
return err
}
// HINT: PACKAGE-DEFER-STORAGE-DELETE: Blobs are not deleted immediately, instead they are deleted by cleanup_packages cron task.
err = db.WithTx(ctx, func(ctx context.Context) error {
err := packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypePackage, p.ID)
if err != nil {
return err
}
err = packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypeFile, p.ID)
if err != nil {
return err
}
err = packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypeVersion, p.ID)
if err != nil {
return err
}
err = packages_model.DeleteFilesByPackageID(ctx, p.ID)
if err != nil {
return err
}
err = packages_model.DeleteVersionsByPackageID(ctx, p.ID)
if err != nil {
return err
}
return packages_model.DeletePackageByID(ctx, p.ID)
})
if err != nil {
return err
}
for _, pd := range pds {
notify_service.PackageDelete(ctx, doer, pd)
}
return nil
}
// RemoveAllPackages for User
func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
count := 0
for {
pkgVersions, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
Paginator: &db.ListOptions{
PageSize: repo_model.RepositoryListDefaultPageSize,
Page: 1,
},
OwnerID: userID,
IsInternal: optional.None[bool](),
})
if err != nil {
return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)
}
if len(pkgVersions) == 0 {
break
}
for _, pv := range pkgVersions {
if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
return count, fmt.Errorf("unable to delete package %d:%s[%d]. Error: %w", pv.PackageID, pv.Version, pv.ID, err)
}
count++
}
}
return count, nil
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pkgspec
import (
packages_model "gitea.dev/models/packages"
packages_service "gitea.dev/services/packages"
"gitea.dev/services/packages/terraform"
)
func InitManager() error {
mgr := packages_service.GetSpecManager()
mgr.Add(packages_model.TypeTerraformState, &terraform.Specialization{})
// TODO: add more in the future, refactor the existing code to use this approach
return nil
}
+736
View File
@@ -0,0 +1,736 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rpm
import (
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"io"
"slices"
"strings"
"time"
packages_model "gitea.dev/models/packages"
rpm_model "gitea.dev/models/packages/rpm"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
rpm_module "gitea.dev/modules/packages/rpm"
"gitea.dev/modules/util"
packages_service "gitea.dev/services/packages"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
// GetOrCreateRepositoryVersion gets or creates the internal repository package
// The RPM registry needs multiple metadata 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.TypeRpm, rpm_module.RepositoryPackage, rpm_module.RepositoryVersion)
}
// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
priv, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPrivate)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return "", "", err
}
pub, err := user_model.GetSetting(ctx, ownerID, rpm_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, rpm_module.SettingKeyPrivate, priv); err != nil {
return "", "", err
}
if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPublic, pub); err != nil {
return "", "", err
}
}
return priv, pub, nil
}
func generateKeypair() (string, string, error) {
e, err := openpgp.NewEntity("", "RPM 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
}
// BuildAllRepositoryFiles (re)builds all repository files for every available group
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
groups, err := rpm_model.GetGroups(ctx, ownerID)
if err != nil {
return err
}
for _, group := range groups {
if err := BuildSpecificRepositoryFiles(ctx, ownerID, group); err != nil {
return fmt.Errorf("failed to build repository files [%s]: %w", group, err)
}
}
return nil
}
type repoChecksum struct {
Value string `xml:",chardata"`
Type string `xml:"type,attr"`
}
type repoLocation struct {
Href string `xml:"href,attr"`
}
type repoData struct {
Type string `xml:"type,attr"`
Checksum repoChecksum `xml:"checksum"`
OpenChecksum repoChecksum `xml:"open-checksum"`
Location repoLocation `xml:"location"`
Timestamp int64 `xml:"timestamp"`
Size int64 `xml:"size"`
OpenSize int64 `xml:"open-size"`
}
type packageData struct {
Package *packages_model.Package
Version *packages_model.PackageVersion
Blob *packages_model.PackageBlob
VersionMetadata *rpm_module.VersionMetadata
FileMetadata *rpm_module.FileMetadata
}
type packageCache = map[*packages_model.PackageFile]*packageData
// BuildSpecificRepositoryFiles builds metadata files for the repository
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, group string) error {
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
if err != nil {
return err
}
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
OwnerID: ownerID,
PackageType: packages_model.TypeRpm,
Query: "%.rpm",
CompositeKey: group,
})
if err != nil {
return err
}
// Delete the repository files if there are no packages
if len(pfs) == 0 {
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
}
}
return nil
}
// Cache data needed for all repository files
cache := make(packageCache)
for _, pf := range pfs {
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
if err != nil {
return err
}
p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
if err != nil {
return err
}
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
if err != nil {
return err
}
pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, rpm_module.PropertyMetadata)
if err != nil {
return err
}
pd := &packageData{
Package: p,
Version: pv,
Blob: pb,
}
if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
return err
}
if len(pps) > 0 {
if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
return err
}
}
cache[pf] = pd
}
primary, err := buildPrimary(ctx, pv, pfs, cache, group)
if err != nil {
return err
}
filelists, err := buildFilelists(ctx, pv, pfs, cache, group)
if err != nil {
return err
}
other, err := buildOther(ctx, pv, pfs, cache, group)
if err != nil {
return err
}
data := []*repoData{primary, filelists, other}
updates := collectUpdateInfoUpdates(pfs, cache)
if len(updates) > 0 {
updateInfo, err := buildUpdateInfo(ctx, pv, updates, group)
if err != nil {
return err
}
data = append(data, updateInfo)
}
return buildRepomd(
ctx,
pv,
ownerID,
data,
group,
)
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml
func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData, group string) error {
type Repomd struct {
XMLName xml.Name `xml:"repomd"`
Xmlns string `xml:"xmlns,attr"`
XmlnsRpm string `xml:"xmlns:rpm,attr"`
Data []*repoData `xml:"data"`
}
var buf bytes.Buffer
buf.WriteString(xml.Header)
if err := xml.NewEncoder(&buf).Encode(&Repomd{
Xmlns: "http://linux.duke.edu/metadata/repo",
XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
Data: data,
}); err != nil {
return err
}
priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
if err != nil {
return err
}
block, err := armor.Decode(strings.NewReader(priv))
if err != nil {
return err
}
e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
if err != nil {
return err
}
repomdAscContent, _ := packages_module.NewHashedBuffer()
defer repomdAscContent.Close()
if err := openpgp.ArmoredDetachSign(repomdAscContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
return err
}
repomdContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
defer repomdContent.Close()
for _, file := range []struct {
Name string
Data packages_module.HashedSizeReader
}{
{"repomd.xml", repomdContent},
{"repomd.xml.asc", repomdAscContent},
} {
_, err = packages_service.AddFileToPackageVersionInternal(
ctx,
pv,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: file.Name,
CompositeKey: group,
},
Creator: user_model.NewGhostUser(),
Data: file.Data,
IsLead: false,
OverwriteExisting: true,
},
)
if err != nil {
return err
}
}
return nil
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#primary-xml
func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) {
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
Release string `xml:"rel,attr"`
}
type Checksum struct {
Checksum string `xml:",chardata"`
Type string `xml:"type,attr"`
Pkgid string `xml:"pkgid,attr"`
}
type Times struct {
File uint64 `xml:"file,attr"`
Build uint64 `xml:"build,attr"`
}
type Sizes struct {
Package int64 `xml:"package,attr"`
Installed uint64 `xml:"installed,attr"`
Archive uint64 `xml:"archive,attr"`
}
type Location struct {
Href string `xml:"href,attr"`
}
type EntryList struct {
Entries []*rpm_module.Entry `xml:"rpm:entry"`
}
type Format struct {
License string `xml:"rpm:license"`
Vendor string `xml:"rpm:vendor"`
Group string `xml:"rpm:group"`
Buildhost string `xml:"rpm:buildhost"`
Sourcerpm string `xml:"rpm:sourcerpm"`
Provides EntryList `xml:"rpm:provides"`
Requires EntryList `xml:"rpm:requires"`
Conflicts EntryList `xml:"rpm:conflicts"`
Obsoletes EntryList `xml:"rpm:obsoletes"`
Files []*rpm_module.File `xml:"file"`
}
type Package struct {
XMLName xml.Name `xml:"package"`
Type string `xml:"type,attr"`
Name string `xml:"name"`
Architecture string `xml:"arch"`
Version Version `xml:"version"`
Checksum Checksum `xml:"checksum"`
Summary string `xml:"summary"`
Description string `xml:"description"`
Packager string `xml:"packager"`
URL string `xml:"url"`
Time Times `xml:"time"`
Size Sizes `xml:"size"`
Location Location `xml:"location"`
Format Format `xml:"format"`
}
type Metadata struct {
XMLName xml.Name `xml:"metadata"`
Xmlns string `xml:"xmlns,attr"`
XmlnsRpm string `xml:"xmlns:rpm,attr"`
PackageCount int `xml:"packages,attr"`
Packages []*Package `xml:"package"`
}
packages := make([]*Package, 0, len(pfs))
for _, pf := range pfs {
pd := c[pf]
files := make([]*rpm_module.File, 0, 3)
for _, f := range pd.FileMetadata.Files {
if f.IsExecutable {
files = append(files, f)
}
}
packages = append(packages, &Package{
Type: "rpm",
Name: pd.Package.Name,
Architecture: pd.FileMetadata.Architecture,
Version: Version{
Epoch: pd.FileMetadata.Epoch,
Version: pd.FileMetadata.Version,
Release: pd.FileMetadata.Release,
},
Checksum: Checksum{
Type: "sha256",
Checksum: pd.Blob.HashSHA256,
Pkgid: "YES",
},
Summary: pd.VersionMetadata.Summary,
Description: pd.VersionMetadata.Description,
Packager: pd.FileMetadata.Packager,
URL: pd.VersionMetadata.ProjectURL,
Time: Times{
File: pd.FileMetadata.FileTime,
Build: pd.FileMetadata.BuildTime,
},
Size: Sizes{
Package: pd.Blob.Size,
Installed: pd.FileMetadata.InstalledSize,
Archive: pd.FileMetadata.ArchiveSize,
},
Location: Location{
Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture),
},
Format: Format{
License: pd.VersionMetadata.License,
Vendor: pd.FileMetadata.Vendor,
Group: pd.FileMetadata.Group,
Buildhost: pd.FileMetadata.BuildHost,
Sourcerpm: pd.FileMetadata.SourceRpm,
Provides: EntryList{
Entries: pd.FileMetadata.Provides,
},
Requires: EntryList{
Entries: pd.FileMetadata.Requires,
},
Conflicts: EntryList{
Entries: pd.FileMetadata.Conflicts,
},
Obsoletes: EntryList{
Entries: pd.FileMetadata.Obsoletes,
},
Files: files,
},
})
}
return addDataAsFileToRepo(ctx, pv, "primary", &Metadata{
Xmlns: "http://linux.duke.edu/metadata/common",
XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
PackageCount: len(pfs),
Packages: packages,
}, group)
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml
func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl // duplicates with buildOther
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
Release string `xml:"rel,attr"`
}
type Package struct {
Pkgid string `xml:"pkgid,attr"`
Name string `xml:"name,attr"`
Architecture string `xml:"arch,attr"`
Version Version `xml:"version"`
Files []*rpm_module.File `xml:"file"`
}
type Filelists struct {
XMLName xml.Name `xml:"filelists"`
Xmlns string `xml:"xmlns,attr"`
PackageCount int `xml:"packages,attr"`
Packages []*Package `xml:"package"`
}
packages := make([]*Package, 0, len(pfs))
for _, pf := range pfs {
pd := c[pf]
packages = append(packages, &Package{
Pkgid: pd.Blob.HashSHA256,
Name: pd.Package.Name,
Architecture: pd.FileMetadata.Architecture,
Version: Version{
Epoch: pd.FileMetadata.Epoch,
Version: pd.FileMetadata.Version,
Release: pd.FileMetadata.Release,
},
Files: pd.FileMetadata.Files,
})
}
return addDataAsFileToRepo(ctx, pv, "filelists", &Filelists{
Xmlns: "http://linux.duke.edu/metadata/other",
PackageCount: len(pfs),
Packages: packages,
}, group)
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml
func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl // duplicates with buildFilelists
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
Release string `xml:"rel,attr"`
}
type Package struct {
Pkgid string `xml:"pkgid,attr"`
Name string `xml:"name,attr"`
Architecture string `xml:"arch,attr"`
Version Version `xml:"version"`
Changelogs []*rpm_module.Changelog `xml:"changelog"`
}
type Otherdata struct {
XMLName xml.Name `xml:"otherdata"`
Xmlns string `xml:"xmlns,attr"`
PackageCount int `xml:"packages,attr"`
Packages []*Package `xml:"package"`
}
packages := make([]*Package, 0, len(pfs))
for _, pf := range pfs {
pd := c[pf]
packages = append(packages, &Package{
Pkgid: pd.Blob.HashSHA256,
Name: pd.Package.Name,
Architecture: pd.FileMetadata.Architecture,
Version: Version{
Epoch: pd.FileMetadata.Epoch,
Version: pd.FileMetadata.Version,
Release: pd.FileMetadata.Release,
},
Changelogs: pd.FileMetadata.Changelogs,
})
}
return addDataAsFileToRepo(ctx, pv, "other", &Otherdata{
Xmlns: "http://linux.duke.edu/metadata/other",
PackageCount: len(pfs),
Packages: packages,
}, group)
}
func collectUpdateInfoUpdates(pfs []*packages_model.PackageFile, c packageCache) (updates []*rpm_module.Update) {
seenVersions := make(map[int64]bool)
for _, pf := range pfs {
pd := c[pf]
if pd.Version != nil && !seenVersions[pd.Version.ID] && pd.VersionMetadata.Updates != nil {
updates = append(updates, pd.VersionMetadata.Updates...)
seenVersions[pd.Version.ID] = true
}
}
return updates
}
// buildUpdateInfo builds the updateinfo.xml file
func buildUpdateInfo(ctx context.Context, pv *packages_model.PackageVersion, updates []*rpm_module.Update, group string) (*repoData, error) {
// Group updates by ID to merge package lists
type updateKey struct {
ID string
}
updateMap := make(map[updateKey]*rpm_module.Update)
for _, u := range updates {
key := updateKey{ID: u.ID}
if existing, ok := updateMap[key]; ok {
for _, newColl := range u.PkgList {
collFound := false
for j, existingColl := range existing.PkgList {
if existingColl.Short == newColl.Short {
for _, newPkg := range newColl.Packages {
pkgFound := false
for _, existingPkg := range existingColl.Packages {
if existingPkg.Name == newPkg.Name &&
existingPkg.Version == newPkg.Version &&
existingPkg.Release == newPkg.Release &&
existingPkg.Arch == newPkg.Arch {
pkgFound = true
break
}
}
if !pkgFound {
existing.PkgList[j].Packages = append(existing.PkgList[j].Packages, newPkg)
}
}
collFound = true
break
}
}
if !collFound {
collCopy := *newColl
collCopy.Packages = append([]*rpm_module.UpdatePackage(nil), newColl.Packages...)
existing.PkgList = append(existing.PkgList, &collCopy)
}
}
} else {
// Create a shallow copy so we don't mutate the original cached pointer
uCopy := *u
// Deep copy PkgList and Collections to avoid mutating cache
// Note: References is shallow-copied, but safe as long as it remains immutable
uCopy.PkgList = make([]*rpm_module.Collection, len(u.PkgList))
for i, coll := range u.PkgList {
collCopy := *coll
collCopy.Packages = append([]*rpm_module.UpdatePackage(nil), coll.Packages...)
uCopy.PkgList[i] = &collCopy
}
updateMap[key] = &uCopy
}
}
var mergedUpdates []*rpm_module.Update
for _, u := range updateMap {
mergedUpdates = append(mergedUpdates, u)
}
slices.SortFunc(mergedUpdates, func(a, b *rpm_module.Update) int {
return strings.Compare(a.ID, b.ID)
})
type updateInfo struct {
XMLName xml.Name `xml:"updates"`
Xmlns string `xml:"xmlns,attr"`
Updates []*rpm_module.Update `xml:"update"`
}
return addDataAsFileToRepo(ctx, pv, "updateinfo", &updateInfo{
Xmlns: "http://linux.duke.edu/metadata/updateinfo",
Updates: mergedUpdates,
}, group)
}
// writtenCounter counts all written bytes
type writtenCounter struct {
written int64
}
func (wc *writtenCounter) Write(buf []byte) (int, error) {
n := len(buf)
wc.written += int64(n)
return n, nil
}
func (wc *writtenCounter) Written() int64 {
return wc.written
}
func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, group string) (*repoData, error) {
content, _ := packages_module.NewHashedBuffer()
defer content.Close()
gzw := gzip.NewWriter(content)
wc := &writtenCounter{}
h := sha256.New()
w := io.MultiWriter(gzw, wc, h)
_, _ = w.Write([]byte(xml.Header))
if err := xml.NewEncoder(w).Encode(obj); err != nil {
return nil, err
}
if err := gzw.Close(); err != nil {
return nil, err
}
filename := filetype + ".xml.gz"
_, err := packages_service.AddFileToPackageVersionInternal(
ctx,
pv,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: filename,
CompositeKey: group,
},
Creator: user_model.NewGhostUser(),
Data: content,
IsLead: false,
OverwriteExisting: true,
},
)
if err != nil {
return nil, err
}
_, _, hashSHA256, _ := content.Sums()
return &repoData{
Type: filetype,
Checksum: repoChecksum{
Type: "sha256",
Value: hex.EncodeToString(hashSHA256),
},
OpenChecksum: repoChecksum{
Type: "sha256",
Value: hex.EncodeToString(h.Sum(nil)),
},
Location: repoLocation{
Href: "repodata/" + filename,
},
Timestamp: time.Now().Unix(),
Size: content.Size(),
OpenSize: wc.Written(),
}, nil
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rpm
import (
"bytes"
"io"
"strings"
packages_module "gitea.dev/modules/packages"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/sassoftware/go-rpmutils"
)
func SignPackage(buf *packages_module.HashedBuffer, privateKey string) (*packages_module.HashedBuffer, error) {
keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(privateKey))
if err != nil {
return nil, err
}
h, err := rpmutils.SignRpmStream(buf, keyring[0].PrivateKey, nil)
if err != nil {
return nil, err
}
signBlob, err := h.DumpSignatureHeader(false)
if err != nil {
return nil, err
}
if _, err := buf.Seek(int64(h.OriginalSignatureHeaderSize()), io.SeekStart); err != nil {
return nil, err
}
// create new buf with signature prefix
return packages_module.CreateHashedBufferFromReader(io.MultiReader(bytes.NewReader(signBlob), buf))
}
+51
View File
@@ -0,0 +1,51 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"context"
"sync"
packages_model "gitea.dev/models/packages"
user_model "gitea.dev/models/user"
)
type nop struct{}
func (n *nop) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) {
return nil, nil //nolint:nilnil // no data, no error
}
func (n *nop) OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error {
return nil
}
func (n *nop) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error {
return nil
}
var _ Specialization = (*nop)(nil)
type SpecManagerType struct {
specMap map[packages_model.Type]Specialization
}
func (m *SpecManagerType) Add(t packages_model.Type, spec Specialization) {
m.specMap[t] = spec
}
func (m *SpecManagerType) Get(t packages_model.Type) Specialization {
if len(m.specMap) == 0 {
panic("specialization not initialized")
}
spec := m.specMap[t]
if spec == nil {
return &nop{}
}
return spec
}
var GetSpecManager = sync.OnceValue(func() *SpecManagerType {
return &SpecManagerType{specMap: make(map[packages_model.Type]Specialization)}
})
+85
View File
@@ -0,0 +1,85 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"context"
packages_model "gitea.dev/models/packages"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
terraform_module "gitea.dev/modules/packages/terraform"
"gitea.dev/modules/util"
packages_service "gitea.dev/services/packages"
)
type Specialization struct{}
var _ packages_service.Specialization = (*Specialization)(nil)
func (s Specialization) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) {
var ret struct {
IsLatestVersion bool
TerraformLock *terraform_module.LockInfo
}
latestPvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: pd.Package.ID,
IsInternal: optional.Some(false),
})
if err != nil {
return ret, err
}
isLatest := len(latestPvs) > 0 && latestPvs[0].ID == pd.Version.ID
ret.IsLatestVersion = isLatest
if isLatest {
lockInfo, err := terraform_module.GetLock(ctx, pd.Package.ID)
if err != nil {
return ret, nil
}
if lockInfo.IsLocked() {
ret.TerraformLock = &lockInfo
}
}
return ret, nil
}
func (s Specialization) OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error {
locked, err := IsLocked(ctx, pkg)
if err != nil {
return err
}
if locked {
return util.ErrorWrapTranslatable(
util.ErrorWrap(util.ErrUnprocessableContent, "terraform state is locked and cannot be deleted"),
"packages.terraform.delete.locked",
)
}
return nil
}
func (s Specialization) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error {
locked, err := IsLocked(ctx, pd.Package)
if err != nil {
return err
}
if locked {
return util.ErrorWrapTranslatable(
util.ErrorWrap(util.ErrUnprocessableContent, "terraform state is locked and cannot be deleted"),
"packages.terraform.delete.locked",
)
}
latest, err := IsLatest(ctx, pd)
if err != nil {
return err
}
if latest {
return util.ErrorWrapTranslatable(
util.ErrorWrap(util.ErrUnprocessableContent, "the latest version of a Terraform state cannot be deleted"),
"packages.terraform.delete.latest",
)
}
return nil
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terraform
import (
"context"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/optional"
terraform_module "gitea.dev/modules/packages/terraform"
)
// IsLocked is a helper function to check if the terraform state is locked
func IsLocked(ctx context.Context, pkg *packages_model.Package) (bool, error) {
// Non terraform state packages aren't handled here
if pkg.Type == packages_model.TypeTerraformState {
return false, nil
}
lock, err := terraform_module.GetLock(ctx, pkg.ID)
if err != nil {
return false, err
}
return lock.IsLocked(), nil
}
// IsLatest is a helper function to check if the terraform state is the latest version
func IsLatest(ctx context.Context, pd *packages_model.PackageDescriptor) (bool, error) {
if pd.Package.Type == packages_model.TypeTerraformState {
return false, nil
}
latestPvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: pd.Package.ID,
IsInternal: optional.Some(false),
})
if err != nil {
return false, err
}
if len(latestPvs) > 0 && latestPvs[0].ID == pd.Version.ID {
return true, nil
}
return false, nil
}