Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2655,7 +2655,12 @@ LEVEL = Info
;LIMIT_SIZE_HELM = -1
;; Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_MAVEN = -1
;; Specifies the number of most recent Maven snapshot builds to retain. `-1` retains all builds, while `1` retains only the latest build. Value should be -1 or positive.
;; Cleanup expired packages/data then targets the files within all maven snapshots versions
;RETAIN_MAVEN_SNAPSHOT_BUILDS = -1
;; Maximum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
; Enable debug logging for Maven cleanup. Enabling debug will stop snapshot version artifacts from being deleted but will log the files which were meant for deletion.
; DEBUG_MAVEN_CLEANUP = true
;LIMIT_SIZE_NPM = -1
;; Maximum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
;LIMIT_SIZE_NUGET = -1
Expand Down
6 changes: 6 additions & 0 deletions models/fixtures/package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-
id: 1
owner_id: 2
type: maven
name: com.gitea:test-project
lower_name: com.gitea:test-project
41 changes: 41 additions & 0 deletions models/fixtures/package_blob.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-
id: 1
size: 14
hash_md5: 27224a672372115e5a1d125ed7b2a0b1
hash_sha1: 9854582a2958b2d31541ce4a6a1a36201b80c01
hash_sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a01
hash_sha512: ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a801
created_unix: 1672531200
-
id: 2
size: 14
hash_md5: 27224a672372115e5a1d125ed7b2a0b2
hash_sha1: 9854582a2958b2d31541ce4a6a1a36201b80c02
hash_sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a02
hash_sha512: ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a802
created_unix: 1672531200
-
id: 3
size: 14
hash_md5: 27224a672372115e5a1d125ed7b2a0b3
hash_sha1: 9854582a2958b2d31541ce4a6a1a36201b80c03
hash_sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a03
hash_sha512: ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a803
created_unix: 1672531200
-
id: 4
size: 14
hash_md5: 27224a672372115e5a1d125ed7b2a0b4
hash_sha1: 9854582a2958b2d31541ce4a6a1a36201b80c04
hash_sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a04
hash_sha512: ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a804
created_unix: 1672531200
-
id: 5
size: 14
hash_md5: 27224a672372115e5a1d125ed7b2a0b5
hash_sha1: 9854582a2958b2d31541ce4a6a1a36201b80c05
hash_sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a05
hash_sha512: ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a805
created_unix: 1672531200

46 changes: 46 additions & 0 deletions models/fixtures/package_file.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-
id: 1
version_id: 1
blob_id: 1
name: gitea-test-1.0-20230101.000000-1.jar
lower_name: gitea-test-1.0-20230101.000000-1.jar
composite_key: ""
is_lead: false
created_unix: 1672531200
-
id: 2
version_id: 1
blob_id: 2
name: gitea-test-1.0-20230101.000000-2.jar
lower_name: gitea-test-1.0-20230101.000000-2.jar
composite_key: ""
is_lead: false
created_unix: 1672531200
-
id: 3
version_id: 1
blob_id: 3
name: gitea-test-1.0-20230101.000000-3.jar
lower_name: gitea-test-1.0-20230101.000000-3.jar
composite_key: ""
is_lead: false
created_unix: 1672531200
-
id: 4
version_id: 1
blob_id: 4
name: gitea-test-1.0-20230101.000000-4.jar
lower_name: gitea-test-1.0-20230101.000000-4.jar
composite_key: ""
is_lead: false
created_unix: 1672531200
-
id: 5
version_id: 1
blob_id: 5
name: gitea-test-1.0-20230101.000000-5.jar
lower_name: gitea-test-1.0-20230101.000000-5.jar
composite_key: ""
is_lead: false
created_unix: 1672531200

8 changes: 8 additions & 0 deletions models/fixtures/package_version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-
id: 1
package_id: 1
creator_id: 2
version: 1.0-SNAPSHOT
lower_version: 1.0-snapshot
metadata_json: '{"artifact_id":"test-project","group_id":"com.gitea"}'
created_unix: 1672531200
79 changes: 79 additions & 0 deletions models/packages/package_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package packages

