Skip to content

Commit bafb80f

Browse files
viceiceKN4CK3R
andauthored
Support nuspec manifest download for nuget packages (#28921)
Support downloading nuget nuspec manifest[^1]. This is useful for renovate because it uses this api to find the corresponding repository - Store nuspec along with nupkg on upload - allow downloading nuspec - add doctor command to add missing nuspec files [^1]: https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec --------- Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
1 parent 02e183b commit bafb80f

File tree

3 files changed

+95
-35
lines changed

3 files changed

+95
-35
lines changed

modules/packages/nuget/metadata.go

+12-9
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ const maxNuspecFileSize = 3 * 1024 * 1024
4848

4949
// Package represents a Nuget package
5050
type Package struct {
51-
PackageType PackageType
52-
ID string
53-
Version string
54-
Metadata *Metadata
51+
PackageType PackageType
52+
ID string
53+
Version string
54+
Metadata *Metadata
55+
NuspecContent *bytes.Buffer
5556
}
5657

5758
// Metadata represents the metadata of a Nuget package
@@ -138,8 +139,9 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
138139

139140
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
140141
func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
142+
var nuspecBuf bytes.Buffer
141143
var p nuspecPackage
142-
if err := xml.NewDecoder(r).Decode(&p); err != nil {
144+
if err := xml.NewDecoder(io.TeeReader(r, &nuspecBuf)).Decode(&p); err != nil {
143145
return nil, err
144146
}
145147

@@ -212,10 +214,11 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
212214
}
213215
}
214216
return &Package{
215-
PackageType: packageType,
216-
ID: p.Metadata.ID,
217-
Version: toNormalizedVersion(v),
218-
Metadata: m,
217+
PackageType: packageType,
218+
ID: p.Metadata.ID,
219+
Version: toNormalizedVersion(v),
220+
Metadata: m,
221+
NuspecContent: &nuspecBuf,
219222
}, nil
220223
}
221224

routers/api/packages/nuget/nuget.go

+30-2
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,8 @@ func EnumeratePackageVersionsV3(ctx *context.Context) {
388388
ctx.JSON(http.StatusOK, resp)
389389
}
390390

