Skip to content

Commit cad5f14

Browse files
committed
Added cleanup method for files in Maven snapshot versions
1 parent 90cb5f9 commit cad5f14

File tree

8 files changed

+301
-10
lines changed

8 files changed

+301
-10
lines changed

custom/conf/app.example.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2655,6 +2655,9 @@ LEVEL = Info
26552655
;LIMIT_SIZE_HELM = -1
26562656
;; Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
26572657
;LIMIT_SIZE_MAVEN = -1
2658+
;; 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.
2659+
;; Cleanup expired packages/data then targets the files within all maven snapshots versions
2660+
;RETAIN_MAVEN_SNAPSHOT_BUILDS = -1
26582661
;; Maximum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
26592662
;LIMIT_SIZE_NPM = -1
26602663
;; Maximum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)

models/packages/package_file.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ package packages
55

66
import (
77
"context"
8+
"errors"
9+
"fmt"
10+
"sort"
811
"strconv"
912
"strings"
1013
"time"
@@ -21,6 +24,8 @@ func init() {
2124
}
2225

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

239+
// GetFilesBelowBuildNumber retrieves all files for a Maven snapshot version where the build number is <= maxBuildNumber.
240+
// Returns two slices: one for filtered files and one for skipped files.
241+
func GetFilesBelowBuildNumber(ctx context.Context, versionID int64, maxBuildNumber int, classifiers ...string) ([]*PackageFile, []*PackageFile, error) {
242+
if maxBuildNumber <= 0 {
243+
return nil, nil, errors.New("maxBuildNumber must be a positive integer")
244+
}
245+
246+
files, err := GetFilesByVersionID(ctx, versionID)
247+
if err != nil {
248+
return nil, nil, fmt.Errorf("failed to retrieve files: %w", err)
249+
}
250+
251+
// Sort classifiers by length (longest first) once per call
252+
sort.SliceStable(classifiers, func(i, j int) bool {
253+
return len(classifiers[i]) > len(classifiers[j])
254+
})
255+
256+
var filteredFiles, skippedFiles []*PackageFile
257+
for _, file := range files {
258+
buildNumber, err := extractBuildNumberFromFileName(file.Name, classifiers...)
259+
if err != nil {
260+
if !errors.Is(err, ErrMetadataFile) {
261+
skippedFiles = append(skippedFiles, file)
262+
}
263+
continue
264+
}
265+
266+
if buildNumber <= maxBuildNumber {
267+
filteredFiles = append(filteredFiles, file)
268+
}
269+
}
270+
271+
return filteredFiles, skippedFiles, nil
272+
}
273+
274+
// extractBuildNumberFromFileName extracts the build number from a Maven snapshot file name.
275+
// Expected formats:
276+
//
277+
// "artifact-1.0.0-20250311.083409-9.tgz" returns 9
278+
// "artifact-to-test-2.0.0-20250311.083409-10-sources.tgz" returns 10
279+
func extractBuildNumberFromFileName(filename string, classifiers ...string) (int, error) {
280+
if strings.Contains(filename, "maven-metadata.xml") {
281+
return 0, ErrMetadataFile
282+
}
283+
284+
dotIdx := strings.LastIndex(filename, ".")
285+
if dotIdx == -1 {
286+
return 0, fmt.Errorf("extract build number from filename: no file extension found in '%s'", filename)
287+
}
288+
base := filename[:dotIdx]
289+
290+
// Remove classifier suffix if present.
291+
for _, classifier := range classifiers {
292+
suffix := "-" + classifier
293+
if strings.HasSuffix(base, suffix) {
294+
base = base[:len(base)-len(suffix)]
295+
break
296+
}
297+
}
298+
299+
// The build number should be the token after the last dash.
300+
lastDash := strings.LastIndex(base, "-")
301+
if lastDash == -1 {
302+
return 0, fmt.Errorf("extract build number from filename: invalid file name format in '%s'", filename)
303+
}
304+
buildNumberStr := base[lastDash+1:]
305+
buildNumber, err := strconv.Atoi(buildNumberStr)
306+
if err != nil {
307+
return 0, fmt.Errorf("extract build number from filename: failed to convert build number '%s' to integer in '%s': %v", buildNumberStr, filename, err)
308+
}
309+
310+
return buildNumber, nil
311+
}
312+
234313
// CalculateFileSize sums up all blob sizes matching the search options.
235314
// It does NOT respect the deduplication of blobs.
236315
func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) {

models/packages/package_version.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,16 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType
129129

130130
// GetVersionsByPackageType gets all versions of a specific type
131131
func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Type) ([]*PackageVersion, error) {
132-
pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
133-
OwnerID: ownerID,
132+
opts := &PackageSearchOptions{
134133
Type: packageType,
135134
IsInternal: optional.Some(false),
136-
})
135+
}
136+
137+
if ownerID != 0 {
138+
opts.OwnerID = ownerID
139+
}
140+
141+
pvs, _, err := SearchVersions(ctx, opts)
137142
return pvs, err
138143
}
139144

