summaryrefslogtreecommitdiffstats
path: root/routers/api/packages/nuget
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /routers/api/packages/nuget
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'routers/api/packages/nuget')
-rw-r--r--routers/api/packages/nuget/api_v2.go402
-rw-r--r--routers/api/packages/nuget/api_v3.go255
-rw-r--r--routers/api/packages/nuget/auth.go47
-rw-r--r--routers/api/packages/nuget/links.go52
-rw-r--r--routers/api/packages/nuget/nuget.go710
5 files changed, 1466 insertions, 0 deletions
diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go
new file mode 100644
index 0000000..a726065
--- /dev/null
+++ b/routers/api/packages/nuget/api_v2.go
@@ -0,0 +1,402 @@
+// 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 {
+ 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"`
+ 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{
+ Version: pd.Version.Version,
+ NormalizedVersion: pd.Version.Version,
+ 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()
+}
diff --git a/routers/api/packages/nuget/api_v3.go b/routers/api/packages/nuget/api_v3.go
new file mode 100644
index 0000000..2fe25dc
--- /dev/null
+++ b/routers/api/packages/nuget/api_v3.go
@@ -0,0 +1,255 @@
+// 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"`
+ PackageContentURL string `json:"packageContent"`
+ ID string `json:"id"`
+ Version string `json:"version"`
+ Description string `json:"description"`
+ ReleaseNotes string `json:"releaseNotes"`
+ Authors string `json:"authors"`
+ RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
+ ProjectURL string `json:"projectURL"`
+ DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
+}
+
+// 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),
+ PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
+ ID: pd.Package.Name,
+ Version: pd.Version.Version,
+ Description: metadata.Description,
+ ReleaseNotes: metadata.ReleaseNotes,
+ Authors: metadata.Authors,
+ ProjectURL: metadata.ProjectURL,
+ DependencyGroups: createDependencyGroups(pd),
+ },
+ }
+}
+
+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"`
+ Listed bool `json:"listed"`
+ PackageContentURL string `json:"packageContent"`
+ Published time.Time `json:"published"`
+ RegistrationIndexURL string `json:"registration"`
+}
+
+func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse {
+ return &RegistrationLeafResponse{
+ Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
+ Listed: true,
+ Published: pd.Version.CreatedUnix.AsLocalTime(),
+ RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
+ PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
+ RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name),
+ }
+}
+
+// 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 {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ Versions []*SearchResultVersion `json:"versions"`
+ Description string `json:"description"`
+ Authors string `json:"authors"`
+ ProjectURL string `json:"projectURL"`
+ 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))
+ for _, pd := range pds {
+ if latest.SemVer.LessThan(pd.SemVer) {
+ latest = pd
+ }
+
+ 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{
+ ID: latest.Package.Name,
+ Version: latest.Version.Version,
+ Versions: versions,
+ Description: metadata.Description,
+ Authors: metadata.Authors,
+ ProjectURL: metadata.ProjectURL,
+ RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
+ }
+}
diff --git a/routers/api/packages/nuget/auth.go b/routers/api/packages/nuget/auth.go
new file mode 100644
index 0000000..1bb68d0
--- /dev/null
+++ b/routers/api/packages/nuget/auth.go
@@ -0,0 +1,47 @@
+// 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)
+ }
+
+ return u, nil
+}
diff --git a/routers/api/packages/nuget/links.go b/routers/api/packages/nuget/links.go
new file mode 100644
index 0000000..4c573fe
--- /dev/null
+++ b/routers/api/packages/nuget/links.go
@@ -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()
+}
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
new file mode 100644
index 0000000..0d7212d
--- /dev/null
+++ b/routers/api/packages/nuget/nuget.go
@@ -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
+ 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.Params("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.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{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.Params("id")
+ packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json")
+
+ 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 := 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.Params("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.Params("id")
+ packageVersion := ctx.Params("version")
+ filename := ctx.Params("filename")
+
+ s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ ctx,
+ &packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeNuGet,
+ Name: packageName,
+ Version: packageVersion,
+ },
+ &packages_service.PackageFileInfo{
+ Filename: filename,
+ },
+ )
+ if err != nil {
+ if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
+ 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(fmt.Sprintf("%s.nuspec", np.ID)),
+ },
+ 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.Params("filename")
+ guid := ctx.Params("guid")[:32]
+ filename2 := ctx.Params("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.GetPackageFileStream(ctx, pfs[0])
+ if err != nil {
+ if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ helper.ServePackageFile(ctx, s, u, pf)
+}
+
+// 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.Params("id")
+ packageVersion := ctx.Params("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 err == packages_model.ErrPackageNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+
+ ctx.Status(http.StatusNoContent)
+}