forked from gitea/gitea
		
	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
					
				| @ -14,7 +14,7 @@ menu: | ||||
| 
 | ||||
| # 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** | ||||
| 
 | ||||
|  | ||||
| @ -55,12 +55,13 @@ type Package struct { | ||||
| 
 | ||||
| // Metadata represents the metadata of a Nuget package | ||||
| type Metadata struct { | ||||
| 	Description   string                  `json:"description,omitempty"` | ||||
| 	ReleaseNotes  string                  `json:"release_notes,omitempty"` | ||||
| 	Authors       string                  `json:"authors,omitempty"` | ||||
| 	ProjectURL    string                  `json:"project_url,omitempty"` | ||||
| 	RepositoryURL string                  `json:"repository_url,omitempty"` | ||||
| 	Dependencies  map[string][]Dependency `json:"dependencies,omitempty"` | ||||
| 	Description              string                  `json:"description,omitempty"` | ||||
| 	ReleaseNotes             string                  `json:"release_notes,omitempty"` | ||||
| 	Authors                  string                  `json:"authors,omitempty"` | ||||
| 	ProjectURL               string                  `json:"project_url,omitempty"` | ||||
| 	RepositoryURL            string                  `json:"repository_url,omitempty"` | ||||
| 	RequireLicenseAcceptance bool                    `json:"require_license_acceptance"` | ||||
| 	Dependencies             map[string][]Dependency `json:"dependencies,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // Dependency represents a dependency of a Nuget package | ||||
| @ -155,12 +156,13 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) { | ||||
| 	} | ||||
| 
 | ||||
| 	m := &Metadata{ | ||||
| 		Description:   p.Metadata.Description, | ||||
| 		ReleaseNotes:  p.Metadata.ReleaseNotes, | ||||
| 		Authors:       p.Metadata.Authors, | ||||
| 		ProjectURL:    p.Metadata.ProjectURL, | ||||
| 		RepositoryURL: p.Metadata.Repository.URL, | ||||
| 		Dependencies:  make(map[string][]Dependency), | ||||
| 		Description:              p.Metadata.Description, | ||||
| 		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 { | ||||
|  | ||||
| @ -180,15 +180,19 @@ func Routes(ctx gocontext.Context) *web.Route { | ||||
| 			r.Get("/*", maven.DownloadPackageFile) | ||||
| 		}, reqPackageAccess(perm.AccessModeRead)) | ||||
| 		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.Get("/query", nuget.SearchService) | ||||
| 				r.Get("/query", nuget.SearchServiceV3) | ||||
| 				r.Group("/registration/{id}", func() { | ||||
| 					r.Get("/index.json", nuget.RegistrationIndex) | ||||
| 					r.Get("/{version}", nuget.RegistrationLeaf) | ||||
| 					r.Get("/{version}", nuget.RegistrationLeafV3) | ||||
| 				}) | ||||
| 				r.Group("/package/{id}", func() { | ||||
| 					r.Get("/index.json", nuget.EnumeratePackageVersions) | ||||
| 					r.Get("/index.json", nuget.EnumeratePackageVersionsV3) | ||||
| 					r.Get("/{version}/{filename}", nuget.DownloadPackageFile) | ||||
| 				}) | ||||
| 				r.Group("", func() { | ||||
| @ -197,6 +201,10 @@ func Routes(ctx gocontext.Context) *web.Route { | ||||
| 					r.Delete("/{id}/{version}", nuget.DeletePackage) | ||||
| 				}, reqPackageAccess(perm.AccessModeWrite)) | ||||
| 				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)) | ||||
| 		}) | ||||
| 		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" | ||||
| ) | ||||
| 
 | ||||
| // ServiceIndexResponse https://docs.microsoft.com/en-us/nuget/api/service-index#resources | ||||
| type ServiceIndexResponse struct { | ||||
| // https://docs.microsoft.com/en-us/nuget/api/service-index#resources | ||||
| type ServiceIndexResponseV3 struct { | ||||
| 	Version   string            `json:"version"` | ||||
| 	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 { | ||||
| 	ID   string `json:"@id"` | ||||
| 	Type string `json:"@type"` | ||||
| } | ||||
| 
 | ||||
| func createServiceIndexResponse(root string) *ServiceIndexResponse { | ||||
| 	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 | ||||
| // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response | ||||
| type RegistrationIndexResponse struct { | ||||
| 	RegistrationIndexURL string                   `json:"@id"` | ||||
| 	Type                 []string                 `json:"@type"` | ||||
| @ -53,7 +36,7 @@ type RegistrationIndexResponse struct { | ||||
| 	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 { | ||||
| 	RegistrationPageURL string                       `json:"@id"` | ||||
| 	Lower               string                       `json:"lower"` | ||||
| @ -62,14 +45,14 @@ type RegistrationIndexPage struct { | ||||
| 	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 { | ||||
| 	RegistrationLeafURL string        `json:"@id"` | ||||
| 	PackageContentURL   string        `json:"packageContent"` | ||||
| 	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 { | ||||
| 	CatalogLeafURL           string                    `json:"@id"` | ||||
| 	PackageContentURL        string                    `json:"packageContent"` | ||||
| @ -83,13 +66,13 @@ type CatalogEntry struct { | ||||
| 	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 { | ||||
| 	TargetFramework string               `json:"targetFramework"` | ||||
| 	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 { | ||||
| 	ID    string `json:"id"` | ||||
| 	Range string `json:"range"` | ||||
| @ -162,7 +145,7 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe | ||||
| 	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 { | ||||
| 	RegistrationLeafURL  string    `json:"@id"` | ||||
| 	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 { | ||||
| 	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 { | ||||
| 	TotalHits int64           `json:"totalHits"` | ||||
| 	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 { | ||||
| 	ID                   string                 `json:"id"` | ||||
| 	Version              string                 `json:"version"` | ||||
| @ -216,7 +199,7 @@ type SearchResult struct { | ||||
| 	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 { | ||||
| 	RegistrationLeafURL string `json:"@id"` | ||||
| 	Version             string `json:"version"` | ||||
| @ -26,3 +26,8 @@ func (l *linkBuilder) GetRegistrationLeafURL(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) | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/xml" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	packages_model "code.gitea.io/gitea/models/packages" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	packages_module "code.gitea.io/gitea/modules/packages" | ||||
| 	nuget_module "code.gitea.io/gitea/modules/packages/nuget" | ||||
| 	"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 ServiceIndex(ctx *context.Context) { | ||||
| 	resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget") | ||||
| 
 | ||||
| 	ctx.JSON(http.StatusOK, resp) | ||||
| func xmlResponse(ctx *context.Context, status int, obj interface{}) { | ||||
| 	ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8") | ||||
| 	ctx.Resp.WriteHeader(status) | ||||
| 	if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil { | ||||
| 		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 | ||||
| func SearchService(ctx *context.Context) { | ||||
| // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs | ||||
| 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{ | ||||
| 		OwnerID:    ctx.Package.Owner.ID, | ||||
| 		Type:       packages_model.TypeNuGet, | ||||
| @ -69,7 +178,7 @@ func SearchService(ctx *context.Context) { | ||||
| 	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) { | ||||
| 	packageName := ctx.Params("id") | ||||
| 
 | ||||
| @ -97,8 +206,37 @@ func RegistrationIndex(ctx *context.Context) { | ||||
| 	ctx.JSON(http.StatusOK, resp) | ||||
| } | ||||
| 
 | ||||
| // RegistrationLeaf https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf | ||||
| func RegistrationLeaf(ctx *context.Context) { | ||||
| // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs | ||||
| 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") | ||||
| 	packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json") | ||||
| 
 | ||||
| @ -126,8 +264,33 @@ func RegistrationLeaf(ctx *context.Context) { | ||||
| 	ctx.JSON(http.StatusOK, resp) | ||||
| } | ||||
| 
 | ||||
| // EnumeratePackageVersions https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions | ||||
| func EnumeratePackageVersions(ctx *context.Context) { | ||||
| // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs | ||||
| 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") | ||||
| 
 | ||||
| 	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) | ||||
| } | ||||
| 
 | ||||
| // 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) { | ||||
| 	packageName := ctx.Params("id") | ||||
| 	packageVersion := ctx.Params("version") | ||||
| @ -350,7 +513,7 @@ func processUploadedFile(ctx *context.Context, expectedType nuget_module.Package | ||||
| 	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) { | ||||
| 	filename := ctx.Params("filename") | ||||
| 	guid := ctx.Params("guid")[:32] | ||||
|  | ||||
| @ -8,10 +8,13 @@ import ( | ||||
| 	"archive/zip" | ||||
| 	"bytes" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/xml" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/packages" | ||||
| @ -31,9 +34,45 @@ func addNuGetAPIKeyHeader(request *http.Request, token string) *http.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) { | ||||
| 	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}) | ||||
| 	token := getUserToken(t, user.Name) | ||||
| 
 | ||||
| @ -54,9 +93,11 @@ func TestPackageNuGet(t *testing.T) { | ||||
| 		<version>` + packageVersion + `</version> | ||||
| 		<authors>` + packageAuthors + `</authors> | ||||
| 		<description>` + packageDescription + `</description> | ||||
| 		<group targetFramework=".NETStandard2.0"> | ||||
| 			<dependency id="Microsoft.CSharp" version="4.5.0" /> | ||||
| 		</group> | ||||
| 		<dependencies> | ||||
| 			<group targetFramework=".NETStandard2.0"> | ||||
| 				<dependency id="Microsoft.CSharp" version="4.5.0" /> | ||||
| 			</group> | ||||
| 		</dependencies> | ||||
| 	  </metadata> | ||||
| 	</package>`)) | ||||
| 	archive.Close() | ||||
| @ -67,60 +108,101 @@ func TestPackageNuGet(t *testing.T) { | ||||
| 	t.Run("ServiceIndex", func(t *testing.T) { | ||||
| 		defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate}) | ||||
| 		t.Run("v2", func(t *testing.T) { | ||||
| 			defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		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}, | ||||
| 		} | ||||
| 			privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate}) | ||||
| 
 | ||||
