diff --git a/modules/context/base.go b/modules/context/base.go index 8df1dde866..625fd2680c 100644 --- a/modules/context/base.go +++ b/modules/context/base.go @@ -5,6 +5,7 @@ package context import ( "context" + "encoding/xml" "fmt" "io" "net/http" @@ -136,6 +137,15 @@ func (b *Base) JSON(status int, content any) { } } +// XML render content as XML +func (b *Base) XML(status int, content any) { + b.Resp.Header().Set("Content-Type", "application/xml;charset=utf-8") + b.Resp.WriteHeader(status) + if err := xml.NewEncoder(b.Resp).Encode(content); err != nil { + log.Error("Render XML failed: %v", err) + } +} + // RemoteAddr returns the client machine ip address func (b *Base) RemoteAddr() string { return b.Req.RemoteAddr diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 3c478b1c02..e7a4d2c0ed 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -71,34 +71,47 @@ 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"` +} +type nuspecDependency struct { + ID string `xml:"id,attr"` + Version string `xml:"version,attr"` + Exclude string `xml:"exclude,attr"` +} + +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"` + ProjectURL string `xml:"projectUrl"` + Description string `xml:"description"` + ReleaseNotes string `xml:"releaseNotes"` + PackageTypes nuspecPackageTypes `xml:"packageTypes"` + Repository nuspecRepository `xml:"repository"` + Dependencies nuspecDependencies `xml:"dependencies"` +} + 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"` + Metadata nuspeceMetadata `xml:"metadata"` } // ParsePackageMetaData parses the metadata of a Nuget package file @@ -204,3 +217,36 @@ 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, + ProjectURL: pd.Metadata.ProjectURL, + Repository: nuspecRepository{URL: pd.Metadata.RepositoryURL}, + RequireLicenseAcceptance: pd.Metadata.RequireLicenseAcceptance, + Dependencies: nuspecDependencies{ + Group: make([]nuspecGroup, len(pd.Metadata.Dependencies)), + }, + } + + for tgf, deps := range pd.Metadata.Dependencies { + gDeps := make([]nuspecDependency, len(deps)) + for i, dep := range deps { + gDeps[i] = nuspecDependency{ + ID: dep.ID, + Version: dep.Version, + } + } + m.Dependencies.Group = append(m.Dependencies.Group, nuspecGroup{ + TargetFramework: tgf, + Dependency: gDeps, + }) + } + + return &nuspecPackage{ + Metadata: m, + } +} diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 769c4c1824..1ba102bfff 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -387,34 +387,59 @@ 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), + } + + ctx.XML(http.StatusOK, nuget_module.GenerateNuspec(pkg)) + } else if filename == fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion) { + + 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) + } else { + apiError(ctx, http.StatusInternalServerError, "Invalid filename") + } } // UploadPackage creates a new package with the metadata contained in the uploaded nupgk file