Skip to content

Commit a447a1c

Browse files
committed
feat(nuget): support nuspec manifest download
1 parent 663acd0 commit a447a1c

File tree

5 files changed

+343
-77
lines changed

5 files changed

+343
-77
lines changed

modules/packages/nuget/metadata.go

Lines changed: 84 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"regexp"
1414
"strings"
1515

16+
packages_module "code.gitea.io/gitea/modules/packages"
1617
"code.gitea.io/gitea/modules/util"
1718
"code.gitea.io/gitea/modules/validation"
1819

@@ -71,41 +72,57 @@ type Dependency struct {
7172
Version string `json:"version"`
7273
}
7374

75+
type nuspecPackageType struct {
76+
Name string `xml:"name,attr"`
77+
}
78+
79+
type nuspecPackageTypes struct {
80+
PackageType []nuspecPackageType `xml:"packageType"`
81+
}
82+
83+
type nuspecRepository struct {
84+
URL string `xml:"url,attr,omitempty"`
85+
Type string `xml:"type,attr,omitempty"`
86+
}
87+
type nuspecDependency struct {
88+
ID string `xml:"id,attr"`
89+
Version string `xml:"version,attr"`
90+
Exclude string `xml:"exclude,attr,omitempty"`
91+
}
92+
93+
type nuspecGroup struct {
94+
TargetFramework string `xml:"targetFramework,attr"`
95+
Dependency []nuspecDependency `xml:"dependency"`
96+
}
97+
98+
type nuspecDependencies struct {
99+
Group []nuspecGroup `xml:"group"`
100+
}
101+
102+
type nuspeceMetadata struct {
103+
ID string `xml:"id"`
104+
Version string `xml:"version"`
105+
Authors string `xml:"authors"`
106+
RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance,omitempty"`
107+
ProjectURL string `xml:"projectUrl,omitempty"`
108+
Description string `xml:"description"`
109+
ReleaseNotes string `xml:"releaseNotes,omitempty"`
110+
PackageTypes *nuspecPackageTypes `xml:"packageTypes,omitempty"`
111+
Repository *nuspecRepository `xml:"repository,omitempty"`
112+
Dependencies *nuspecDependencies `xml:"dependencies,omitempty"`
113+
}
114+
74115
type nuspecPackage struct {
75-
Metadata struct {
76-
ID string `xml:"id"`
77-
Version string `xml:"version"`
78-
Authors string `xml:"authors"`
79-
RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
80-
ProjectURL string `xml:"projectUrl"`
81-
Description string `xml:"description"`
82-
ReleaseNotes string `xml:"releaseNotes"`
83-
PackageTypes struct {
84-
PackageType []struct {
85-
Name string `xml:"name,attr"`
86-
} `xml:"packageType"`
87-
} `xml:"packageTypes"`
88-
Repository struct {
89-
URL string `xml:"url,attr"`
90-
} `xml:"repository"`
91-
Dependencies struct {
92-
Group []struct {
93-
TargetFramework string `xml:"targetFramework,attr"`
94-
Dependency []struct {
95-
ID string `xml:"id,attr"`
96-
Version string `xml:"version,attr"`
97-
Exclude string `xml:"exclude,attr"`
98-
} `xml:"dependency"`
99-
} `xml:"group"`
100-
} `xml:"dependencies"`
101-
} `xml:"metadata"`
116+
XMLName xml.Name `xml:"package"`
117+
Xmlns string `xml:"xmlns,attr"`
118+
Metadata nuspeceMetadata `xml:"metadata"`
102119
}
103120

104121
// ParsePackageMetaData parses the metadata of a Nuget package file
105-
func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
122+
func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, *packages_module.HashedBuffer, error) {
106123
archive, err := zip.NewReader(r, size)
107124
if err != nil {
108-
return nil, err
125+
return nil, nil, err
109126
}
110127

111128
for _, file := range archive.File {
@@ -114,18 +131,29 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
114131
}
115132
if strings.HasSuffix(strings.ToLower(file.Name), ".nuspec") {
116133
if file.UncompressedSize64 > maxNuspecFileSize {
117-
return nil, ErrNuspecFileTooLarge
134+
return nil, nil, ErrNuspecFileTooLarge
118135
}
119136
f, err := archive.Open(file.Name)
120137
if err != nil {
121-
return nil, err
138+
return nil, nil, err
122139
}
123140
defer f.Close()
124141

125-
return ParseNuspecMetaData(f)
142+
hb, err := packages_module.CreateHashedBufferFromReader(f)
143+
if err != nil {
144+
return nil, nil, err
145+
}
146+
147+
meta, err := ParseNuspecMetaData(hb)
148+
if err != nil {
149+
hb.Close()
150+
return nil, nil, err
151+
}
152+
153+
return meta, hb, err
126154
}
127155
}
128-
return nil, ErrMissingNuspecFile
156+
return nil, nil, ErrMissingNuspecFile
129157
}
130158

