Add support for Chocolatey/NuGet v2 API (#21393)
Fixes #21294 This PR adds support for NuGet v2 API. Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
c35531dd11
commit
0e58201d1a
8 changed files with 850 additions and 135 deletions
|
@ -14,7 +14,7 @@ menu:
|
||||||
|
|
||||||
# NuGet Packages Repository
|
# NuGet Packages Repository
|
||||||
|
|
||||||
Publish [NuGet](https://www.nuget.org/) packages for your user or organization. The package registry supports [NuGet Symbol Packages](https://docs.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg) too.
|
Publish [NuGet](https://www.nuget.org/) packages for your user or organization. The package registry supports the V2 and V3 API protocol and you can work with [NuGet Symbol Packages](https://docs.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg) too.
|
||||||
|
|
||||||
**Table of Contents**
|
**Table of Contents**
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ type Metadata struct {
|
||||||
Authors string `json:"authors,omitempty"`
|
Authors string `json:"authors,omitempty"`
|
||||||
ProjectURL string `json:"project_url,omitempty"`
|
ProjectURL string `json:"project_url,omitempty"`
|
||||||
RepositoryURL string `json:"repository_url,omitempty"`
|
RepositoryURL string `json:"repository_url,omitempty"`
|
||||||
|
RequireLicenseAcceptance bool `json:"require_license_acceptance"`
|
||||||
Dependencies map[string][]Dependency `json:"dependencies,omitempty"`
|
Dependencies map[string][]Dependency `json:"dependencies,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +161,7 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
|
||||||
Authors: p.Metadata.Authors,
|
Authors: p.Metadata.Authors,
|
||||||
ProjectURL: p.Metadata.ProjectURL,
|
ProjectURL: p.Metadata.ProjectURL,
|
||||||
RepositoryURL: p.Metadata.Repository.URL,
|
RepositoryURL: p.Metadata.Repository.URL,
|
||||||
|
RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
|
||||||
Dependencies: make(map[string][]Dependency),
|
Dependencies: make(map[string][]Dependency),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -180,15 +180,19 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||||
r.Get("/*", maven.DownloadPackageFile)
|
r.Get("/*", maven.DownloadPackageFile)
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/nuget", func() {
|
r.Group("/nuget", func() {
|
||||||
r.Get("/index.json", nuget.ServiceIndex) // Needs to be unauthenticated for the NuGet client.
|
r.Group("", func() { // Needs to be unauthenticated for the NuGet client.
|
||||||
|
r.Get("/", nuget.ServiceIndexV2)
|
||||||
|
r.Get("/index.json", nuget.ServiceIndexV3)
|
||||||
|
r.Get("/$metadata", nuget.FeedCapabilityResource)
|
||||||
|
})
|
||||||
r.Group("", func() {
|
r.Group("", func() {
|
||||||
r.Get("/query", nuget.SearchService)
|
r.Get("/query", nuget.SearchServiceV3)
|
||||||
r.Group("/registration/{id}", func() {
|
r.Group("/registration/{id}", func() {
|
||||||
r.Get("/index.json", nuget.RegistrationIndex)
|
r.Get("/index.json", nuget.RegistrationIndex)
|
||||||
r.Get("/{version}", nuget.RegistrationLeaf)
|
r.Get("/{version}", nuget.RegistrationLeafV3)
|
||||||
})
|
})
|
||||||
r.Group("/package/{id}", func() {
|
r.Group("/package/{id}", func() {
|
||||||
r.Get("/index.json", nuget.EnumeratePackageVersions)
|
r.Get("/index.json", nuget.EnumeratePackageVersionsV3)
|
||||||
r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
|
r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
|
||||||
})
|
})
|
||||||
r.Group("", func() {
|
r.Group("", func() {
|
||||||
|
@ -197,6 +201,10 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||||
r.Delete("/{id}/{version}", nuget.DeletePackage)
|
r.Delete("/{id}/{version}", nuget.DeletePackage)
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
}, reqPackageAccess(perm.AccessModeWrite))
|
||||||
r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
|
r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
|
||||||
|
r.Get("/Packages(Id='{id:[^']+}',Version='{version:[^']+}')", nuget.RegistrationLeafV2)
|
||||||
|
r.Get("/Packages()", nuget.SearchServiceV2)
|
||||||
|
r.Get("/FindPackagesById()", nuget.EnumeratePackageVersionsV2)
|
||||||
|
r.Get("/Search()", nuget.SearchServiceV2)
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
})
|
})
|
||||||
r.Group("/npm", func() {
|
r.Group("/npm", func() {
|
||||||
|
|
393
routers/api/packages/nuget/api_v2.go
Normal file
393
routers/api/packages/nuget/api_v2.go
Normal file
|
@ -0,0 +1,393 @@
|
||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package nuget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AtomTitle struct {
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceCollection struct {
|
||||||
|
Href string `xml:"href,attr"`
|
||||||
|
Title AtomTitle `xml:"atom:title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceWorkspace struct {
|
||||||
|
Title AtomTitle `xml:"atom:title"`
|
||||||
|
Collection ServiceCollection `xml:"collection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceIndexResponseV2 struct {
|
||||||
|
XMLName xml.Name `xml:"service"`
|
||||||
|
Base string `xml:"base,attr"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
XmlnsAtom string `xml:"xmlns:atom,attr"`
|
||||||
|
Workspace ServiceWorkspace `xml:"workspace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxPropertyRef struct {
|
||||||
|
Name string `xml:"Name,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxProperty struct {
|
||||||
|
Name string `xml:"Name,attr"`
|
||||||
|
Type string `xml:"Type,attr"`
|
||||||
|
Nullable bool `xml:"Nullable,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxEntityType struct {
|
||||||
|
Name string `xml:"Name,attr"`
|
||||||
|
HasStream bool `xml:"m:HasStream,attr"`
|
||||||
|
Keys []EdmxPropertyRef `xml:"Key>PropertyRef"`
|
||||||
|
Properties []EdmxProperty `xml:"Property"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxFunctionParameter struct {
|
||||||
|
Name string `xml:"Name,attr"`
|
||||||
|
Type string `xml:"Type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxFunctionImport struct {
|
||||||
|
Name string `xml:"Name,attr"`
|
||||||
|
ReturnType string `xml:"ReturnType,attr"`
|
||||||
|
EntitySet string `xml:"EntitySet,attr"`
|
||||||
|
Parameter []EdmxFunctionParameter `xml:"Parameter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxEntitySet struct {
|
||||||
|
Name string `xml:"Name,attr"`
|
||||||
|
EntityType string `xml:"EntityType,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxEntityContainer struct {
|
||||||
|
Name string `xml:"Name,attr"`
|
||||||
|
IsDefaultEntityContainer bool `xml:"m:IsDefaultEntityContainer,attr"`
|
||||||
|
EntitySet EdmxEntitySet `xml:"EntitySet"`
|
||||||
|
FunctionImports []EdmxFunctionImport `xml:"FunctionImport"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxSchema struct {
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
Namespace string `xml:"Namespace,attr"`
|
||||||
|
EntityType *EdmxEntityType `xml:"EntityType,omitempty"`
|
||||||
|
EntityContainer *EdmxEntityContainer `xml:"EntityContainer,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxDataServices struct {
|
||||||
|
XmlnsM string `xml:"xmlns:m,attr"`
|
||||||
|
DataServiceVersion string `xml:"m:DataServiceVersion,attr"`
|
||||||
|
MaxDataServiceVersion string `xml:"m:MaxDataServiceVersion,attr"`
|
||||||
|
Schema []EdmxSchema `xml:"Schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdmxMetadata struct {
|
||||||
|
XMLName xml.Name `xml:"edmx:Edmx"`
|
||||||
|
XmlnsEdmx string `xml:"xmlns:edmx,attr"`
|
||||||
|
Version string `xml:"Version,attr"`
|
||||||
|
DataServices EdmxDataServices `xml:"edmx:DataServices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Metadata = &EdmxMetadata{
|
||||||
|
XmlnsEdmx: "http://schemas.microsoft.com/ado/2007/06/edmx",
|
||||||
|
Version: "1.0",
|
||||||
|
DataServices: EdmxDataServices{
|
||||||
|
XmlnsM: "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
|
||||||
|
DataServiceVersion: "2.0",
|
||||||
|
MaxDataServiceVersion: "2.0",
|
||||||
|
Schema: []EdmxSchema{
|
||||||
|
{
|
||||||
|
Xmlns: "http://schemas.microsoft.com/ado/2006/04/edm",
|
||||||
|
Namespace: "NuGetGallery.OData",
|
||||||
|
EntityType: &EdmxEntityType{
|
||||||
|
Name: "V2FeedPackage",
|
||||||
|
HasStream: true,
|
||||||
|
Keys: []EdmxPropertyRef{
|
||||||
|
{Name: "Id"},
|
||||||
|
{Name: "Version"},
|
||||||
|
},
|
||||||
|
Properties: []EdmxProperty{
|
||||||
|
{
|
||||||
|
Name: "Id",
|
||||||
|
Type: "Edm.String",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Version",
|
||||||
|
Type: "Edm.String",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "NormalizedVersion",
|
||||||
|
Type: "Edm.String",
|
||||||
|
Nullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Authors",
|
||||||
|
Type: "Edm.String",
|
||||||
|
Nullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Created",
|
||||||
|
Type: "Edm.DateTime",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Dependencies",
|
||||||
|
Type: "Edm.String",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Description",
|
||||||
|
Type: "Edm.String",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "DownloadCount",
|
||||||
|
Type: "Edm.Int64",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "LastUpdated",
|
||||||
|
Type: "Edm.DateTime",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Published",
|
||||||
|
Type: "Edm.DateTime",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "PackageSize",
|
||||||
|
Type: "Edm.Int64",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ProjectUrl",
|
||||||
|
Type: "Edm.String",
|
||||||
|
Nullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ReleaseNotes",
|
||||||
|
Type: "Edm.String",
|
||||||
|
Nullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "RequireLicenseAcceptance",
|
||||||
|
Type: "Edm.Boolean",
|
||||||
|
Nullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Title",
|
||||||
|
Type: "Edm.String",
|
||||||
|
Nullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "VersionDownloadCount",
|
||||||
|
Type: "Edm.Int64",
|
||||||
|
Nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Xmlns: "http://schemas.microsoft.com/ado/2006/04/edm",
|
||||||
|
Namespace: "NuGetGallery",
|
||||||
|
EntityContainer: &EdmxEntityContainer{
|
||||||
|
Name: "V2FeedContext",
|
||||||
|
IsDefaultEntityContainer: true,
|
||||||
|
EntitySet: EdmxEntitySet{
|
||||||
|
Name: "Packages",
|
||||||
|
EntityType: "NuGetGallery.OData.V2FeedPackage",
|
||||||
|
},
|
||||||
|
FunctionImports: []EdmxFunctionImport{
|
||||||
|
{
|
||||||
|
Name: "Search",
|
||||||
|
ReturnType: "Collection(NuGetGallery.OData.V2FeedPackage)",
|
||||||
|
EntitySet: "Packages",
|
||||||
|
Parameter: []EdmxFunctionParameter{
|
||||||
|
{
|
||||||
|
Name: "searchTerm",
|
||||||
|
Type: "Edm.String",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "FindPackagesById",
|
||||||
|
ReturnType: "Collection(NuGetGallery.OData.V2FeedPackage)",
|
||||||
|
EntitySet: "Packages",
|
||||||
|
Parameter: []EdmxFunctionParameter{
|
||||||
|
{
|
||||||
|
Name: "id",
|
||||||
|
Type: "Edm.String",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedEntryCategory struct {
|
||||||
|
Term string `xml:"term,attr"`
|
||||||
|
Scheme string `xml:"scheme,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedEntryLink struct {
|
||||||
|
Rel string `xml:"rel,attr"`
|
||||||
|
Href string `xml:"href,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypedValue[T any] struct {
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
Value T `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedEntryProperties struct {
|
||||||
|
Version string `xml:"d:Version"`
|
||||||
|
NormalizedVersion string `xml:"d:NormalizedVersion"`
|
||||||
|
Authors string `xml:"d:Authors"`
|
||||||
|
Dependencies string `xml:"d:Dependencies"`
|
||||||
|
Description string `xml:"d:Description"`
|
||||||
|
VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"`
|
||||||
|
DownloadCount TypedValue[int64] `xml:"d:DownloadCount"`
|
||||||
|
PackageSize TypedValue[int64] `xml:"d:PackageSize"`
|
||||||
|
Created TypedValue[time.Time] `xml:"d:Created"`
|
||||||
|
LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"`
|
||||||
|
Published TypedValue[time.Time] `xml:"d:Published"`
|
||||||
|
ProjectURL string `xml:"d:ProjectUrl,omitempty"`
|
||||||
|
ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"`
|
||||||
|
RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"`
|
||||||
|
Title string `xml:"d:Title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedEntry struct {
|
||||||
|
XMLName xml.Name `xml:"entry"`
|
||||||
|
Xmlns string `xml:"xmlns,attr,omitempty"`
|
||||||
|
XmlnsD string `xml:"xmlns:d,attr,omitempty"`
|
||||||
|
XmlnsM string `xml:"xmlns:m,attr,omitempty"`
|
||||||
|
Base string `xml:"xml:base,attr,omitempty"`
|
||||||
|
ID string `xml:"id"`
|
||||||
|
Category FeedEntryCategory `xml:"category"`
|
||||||
|
Links []FeedEntryLink `xml:"link"`
|
||||||
|
Title TypedValue[string] `xml:"title"`
|
||||||
|
Updated time.Time `xml:"updated"`
|
||||||
|
Author string `xml:"author>name"`
|
||||||
|
Summary string `xml:"summary"`
|
||||||
|
Properties *FeedEntryProperties `xml:"m:properties"`
|
||||||
|
Content string `xml:",innerxml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedResponse struct {
|
||||||
|
XMLName xml.Name `xml:"feed"`
|
||||||
|
Xmlns string `xml:"xmlns,attr,omitempty"`
|
||||||
|
XmlnsD string `xml:"xmlns:d,attr,omitempty"`
|
||||||
|
XmlnsM string `xml:"xmlns:m,attr,omitempty"`
|
||||||
|
Base string `xml:"xml:base,attr,omitempty"`
|
||||||
|
ID string `xml:"id"`
|
||||||
|
Title TypedValue[string] `xml:"title"`
|
||||||
|
Updated time.Time `xml:"updated"`
|
||||||
|
Link FeedEntryLink `xml:"link"`
|
||||||
|
Entries []*FeedEntry `xml:"entry"`
|
||||||
|
Count int64 `xml:"m:count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFeedResponse(l *linkBuilder, totalEntries int64, pds []*packages_model.PackageDescriptor) *FeedResponse {
|
||||||
|
entries := make([]*FeedEntry, 0, len(pds))
|
||||||
|
for _, pd := range pds {
|
||||||
|
entries = append(entries, createEntry(l, pd, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FeedResponse{
|
||||||
|
Xmlns: "http://www.w3.org/2005/Atom",
|
||||||
|
Base: l.Base,
|
||||||
|
XmlnsD: "http://schemas.microsoft.com/ado/2007/08/dataservices",
|
||||||
|
XmlnsM: "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
|
||||||
|
ID: "http://schemas.datacontract.org/2004/07/",
|
||||||
|
Updated: time.Now(),
|
||||||
|
Link: FeedEntryLink{Rel: "self", Href: l.Base},
|
||||||
|
Count: totalEntries,
|
||||||
|
Entries: entries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEntryResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *FeedEntry {
|
||||||
|
return createEntry(l, pd, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNamespace bool) *FeedEntry {
|
||||||
|
metadata := pd.Metadata.(*nuget_module.Metadata)
|
||||||
|
|
||||||
|
id := l.GetPackageMetadataURL(pd.Package.Name, pd.Version.Version)
|
||||||
|
|
||||||
|
// Workaround to force a self-closing tag to satisfy XmlReader.IsEmptyElement used by the NuGet client.
|
||||||
|
// https://learn.microsoft.com/en-us/dotnet/api/system.xml.xmlreader.isemptyelement
|
||||||
|
content := `<content type="application/zip" src="` + l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version) + `"/>`
|
||||||
|
|
||||||
|
createdValue := TypedValue[time.Time]{
|
||||||
|
Type: "Edm.DateTime",
|
||||||
|
Value: pd.Version.CreatedUnix.AsLocalTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &FeedEntry{
|
||||||
|
ID: id,
|
||||||
|
Category: FeedEntryCategory{Term: "NuGetGallery.OData.V2FeedPackage", Scheme: "http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"},
|
||||||
|
Links: []FeedEntryLink{
|
||||||
|
{Rel: "self", Href: id},
|
||||||
|
{Rel: "edit", Href: id},
|
||||||
|
},
|
||||||
|
Title: TypedValue[string]{Type: "text", Value: pd.Package.Name},
|
||||||
|
Updated: pd.Version.CreatedUnix.AsLocalTime(),
|
||||||
|
Author: metadata.Authors,
|
||||||
|
Content: content,
|
||||||
|
Properties: &FeedEntryProperties{
|
||||||
|
Version: pd.Version.Version,
|
||||||
|
NormalizedVersion: normalizeVersion(pd.SemVer),
|
||||||
|
Authors: metadata.Authors,
|
||||||
|
Dependencies: buildDependencyString(metadata),
|
||||||
|
Description: metadata.Description,
|
||||||
|
VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
|
||||||
|
DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
|
||||||
|
PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()},
|
||||||
|
Created: createdValue,
|
||||||
|
LastUpdated: createdValue,
|
||||||
|
Published: createdValue,
|
||||||
|
ProjectURL: metadata.ProjectURL,
|
||||||
|
ReleaseNotes: metadata.ReleaseNotes,
|
||||||
|
RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance},
|
||||||
|
Title: pd.Package.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if withNamespace {
|
||||||
|
entry.Xmlns = "http://www.w3.org/2005/Atom"
|
||||||
|
entry.Base = l.Base
|
||||||
|
entry.XmlnsD = "http://schemas.microsoft.com/ado/2007/08/dataservices"
|
||||||
|
entry.XmlnsM = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDependencyString(metadata *nuget_module.Metadata) string {
|
||||||
|
var b strings.Builder
|
||||||
|
first := true
|
||||||
|
for group, deps := range metadata.Dependencies {
|
||||||
|
for _, dep := range deps {
|
||||||
|
if !first {
|
||||||
|
b.WriteByte('|')
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
b.WriteString(dep.ID)
|
||||||
|
b.WriteByte(':')
|
||||||
|
b.WriteString(dep.Version)
|
||||||
|
b.WriteByte(':')
|
||||||
|
b.WriteString(group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
|
@ -16,36 +16,19 @@ import (
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceIndexResponse https://docs.microsoft.com/en-us/nuget/api/service-index#resources
|
// https://docs.microsoft.com/en-us/nuget/api/service-index#resources
|
||||||
type ServiceIndexResponse struct {
|
type ServiceIndexResponseV3 struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Resources []ServiceResource `json:"resources"`
|
Resources []ServiceResource `json:"resources"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceResource https://docs.microsoft.com/en-us/nuget/api/service-index#resource
|
// https://docs.microsoft.com/en-us/nuget/api/service-index#resource
|
||||||
type ServiceResource struct {
|
type ServiceResource struct {
|
||||||
ID string `json:"@id"`
|
ID string `json:"@id"`
|
||||||
Type string `json:"@type"`
|
Type string `json:"@type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func createServiceIndexResponse(root string) *ServiceIndexResponse {
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response
|
||||||
return &ServiceIndexResponse{
|
|
||||||
Version: "3.0.0",
|
|
||||||
Resources: []ServiceResource{
|
|
||||||
{ID: root + "/query", Type: "SearchQueryService"},
|
|
||||||
{ID: root + "/query", Type: "SearchQueryService/3.0.0-beta"},
|
|
||||||
{ID: root + "/query", Type: "SearchQueryService/3.0.0-rc"},
|
|
||||||
{ID: root + "/registration", Type: "RegistrationsBaseUrl"},
|
|
||||||
{ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"},
|
|
||||||
{ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"},
|
|
||||||
{ID: root + "/package", Type: "PackageBaseAddress/3.0.0"},
|
|
||||||
{ID: root, Type: "PackagePublish/2.0.0"},
|
|
||||||
{ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegistrationIndexResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response
|
|
||||||
type RegistrationIndexResponse struct {
|
type RegistrationIndexResponse struct {
|
||||||
RegistrationIndexURL string `json:"@id"`
|
RegistrationIndexURL string `json:"@id"`
|
||||||
Type []string `json:"@type"`
|
Type []string `json:"@type"`
|
||||||
|
@ -53,7 +36,7 @@ type RegistrationIndexResponse struct {
|
||||||
Pages []*RegistrationIndexPage `json:"items"`
|
Pages []*RegistrationIndexPage `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistrationIndexPage https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page-object
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page-object
|
||||||
type RegistrationIndexPage struct {
|
type RegistrationIndexPage struct {
|
||||||
RegistrationPageURL string `json:"@id"`
|
RegistrationPageURL string `json:"@id"`
|
||||||
Lower string `json:"lower"`
|
Lower string `json:"lower"`
|
||||||
|
@ -62,14 +45,14 @@ type RegistrationIndexPage struct {
|
||||||
Items []*RegistrationIndexPageItem `json:"items"`
|
Items []*RegistrationIndexPageItem `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistrationIndexPageItem https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf-object-in-a-page
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf-object-in-a-page
|
||||||
type RegistrationIndexPageItem struct {
|
type RegistrationIndexPageItem struct {
|
||||||
RegistrationLeafURL string `json:"@id"`
|
RegistrationLeafURL string `json:"@id"`
|
||||||
PackageContentURL string `json:"packageContent"`
|
PackageContentURL string `json:"packageContent"`
|
||||||
CatalogEntry *CatalogEntry `json:"catalogEntry"`
|
CatalogEntry *CatalogEntry `json:"catalogEntry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CatalogEntry https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry
|
||||||
type CatalogEntry struct {
|
type CatalogEntry struct {
|
||||||
CatalogLeafURL string `json:"@id"`
|
CatalogLeafURL string `json:"@id"`
|
||||||
PackageContentURL string `json:"packageContent"`
|
PackageContentURL string `json:"packageContent"`
|
||||||
|
@ -83,13 +66,13 @@ type CatalogEntry struct {
|
||||||
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
|
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageDependencyGroup https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group
|
||||||
type PackageDependencyGroup struct {
|
type PackageDependencyGroup struct {
|
||||||
TargetFramework string `json:"targetFramework"`
|
TargetFramework string `json:"targetFramework"`
|
||||||
Dependencies []*PackageDependency `json:"dependencies"`
|
Dependencies []*PackageDependency `json:"dependencies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageDependency https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency
|
||||||
type PackageDependency struct {
|
type PackageDependency struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Range string `json:"range"`
|
Range string `json:"range"`
|
||||||
|
@ -162,7 +145,7 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe
|
||||||
return dependencyGroups
|
return dependencyGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistrationLeafResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
|
||||||
type RegistrationLeafResponse struct {
|
type RegistrationLeafResponse struct {
|
||||||
RegistrationLeafURL string `json:"@id"`
|
RegistrationLeafURL string `json:"@id"`
|
||||||
Type []string `json:"@type"`
|
Type []string `json:"@type"`
|
||||||
|
@ -183,7 +166,7 @@ func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageVersionsResponse https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response
|
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response
|
||||||
type PackageVersionsResponse struct {
|
type PackageVersionsResponse struct {
|
||||||
Versions []string `json:"versions"`
|
Versions []string `json:"versions"`
|
||||||
}
|
}
|
||||||
|
@ -199,13 +182,13 @@ func createPackageVersionsResponse(pds []*packages_model.PackageDescriptor) *Pac
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchResultResponse https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response
|
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response
|
||||||
type SearchResultResponse struct {
|
type SearchResultResponse struct {
|
||||||
TotalHits int64 `json:"totalHits"`
|
TotalHits int64 `json:"totalHits"`
|
||||||
Data []*SearchResult `json:"data"`
|
Data []*SearchResult `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchResult https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
|
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
|
||||||
type SearchResult struct {
|
type SearchResult struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
@ -216,7 +199,7 @@ type SearchResult struct {
|
||||||
RegistrationIndexURL string `json:"registration"`
|
RegistrationIndexURL string `json:"registration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchResultVersion https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
|
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
|
||||||
type SearchResultVersion struct {
|
type SearchResultVersion struct {
|
||||||
RegistrationLeafURL string `json:"@id"`
|
RegistrationLeafURL string `json:"@id"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
|
@ -26,3 +26,8 @@ func (l *linkBuilder) GetRegistrationLeafURL(id, version string) string {
|
||||||
func (l *linkBuilder) GetPackageDownloadURL(id, version string) string {
|
func (l *linkBuilder) GetPackageDownloadURL(id, version string) string {
|
||||||
return fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", l.Base, id, version, id, version)
|
return fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", l.Base, id, version, id, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPackageMetadataURL builds the package metadata url
|
||||||
|
func (l *linkBuilder) GetPackageMetadataURL(id, version string) string {
|
||||||
|
return fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", l.Base, id, version)
|
||||||
|
}
|
||||||
|
|
|
@ -5,15 +5,18 @@
|
||||||
package nuget
|
package nuget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
packages_model "code.gitea.io/gitea/models/packages"
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
packages_module "code.gitea.io/gitea/modules/packages"
|
packages_module "code.gitea.io/gitea/modules/packages"
|
||||||
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -30,15 +33,121 @@ func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceIndex https://docs.microsoft.com/en-us/nuget/api/service-index
|
func xmlResponse(ctx *context.Context, status int, obj interface{}) {
|
||||||
func ServiceIndex(ctx *context.Context) {
|
ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
|
||||||
resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget")
|
ctx.Resp.WriteHeader(status)
|
||||||
|
if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil {
|
||||||
ctx.JSON(http.StatusOK, resp)
|
log.Error("Write failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := xml.NewEncoder(ctx.Resp).Encode(obj); err != nil {
|
||||||
|
log.Error("XML encode failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
|
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||||
func SearchService(ctx *context.Context) {
|
func ServiceIndexV2(ctx *context.Context) {
|
||||||
|
base := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"
|
||||||
|
|
||||||
|
xmlResponse(ctx, http.StatusOK, &ServiceIndexResponseV2{
|
||||||
|
Base: base,
|
||||||
|
Xmlns: "http://www.w3.org/2007/app",
|
||||||
|
XmlnsAtom: "http://www.w3.org/2005/Atom",
|
||||||
|
Workspace: ServiceWorkspace{
|
||||||
|
Title: AtomTitle{
|
||||||
|
Type: "text",
|
||||||
|
Text: "Default",
|
||||||
|
},
|
||||||
|
Collection: ServiceCollection{
|
||||||
|
Href: "Packages",
|
||||||
|
Title: AtomTitle{
|
||||||
|
Type: "text",
|
||||||
|
Text: "Packages",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/nuget/api/service-index
|
||||||
|
func ServiceIndexV3(ctx *context.Context) {
|
||||||
|
root := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, &ServiceIndexResponseV3{
|
||||||
|
Version: "3.0.0",
|
||||||
|
Resources: []ServiceResource{
|
||||||
|
{ID: root + "/query", Type: "SearchQueryService"},
|
||||||
|
{ID: root + "/query", Type: "SearchQueryService/3.0.0-beta"},
|
||||||
|
{ID: root + "/query", Type: "SearchQueryService/3.0.0-rc"},
|
||||||
|
{ID: root + "/registration", Type: "RegistrationsBaseUrl"},
|
||||||
|
{ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"},
|
||||||
|
{ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"},
|
||||||
|
{ID: root + "/package", Type: "PackageBaseAddress/3.0.0"},
|
||||||
|
{ID: root, Type: "PackagePublish/2.0.0"},
|
||||||
|
{ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/LegacyFeedCapabilityResourceV2Feed.cs
|
||||||
|
func FeedCapabilityResource(ctx *context.Context) {
|
||||||
|
xmlResponse(ctx, http.StatusOK, Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchTermExtract = regexp.MustCompile(`'([^']+)'`)
|
||||||
|
|
||||||
|
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||||
|
func SearchServiceV2(ctx *context.Context) {
|
||||||
|
searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
|
||||||
|
if searchTerm == "" {
|
||||||
|
// $filter contains a query like:
|
||||||
|
// (((Id ne null) and substringof('microsoft',tolower(Id)))
|
||||||
|
// We don't support these queries, just extract the search term.
|
||||||
|
match := searchTermExtract.FindStringSubmatch(ctx.FormTrim("$filter"))
|
||||||
|
if len(match) == 2 {
|
||||||
|
searchTerm = strings.TrimSpace(match[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skip, take := ctx.FormInt("skip"), ctx.FormInt("take")
|
||||||
|
if skip == 0 {
|
||||||
|
skip = ctx.FormInt("$skip")
|
||||||
|
}
|
||||||
|
if take == 0 {
|
||||||
|
take = ctx.FormInt("$top")
|
||||||
|
}
|
||||||
|
|
||||||
|
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
|
Type: packages_model.TypeNuGet,
|
||||||
|
Name: packages_model.SearchValue{Value: searchTerm},
|
||||||
|
IsInternal: util.OptionalBoolFalse,
|
||||||
|
Paginator: db.NewAbsoluteListOptions(
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := createFeedResponse(
|
||||||
|
&linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||||
|
total,
|
||||||
|
pds,
|
||||||
|
)
|
||||||
|
|
||||||
|
xmlResponse(ctx, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
|
||||||
|
func SearchServiceV3(ctx *context.Context) {
|
||||||
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||||
OwnerID: ctx.Package.Owner.ID,
|
OwnerID: ctx.Package.Owner.ID,
|
||||||
Type: packages_model.TypeNuGet,
|
Type: packages_model.TypeNuGet,
|
||||||
|
@ -69,7 +178,7 @@ func SearchService(ctx *context.Context) {
|
||||||
ctx.JSON(http.StatusOK, resp)
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistrationIndex https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index
|
||||||
func RegistrationIndex(ctx *context.Context) {
|
func RegistrationIndex(ctx *context.Context) {
|
||||||
packageName := ctx.Params("id")
|
packageName := ctx.Params("id")
|
||||||
|
|
||||||
|
@ -97,8 +206,37 @@ func RegistrationIndex(ctx *context.Context) {
|
||||||
ctx.JSON(http.StatusOK, resp)
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistrationLeaf https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
|
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||||
func RegistrationLeaf(ctx *context.Context) {
|
func RegistrationLeafV2(ctx *context.Context) {
|
||||||
|
packageName := ctx.Params("id")
|
||||||
|
packageVersion := ctx.Params("version")
|
||||||
|
|
||||||
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
|
||||||
|
if err != nil {
|
||||||
|
if err == packages_model.ErrPackageNotExist {
|
||||||
|
apiError(ctx, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := createEntryResponse(
|
||||||
|
&linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||||
|
pd,
|
||||||
|
)
|
||||||
|
|
||||||
|
xmlResponse(ctx, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
|
||||||
|
func RegistrationLeafV3(ctx *context.Context) {
|
||||||
packageName := ctx.Params("id")
|
packageName := ctx.Params("id")
|
||||||
packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json")
|
packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json")
|
||||||
|
|
||||||
|
@ -126,8 +264,33 @@ func RegistrationLeaf(ctx *context.Context) {
|
||||||
ctx.JSON(http.StatusOK, resp)
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnumeratePackageVersions https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions
|
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||||
func EnumeratePackageVersions(ctx *context.Context) {
|
func EnumeratePackageVersionsV2(ctx *context.Context) {
|
||||||
|
packageName := strings.Trim(ctx.FormTrim("id"), "'")
|
||||||
|
|
||||||
|
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||||
|
if err != nil {
|
||||||
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := createFeedResponse(
|
||||||
|
&linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||||
|
int64(len(pds)),
|
||||||
|
pds,
|
||||||
|
)
|
||||||
|
|
||||||
|
xmlResponse(ctx, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions
|
||||||
|
func EnumeratePackageVersionsV3(ctx *context.Context) {
|
||||||
packageName := ctx.Params("id")
|
packageName := ctx.Params("id")
|
||||||
|
|
||||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
|
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
|
||||||
|
@ -151,7 +314,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
|
||||||
ctx.JSON(http.StatusOK, resp)
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadPackageFile https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
|
// https://docs.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")
|
||||||
|
@ -350,7 +513,7 @@ func processUploadedFile(ctx *context.Context, expectedType nuget_module.Package
|
||||||
return np, buf, closables
|
return np, buf, closables
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadSymbolFile https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request
|
// https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request
|
||||||
func DownloadSymbolFile(ctx *context.Context) {
|
func DownloadSymbolFile(ctx *context.Context) {
|
||||||
filename := ctx.Params("filename")
|
filename := ctx.Params("filename")
|
||||||
guid := ctx.Params("guid")[:32]
|
guid := ctx.Params("guid")[:32]
|
||||||
|
|
|
@ -8,10 +8,13 @@ import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/packages"
|
"code.gitea.io/gitea/models/packages"
|
||||||
|
@ -31,9 +34,45 @@ func addNuGetAPIKeyHeader(request *http.Request, token string) *http.Request {
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeXML(t testing.TB, resp *httptest.ResponseRecorder, v interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
assert.NoError(t, xml.NewDecoder(resp.Body).Decode(v))
|
||||||
|
}
|
||||||
|
|
||||||
func TestPackageNuGet(t *testing.T) {
|
func TestPackageNuGet(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
type FeedEntryProperties struct {
|
||||||
|
Version string `xml:"Version"`
|
||||||
|
NormalizedVersion string `xml:"NormalizedVersion"`
|
||||||
|
Authors string `xml:"Authors"`
|
||||||
|
Dependencies string `xml:"Dependencies"`
|
||||||
|
Description string `xml:"Description"`
|
||||||
|
VersionDownloadCount nuget.TypedValue[int64] `xml:"VersionDownloadCount"`
|
||||||
|
DownloadCount nuget.TypedValue[int64] `xml:"DownloadCount"`
|
||||||
|
PackageSize nuget.TypedValue[int64] `xml:"PackageSize"`
|
||||||
|
Created nuget.TypedValue[time.Time] `xml:"Created"`
|
||||||
|
LastUpdated nuget.TypedValue[time.Time] `xml:"LastUpdated"`
|
||||||
|
Published nuget.TypedValue[time.Time] `xml:"Published"`
|
||||||
|
ProjectURL string `xml:"ProjectUrl,omitempty"`
|
||||||
|
ReleaseNotes string `xml:"ReleaseNotes,omitempty"`
|
||||||
|
RequireLicenseAcceptance nuget.TypedValue[bool] `xml:"RequireLicenseAcceptance"`
|
||||||
|
Title string `xml:"Title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedEntry struct {
|
||||||
|
XMLName xml.Name `xml:"entry"`
|
||||||
|
Properties *FeedEntryProperties `xml:"properties"`
|
||||||
|
Content string `xml:",innerxml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedResponse struct {
|
||||||
|
XMLName xml.Name `xml:"feed"`
|
||||||
|
Entries []*FeedEntry `xml:"entry"`
|
||||||
|
Count int64 `xml:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
token := getUserToken(t, user.Name)
|
token := getUserToken(t, user.Name)
|
||||||
|
|
||||||
|
@ -54,9 +93,11 @@ func TestPackageNuGet(t *testing.T) {
|
||||||
<version>` + packageVersion + `</version>
|
<version>` + packageVersion + `</version>
|
||||||
<authors>` + packageAuthors + `</authors>
|
<authors>` + packageAuthors + `</authors>
|
||||||
<description>` + packageDescription + `</description>
|
<description>` + packageDescription + `</description>
|
||||||
|
<dependencies>
|
||||||
<group targetFramework=".NETStandard2.0">
|
<group targetFramework=".NETStandard2.0">
|
||||||
<dependency id="Microsoft.CSharp" version="4.5.0" />
|
<dependency id="Microsoft.CSharp" version="4.5.0" />
|
||||||
</group>
|
</group>
|
||||||
|
</dependencies>
|
||||||
</metadata>
|
</metadata>
|
||||||
</package>`))
|
</package>`))
|
||||||
archive.Close()
|
archive.Close()
|
||||||
|
@ -67,6 +108,46 @@ func TestPackageNuGet(t *testing.T) {
|
||||||
t.Run("ServiceIndex", func(t *testing.T) {
|
t.Run("ServiceIndex", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
t.Run("v2", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
Owner string
|
||||||
|
UseBasicAuth bool
|
||||||
|
UseTokenAuth bool
|
||||||
|
}{
|
||||||
|
{privateUser.Name, false, false},
|
||||||
|
{privateUser.Name, true, false},
|
||||||
|
{privateUser.Name, false, true},
|
||||||
|
{user.Name, false, false},
|
||||||
|
{user.Name, true, false},
|
||||||
|
{user.Name, false, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", url)
|
||||||
|
if c.UseBasicAuth {
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
} else if c.UseTokenAuth {
|
||||||
|
req = addNuGetAPIKeyHeader(req, token)
|
||||||
|
}
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var result nuget.ServiceIndexResponseV2
|
||||||
|
decodeXML(t, resp, &result)
|
||||||
|
|
||||||
|
assert.Equal(t, setting.AppURL+url[1:], result.Base)
|
||||||
|
assert.Equal(t, "Packages", result.Workspace.Collection.Href)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v3", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
|
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
@ -93,7 +174,7 @@ func TestPackageNuGet(t *testing.T) {
|
||||||
}
|
}
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
var result nuget.ServiceIndexResponse
|
var result nuget.ServiceIndexResponseV3
|
||||||
DecodeJSON(t, resp, &result)
|
DecodeJSON(t, resp, &result)
|
||||||
|
|
||||||
assert.Equal(t, "3.0.0", result.Version)
|
assert.Equal(t, "3.0.0", result.Version)
|
||||||
|
@ -122,6 +203,7 @@ func TestPackageNuGet(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Upload", func(t *testing.T) {
|
t.Run("Upload", func(t *testing.T) {
|
||||||
t.Run("DependencyPackage", func(t *testing.T) {
|
t.Run("DependencyPackage", func(t *testing.T) {
|
||||||
|
@ -305,6 +387,45 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
{"test", 1, 10, 1, 0},
|
{"test", 1, 10, 1, 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Run("v2", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
t.Run("Search()", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
for i, c := range cases {
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?searchTerm='%s'&skip=%d&take=%d", url, c.Query, c.Skip, c.Take))
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var result FeedResponse
|
||||||
|
decodeXML(t, resp, &result)
|
||||||
|
|
||||||
|
assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
|
||||||
|
assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Packages()", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
for i, c := range cases {
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take))
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var result FeedResponse
|
||||||
|
decodeXML(t, resp, &result)
|
||||||
|
|
||||||
|
assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
|
||||||
|
assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v3", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
for i, c := range cases {
|
for i, c := range cases {
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take))
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take))
|
||||||
req = AddBasicAuthHeader(req, user.Name)
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
@ -317,6 +438,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i)
|
assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("RegistrationService", func(t *testing.T) {
|
t.Run("RegistrationService", func(t *testing.T) {
|
||||||
indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName)
|
indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName)
|
||||||
|
@ -352,6 +474,26 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
t.Run("RegistrationLeaf", func(t *testing.T) {
|
t.Run("RegistrationLeaf", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
t.Run("v2", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", url, packageName, packageVersion))
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var result FeedEntry
|
||||||
|
decodeXML(t, resp, &result)
|
||||||
|
|
||||||
|
assert.Equal(t, packageName, result.Properties.Title)
|
||||||
|
assert.Equal(t, packageVersion, result.Properties.Version)
|
||||||
|
assert.Equal(t, packageAuthors, result.Properties.Authors)
|
||||||
|
assert.Equal(t, packageDescription, result.Properties.Description)
|
||||||
|
assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v3", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion))
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion))
|
||||||
req = AddBasicAuthHeader(req, user.Name)
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
@ -364,10 +506,28 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
assert.Equal(t, indexURL, result.RegistrationIndexURL)
|
assert.Equal(t, indexURL, result.RegistrationIndexURL)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("PackageService", func(t *testing.T) {
|
t.Run("PackageService", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
t.Run("v2", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/FindPackagesById()?id='%s'", url, packageName))
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var result FeedResponse
|
||||||
|
decodeXML(t, resp, &result)
|
||||||
|
|
||||||
|
assert.Len(t, result.Entries, 1)
|
||||||
|
assert.Equal(t, packageVersion, result.Entries[0].Properties.Version)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("v3", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName))
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName))
|
||||||
req = AddBasicAuthHeader(req, user.Name)
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
@ -378,6 +538,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||||
assert.Len(t, result.Versions, 1)
|
assert.Len(t, result.Versions, 1)
|
||||||
assert.Equal(t, packageVersion, result.Versions[0])
|
assert.Equal(t, packageVersion, result.Versions[0])
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Delete", func(t *testing.T) {
|
t.Run("Delete", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
Loading…
Add table
Reference in a new issue