| 		for _, c := range cases { | ||||
| 			url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) | ||||
| 
 | ||||
| 			req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url)) | ||||
| 			if c.UseBasicAuth { | ||||
| 				req = AddBasicAuthHeader(req, user.Name) | ||||
| 			} else if c.UseTokenAuth { | ||||
| 				req = addNuGetAPIKeyHeader(req, token) | ||||
| 			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}, | ||||
| 			} | ||||
| 			resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 			var result nuget.ServiceIndexResponse | ||||
| 			DecodeJSON(t, resp, &result) | ||||
| 			for _, c := range cases { | ||||
| 				url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) | ||||
| 
 | ||||
| 			assert.Equal(t, "3.0.0", result.Version) | ||||
| 			assert.NotEmpty(t, result.Resources) | ||||
| 				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) | ||||
| 
 | ||||
| 			root := setting.AppURL + url[1:] | ||||
| 			for _, r := range result.Resources { | ||||
| 				switch r.Type { | ||||
| 				case "SearchQueryService": | ||||
| 					fallthrough | ||||
| 				case "SearchQueryService/3.0.0-beta": | ||||
| 					fallthrough | ||||
| 				case "SearchQueryService/3.0.0-rc": | ||||
| 					assert.Equal(t, root+"/query", r.ID) | ||||
| 				case "RegistrationsBaseUrl": | ||||
| 					fallthrough | ||||
| 				case "RegistrationsBaseUrl/3.0.0-beta": | ||||
| 					fallthrough | ||||
| 				case "RegistrationsBaseUrl/3.0.0-rc": | ||||
| 					assert.Equal(t, root+"/registration", r.ID) | ||||
| 				case "PackageBaseAddress/3.0.0": | ||||
| 					assert.Equal(t, root+"/package", r.ID) | ||||
| 				case "PackagePublish/2.0.0": | ||||
| 					assert.Equal(t, root, r.ID) | ||||
| 				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}) | ||||
| 
 | ||||