391-
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
391+
// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec
392+
// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
392393
func DownloadPackageFile(ctx *context.Context) {
393394
packageName := ctx.Params("id")
394395
packageVersion := ctx.Params("version")
@@ -431,7 +432,7 @@ func UploadPackage(ctx *context.Context) {
431432
return
432433
}
433434

434-
_, _, err := packages_service.CreatePackageAndAddFile(
435+
pv, _, err := packages_service.CreatePackageAndAddFile(
435436
ctx,
436437
&packages_service.PackageCreationInfo{
437438
PackageInfo: packages_service.PackageInfo{
@@ -465,6 +466,33 @@ func UploadPackage(ctx *context.Context) {
465466
return
466467
}
467468

469+
nuspecBuf, err := packages_module.CreateHashedBufferFromReaderWithSize(np.NuspecContent, np.NuspecContent.Len())
470+
if err != nil {
471+
apiError(ctx, http.StatusInternalServerError, err)
472+
return
473+
}
474+
defer nuspecBuf.Close()
475+
476+
_, err = packages_service.AddFileToPackageVersionInternal(
477+
ctx,
478+
pv,
479+
&packages_service.PackageFileCreationInfo{
480+
PackageFileInfo: packages_service.PackageFileInfo{
481+
Filename: strings.ToLower(fmt.Sprintf("%s.nuspec", np.ID)),
482+
},
483+
Data: nuspecBuf,
484+
},
485+
)
486+
if err != nil {
487+
switch err {
488+
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
489+
apiError(ctx, http.StatusForbidden, err)
490+
default:
491+
apiError(ctx, http.StatusInternalServerError, err)
492+
}
493+
return
494+
}
495+
468496
ctx.Status(http.StatusCreated)
469497
}
470498

tests/integration/api_packages_nuget_test.go

+53-24
Original file line numberDiff line numberDiff line change
@@ -90,29 +90,33 @@ func TestPackageNuGet(t *testing.T) {
9090
symbolFilename := "test.pdb"
9191
symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
9292

93-
createPackage := func(id, version string) io.Reader {
93+
createNuspec := func(id, version string) string {
94+
return `<?xml version="1.0" encoding="utf-8"?>
95+
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
96+
<metadata>
97+
<id>` + id + `</id>
98+
<version>` + version + `</version>
99+
<authors>` + packageAuthors + `</authors>
100+
<description>` + packageDescription + `</description>
101+
<dependencies>
102+
<group targetFramework=".NETStandard2.0">
103+
<dependency id="Microsoft.CSharp" version="4.5.0" />
104+
</group>
105+
</dependencies>
106+
</metadata>
107+
</package>`
108+
}
109+
110+
createPackage := func(id, version string) *bytes.Buffer {
94111
var buf bytes.Buffer
95112
archive := zip.NewWriter(&buf)
96113
w, _ := archive.Create("package.nuspec")
97-
w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
98-
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
99-
<metadata>
100-
<id>` + id + `</id>
101-
<version>` + version + `</version>
102-
<authors>` + packageAuthors + `</authors>
103-
<description>` + packageDescription + `</description>
104-
<dependencies>
105-
<group targetFramework=".NETStandard2.0">
106-
<dependency id="Microsoft.CSharp" version="4.5.0" />
107-
</group>
108-
</dependencies>
109-
</metadata>
110-
</package>`))
114+
w.Write([]byte(createNuspec(id, version)))
111115
archive.Close()
112116
return &buf
113117
}
114118

115-
content, _ := io.ReadAll(createPackage(packageName, packageVersion))
119+
content := createPackage(packageName, packageVersion).Bytes()
116120

117121
url := fmt.Sprintf("/api/packages/%s/nuget", user.Name)
118122

@@ -224,7 +228,7 @@ func TestPackageNuGet(t *testing.T) {
224228

225229
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
226230
assert.NoError(t, err)
227-
assert.Len(t, pvs, 1)
231+
assert.Len(t, pvs, 1, "Should have one version")
228232

229233
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
230234
assert.NoError(t, err)
@@ -235,13 +239,21 @@ func TestPackageNuGet(t *testing.T) {
235239

236240
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
237241
assert.NoError(t, err)
238-
assert.Len(t, pfs, 1)
239-
assert.Equal(t, fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion), pfs[0].Name)
240-
assert.True(t, pfs[0].IsLead)
242+
assert.Len(t, pfs, 2, "Should have 2 files: nuget and nuspec")
243+
for _, pf := range pfs {
244+
switch pf.Name {
245+
case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
246+
assert.True(t, pf.IsLead)
241247

242-
pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
243-
assert.NoError(t, err)
244-
assert.Equal(t, int64(len(content)), pb.Size)
248+
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
249+
assert.NoError(t, err)
250+
assert.Equal(t, int64(len(content)), pb.Size)
251+
case fmt.Sprintf("%s.nuspec", packageName):
252+
assert.False(t, pf.IsLead)
253+
default:
254+
assert.Fail(t, "unexpected filename: %v", pf.Name)
255+
}
256+
}
245257

246258
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
247259
AddBasicAuth(user.Name)
@@ -302,16 +314,27 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
302314

303315
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
304316
assert.NoError(t, err)
305-
assert.Len(t, pfs, 3)
317+
assert.Len(t, pfs, 4, "Should have 4 files: nupkg, snupkg, nuspec and pdb")
306318
for _, pf := range pfs {
307319
switch pf.Name {
308320
case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
321+
assert.True(t, pf.IsLead)
322+
323+
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
324+
assert.NoError(t, err)
325+
assert.Equal(t, int64(412), pb.Size)
309326
case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
310327
assert.False(t, pf.IsLead)
311328

312329
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
313330
assert.NoError(t, err)
314331
assert.Equal(t, int64(616), pb.Size)
332+
case fmt.Sprintf("%s.nuspec", packageName):
333+
assert.False(t, pf.IsLead)
334+
335+
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
336+
assert.NoError(t, err)
337+
assert.Equal(t, int64(427), pb.Size)
315338
case symbolFilename:
316339
assert.False(t, pf.IsLead)
317340

@@ -353,6 +376,12 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
353376

354377
assert.Equal(t, content, resp.Body.Bytes())
355378

379+
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.nuspec", url, packageName, packageVersion, packageName)).
380+
AddBasicAuth(user.Name)
381+
resp = MakeRequest(t, req, http.StatusOK)
382+
383+
assert.Equal(t, createNuspec(packageName, packageVersion), resp.Body.String())
384+
356385
checkDownloadCount(1)
357386

358387
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion)).

0 commit comments

Comments
 (0)