modules/packages/maven/metadata.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ package maven
55

66
import (
77
"encoding/xml"
8+
"errors"
89
"io"
10+
"strconv"
911

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

36+
// SnapshotMetadata struct holds the build number and the list of classifiers for a snapshot version
37+
type SnapshotMetadata struct {
38+
BuildNumber int `json:"build_number,omitempty"`
39+
Classifiers []string `json:"classifiers,omitempty"`
40+
}
41+
3442
type pomStruct struct {
3543
XMLName xml.Name `xml:"project"`
3644

@@ -61,6 +69,26 @@ type pomStruct struct {
6169
} `xml:"dependencies>dependency"`
6270
}
6371

72+
type snapshotMetadataStruct struct {
73+
XMLName xml.Name `xml:"metadata"`
74+
GroupID string `xml:"groupId"`
75+
ArtifactID string `xml:"artifactId"`
76+
Version string `xml:"version"`
77+
Versioning struct {
78+
LastUpdated string `xml:"lastUpdated"`
79+
Snapshot struct {
80+
Timestamp string `xml:"timestamp"`
81+
BuildNumber string `xml:"buildNumber"`
82+
} `xml:"snapshot"`
83+
SnapshotVersions []struct {
84+
Extension string `xml:"extension"`
85+
Classifier string `xml:"classifier"`
86+
Value string `xml:"value"`
87+
Updated string `xml:"updated"`
88+
} `xml:"snapshotVersions>snapshotVersion"`
89+
} `xml:"versioning"`
90+
}
91+
6492
// ParsePackageMetaData parses the metadata of a pom file
6593
func ParsePackageMetaData(r io.Reader) (*Metadata, error) {
6694
var pom pomStruct
@@ -109,3 +137,31 @@ func ParsePackageMetaData(r io.Reader) (*Metadata, error) {
109137
Dependencies: dependencies,
110138
}, nil
111139
}
140+
141+
// ParseSnapshotVersionMetadata parses the Maven Snapshot Version metadata to extract the build number and list of available classifiers.
142+
func ParseSnapshotVersionMetaData(r io.Reader) (*SnapshotMetadata, error) {
143+
var metadata snapshotMetadataStruct
144+
145+
dec := xml.NewDecoder(r)
146+
dec.CharsetReader = charset.NewReaderLabel
147+
if err := dec.Decode(&metadata); err != nil {
148+
return nil, err
149+
}
150+
151+
buildNumber, err := strconv.Atoi(metadata.Versioning.Snapshot.BuildNumber)
152+
if err != nil {
153+
return nil, errors.New("invalid or missing build number in snapshot metadata")
154+
}
155+
156+
var classifiers []string
157+
for _, snapshotVersion := range metadata.Versioning.SnapshotVersions {
158+
if snapshotVersion.Classifier != "" {
159+
classifiers = append(classifiers, snapshotVersion.Classifier)
160+
}
161+
}
162+
163+
return &SnapshotMetadata{
164+
BuildNumber: buildNumber,
165+
Classifiers: classifiers,
166+
}, nil
167+
}

modules/setting/packages.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ var (
4141
LimitSizeSwift int64
4242
LimitSizeVagrant int64
4343

44-
DefaultRPMSignEnabled bool
44+
DefaultRPMSignEnabled bool
45+
RetainMavenSnapshotBuilds int
4546
}{
46-
Enabled: true,
47-
LimitTotalOwnerCount: -1,
47+
Enabled: true,
48+
LimitTotalOwnerCount: -1,
49+
RetainMavenSnapshotBuilds: -1,
4850
}
4951
)
5052

@@ -88,6 +90,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
8890
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
8991
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
9092
Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false)
93+
Packages.RetainMavenSnapshotBuilds = sec.Key("RETAIN_MAVEN_SNAPSHOT_BUILDS").MustInt(Packages.RetainMavenSnapshotBuilds)
9194
return nil
9295
}
9396

services/packages/cleanup/cleanup.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2022 The Gitea Authors. All rights reserved.
22
// SPDX-License-Identifier: MIT
33

4-
package container
4+
package cleanup
55

66
import (
77
"context"
@@ -20,6 +20,7 @@ import (
2020
cargo_service "code.gitea.io/gitea/services/packages/cargo"
2121
container_service "code.gitea.io/gitea/services/packages/container"
2222
debian_service "code.gitea.io/gitea/services/packages/debian"
23+
maven_service "code.gitea.io/gitea/services/packages/maven"
2324
rpm_service "code.gitea.io/gitea/services/packages/rpm"
2425
)
2526

@@ -171,6 +172,10 @@ func CleanupExpiredData(ctx context.Context, olderThan time.Duration) error {
171172
return err
172173
}
173174

175+
if err := maven_service.CleanupSnapshotVersions(ctx); err != nil {
176+
log.Error("Error during maven snapshot versions cleanup: %v", err)
177+
}
178+
174179
ps, err := packages_model.FindUnreferencedPackages(ctx)
175180
if err != nil {
176181
return err

0 commit comments

Comments
 (0)