| 			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", fmt.Sprintf("%s/index.json", 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.ServiceIndexResponseV3 | ||||
| 				DecodeJSON(t, resp, &result) | ||||
| 
 | ||||
| 				assert.Equal(t, "3.0.0", result.Version) | ||||
| 				assert.NotEmpty(t, result.Resources) | ||||
| 
 | ||||
| 				root := setting.AppURL + url[1:] | ||||
| 				for _, r := range result.Resources { | ||||
| 					switch r.Type { | ||||
| 					case "SearchQueryService": | ||||
| 						fallthrough | ||||
| 					case "SearchQueryService/3.0.0-beta": | ||||
| 						fallthrough | ||||
| 					case "SearchQueryService/3.0.0-rc": | ||||
| 						assert.Equal(t, root+"/query", r.ID) | ||||
| 					case "RegistrationsBaseUrl": | ||||
| 						fallthrough | ||||
| 					case "RegistrationsBaseUrl/3.0.0-beta": | ||||
| 						fallthrough | ||||
| 					case "RegistrationsBaseUrl/3.0.0-rc": | ||||
| 						assert.Equal(t, root+"/registration", r.ID) | ||||
| 					case "PackageBaseAddress/3.0.0": | ||||
| 						assert.Equal(t, root+"/package", r.ID) | ||||
| 					case "PackagePublish/2.0.0": | ||||
| 						assert.Equal(t, root, r.ID) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Upload", func(t *testing.T) { | ||||
| @ -305,17 +387,57 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) | ||||
| 			{"test", 1, 10, 1, 0}, | ||||
| 		} | ||||
| 
 | ||||