import (
"context"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
Expand All @@ -21,6 +24,8 @@ func init() {
}

var (
// ErrMetadataFile indicated a metadata file
ErrMetadataFile = errors.New("metadata file")
// ErrDuplicatePackageFile indicates a duplicated package file error
ErrDuplicatePackageFile = util.NewAlreadyExistErrorf("package file already exists")
// ErrPackageFileNotExist indicates a package file not exist error
Expand Down Expand Up @@ -231,6 +236,80 @@ func HasFiles(ctx context.Context, opts *PackageFileSearchOptions) (bool, error)
return db.Exist[PackageFile](ctx, opts.toConds())
}

// GetFilesBelowBuildNumber retrieves all files for a Maven snapshot version where the build number is <= maxBuildNumber.
// Returns two slices: one for filtered files and one for skipped files.
func GetFilesBelowBuildNumber(ctx context.Context, versionID int64, maxBuildNumber int, classifiers ...string) ([]*PackageFile, []*PackageFile, error) {
if maxBuildNumber <= 0 {
return nil, nil, errors.New("maxBuildNumber must be a positive integer")
}

files, err := GetFilesByVersionID(ctx, versionID)
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve files: %w", err)
}

// Sort classifiers by length (longest first) once per call
sort.SliceStable(classifiers, func(i, j int) bool {
return len(classifiers[i]) > len(classifiers[j])
})

var filteredFiles, skippedFiles []*PackageFile
for _, file := range files {
buildNumber, err := extractBuildNumberFromFileName(file.Name, classifiers...)
if err != nil {
if !errors.Is(err, ErrMetadataFile) {
skippedFiles = append(skippedFiles, file)
}
continue
}

if buildNumber <= maxBuildNumber {
filteredFiles = append(filteredFiles, file)
}
}

return filteredFiles, skippedFiles, nil
}

// extractBuildNumberFromFileName extracts the build number from a Maven snapshot file name.
// Expected formats:
//
// "artifact-1.0.0-20250311.083409-9.tgz" returns 9
// "artifact-to-test-2.0.0-20250311.083409-10-sources.tgz" returns 10
func extractBuildNumberFromFileName(filename string, classifiers ...string) (int, error) {
if strings.Contains(filename, "maven-metadata.xml") {
return 0, ErrMetadataFile
}

dotIdx := strings.LastIndex(filename, ".")
if dotIdx == -1 {
return 0, fmt.Errorf("extract build number from filename: no file extension found in '%s'", filename)
}
base := filename[:dotIdx]

// Remove classifier suffix if present.
for _, classifier := range classifiers {
suffix := "-" + classifier
if strings.HasSuffix(base, suffix) {
base = base[:len(base)-len(suffix)]
break
}
}

// The build number should be the token after the last dash.
lastDash := strings.LastIndex(base, "-")
if lastDash == -1 {
return 0, fmt.Errorf("extract build number from filename: invalid file name format in '%s'", filename)
}
buildNumberStr := base[lastDash+1:]
buildNumber, err := strconv.Atoi(buildNumberStr)
if err != nil {
return 0, fmt.Errorf("extract build number from filename: failed to convert build number '%s' to integer in '%s': %v", buildNumberStr, filename, err)
}

return buildNumber, nil
}

// CalculateFileSize sums up all blob sizes matching the search options.
// It does NOT respect the deduplication of blobs.
func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) {
Expand Down
20 changes: 17 additions & 3 deletions models/packages/package_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,16 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType

// GetVersionsByPackageType gets all versions of a specific type
func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Type) ([]*PackageVersion, error) {
pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
OwnerID: ownerID,
opts := &PackageSearchOptions{
Type: packageType,
IsInternal: optional.Some(false),
})
}

if ownerID != 0 {
opts.OwnerID = ownerID
}

pvs, _, err := SearchVersions(ctx, opts)
return pvs, err
}

Expand All @@ -151,6 +156,15 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty
return pvs, err
}

// GetVersionsByPackageID gets all versions of a specific package
func GetVersionsByPackageID(ctx context.Context, packageID int64) ([]*PackageVersion, error) {
pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
PackageID: packageID,
IsInternal: optional.Some(false),
})
return pvs, err
}

