From 97cc95510159892288fc8cc92c6480d8f0b57799 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Tue, 23 Jan 2024 22:42:46 +0100 Subject: [PATCH] [GITEA] feat(nuget): basic manifest download Refs: https://codeberg.org/forgejo/forgejo/pulls/2222 (cherry picked from commit 5f837efc15f3d1e0d7fbed7fc569251143266584) fix: write xml header (cherry picked from commit a715984a42be9da81c48106d5eae244098ac1108) fix: optional elements and xml schema (cherry picked from commit 6ea6895a3616246e7282aa20d8f010fa931b60ea) fix: pass all other requests to file search (cherry picked from commit 9bfc74833a3b657453b4519573598432a87e3e3c) test: add integration test (cherry picked from commit b798f4ce86daa78e694c5c142e6f5f44938e6cb6) fix: use xmlResponse (cherry picked from commit 7f76df0b246c64fac0eeb115642c8cb6eb676f36) (cherry picked from commit e18d574ca40905aec52fbbe8247ba83fd01874dd) --- modules/packages/nuget/metadata.go | 159 +++++++++++++------ routers/api/packages/nuget/nuget.go | 60 ++++--- tests/integration/api_packages_nuget_test.go | 15 ++ 3 files changed, 170 insertions(+), 64 deletions(-) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 3c478b1c0..b32151cdf 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -71,34 +71,50 @@ type Dependency struct { Version string `json:"version"` } +type nuspecPackageType struct { + Name string `xml:"name,attr"` +} + +type nuspecPackageTypes struct { + PackageType []nuspecPackageType `xml:"packageType"` +} + +type nuspecRepository struct { + URL string `xml:"url,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` +} +type nuspecDependency struct { + ID string `xml:"id,attr"` + Version string `xml:"version,attr"` + Exclude string `xml:"exclude,attr,omitempty"` +} + +type nuspecGroup struct { + TargetFramework string `xml:"targetFramework,attr"` + Dependency []nuspecDependency `xml:"dependency"` +} + +type nuspecDependencies struct { + Group []nuspecGroup `xml:"group"` +} + +type nuspeceMetadata struct { + ID string `xml:"id"` + Version string `xml:"version"` + Authors string `xml:"authors"` + RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance,omitempty"` + ProjectURL string `xml:"projectUrl,omitempty"` + Description string `xml:"description"` + ReleaseNotes string `xml:"releaseNotes,omitempty"` + PackageTypes *nuspecPackageTypes `xml:"packageTypes,omitempty"` + Repository *nuspecRepository `xml:"repository,omitempty"` + Dependencies *nuspecDependencies `xml:"dependencies,omitempty"` +} + type nuspecPackage struct { - Metadata struct { - ID string `xml:"id"` - Version string `xml:"version"` - Authors string `xml:"authors"` - RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` - ProjectURL string `xml:"projectUrl"` - Description string `xml:"description"` - ReleaseNotes string `xml:"releaseNotes"` - PackageTypes struct { - PackageType []struct { - Name string `xml:"name,attr"` - } `xml:"packageType"` - } `xml:"packageTypes"` - Repository struct { - URL string `xml:"url,attr"` - } `xml:"repository"` - Dependencies struct { - Group []struct { - TargetFramework string `xml:"targetFramework,attr"` - Dependency []struct { - ID string `xml:"id,attr"` - Version string `xml:"version,attr"` - Exclude string `xml:"exclude,attr"` - } `xml:"dependency"` - } `xml:"group"` - } `xml:"dependencies"` - } `xml:"metadata"` + XMLName xml.Name `xml:"package"` + Xmlns string `xml:"xmlns,attr"` + Metadata nuspeceMetadata `xml:"metadata"` } // ParsePackageMetaData parses the metadata of a Nuget package file @@ -149,10 +165,12 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) { } packageType := DependencyPackage - for _, pt := range p.Metadata.PackageTypes.PackageType { - if pt.Name == "SymbolsPackage" { - packageType = SymbolsPackage - break + if p.Metadata.PackageTypes != nil { + for _, pt := range p.Metadata.PackageTypes.PackageType { + if pt.Name == "SymbolsPackage" { + packageType = SymbolsPackage + break + } } } @@ -161,24 +179,27 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) { ReleaseNotes: p.Metadata.ReleaseNotes, Authors: p.Metadata.Authors, ProjectURL: p.Metadata.ProjectURL, - RepositoryURL: p.Metadata.Repository.URL, RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, Dependencies: make(map[string][]Dependency), } - - for _, group := range p.Metadata.Dependencies.Group { - deps := make([]Dependency, 0, len(group.Dependency)) - for _, dep := range group.Dependency { - if dep.ID == "" || dep.Version == "" { - continue + if p.Metadata.Repository != nil { + m.RepositoryURL = p.Metadata.Repository.URL + } + if p.Metadata.Dependencies != nil { + for _, group := range p.Metadata.Dependencies.Group { + deps := make([]Dependency, 0, len(group.Dependency)) + for _, dep := range group.Dependency { + if dep.ID == "" || dep.Version == "" { + continue + } + deps = append(deps, Dependency{ + ID: dep.ID, + Version: dep.Version, + }) + } + if len(deps) > 0 { + m.Dependencies[group.TargetFramework] = deps } - deps = append(deps, Dependency{ - ID: dep.ID, - Version: dep.Version, - }) - } - if len(deps) > 0 { - m.Dependencies[group.TargetFramework] = deps } } return &Package{ @@ -204,3 +225,51 @@ func toNormalizedVersion(v *version.Version) string { } return buf.String() } + +// returning any here because we use a private type and we don't need the type for xml marshalling +func GenerateNuspec(pd *Package) any { + m := nuspeceMetadata{ + ID: pd.ID, + Version: pd.Version, + Authors: pd.Metadata.Authors, + Description: pd.Metadata.Description, + ProjectURL: pd.Metadata.ProjectURL, + RequireLicenseAcceptance: pd.Metadata.RequireLicenseAcceptance, + } + + if pd.Metadata.RepositoryURL != "" { + m.Repository = &nuspecRepository{ + URL: pd.Metadata.RepositoryURL, + } + } + + groups := len(pd.Metadata.Dependencies) + if groups > 0 { + m.Dependencies = &nuspecDependencies{ + Group: make([]nuspecGroup, 0, groups), + } + + for tgf, deps := range pd.Metadata.Dependencies { + if len(deps) == 0 { + continue + } + gDeps := make([]nuspecDependency, 0, len(deps)) + for _, dep := range deps { + gDeps = append(gDeps, nuspecDependency{ + ID: dep.ID, + Version: dep.Version, + }) + } + + m.Dependencies.Group = append(m.Dependencies.Group, nuspecGroup{ + TargetFramework: tgf, + Dependency: gDeps, + }) + } + } + + return &nuspecPackage{ + Xmlns: "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", + Metadata: m, + } +} diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 769c4c182..a63df2a1f 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -387,34 +387,56 @@ func EnumeratePackageVersionsV3(ctx *context.Context) { 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) { packageName := ctx.Params("id") packageVersion := ctx.Params("version") filename := ctx.Params("filename") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( - ctx, - &packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeNuGet, - Name: packageName, - Version: packageVersion, - }, - &packages_service.PackageFileInfo{ - Filename: filename, - }, - ) - if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if filename == fmt.Sprintf("%s.nuspec", packageName) { + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion) + if err != nil { apiError(ctx, http.StatusNotFound, err) return } - apiError(ctx, http.StatusInternalServerError, err) - return - } - helper.ServePackageFile(ctx, s, u, pf) + pd, err := packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + pkg := &nuget_module.Package{ + ID: pd.Package.Name, + Version: packageVersion, + Metadata: pd.Metadata.(*nuget_module.Metadata), + } + + xmlResponse(ctx, http.StatusOK, nuget_module.GenerateNuspec(pkg)) + } else { + s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeNuGet, + Name: packageName, + Version: packageVersion, + }, + &packages_service.PackageFileInfo{ + Filename: filename, + }, + ) + if err != nil { + if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + helper.ServePackageFile(ctx, s, u, pf) + } } // UploadPackage creates a new package with the metadata contained in the uploaded nupgk file diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index 20dafd5cc..eb6769301 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -353,6 +353,21 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) 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) + + nuspec := `` + "\n" + + `` + + `` + packageName + `` + packageVersion + `` + packageAuthors + `` + packageDescription + `` + + `` + + // https://github.com/golang/go/issues/21399 go can't generate self-closing tags + `` + + `` + + `` + + assert.Equal(t, nuspec, resp.Body.String()) + checkDownloadCount(1) req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion)).