Skip to content

Commit f9688da

Browse files
Add support for repository names containing slashes.
Add support for different file compressions. Prevent duplicates caused by different file compressions. Co-authored-by: dragon <[email protected]>
1 parent 0c6d40c commit f9688da

File tree

12 files changed

+415
-260
lines changed

12 files changed

+415
-260
lines changed

models/packages/package_file.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag
221221
return pfs, count, err
222222
}
223223

224+
// HasFiles tests if there are files of packages matching the search options
225+
func HasFiles(ctx context.Context, opts *PackageFileSearchOptions) (bool, error) {
226+
return db.Exist[PackageFile](ctx, opts.toConds())
227+
}
228+
224229
// CalculateFileSize sums up all blob sizes matching the search options.
225230
// It does NOT respect the deduplication of blobs.
226231
func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) {

modules/packages/arch/metadata.go

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package arch
66
import (
77
"archive/tar"
88
"bufio"
9+
"bytes"
10+
"compress/gzip"
911
"io"
1012
"regexp"
1113
"strconv"
@@ -15,6 +17,7 @@ import (
1517
"code.gitea.io/gitea/modules/validation"
1618

1719
"github.com/klauspost/compress/zstd"
20+
"github.com/ulikunitz/xz"
1821
)
1922

2023
const (
@@ -34,6 +37,7 @@ const (
3437

3538
var (
3639
ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing")
40+
ErrUnsupportedFormat = util.NewInvalidArgumentErrorf("unsupported package container format")
3741
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
3842
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
3943
ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
@@ -44,10 +48,11 @@ var (
4448
)
4549

4650
type Package struct {
47-
Name string `json:"name"`
48-
Version string `json:"version"`
49-
VersionMetadata VersionMetadata
50-
FileMetadata FileMetadata
51+
Name string
52+
Version string
53+
VersionMetadata VersionMetadata
54+
FileMetadata FileMetadata
55+
FileCompressionExtension string
5156
}
5257

5358
type VersionMetadata struct {
@@ -75,16 +80,50 @@ type FileMetadata struct {
7580

7681
// ParsePackage parses an Arch package file
7782
func ParsePackage(r io.Reader) (*Package, error) {
78-
zr, err := zstd.NewReader(r)
83+
header := make([]byte, 10)
84+
n, err := util.ReadAtMost(r, header)
7985
if err != nil {
8086
return nil, err
8187
}
82-
defer zr.Close()
88+
89+
r = io.MultiReader(bytes.NewReader(header[:n]), r)
90+
91+
var inner io.Reader
92+
var compressionType string
93+
if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst
94+
zr, err := zstd.NewReader(r)
95+
if err != nil {
96+
return nil, err
97+
}
98+
defer zr.Close()
99+
100+
inner = zr
101+
compressionType = "zst"
102+
} else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz
103+
xzr, err := xz.NewReader(r)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
inner = xzr
109+
compressionType = "xz"
110+
} else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz
111+
gzr, err := gzip.NewReader(r)
112+
if err != nil {
113+
return nil, err
114+
}
115+
defer gzr.Close()
116+
117+
inner = gzr
118+
compressionType = "gz"
119+
} else {
120+
return nil, ErrUnsupportedFormat
121+
}
83122

84123
var p *Package
85124
files := make([]string, 0, 10)
86125

87-
tr := tar.NewReader(zr)
126+
tr := tar.NewReader(inner)
88127
for {
89128
hd, err := tr.Next()
90129
if err == io.EOF {
@@ -114,6 +153,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
114153
}
115154

116155
p.FileMetadata.Files = files
156+
p.FileCompressionExtension = compressionType
117157

118158
return p, nil
119159
}

modules/packages/arch/metadata_test.go

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ package arch
66
import (
77
"archive/tar"
88
"bytes"
9+
"compress/gzip"
910
"io"
1011
"testing"
1112

1213
"github.com/klauspost/compress/zstd"
1314
"github.com/stretchr/testify/assert"
15+
"github.com/ulikunitz/xz"
1416
)
1517

1618
const (
@@ -46,10 +48,18 @@ backup = usr/bin/paket1`)
4648
}
4749

4850
func TestParsePackage(t *testing.T) {
49-
createPackage := func(files map[string][]byte) io.Reader {
51+
createPackage := func(compression string, files map[string][]byte) io.Reader {
5052
var buf bytes.Buffer
51-
zw, _ := zstd.NewWriter(&buf)
52-
tw := tar.NewWriter(zw)
53+
var cw io.WriteCloser
54+
switch compression {
55+
case "zst":
56+
cw, _ = zstd.NewWriter(&buf)
57+
case "xz":
58+
cw, _ = xz.NewWriter(&buf)
59+
case "gz":
60+
cw = gzip.NewWriter(&buf)
61+
}
62+
tw := tar.NewWriter(cw)
5363

5464
for name, content := range files {
5565
hdr := &tar.Header{
@@ -62,39 +72,43 @@ func TestParsePackage(t *testing.T) {
6272
}
6373

6474
tw.Close()
65-
zw.Close()
75+
cw.Close()
6676

6777
return &buf
6878
}
6979

70-
t.Run("MissingPKGINFOFile", func(t *testing.T) {
71-
data := createPackage(map[string][]byte{"dummy.txt": {}})
80+
for _, c := range []string{"gz", "xz", "zst"} {
81+
t.Run(c, func(t *testing.T) {
82+
t.Run("MissingPKGINFOFile", func(t *testing.T) {
83+
data := createPackage(c, map[string][]byte{"dummy.txt": {}})
7284

73-
pp, err := ParsePackage(data)
74-
assert.Nil(t, pp)
75-
assert.ErrorIs(t, err, ErrMissingPKGINFOFile)
76-
})
85+
pp, err := ParsePackage(data)
86+
assert.Nil(t, pp)
87+
assert.ErrorIs(t, err, ErrMissingPKGINFOFile)
88+
})
7789

78-
t.Run("InvalidPKGINFOFile", func(t *testing.T) {
79-
data := createPackage(map[string][]byte{".PKGINFO": {}})
90+
t.Run("InvalidPKGINFOFile", func(t *testing.T) {
91+
data := createPackage(c, map[string][]byte{".PKGINFO": {}})
8092

81-
pp, err := ParsePackage(data)
82-
assert.Nil(t, pp)
83-
assert.ErrorIs(t, err, ErrInvalidName)
84-
})
93+
pp, err := ParsePackage(data)
94+
assert.Nil(t, pp)
95+
assert.ErrorIs(t, err, ErrInvalidName)
96+
})
8597

86-
t.Run("Valid", func(t *testing.T) {
87-
data := createPackage(map[string][]byte{
88-
".PKGINFO": createPKGINFOContent(packageName, packageVersion),
89-
"/test/dummy.txt": {},
90-
})
98+
t.Run("Valid", func(t *testing.T) {
99+
data := createPackage(c, map[string][]byte{
100+
".PKGINFO": createPKGINFOContent(packageName, packageVersion),
101+
"/test/dummy.txt": {},
102+
})
91103

92-
p, err := ParsePackage(data)
93-
assert.NoError(t, err)
94-
assert.NotNil(t, p)
104+
p, err := ParsePackage(data)
105+
assert.NoError(t, err)
106+
assert.NotNil(t, p)
95107

96-
assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files)
97-
})
108+
assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files)
109+
})
110+
})
111+
}
98112
}
99113

100114
func TestParsePackageInfo(t *testing.T) {

routers/api/packages/api.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,47 @@ func CommonRoutes() *web.Router {
137137
})
138138
}, reqPackageAccess(perm.AccessModeRead))
139139
r.Group("/arch", func() {
140-
r.Get("/key", arch.GetRepositoryKey)
141-
r.Group("/{repository}", func() {
142-
r.Put("", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile)
143-
r.Group("/{architecture}/{filename}", func() {
144-
r.Get("", arch.DownloadPackageOrRepositoryFile)
145-
r.Delete("", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageFile)
146-
})
140+
r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey)
141+
142+
r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) {
143+
path := strings.Trim(ctx.PathParam("*"), "/")
144+
145+
if ctx.Req.Method == "PUT" {
146+
reqPackageAccess(perm.AccessModeWrite)(ctx)
147+
if ctx.Written() {
148+
return
149+
}
150+
ctx.SetPathParam("repository", path)
151+
arch.UploadPackageFile(ctx)
152+
return
153+
}
154+
155+
pathFields := strings.Split(path, "/")
156+
pathFieldsLen := len(pathFields)
157+
158+
if pathFieldsLen >= 2 {
159+
ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/"))
160+
ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2])
161+
ctx.SetPathParam("filename", pathFields[pathFieldsLen-1])
162+
163+
if ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET" {
164+
arch.GetPackageOrRepositoryFile(ctx)
165+
return
166+
}
167+
168+
if ctx.Req.Method == "DELETE" {
169+
reqPackageAccess(perm.AccessModeWrite)(ctx)
170+
if ctx.Written() {
171+
return
172+
}
173+
arch.DeletePackageFile(ctx)
174+
return
175+
}
176+
}
177+
178+
ctx.Status(http.StatusNotFound)
147179
})
148-
})
180+
}, reqPackageAccess(perm.AccessModeRead))
149181
r.Group("/cargo", func() {
150182
r.Group("/api/v1/crates", func() {
151183
r.Get("", cargo.SearchPackages)

routers/api/packages/arch/arch.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func GetRepositoryKey(ctx *context.Context) {
4242
}
4343

4444
func UploadPackageFile(ctx *context.Context) {
45-
repository := strings.TrimSpace(ctx.Params("repository"))
45+
repository := strings.TrimSpace(ctx.PathParam("repository"))
4646
if repository == "" {
4747
apiError(ctx, http.StatusBadRequest, "invalid repository")
4848
return
@@ -96,6 +96,32 @@ func UploadPackageFile(ctx *context.Context) {
9696
return
9797
}
9898

99+
release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID)
100+
if err != nil {
101+
apiError(ctx, http.StatusInternalServerError, err)
102+
return
103+
}
104+
defer release()
105+
106+
// Search for duplicates with different file compression
107+
has, err := packages_model.HasFiles(ctx, &packages_model.PackageFileSearchOptions{
108+
OwnerID: ctx.Package.Owner.ID,
109+
PackageType: packages_model.TypeArch,
110+
Query: fmt.Sprintf("%s-%s-%s.pkg.tar.%%", pck.Name, pck.Version, pck.FileMetadata.Architecture),
111+
Properties: map[string]string{
112+
arch_module.PropertyRepository: repository,
113+
arch_module.PropertyArchitecture: pck.FileMetadata.Architecture,
114+
},
115+
})
116+
if err != nil {
117+
apiError(ctx, http.StatusInternalServerError, err)
118+
return
119+
}
120+
if has {
121+
apiError(ctx, http.StatusConflict, packages_model.ErrDuplicatePackageFile)
122+
return
123+
}
124+
99125
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
100126
ctx,
101127
&packages_service.PackageCreationInfo{
@@ -110,7 +136,7 @@ func UploadPackageFile(ctx *context.Context) {
110136
},
111137
&packages_service.PackageFileCreationInfo{
112138
PackageFileInfo: packages_service.PackageFileInfo{
113-
Filename: fmt.Sprintf("%s-%s-%s.pck.tar.zst", pck.Name, pck.Version, pck.FileMetadata.Architecture),
139+
Filename: fmt.Sprintf("%s-%s-%s.pkg.tar.%s", pck.Name, pck.Version, pck.FileMetadata.Architecture, pck.FileCompressionExtension),
114140
CompositeKey: fmt.Sprintf("%s|%s", repository, pck.FileMetadata.Architecture),
115141
},
116142
Creator: ctx.Doer,
@@ -144,10 +170,10 @@ func UploadPackageFile(ctx *context.Context) {
144170
ctx.Status(http.StatusCreated)
145171
}
146172

147-
func DownloadPackageOrRepositoryFile(ctx *context.Context) {
148-
repository := ctx.Params("repository")
149-
architecture := ctx.Params("architecture")
150-
filename := ctx.Params("filename")
173+
func GetPackageOrRepositoryFile(ctx *context.Context) {
174+
repository := ctx.PathParam("repository")
175+
architecture := ctx.PathParam("architecture")
176+
filename := ctx.PathParam("filename")
151177
filenameOrig := filename
152178

153179
isSignature := strings.HasSuffix(filename, ".sig")
@@ -231,12 +257,19 @@ func DownloadPackageOrRepositoryFile(ctx *context.Context) {
231257
}
232258

233259
func DeletePackageFile(ctx *context.Context) {
234-
repository, architecture := ctx.Params("repository"), ctx.Params("architecture")
260+
repository, architecture := ctx.PathParam("repository"), ctx.PathParam("architecture")
261+
262+
release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID)
263+
if err != nil {
264+
apiError(ctx, http.StatusInternalServerError, err)
265+
return
266+
}
267+
defer release()
235268

236269
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
237270
OwnerID: ctx.Package.Owner.ID,
238271
PackageType: packages_model.TypeArch,
239-
Query: ctx.Params("filename"),
272+
Query: ctx.PathParam("filename"),
240273
CompositeKey: fmt.Sprintf("%s|%s", repository, architecture),
241274
})
242275
if err != nil {

routers/web/user/package.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,13 @@ func ViewPackageVersion(ctx *context.Context) {
179179
ctx.Data["IsPackagesPage"] = true
180180
ctx.Data["PackageDescriptor"] = pd
181181

182+
registryHostURL, err := url.Parse(httplib.GuessCurrentHostURL(ctx))
183+
if err != nil {
184+
registryHostURL, _ = url.Parse(setting.AppURL)
185+
}
186+
ctx.Data["PackageRegistryHost"] = registryHostURL.Host
187+
182188
switch pd.Package.Type {
183-
case packages_model.TypeContainer:
184-
registryAppURL, err := url.Parse(httplib.GuessCurrentAppURL(ctx))
185-
if err != nil {
186-
registryAppURL, _ = url.Parse(setting.AppURL)
187-
}
188-
ctx.Data["RegistryHost"] = registryAppURL.Host
189189
case packages_model.TypeAlpine:
190190
branches := make(container.Set[string])
191191
repositories := make(container.Set[string])
@@ -267,7 +267,6 @@ func ViewPackageVersion(ctx *context.Context) {
267267
var (
268268
total int64
269269
pvs []*packages_model.PackageVersion
270-
err error
271270
)
272271
switch pd.Package.Type {
273272
case packages_model.TypeContainer:

services/forms/package_form.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
type PackageCleanupRuleForm struct {
1616
ID int64
1717
Enabled bool
18-
Type string `binding:"Required;In(alpine,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"`
18+
Type string `binding:"Required;In(alpine,arch,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"`
1919
KeepCount int `binding:"In(0,1,5,10,25,50,100)"`
2020
KeepPattern string `binding:"RegexPattern"`
2121
RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`

0 commit comments

Comments
 (0)