// DeleteVersionByID deletes a version by id
func DeleteVersionByID(ctx context.Context, versionID int64) error {
_, err := db.GetEngine(ctx).ID(versionID).Delete(&PackageVersion{})
Expand Down
56 changes: 56 additions & 0 deletions modules/packages/maven/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package maven

import (
"encoding/xml"
"errors"
"io"
"strconv"

"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
Expand All @@ -31,6 +33,12 @@ type Dependency struct {
Version string `json:"version,omitempty"`
}

// SnapshotMetadata struct holds the build number and the list of classifiers for a snapshot version
type SnapshotMetadata struct {
BuildNumber int `json:"build_number,omitempty"`
Classifiers []string `json:"classifiers,omitempty"`
}

type pomStruct struct {
XMLName xml.Name `xml:"project"`

Expand Down Expand Up @@ -61,6 +69,26 @@ type pomStruct struct {
} `xml:"dependencies>dependency"`
}

type snapshotMetadataStruct struct {
XMLName xml.Name `xml:"metadata"`
GroupID string `xml:"groupId"`
ArtifactID string `xml:"artifactId"`
Version string `xml:"version"`
Versioning struct {
LastUpdated string `xml:"lastUpdated"`
Snapshot struct {
Timestamp string `xml:"timestamp"`
BuildNumber string `xml:"buildNumber"`
} `xml:"snapshot"`
SnapshotVersions []struct {
Extension string `xml:"extension"`
Classifier string `xml:"classifier"`
Value string `xml:"value"`
Updated string `xml:"updated"`
} `xml:"snapshotVersions>snapshotVersion"`
} `xml:"versioning"`
}

// ParsePackageMetaData parses the metadata of a pom file
func ParsePackageMetaData(r io.Reader) (*Metadata, error) {
var pom pomStruct
Expand Down Expand Up @@ -109,3 +137,31 @@ func ParsePackageMetaData(r io.Reader) (*Metadata, error) {
Dependencies: dependencies,
}, nil
}

// ParseSnapshotVersionMetadata parses the Maven Snapshot Version metadata to extract the build number and list of available classifiers.
func ParseSnapshotVersionMetaData(r io.Reader) (*SnapshotMetadata, error) {
var metadata snapshotMetadataStruct

dec := xml.NewDecoder(r)
dec.CharsetReader = charset.NewReaderLabel
if err := dec.Decode(&metadata); err != nil {
return nil, err
}

buildNumber, err := strconv.Atoi(metadata.Versioning.Snapshot.BuildNumber)
if err != nil {
return nil, errors.New("invalid or missing build number in snapshot metadata")
}

var classifiers []string
for _, snapshotVersion := range metadata.Versioning.SnapshotVersions {
if snapshotVersion.Classifier != "" {
classifiers = append(classifiers, snapshotVersion.Classifier)
}
}

return &SnapshotMetadata{
BuildNumber: buildNumber,
Classifiers: classifiers,
}, nil
}
11 changes: 8 additions & 3 deletions modules/setting/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ var (
LimitSizeSwift int64
LimitSizeVagrant int64

DefaultRPMSignEnabled bool
DefaultRPMSignEnabled bool
RetainMavenSnapshotBuilds int
DebugMavenCleanup bool
}{
Enabled: true,
LimitTotalOwnerCount: -1,
Enabled: true,
LimitTotalOwnerCount: -1,
RetainMavenSnapshotBuilds: -1,
}
)

Expand Down Expand Up @@ -88,6 +91,8 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false)
Packages.RetainMavenSnapshotBuilds = sec.Key("RETAIN_MAVEN_SNAPSHOT_BUILDS").MustInt(Packages.RetainMavenSnapshotBuilds)
Packages.DebugMavenCleanup = sec.Key("DEBUG_MAVEN_CLEANUP").MustBool(true)
return nil
}

Expand Down
7 changes: 6 additions & 1 deletion services/packages/cleanup/cleanup.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package container
package cleanup

import (
"context"
Expand All @@ -20,11 +20,12 @@
cargo_service "code.gitea.io/gitea/services/packages/cargo"
container_service "code.gitea.io/gitea/services/packages/container"
debian_service "code.gitea.io/gitea/services/packages/debian"
maven_service "code.gitea.io/gitea/services/packages/maven"
rpm_service "code.gitea.io/gitea/services/packages/rpm"
)

// CleanupTask executes cleanup rules and cleanup expired package data
func CleanupTask(ctx context.Context, olderThan time.Duration) error {

Check failure on line 28 in services/packages/cleanup/cleanup.go

View workflow job for this annotation

GitHub Actions / lint-backend

exported: func name will be used as cleanup.CleanupTask by other packages, and that stutters; consider calling this Task (revive)

Check failure on line 28 in services/packages/cleanup/cleanup.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

exported: func name will be used as cleanup.CleanupTask by other packages, and that stutters; consider calling this Task (revive)

Check failure on line 28 in services/packages/cleanup/cleanup.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

exported: func name will be used as cleanup.CleanupTask by other packages, and that stutters; consider calling this Task (revive)
if err := ExecuteCleanupRules(ctx); err != nil {
return err
}
Expand Down Expand Up @@ -164,13 +165,17 @@
})
}

func CleanupExpiredData(ctx context.Context, olderThan time.Duration) error {

Check failure on line 168 in services/packages/cleanup/cleanup.go

View workflow job for this annotation

GitHub Actions / lint-backend

exported: func name will be used as cleanup.CleanupExpiredData by other packages, and that stutters; consider calling this ExpiredData (revive)

Check failure on line 168 in services/packages/cleanup/cleanup.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

exported: func name will be used as cleanup.CleanupExpiredData by other packages, and that stutters; consider calling this ExpiredData (revive)

Check failure on line 168 in services/packages/cleanup/cleanup.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

exported: func name will be used as cleanup.CleanupExpiredData by other packages, and that stutters; consider calling this ExpiredData (revive)
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
}

if err := maven_service.CleanupSnapshotVersions(ctx); err != nil {
log.Error("Error cleaning up Maven snapshot versions: %v", err)
}

ps, err := packages_model.FindUnreferencedPackages(ctx)
if err != nil {
return err
Expand Down
Loading
Loading