mirror of https://github.com/go-gitea/gitea.git
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>
This commit is contained in:
parent
02e183bf3f
commit
bafb80f80d
|
@ -48,10 +48,11 @@ const maxNuspecFileSize = 3 * 1024 * 1024
|
||||||
|
|
||||||
// Package represents a Nuget package
|
// Package represents a Nuget package
|
||||||
type Package struct {
|
type Package struct {
|
||||||
PackageType PackageType
|
PackageType PackageType
|
||||||
ID string
|
ID string
|
||||||
Version string
|
Version string
|
||||||
Metadata *Metadata
|
Metadata *Metadata
|
||||||
|
NuspecContent *bytes.Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata represents the metadata of a Nuget package
|
// Metadata represents the metadata of a Nuget package
|
||||||
|
@ -138,8 +139,9 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
|
||||||
|
|
||||||
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
|
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
|
||||||
func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
|
func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
|
||||||
|
var nuspecBuf bytes.Buffer
|
||||||
var p nuspecPackage
|
var p nuspecPackage
|
||||||
if err := xml.NewDecoder(r).Decode(&p); err != nil {
|
if err := xml.NewDecoder(io.TeeReader(r, &nuspecBuf)).Decode(&p); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,10 +214,11 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Package{
|
return &Package{
|
||||||
PackageType: packageType,
|
PackageType: packageType,
|
||||||
ID: p.Metadata.ID,
|
ID: p.Metadata.ID,
|
||||||
Version: toNormalizedVersion(v),
|
Version: toNormalizedVersion(v),
|
||||||
Metadata: m,
|
Metadata: m,
|
||||||
|
NuspecContent: &nuspecBuf,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -388,7 +388,8 @@ func EnumeratePackageVersionsV3(ctx *context.Context) {
|
||||||
ctx.JSON(http.StatusOK, resp)
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
|
// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec
|
||||||
|
// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
|
||||||
func DownloadPackageFile(ctx *context.Context) {
|
func DownloadPackageFile(ctx *context.Context) {
|
||||||
packageName := ctx.Params("id")
|
packageName := ctx.Params("id")
|
||||||
packageVersion := ctx.Params("version")
|
packageVersion := ctx.Params("version")
|
||||||
|
@ -431,7 +432,7 @@ func UploadPackage(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err := packages_service.CreatePackageAndAddFile(
|
pv, _, err := packages_service.CreatePackageAndAddFile(
|
||||||
ctx,
|
ctx,
|
||||||
&packages_service.PackageCreationInfo{
|
&packages_service.PackageCreationInfo{
|
||||||
PackageInfo: packages_service.PackageInfo{
|
PackageInfo: packages_service.PackageInfo{
|
||||||
|
@ -465,6 +466,33 @@ func UploadPackage(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nuspecBuf, err := packages_module.CreateHashedBufferFromReaderWithSize(np.NuspecContent, np.NuspecContent.Len())
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer nuspecBuf.Close()
|
||||||
|
|
||||||
|
_, err = packages_service.AddFileToPackageVersionInternal(
|
||||||
|
ctx,
|
||||||
|
pv,
|
||||||
|
&packages_service.PackageFileCreationInfo{
|
||||||
|
PackageFileInfo: packages_service.PackageFileInfo{
|
||||||
|
Filename: strings.ToLower(fmt.Sprintf("%s.nuspec", np.ID)),
|
||||||
|
},
|
||||||
|
Data: nuspecBuf,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||||
|
apiError(ctx, http.StatusForbidden, err)
|
||||||
|
default:
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Status(http.StatusCreated)
|
ctx.Status(http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,29 +90,33 @@ func TestPackageNuGet(t *testing.T) {
|
||||||
symbolFilename := "test.pdb"
|
symbolFilename := "test.pdb"
|
||||||
symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
|
symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
|
||||||
|
|
||||||
createPackage := func(id, version string) io.Reader {
|
createNuspec := func(id, version string) string {
|
||||||
|
return `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||||
|
<metadata>
|
||||||
|
<id>` + id + `</id>
|
||||||
|
<version>` + version + `</version>
|
||||||
|
<authors>` + packageAuthors + `</authors>
|
||||||
|
<description>` + packageDescription + `</description>
|
||||||
|
<dependencies>
|
||||||
|
<group targetFramework=".NETStandard2.0">
|
||||||
|
<dependency id="Microsoft.CSharp" version="4.5.0" />
|
||||||
|
</group>
|
||||||
|
</dependencies>
|
||||||
|
</metadata>
|
||||||
|
</package>`
|
||||||
|
}
|
||||||
|
|
||||||
|
createPackage := func(id, version string) *bytes.Buffer {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
archive := zip.NewWriter(&buf)
|
archive := zip.NewWriter(&buf)
|
||||||
w, _ := archive.Create("package.nuspec")
|
w, _ := archive.Create("package.nuspec")
|
||||||
w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
|
w.Write([]byte(createNuspec(id, version)))
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
|
||||||
<metadata>
|
|
||||||
<id>` + id + `</id>
|
|
||||||
<version>` + version + `</version>
|
|
||||||
<authors>` + packageAuthors + `</authors>
|
|
||||||
<description>` + packageDescription + `</description>
|
|
||||||
<dependencies>
|
|
||||||
<group targetFramework=".NETStandard2.0">
|
|
||||||
<dependency id="Microsoft.CSharp" version="4.5.0" />
|
|
||||||
</group>
|
|
||||||
</dependencies>
|
|
||||||
</metadata>
|
|
||||||
</package>`))
|
|
||||||
archive.Close()
|
archive.Close()
|
||||||
return &buf
|
return &buf
|
||||||
}
|
}
|
||||||
|
|
||||||
content, _ := io.ReadAll(createPackage(packageName, packageVersion))
|
content := createPackage(packageName, packageVersion).Bytes()
|
||||||
|
|
||||||
url := fmt.Sprintf("/api/packages/%s/nuget", user.Name)
|
url := fmt.Sprintf("/api/packages/%s/nuget", user.Name)
|
||||||
|
|
||||||
|
@ -224,7 +228,7 @@ func TestPackageNuGet(t *testing.T) {
|
||||||
|
|
||||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
|
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, pvs, 1)
|
assert.Len(t, pvs, 1, "Should have one version")
|
||||||
|
|
||||||
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
|
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -235,13 +239,21 @@ func TestPackageNuGet(t *testing.T) {
|
||||||
|
|
||||||
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
|
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, pfs, 1)
|
assert.Len(t, pfs, 2, "Should have 2 files: nuget and nuspec")
|
||||||
assert.Equal(t, fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion), pfs[0].Name)
|
for _, pf := range pfs {
|
||||||
assert.True(t, pfs[0].IsLead)
|
switch pf.Name {
|
||||||
|
case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
|
||||||
|
assert.True(t, pf.IsLead)
|
||||||
|
|
||||||
pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
|
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(len(content)), pb.Size)
|
assert.Equal(t, int64(len(content)), pb.Size)
|
||||||
|
case fmt.Sprintf("%s.nuspec", packageName):
|
||||||
|
assert.False(t, pf.IsLead)
|
||||||
|
default:
|
||||||
|
assert.Fail(t, "unexpected filename: %v", pf.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
||||||
AddBasicAuth(user.Name)
|
AddBasicAuth(user.Name)
|
||||||
|
@ -302,16 +314,27 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
|
|
||||||
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
|
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, pfs, 3)
|
assert.Len(t, pfs, 4, "Should have 4 files: nupkg, snupkg, nuspec and pdb")
|
||||||
for _, pf := range pfs {
|
for _, pf := range pfs {
|
||||||
switch pf.Name {
|
switch pf.Name {
|
||||||
case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
|
case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
|
||||||
|
assert.True(t, pf.IsLead)
|
||||||
|
|
||||||
|
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(412), pb.Size)
|
||||||
case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
|
case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
|
||||||
assert.False(t, pf.IsLead)
|
assert.False(t, pf.IsLead)
|
||||||
|
|
||||||
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
|
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(616), pb.Size)
|
assert.Equal(t, int64(616), pb.Size)
|
||||||
|
case fmt.Sprintf("%s.nuspec", packageName):
|
||||||
|
assert.False(t, pf.IsLead)
|
||||||
|
|
||||||
|
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(427), pb.Size)
|
||||||
case symbolFilename:
|
case symbolFilename:
|
||||||
assert.False(t, pf.IsLead)
|
assert.False(t, pf.IsLead)
|
||||||
|
|
||||||
|
@ -353,6 +376,12 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
|
|
||||||
assert.Equal(t, content, resp.Body.Bytes())
|
assert.Equal(t, content, resp.Body.Bytes())
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.nuspec", url, packageName, packageVersion, packageName)).
|
||||||
|
AddBasicAuth(user.Name)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
assert.Equal(t, createNuspec(packageName, packageVersion), resp.Body.String())
|
||||||
|
|
||||||
checkDownloadCount(1)
|
checkDownloadCount(1)
|
||||||
|
|
||||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion)).
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion)).
|
||||||
|
|
Loading…
Reference in New Issue