| 		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 = AddBasicAuthHeader(req, user.Name) | ||||
| 			resp := MakeRequest(t, req, http.StatusOK) | ||||
| 		t.Run("v2", func(t *testing.T) { | ||||
| 			defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 			var result nuget.SearchResultResponse | ||||
| 			DecodeJSON(t, resp, &result) | ||||
| 			t.Run("Search()", func(t *testing.T) { | ||||
| 				defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 			assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i) | ||||
| 			assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i) | ||||
| 		} | ||||
| 				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 { | ||||
| 				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) | ||||
| 				resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 				var result nuget.SearchResultResponse | ||||
| 				DecodeJSON(t, resp, &result) | ||||
| 
 | ||||
| 				assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i) | ||||
| 				assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i) | ||||
| 			} | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("RegistrationService", func(t *testing.T) { | ||||
| @ -352,31 +474,70 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) | ||||
| 		t.Run("RegistrationLeaf", func(t *testing.T) { | ||||
| 			defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 			req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion)) | ||||
| 			req = AddBasicAuthHeader(req, user.Name) | ||||
| 			resp := MakeRequest(t, req, http.StatusOK) | ||||
| 			t.Run("v2", func(t *testing.T) { | ||||
| 				defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 			var result nuget.RegistrationLeafResponse | ||||
| 			DecodeJSON(t, resp, &result) | ||||
| 				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) | ||||
| 
 | ||||
| 			assert.Equal(t, leafURL, result.RegistrationLeafURL) | ||||
| 			assert.Equal(t, contentURL, result.PackageContentURL) | ||||
| 			assert.Equal(t, indexURL, result.RegistrationIndexURL) | ||||
| 				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 = AddBasicAuthHeader(req, user.Name) | ||||
| 				resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 				var result nuget.RegistrationLeafResponse | ||||
| 				DecodeJSON(t, resp, &result) | ||||
| 
 | ||||
| 				assert.Equal(t, leafURL, result.RegistrationLeafURL) | ||||
| 				assert.Equal(t, contentURL, result.PackageContentURL) | ||||
| 				assert.Equal(t, indexURL, result.RegistrationIndexURL) | ||||
| 			}) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("PackageService", func(t *testing.T) { | ||||
| 		defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName)) | ||||
| 		req = AddBasicAuthHeader(req, user.Name) | ||||
| 		resp := MakeRequest(t, req, http.StatusOK) | ||||
| 		t.Run("v2", func(t *testing.T) { | ||||
| 			defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		var result nuget.PackageVersionsResponse | ||||
| 		DecodeJSON(t, resp, &result) | ||||
| 			req := NewRequest(t, "GET", fmt.Sprintf("%s/FindPackagesById()?id='%s'", url, packageName)) | ||||
| 			req = AddBasicAuthHeader(req, user.Name) | ||||
| 			resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		assert.Len(t, result.Versions, 1) | ||||
| 		assert.Equal(t, packageVersion, result.Versions[0]) | ||||
| 			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 = AddBasicAuthHeader(req, user.Name) | ||||
| 			resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 			var result nuget.PackageVersionsResponse | ||||
| 			DecodeJSON(t, resp, &result) | ||||
| 
 | ||||
| 			assert.Len(t, result.Versions, 1) | ||||
| 			assert.Equal(t, packageVersion, result.Versions[0]) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Delete", func(t *testing.T) { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 KN4CK3R
						KN4CK3R