131159
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
@@ -149,10 +177,12 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
149177
}
150178

151179
packageType := DependencyPackage
152-
for _, pt := range p.Metadata.PackageTypes.PackageType {
153-
if pt.Name == "SymbolsPackage" {
154-
packageType = SymbolsPackage
155-
break
180+
if p.Metadata.PackageTypes != nil {
181+
for _, pt := range p.Metadata.PackageTypes.PackageType {
182+
if pt.Name == "SymbolsPackage" {
183+
packageType = SymbolsPackage
184+
break
185+
}
156186
}
157187
}
158188

@@ -161,24 +191,27 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
161191
ReleaseNotes: p.Metadata.ReleaseNotes,
162192
Authors: p.Metadata.Authors,
163193
ProjectURL: p.Metadata.ProjectURL,
164-
RepositoryURL: p.Metadata.Repository.URL,
165194
RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
166195
Dependencies: make(map[string][]Dependency),
167196
}
168-
169-
for _, group := range p.Metadata.Dependencies.Group {
170-
deps := make([]Dependency, 0, len(group.Dependency))
171-
for _, dep := range group.Dependency {
172-
if dep.ID == "" || dep.Version == "" {
173-
continue
197+
if p.Metadata.Repository != nil {
198+
m.RepositoryURL = p.Metadata.Repository.URL
199+
}
200+
if p.Metadata.Dependencies != nil {
201+
for _, group := range p.Metadata.Dependencies.Group {
202+
deps := make([]Dependency, 0, len(group.Dependency))
203+
for _, dep := range group.Dependency {
204+
if dep.ID == "" || dep.Version == "" {
205+
continue
206+
}
207+
deps = append(deps, Dependency{
208+
ID: dep.ID,
209+
Version: dep.Version,
210+
})
211+
}
212+
if len(deps) > 0 {
213+
m.Dependencies[group.TargetFramework] = deps
174214
}
175-
deps = append(deps, Dependency{
176-
ID: dep.ID,
177-
Version: dep.Version,
178-
})
179-
}
180-
if len(deps) > 0 {
181-
m.Dependencies[group.TargetFramework] = deps
182215
}
183216
}
184217
return &Package{

modules/packages/nuget/metadata_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,23 +72,23 @@ func TestParsePackageMetaData(t *testing.T) {
7272
t.Run("MissingNuspecFile", func(t *testing.T) {
7373
data := createArchive("dummy.txt", "")
7474

75-
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
75+
np, _, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
7676
assert.Nil(t, np)
7777
assert.ErrorIs(t, err, ErrMissingNuspecFile)
7878
})
7979

8080
t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
8181
data := createArchive("sub/package.nuspec", "")
8282

83-
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
83+
np, _, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
8484
assert.Nil(t, np)
8585
assert.ErrorIs(t, err, ErrMissingNuspecFile)
8686
})
8787

8888
t.Run("InvalidNuspecFile", func(t *testing.T) {
8989
data := createArchive("package.nuspec", "")
9090

91-
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
91+
np, _, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
9292
assert.Nil(t, np)
9393
assert.Error(t, err)
9494
})
@@ -99,7 +99,7 @@ func TestParsePackageMetaData(t *testing.T) {
9999
<metadata></metadata>
100100
</package>`)
101101

102-
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
102+
np, _, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
103103
assert.Nil(t, np)
104104
assert.ErrorIs(t, err, ErrNuspecInvalidID)
105105
})
@@ -112,15 +112,15 @@ func TestParsePackageMetaData(t *testing.T) {
112112
</metadata>
113113
</package>`)
114114

115-
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
115+
np, _, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
116116
assert.Nil(t, np)
117117
assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
118118
})
119119

120120
t.Run("Valid", func(t *testing.T) {
121121
data := createArchive("package.nuspec", nuspecContent)
122122

123-
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
123+
np, _, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
124124
assert.NoError(t, err)
125125
assert.NotNil(t, np)
126126
})

routers/api/packages/nuget/nuget.go

Lines changed: 51 additions & 17 deletions
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")
@@ -421,7 +422,7 @@ func DownloadPackageFile(ctx *context.Context) {
421422
// UploadPackage creates a new package with the metadata contained in the uploaded nupgk file
422423
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#push-a-package
423424
func UploadPackage(ctx *context.Context) {
424-
np, buf, closables := processUploadedFile(ctx, nuget_module.DependencyPackage)
425+
np, buf, metaBuf, closables := processUploadedFile(ctx, nuget_module.DependencyPackage)
425426
defer func() {
426427
for _, c := range closables {
427428
c.Close()
@@ -431,15 +432,16 @@ func UploadPackage(ctx *context.Context) {
431432
return
432433
}
433434

435+
pi := &packages_service.PackageInfo{
436+
Owner: ctx.Package.Owner,
437+
PackageType: packages_model.TypeNuGet,
438+
Name: np.ID,
439+
Version: np.Version,
440+
}
434441
_, _, err := packages_service.CreatePackageAndAddFile(
435442
ctx,
436443
&packages_service.PackageCreationInfo{
437-
PackageInfo: packages_service.PackageInfo{
438-
Owner: ctx.Package.Owner,
439-
PackageType: packages_model.TypeNuGet,
440-
Name: np.ID,
441-
Version: np.Version,
442-
},
444+
PackageInfo: *pi,
443445
SemverCompatible: true,
444446
Creator: ctx.Doer,
445447
Metadata: np.Metadata,
@@ -465,13 +467,38 @@ func UploadPackage(ctx *context.Context) {
465467
return
466468
}
467469

470+
_, err = packages_service.AddFileToExistingPackage(
471+
ctx,
472+
pi,
473+
&packages_service.PackageFileCreationInfo{
474+
PackageFileInfo: packages_service.PackageFileInfo{
475+
Filename: strings.ToLower(fmt.Sprintf("%s.nuspec", np.ID)),
476+
},
477+
Creator: ctx.Doer,
478+
Data: metaBuf,
479+
IsLead: false,
480+
},
481+
)
482+
if err != nil {
483+
switch err {
484+
// TODO: Should never happen. Do we need to handle other cases?
485+
case packages_model.ErrPackageNotExist:
486+
apiError(ctx, http.StatusConflict, err)
487+
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
488+
apiError(ctx, http.StatusForbidden, err)
489+
default:
490+
apiError(ctx, http.StatusInternalServerError, err)
491+
}
492+
return
493+
}
494+
468495
ctx.Status(http.StatusCreated)
469496
}
470497

471498
// UploadSymbolPackage adds a symbol package to an existing package
472499
// https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource
473500
func UploadSymbolPackage(ctx *context.Context) {
474-
np, buf, closables := processUploadedFile(ctx, nuget_module.SymbolsPackage)
501+
np, buf, _, closables := processUploadedFile(ctx, nuget_module.SymbolsPackage)
475502
defer func() {
476503
for _, c := range closables {
477504
c.Close()
@@ -563,13 +590,13 @@ func UploadSymbolPackage(ctx *context.Context) {
563590
ctx.Status(http.StatusCreated)
564591
}
565592

566-
func processUploadedFile(ctx *context.Context, expectedType nuget_module.PackageType) (*nuget_module.Package, *packages_module.HashedBuffer, []io.Closer) {
593+
func processUploadedFile(ctx *context.Context, expectedType nuget_module.PackageType) (*nuget_module.Package, *packages_module.HashedBuffer, *packages_module.HashedBuffer, []io.Closer) {
567594
closables := make([]io.Closer, 0, 2)
568595

569596
upload, close, err := ctx.UploadStream()
570597
if err != nil {
571598
apiError(ctx, http.StatusBadRequest, err)
572-
return nil, nil, closables
599+
return nil, nil, nil, closables
573600
}
574601

575602
if close {
@@ -579,28 +606,35 @@ func processUploadedFile(ctx *context.Context, expectedType nuget_module.Package
579606
buf, err := packages_module.CreateHashedBufferFromReader(upload)
580607
if err != nil {
581608
apiError(ctx, http.StatusInternalServerError, err)
582-
return nil, nil, closables
609+
return nil, nil, nil, closables
583610
}
584611
closables = append(closables, buf)
585612

586-
np, err := nuget_module.ParsePackageMetaData(buf, buf.Size())
613+
np, metaBuf, err := nuget_module.ParsePackageMetaData(buf, buf.Size())
587614
if err != nil {
588615
if errors.Is(err, util.ErrInvalidArgument) {
589616
apiError(ctx, http.StatusBadRequest, err)
590617
} else {
591618
apiError(ctx, http.StatusInternalServerError, err)
592619
}
593-
return nil, nil, closables
620+
return nil, nil, nil, closables
594621
}
622+
closables = append(closables, metaBuf)
623+
595624
if np.PackageType != expectedType {
596625
apiError(ctx, http.StatusBadRequest, errors.New("unexpected package type"))
597-
return nil, nil, closables
626+
return nil, nil, nil, closables
598627
}
599628
if _, err := buf.Seek(0, io.SeekStart); err != nil {
600629
apiError(ctx, http.StatusInternalServerError, err)
601-
return nil, nil, closables
630+
return nil, nil, nil, closables
631+
}
632+
633+
if _, err = metaBuf.Seek(0, io.SeekStart); err != nil {
634+
apiError(ctx, http.StatusInternalServerError, err)
635+
return nil, nil, nil, closables
602636
}
603-
return np, buf, closables
637+
return np, buf, metaBuf, closables
604638
}
605639

606640
// https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request

0 commit comments

Comments
 (0)