first-commit
This commit is contained in:
420
routers/api/packages/nuget/api_v2.go
Normal file
420
routers/api/packages/nuget/api_v2.go
Normal file
@@ -0,0 +1,420 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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 {
|
||||
Authors string `xml:"d:Authors"`
|
||||
Copyright string `xml:"d:Copyright,omitempty"`
|
||||
Created TypedValue[time.Time] `xml:"d:Created"`
|
||||
Dependencies string `xml:"d:Dependencies"`
|
||||
Description string `xml:"d:Description"`
|
||||
DevelopmentDependency TypedValue[bool] `xml:"d:DevelopmentDependency"`
|
||||
DownloadCount TypedValue[int64] `xml:"d:DownloadCount"`
|
||||
ID string `xml:"d:Id"`
|
||||
IconURL string `xml:"d:IconUrl,omitempty"`
|
||||
Language string `xml:"d:Language,omitempty"`
|
||||
LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"`
|
||||
LicenseURL string `xml:"d:LicenseUrl,omitempty"`
|
||||
MinClientVersion string `xml:"d:MinClientVersion,omitempty"`
|
||||
NormalizedVersion string `xml:"d:NormalizedVersion"`
|
||||
Owners string `xml:"d:Owners,omitempty"`
|
||||
PackageSize TypedValue[int64] `xml:"d:PackageSize"`
|
||||
ProjectURL string `xml:"d:ProjectUrl,omitempty"`
|
||||
Published TypedValue[time.Time] `xml:"d:Published"`
|
||||
ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"`
|
||||
RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"`
|
||||
Tags string `xml:"d:Tags,omitempty"`
|
||||
Title string `xml:"d:Title,omitempty"`
|
||||
Version string `xml:"d:Version"`
|
||||
VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"`
|
||||
}
|
||||
|
||||
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"`
|
||||
Links []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))
|
||||
}
|
||||
|
||||
links := []FeedEntryLink{
|
||||
{Rel: "self", Href: l.Base},
|
||||
}
|
||||
if l.Next != nil {
|
||||
links = append(links, FeedEntryLink{
|
||||
Rel: "next",
|
||||
Href: l.GetNextURL(),
|
||||
})
|
||||
}
|
||||
|
||||
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(),
|
||||
Links: links,
|
||||
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{
|
||||
Authors: metadata.Authors,
|
||||
Copyright: metadata.Copyright,
|
||||
Created: createdValue,
|
||||
Dependencies: buildDependencyString(metadata),
|
||||
Description: metadata.Description,
|
||||
DevelopmentDependency: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.DevelopmentDependency},
|
||||
DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
|
||||
ID: pd.Package.Name,
|
||||
IconURL: metadata.IconURL,
|
||||
Language: metadata.Language,
|
||||
LastUpdated: createdValue,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
MinClientVersion: metadata.MinClientVersion,
|
||||
NormalizedVersion: pd.Version.Version,
|
||||
Owners: metadata.Owners,
|
||||
PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()},
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
Published: createdValue,
|
||||
ReleaseNotes: metadata.ReleaseNotes,
|
||||
RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance},
|
||||
Tags: metadata.Tags,
|
||||
Title: metadata.Title,
|
||||
Version: pd.Version.Version,
|
||||
VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
|
||||
},
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
315
routers/api/packages/nuget/api_v3.go
Normal file
315
routers/api/packages/nuget/api_v3.go
Normal file
@@ -0,0 +1,315 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nuget
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/service-index#resources
|
||||
type ServiceIndexResponseV3 struct {
|
||||
Version string `json:"version"`
|
||||
Resources []ServiceResource `json:"resources"`
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/service-index#resource
|
||||
type ServiceResource struct {
|
||||
ID string `json:"@id"`
|
||||
Type string `json:"@type"`
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response
|
||||
type RegistrationIndexResponse struct {
|
||||
RegistrationIndexURL string `json:"@id"`
|
||||
Type []string `json:"@type"`
|
||||
Count int `json:"count"`
|
||||
Pages []*RegistrationIndexPage `json:"items"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Upper string `json:"upper"`
|
||||
Count int `json:"count"`
|
||||
Items []*RegistrationIndexPageItem `json:"items"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry
|
||||
type CatalogEntry struct {
|
||||
CatalogLeafURL string `json:"@id"`
|
||||
Authors string `json:"authors"`
|
||||
Copyright string `json:"copyright"`
|
||||
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
ID string `json:"id"`
|
||||
IsPrerelease bool `json:"isPrerelease"`
|
||||
Language string `json:"language"`
|
||||
LicenseURL string `json:"licenseUrl"`
|
||||
PackageContentURL string `json:"packageContent"`
|
||||
ProjectURL string `json:"projectUrl"`
|
||||
RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
|
||||
Summary string `json:"summary"`
|
||||
Tags string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
ReleaseNotes string `json:"releaseNotes"`
|
||||
Published time.Time `json:"published"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
func createRegistrationIndexResponse(l *linkBuilder, pds []*packages_model.PackageDescriptor) *RegistrationIndexResponse {
|
||||
sort.Slice(pds, func(i, j int) bool {
|
||||
return pds[i].SemVer.LessThan(pds[j].SemVer)
|
||||
})
|
||||
|
||||
items := make([]*RegistrationIndexPageItem, 0, len(pds))
|
||||
for _, p := range pds {
|
||||
items = append(items, createRegistrationIndexPageItem(l, p))
|
||||
}
|
||||
|
||||
return &RegistrationIndexResponse{
|
||||
RegistrationIndexURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
|
||||
Type: []string{"catalog:CatalogRoot", "PackageRegistration", "catalog:Permalink"},
|
||||
Count: 1,
|
||||
Pages: []*RegistrationIndexPage{
|
||||
{
|
||||
RegistrationPageURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
|
||||
Count: len(pds),
|
||||
Lower: pds[0].Version.Version,
|
||||
Upper: pds[len(pds)-1].Version.Version,
|
||||
Items: items,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationIndexPageItem {
|
||||
metadata := pd.Metadata.(*nuget_module.Metadata)
|
||||
|
||||
return &RegistrationIndexPageItem{
|
||||
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
CatalogEntry: &CatalogEntry{
|
||||
CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
Authors: metadata.Authors,
|
||||
Copyright: metadata.Copyright,
|
||||
DependencyGroups: createDependencyGroups(pd),
|
||||
Description: metadata.Description,
|
||||
IconURL: metadata.IconURL,
|
||||
ID: pd.Package.Name,
|
||||
IsPrerelease: pd.Version.IsPrerelease(),
|
||||
Language: metadata.Language,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
|
||||
Summary: metadata.Summary,
|
||||
Tags: metadata.Tags,
|
||||
Version: pd.Version.Version,
|
||||
ReleaseNotes: metadata.ReleaseNotes,
|
||||
Published: pd.Version.CreatedUnix.AsLocalTime(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDependencyGroup {
|
||||
metadata := pd.Metadata.(*nuget_module.Metadata)
|
||||
|
||||
dependencyGroups := make([]*PackageDependencyGroup, 0, len(metadata.Dependencies))
|
||||
for k, v := range metadata.Dependencies {
|
||||
dependencies := make([]*PackageDependency, 0, len(v))
|
||||
for _, dep := range v {
|
||||
dependencies = append(dependencies, &PackageDependency{
|
||||
ID: dep.ID,
|
||||
Range: dep.Version,
|
||||
})
|
||||
}
|
||||
|
||||
dependencyGroups = append(dependencyGroups, &PackageDependencyGroup{
|
||||
TargetFramework: k,
|
||||
Dependencies: dependencies,
|
||||
})
|
||||
}
|
||||
return dependencyGroups
|
||||
}
|
||||
|
||||
// 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"`
|
||||
PackageContentURL string `json:"packageContent"`
|
||||
RegistrationIndexURL string `json:"registration"`
|
||||
CatalogEntry CatalogEntry `json:"catalogEntry"`
|
||||
}
|
||||
|
||||
func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse {
|
||||
registrationLeafURL := l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version)
|
||||
packageDownloadURL := l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version)
|
||||
metadata := pd.Metadata.(*nuget_module.Metadata)
|
||||
return &RegistrationLeafResponse{
|
||||
RegistrationLeafURL: registrationLeafURL,
|
||||
RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name),
|
||||
PackageContentURL: packageDownloadURL,
|
||||
Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
|
||||
CatalogEntry: CatalogEntry{
|
||||
CatalogLeafURL: registrationLeafURL,
|
||||
Authors: metadata.Authors,
|
||||
Copyright: metadata.Copyright,
|
||||
DependencyGroups: createDependencyGroups(pd),
|
||||
Description: metadata.Description,
|
||||
IconURL: metadata.IconURL,
|
||||
ID: pd.Package.Name,
|
||||
IsPrerelease: pd.Version.IsPrerelease(),
|
||||
Language: metadata.Language,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
PackageContentURL: packageDownloadURL,
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
|
||||
Summary: metadata.Summary,
|
||||
Tags: metadata.Tags,
|
||||
Version: pd.Version.Version,
|
||||
ReleaseNotes: metadata.ReleaseNotes,
|
||||
Published: pd.Version.CreatedUnix.AsLocalTime(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response
|
||||
type PackageVersionsResponse struct {
|
||||
Versions []string `json:"versions"`
|
||||
}
|
||||
|
||||
func createPackageVersionsResponse(pvs []*packages_model.PackageVersion) *PackageVersionsResponse {
|
||||
versions := make([]string, 0, len(pvs))
|
||||
for _, pv := range pvs {
|
||||
versions = append(versions, pv.Version)
|
||||
}
|
||||
|
||||
return &PackageVersionsResponse{
|
||||
Versions: versions,
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response
|
||||
type SearchResultResponse struct {
|
||||
TotalHits int64 `json:"totalHits"`
|
||||
Data []*SearchResult `json:"data"`
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
|
||||
type SearchResult struct {
|
||||
Authors string `json:"authors"`
|
||||
Copyright string `json:"copyright"`
|
||||
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
ID string `json:"id"`
|
||||
IsPrerelease bool `json:"isPrerelease"`
|
||||
Language string `json:"language"`
|
||||
LicenseURL string `json:"licenseUrl"`
|
||||
ProjectURL string `json:"projectUrl"`
|
||||
RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
|
||||
Summary string `json:"summary"`
|
||||
Tags string `json:"tags"`
|
||||
Title string `json:"title"`
|
||||
TotalDownloads int64 `json:"totalDownloads"`
|
||||
Version string `json:"version"`
|
||||
Versions []*SearchResultVersion `json:"versions"`
|
||||
RegistrationIndexURL string `json:"registration"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Downloads int64 `json:"downloads"`
|
||||
}
|
||||
|
||||
func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages_model.PackageDescriptor) *SearchResultResponse {
|
||||
grouped := make(map[string][]*packages_model.PackageDescriptor)
|
||||
for _, pd := range pds {
|
||||
grouped[pd.Package.Name] = append(grouped[pd.Package.Name], pd)
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(grouped))
|
||||
for key := range grouped {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
collate.New(language.English, collate.IgnoreCase).SortStrings(keys)
|
||||
|
||||
data := make([]*SearchResult, 0, len(pds))
|
||||
for _, key := range keys {
|
||||
data = append(data, createSearchResult(l, grouped[key]))
|
||||
}
|
||||
|
||||
return &SearchResultResponse{
|
||||
TotalHits: totalHits,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) *SearchResult {
|
||||
latest := pds[0]
|
||||
versions := make([]*SearchResultVersion, 0, len(pds))
|
||||
totalDownloads := int64(0)
|
||||
for _, pd := range pds {
|
||||
if latest.SemVer.LessThan(pd.SemVer) {
|
||||
latest = pd
|
||||
}
|
||||
totalDownloads += pd.Version.DownloadCount
|
||||
versions = append(versions, &SearchResultVersion{
|
||||
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
Version: pd.Version.Version,
|
||||
})
|
||||
}
|
||||
|
||||
metadata := latest.Metadata.(*nuget_module.Metadata)
|
||||
|
||||
return &SearchResult{
|
||||
Authors: metadata.Authors,
|
||||
Copyright: metadata.Copyright,
|
||||
Description: metadata.Description,
|
||||
DependencyGroups: createDependencyGroups(latest),
|
||||
IconURL: metadata.IconURL,
|
||||
ID: latest.Package.Name,
|
||||
IsPrerelease: latest.Version.IsPrerelease(),
|
||||
Language: metadata.Language,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
|
||||
Summary: metadata.Summary,
|
||||
Tags: metadata.Tags,
|
||||
Title: metadata.Title,
|
||||
TotalDownloads: totalDownloads,
|
||||
Version: latest.Version.Version,
|
||||
Versions: versions,
|
||||
RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
|
||||
}
|
||||
}
|
50
routers/api/packages/nuget/auth.go
Normal file
50
routers/api/packages/nuget/auth.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nuget
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
)
|
||||
|
||||
var _ auth.Method = &Auth{}
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "nuget"
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
|
||||
token, err := auth_model.GetAccessTokenBySHA(req.Context(), req.Header.Get("X-NuGet-ApiKey"))
|
||||
if err != nil {
|
||||
if !(auth_model.IsErrAccessTokenNotExist(err) || auth_model.IsErrAccessTokenEmpty(err)) {
|
||||
log.Error("GetAccessTokenBySHA: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(req.Context(), token.UID)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token.UpdatedUnix = timeutil.TimeStampNow()
|
||||
if err := auth_model.UpdateAccessToken(req.Context(), token); err != nil {
|
||||
log.Error("UpdateAccessToken: %v", err)
|
||||
}
|
||||
|
||||
store.GetData()["IsApiToken"] = true
|
||||
store.GetData()["ApiToken"] = token
|
||||
|
||||
return u, nil
|
||||
}
|
52
routers/api/packages/nuget/links.go
Normal file
52
routers/api/packages/nuget/links.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nuget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type nextOptions struct {
|
||||
Path string
|
||||
Query url.Values
|
||||
}
|
||||
|
||||
type linkBuilder struct {
|
||||
Base string
|
||||
Next *nextOptions
|
||||
}
|
||||
|
||||
// GetRegistrationIndexURL builds the registration index url
|
||||
func (l *linkBuilder) GetRegistrationIndexURL(id string) string {
|
||||
return fmt.Sprintf("%s/registration/%s/index.json", l.Base, id)
|
||||
}
|
||||
|
||||
// GetRegistrationLeafURL builds the registration leaf url
|
||||
func (l *linkBuilder) GetRegistrationLeafURL(id, version string) string {
|
||||
return fmt.Sprintf("%s/registration/%s/%s.json", l.Base, id, version)
|
||||
}
|
||||
|
||||
// GetPackageDownloadURL builds the download url
|
||||
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)
|
||||
}
|
||||
|
||||
func (l *linkBuilder) GetNextURL() string {
|
||||
u, _ := url.Parse(l.Base)
|
||||
u = u.JoinPath(l.Next.Path)
|
||||
q := u.Query()
|
||||
for k, vs := range l.Next.Query {
|
||||
for _, v := range vs {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
}
|
710
routers/api/packages/nuget/nuget.go
Normal file
710
routers/api/packages/nuget/nuget.go
Normal file
@@ -0,0 +1,710 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nuget
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
nuget_model "code.gitea.io/gitea/models/packages/nuget"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj any) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
ctx.JSON(status, map[string]string{
|
||||
"Message": message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam // status is always StatusOK
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(`'([^']+)'`)
|
||||
searchTermExact = regexp.MustCompile(`\s+eq\s+'`)
|
||||
)
|
||||
|
||||
func getSearchTerm(ctx *context.Context) packages_model.SearchValue {
|
||||
searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
|
||||
if searchTerm != "" {
|
||||
return packages_model.SearchValue{
|
||||
Value: searchTerm,
|
||||
ExactMatch: false,
|
||||
}
|
||||
}
|
||||
|
||||
// $filter contains a query like:
|
||||
// (((Id ne null) and substringof('microsoft',tolower(Id)))
|
||||
// https://www.odata.org/documentation/odata-version-2-0/uri-conventions/ section 4.5
|
||||
// We don't support these queries, just extract the search term.
|
||||
filter := ctx.FormTrim("$filter")
|
||||
match := searchTermExtract.FindStringSubmatch(filter)
|
||||
if len(match) == 2 {
|
||||
return packages_model.SearchValue{
|
||||
Value: strings.TrimSpace(match[1]),
|
||||
ExactMatch: searchTermExact.MatchString(filter),
|
||||
}
|
||||
}
|
||||
|
||||
return packages_model.SearchValue{}
|
||||
}
|
||||
|
||||
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||
func SearchServiceV2(ctx *context.Context) {
|
||||
skip, take := ctx.FormInt("$skip"), ctx.FormInt("$top")
|
||||
paginator := db.NewAbsoluteListOptions(skip, take)
|
||||
|
||||
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: getSearchTerm(ctx),
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: paginator,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
skip, take = paginator.GetSkipTake()
|
||||
|
||||
var next *nextOptions
|
||||
if len(pvs) == take {
|
||||
next = &nextOptions{
|
||||
Path: "Search()",
|
||||
Query: url.Values{},
|
||||
}
|
||||
searchTerm := ctx.FormTrim("searchTerm")
|
||||
if searchTerm != "" {
|
||||
next.Query.Set("searchTerm", searchTerm)
|
||||
}
|
||||
filter := ctx.FormTrim("$filter")
|
||||
if filter != "" {
|
||||
next.Query.Set("$filter", filter)
|
||||
}
|
||||
next.Query.Set("$skip", strconv.Itoa(skip+take))
|
||||
next.Query.Set("$top", strconv.Itoa(take))
|
||||
}
|
||||
|
||||
resp := createFeedResponse(
|
||||
&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget", Next: next},
|
||||
total,
|
||||
pds,
|
||||
)
|
||||
|
||||
xmlResponse(ctx, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
|
||||
func SearchServiceV2Count(ctx *context.Context) {
|
||||
count, err := nuget_model.CountPackages(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Name: getSearchTerm(ctx),
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, strconv.FormatInt(count, 10))
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
|
||||
func SearchServiceV3(ctx *context.Context) {
|
||||
pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: db.NewAbsoluteListOptions(
|
||||
ctx.FormInt("skip"),
|
||||
ctx.FormInt("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 := createSearchResultResponse(
|
||||
&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||
count,
|
||||
pds,
|
||||
)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index
|
||||
func RegistrationIndex(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("id")
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createRegistrationIndexResponse(
|
||||
&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||
pds,
|
||||
)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||
func RegistrationLeafV2(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("id")
|
||||
packageVersion := ctx.PathParam("version")
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
|
||||
if err != nil {
|
||||
if errors.Is(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{Base: 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.PathParam("id")
|
||||
packageVersion := strings.TrimSuffix(ctx.PathParam("version"), ".json")
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
|
||||
if err != nil {
|
||||
if errors.Is(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 := createRegistrationLeafResponse(
|
||||
&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||
pd,
|
||||
)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// 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"), "'")
|
||||
|
||||
skip, take := ctx.FormInt("$skip"), ctx.FormInt("$top")
|
||||
paginator := db.NewAbsoluteListOptions(skip, take)
|
||||
|
||||
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: packages_model.SearchValue{
|
||||
ExactMatch: true,
|
||||
Value: packageName,
|
||||
},
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: paginator,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
skip, take = paginator.GetSkipTake()
|
||||
|
||||
var next *nextOptions
|
||||
if len(pvs) == take {
|
||||
next = &nextOptions{
|
||||
Path: "FindPackagesById()",
|
||||
Query: url.Values{},
|
||||
}
|
||||
next.Query.Set("id", packageName)
|
||||
next.Query.Set("$skip", strconv.Itoa(skip+take))
|
||||
next.Query.Set("$top", strconv.Itoa(take))
|
||||
}
|
||||
|
||||
resp := createFeedResponse(
|
||||
&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget", Next: next},
|
||||
total,
|
||||
pds,
|
||||
)
|
||||
|
||||
xmlResponse(ctx, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
|
||||
func EnumeratePackageVersionsV2Count(ctx *context.Context) {
|
||||
count, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: packages_model.SearchValue{
|
||||
ExactMatch: true,
|
||||
Value: strings.Trim(ctx.FormTrim("id"), "'"),
|
||||
},
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, strconv.FormatInt(count, 10))
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions
|
||||
func EnumeratePackageVersionsV3(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("id")
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createPackageVersionsResponse(pvs)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec
|
||||
// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("id")
|
||||
packageVersion := ctx.PathParam("version")
|
||||
filename := ctx.PathParam("filename")
|
||||
|
||||
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.ServePackageFile(ctx, s, u, pf)
|
||||
}
|
||||
|
||||
// UploadPackage creates a new package with the metadata contained in the uploaded nupgk file
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#push-a-package
|
||||
func UploadPackage(ctx *context.Context) {
|
||||
np, buf, closables := processUploadedFile(ctx, nuget_module.DependencyPackage)
|
||||
defer func() {
|
||||
for _, c := range closables {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
if np == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pv, _, err := packages_service.CreatePackageAndAddFile(
|
||||
ctx,
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: np.ID,
|
||||
Version: np.Version,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: np.Metadata,
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.nupkg", np.ID, np.Version)),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageVersion:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
nuspecBuf, err := packages_module.CreateHashedBufferFromReaderWithSize(np.NuspecContent, np.NuspecContent.Len())
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer nuspecBuf.Close()
|
||||
|
||||
_, err = packages_service.AddFileToPackageVersionInternal(
|
||||
ctx,
|
||||
pv,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(np.ID + ".nuspec"),
|
||||
},
|
||||
Data: nuspecBuf,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// UploadSymbolPackage adds a symbol package to an existing package
|
||||
// https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource
|
||||
func UploadSymbolPackage(ctx *context.Context) {
|
||||
np, buf, closables := processUploadedFile(ctx, nuget_module.SymbolsPackage)
|
||||
defer func() {
|
||||
for _, c := range closables {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
if np == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pdbs, err := nuget_module.ExtractPortablePdb(buf, buf.Size())
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer pdbs.Close()
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pi := &packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: np.ID,
|
||||
Version: np.Version,
|
||||
}
|
||||
|
||||
_, err = packages_service.AddFileToExistingPackage(
|
||||
ctx,
|
||||
pi,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.snupkg", np.ID, np.Version)),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case packages_model.ErrPackageNotExist:
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, pdb := range pdbs {
|
||||
_, err := packages_service.AddFileToExistingPackage(
|
||||
ctx,
|
||||
pi,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(pdb.Name),
|
||||
CompositeKey: strings.ToLower(pdb.ID),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: pdb.Content,
|
||||
IsLead: false,
|
||||
Properties: map[string]string{
|
||||
nuget_module.PropertySymbolID: strings.ToLower(pdb.ID),
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
func processUploadedFile(ctx *context.Context, expectedType nuget_module.PackageType) (*nuget_module.Package, *packages_module.HashedBuffer, []io.Closer) {
|
||||
closables := make([]io.Closer, 0, 2)
|
||||
|
||||
upload, needToClose, err := ctx.UploadStream()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return nil, nil, closables
|
||||
}
|
||||
|
||||
if needToClose {
|
||||
closables = append(closables, upload)
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return nil, nil, closables
|
||||
}
|
||||
closables = append(closables, buf)
|
||||
|
||||
np, err := nuget_module.ParsePackageMetaData(buf, buf.Size())
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return nil, nil, closables
|
||||
}
|
||||
if np.PackageType != expectedType {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("unexpected package type"))
|
||||
return nil, nil, closables
|
||||
}
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return nil, nil, closables
|
||||
}
|
||||
return np, buf, closables
|
||||
}
|
||||
|
||||
// https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request
|
||||
func DownloadSymbolFile(ctx *context.Context) {
|
||||
filename := ctx.PathParam("filename")
|
||||
guid := ctx.PathParam("guid")[:32]
|
||||
filename2 := ctx.PathParam("filename2")
|
||||
|
||||
if filename != filename2 {
|
||||
apiError(ctx, http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Query: filename,
|
||||
Properties: map[string]string{
|
||||
nuget_module.PropertySymbolID: strings.ToLower(guid),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pfs) != 1 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0])
|
||||
if err != nil {
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.ServePackageFile(ctx, s, u, pf)
|
||||
}
|
||||
|
||||
// DeletePackage hard deletes the package
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#delete-a-package
|
||||
func DeletePackage(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("id")
|
||||
packageVersion := ctx.PathParam("version")
|
||||
|
||||
err := packages_service.RemovePackageVersionByNameAndVersion(
|
||||
ctx,
|
||||
ctx.Doer,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
Reference in New Issue
Block a user