code.gitea.io/gitea@v1.22.3/routers/api/packages/nuget/nuget.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package nuget
     5  
     6  import (
     7  	"encoding/xml"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"code.gitea.io/gitea/models/db"
    18  	packages_model "code.gitea.io/gitea/models/packages"
    19  	nuget_model "code.gitea.io/gitea/models/packages/nuget"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/optional"
    22  	packages_module "code.gitea.io/gitea/modules/packages"
    23  	nuget_module "code.gitea.io/gitea/modules/packages/nuget"
    24  	"code.gitea.io/gitea/modules/setting"
    25  	"code.gitea.io/gitea/modules/util"
    26  	"code.gitea.io/gitea/routers/api/packages/helper"
    27  	"code.gitea.io/gitea/services/context"
    28  	packages_service "code.gitea.io/gitea/services/packages"
    29  )
    30  
    31  func apiError(ctx *context.Context, status int, obj any) {
    32  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    33  		ctx.JSON(status, map[string]string{
    34  			"Message": message,
    35  		})
    36  	})
    37  }
    38  
    39  func xmlResponse(ctx *context.Context, status int, obj any) {
    40  	ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
    41  	ctx.Resp.WriteHeader(status)
    42  	if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil {
    43  		log.Error("Write failed: %v", err)
    44  	}
    45  	if err := xml.NewEncoder(ctx.Resp).Encode(obj); err != nil {
    46  		log.Error("XML encode failed: %v", err)
    47  	}
    48  }
    49  
    50  // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
    51  func ServiceIndexV2(ctx *context.Context) {
    52  	base := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"
    53  
    54  	xmlResponse(ctx, http.StatusOK, &ServiceIndexResponseV2{
    55  		Base:      base,
    56  		Xmlns:     "http://www.w3.org/2007/app",
    57  		XmlnsAtom: "http://www.w3.org/2005/Atom",
    58  		Workspace: ServiceWorkspace{
    59  			Title: AtomTitle{
    60  				Type: "text",
    61  				Text: "Default",
    62  			},
    63  			Collection: ServiceCollection{
    64  				Href: "Packages",
    65  				Title: AtomTitle{
    66  					Type: "text",
    67  					Text: "Packages",
    68  				},
    69  			},
    70  		},
    71  	})
    72  }
    73  
    74  // https://docs.microsoft.com/en-us/nuget/api/service-index
    75  func ServiceIndexV3(ctx *context.Context) {
    76  	root := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"
    77  
    78  	ctx.JSON(http.StatusOK, &ServiceIndexResponseV3{
    79  		Version: "3.0.0",
    80  		Resources: []ServiceResource{
    81  			{ID: root + "/query", Type: "SearchQueryService"},
    82  			{ID: root + "/query", Type: "SearchQueryService/3.0.0-beta"},
    83  			{ID: root + "/query", Type: "SearchQueryService/3.0.0-rc"},
    84  			{ID: root + "/registration", Type: "RegistrationsBaseUrl"},
    85  			{ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"},
    86  			{ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"},
    87  			{ID: root + "/package", Type: "PackageBaseAddress/3.0.0"},
    88  			{ID: root, Type: "PackagePublish/2.0.0"},
    89  			{ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"},
    90  		},
    91  	})
    92  }
    93  
    94  // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/LegacyFeedCapabilityResourceV2Feed.cs
    95  func FeedCapabilityResource(ctx *context.Context) {
    96  	xmlResponse(ctx, http.StatusOK, Metadata)
    97  }
    98  
    99  var (
   100  	searchTermExtract = regexp.MustCompile(`'([^']+)'`)
   101  	searchTermExact   = regexp.MustCompile(`\s+eq\s+'`)
   102  )
   103  
   104  func getSearchTerm(ctx *context.Context) packages_model.SearchValue {
   105  	searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
   106  	if searchTerm != "" {
   107  		return packages_model.SearchValue{
   108  			Value:      searchTerm,
   109  			ExactMatch: false,
   110  		}
   111  	}
   112  
   113  	// $filter contains a query like:
   114  	// (((Id ne null) and substringof('microsoft',tolower(Id)))
   115  	// https://www.odata.org/documentation/odata-version-2-0/uri-conventions/ section 4.5
   116  	// We don't support these queries, just extract the search term.
   117  	filter := ctx.FormTrim("$filter")
   118  	match := searchTermExtract.FindStringSubmatch(filter)
   119  	if len(match) == 2 {
   120  		return packages_model.SearchValue{
   121  			Value:      strings.TrimSpace(match[1]),
   122  			ExactMatch: searchTermExact.MatchString(filter),
   123  		}
   124  	}
   125  
   126  	return packages_model.SearchValue{}
   127  }
   128  
   129  // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
   130  func SearchServiceV2(ctx *context.Context) {
   131  	skip, take := ctx.FormInt("$skip"), ctx.FormInt("$top")
   132  	paginator := db.NewAbsoluteListOptions(skip, take)
   133  
   134  	pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
   135  		OwnerID:    ctx.Package.Owner.ID,
   136  		Type:       packages_model.TypeNuGet,
   137  		Name:       getSearchTerm(ctx),
   138  		IsInternal: optional.Some(false),
   139  		Paginator:  paginator,
   140  	})
   141  	if err != nil {
   142  		apiError(ctx, http.StatusInternalServerError, err)
   143  		return
   144  	}
   145  
   146  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   147  	if err != nil {
   148  		apiError(ctx, http.StatusInternalServerError, err)
   149  		return
   150  	}
   151  
   152  	skip, take = paginator.GetSkipTake()
   153  
   154  	var next *nextOptions
   155  	if len(pvs) == take {
   156  		next = &nextOptions{
   157  			Path:  "Search()",
   158  			Query: url.Values{},
   159  		}
   160  		searchTerm := ctx.FormTrim("searchTerm")
   161  		if searchTerm != "" {
   162  			next.Query.Set("searchTerm", searchTerm)
   163  		}
   164  		filter := ctx.FormTrim("$filter")
   165  		if filter != "" {
   166  			next.Query.Set("$filter", filter)
   167  		}
   168  		next.Query.Set("$skip", strconv.Itoa(skip+take))
   169  		next.Query.Set("$top", strconv.Itoa(take))
   170  	}
   171  
   172  	resp := createFeedResponse(
   173  		&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget", Next: next},
   174  		total,
   175  		pds,
   176  	)
   177  
   178  	xmlResponse(ctx, http.StatusOK, resp)
   179  }
   180  
   181  // 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
   182  func SearchServiceV2Count(ctx *context.Context) {
   183  	count, err := nuget_model.CountPackages(ctx, &packages_model.PackageSearchOptions{
   184  		OwnerID:    ctx.Package.Owner.ID,
   185  		Name:       getSearchTerm(ctx),
   186  		IsInternal: optional.Some(false),
   187  	})
   188  	if err != nil {
   189  		apiError(ctx, http.StatusInternalServerError, err)
   190  		return
   191  	}
   192  
   193  	ctx.PlainText(http.StatusOK, strconv.FormatInt(count, 10))
   194  }
   195  
   196  // https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
   197  func SearchServiceV3(ctx *context.Context) {
   198  	pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
   199  		OwnerID:    ctx.Package.Owner.ID,
   200  		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
   201  		IsInternal: optional.Some(false),
   202  		Paginator: db.NewAbsoluteListOptions(
   203  			ctx.FormInt("skip"),
   204  			ctx.FormInt("take"),
   205  		),
   206  	})
   207  	if err != nil {
   208  		apiError(ctx, http.StatusInternalServerError, err)
   209  		return
   210  	}
   211  
   212  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   213  	if err != nil {
   214  		apiError(ctx, http.StatusInternalServerError, err)
   215  		return
   216  	}
   217  
   218  	resp := createSearchResultResponse(
   219  		&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
   220  		count,
   221  		pds,
   222  	)
   223  
   224  	ctx.JSON(http.StatusOK, resp)
   225  }
   226  
   227  // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index
   228  func RegistrationIndex(ctx *context.Context) {
   229  	packageName := ctx.Params("id")
   230  
   231  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
   232  	if err != nil {
   233  		apiError(ctx, http.StatusInternalServerError, err)
   234  		return
   235  	}
   236  	if len(pvs) == 0 {
   237  		apiError(ctx, http.StatusNotFound, err)
   238  		return
   239  	}
   240  
   241  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   242  	if err != nil {
   243  		apiError(ctx, http.StatusInternalServerError, err)
   244  		return
   245  	}
   246  
   247  	resp := createRegistrationIndexResponse(
   248  		&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
   249  		pds,
   250  	)
   251  
   252  	ctx.JSON(http.StatusOK, resp)
   253  }
   254  
   255  // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
   256  func RegistrationLeafV2(ctx *context.Context) {
   257  	packageName := ctx.Params("id")
   258  	packageVersion := ctx.Params("version")
   259  
   260  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
   261  	if err != nil {
   262  		if err == packages_model.ErrPackageNotExist {
   263  			apiError(ctx, http.StatusNotFound, err)
   264  			return
   265  		}
   266  		apiError(ctx, http.StatusInternalServerError, err)
   267  		return
   268  	}
   269  
   270  	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
   271  	if err != nil {
   272  		apiError(ctx, http.StatusInternalServerError, err)
   273  		return
   274  	}
   275  
   276  	resp := createEntryResponse(
   277  		&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
   278  		pd,
   279  	)
   280  
   281  	xmlResponse(ctx, http.StatusOK, resp)
   282  }
   283  
   284  // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
   285  func RegistrationLeafV3(ctx *context.Context) {
   286  	packageName := ctx.Params("id")
   287  	packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json")
   288  
   289  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
   290  	if err != nil {
   291  		if err == packages_model.ErrPackageNotExist {
   292  			apiError(ctx, http.StatusNotFound, err)
   293  			return
   294  		}
   295  		apiError(ctx, http.StatusInternalServerError, err)
   296  		return
   297  	}
   298  
   299  	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
   300  	if err != nil {
   301  		apiError(ctx, http.StatusInternalServerError, err)
   302  		return
   303  	}
   304  
   305  	resp := createRegistrationLeafResponse(
   306  		&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
   307  		pd,
   308  	)
   309  
   310  	ctx.JSON(http.StatusOK, resp)
   311  }
   312  
   313  // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
   314  func EnumeratePackageVersionsV2(ctx *context.Context) {
   315  	packageName := strings.Trim(ctx.FormTrim("id"), "'")
   316  
   317  	skip, take := ctx.FormInt("$skip"), ctx.FormInt("$top")
   318  	paginator := db.NewAbsoluteListOptions(skip, take)
   319  
   320  	pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
   321  		OwnerID: ctx.Package.Owner.ID,
   322  		Type:    packages_model.TypeNuGet,
   323  		Name: packages_model.SearchValue{
   324  			ExactMatch: true,
   325  			Value:      packageName,
   326  		},
   327  		IsInternal: optional.Some(false),
   328  		Paginator:  paginator,
   329  	})
   330  	if err != nil {
   331  		apiError(ctx, http.StatusInternalServerError, err)
   332  		return
   333  	}
   334  
   335  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   336  	if err != nil {
   337  		apiError(ctx, http.StatusInternalServerError, err)
   338  		return
   339  	}
   340  
   341  	skip, take = paginator.GetSkipTake()
   342  
   343  	var next *nextOptions
   344  	if len(pvs) == take {
   345  		next = &nextOptions{
   346  			Path:  "FindPackagesById()",
   347  			Query: url.Values{},
   348  		}
   349  		next.Query.Set("id", packageName)
   350  		next.Query.Set("$skip", strconv.Itoa(skip+take))
   351  		next.Query.Set("$top", strconv.Itoa(take))
   352  	}
   353  
   354  	resp := createFeedResponse(
   355  		&linkBuilder{Base: setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget", Next: next},
   356  		total,
   357  		pds,
   358  	)
   359  
   360  	xmlResponse(ctx, http.StatusOK, resp)
   361  }
   362  
   363  // 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
   364  func EnumeratePackageVersionsV2Count(ctx *context.Context) {
   365  	count, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
   366  		OwnerID: ctx.Package.Owner.ID,
   367  		Type:    packages_model.TypeNuGet,
   368  		Name: packages_model.SearchValue{
   369  			ExactMatch: true,
   370  			Value:      strings.Trim(ctx.FormTrim("id"), "'"),
   371  		},
   372  		IsInternal: optional.Some(false),
   373  	})
   374  	if err != nil {
   375  		apiError(ctx, http.StatusInternalServerError, err)
   376  		return
   377  	}
   378  
   379  	ctx.PlainText(http.StatusOK, strconv.FormatInt(count, 10))
   380  }
   381  
   382  // https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions
   383  func EnumeratePackageVersionsV3(ctx *context.Context) {
   384  	packageName := ctx.Params("id")
   385  
   386  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
   387  	if err != nil {
   388  		apiError(ctx, http.StatusInternalServerError, err)
   389  		return
   390  	}
   391  	if len(pvs) == 0 {
   392  		apiError(ctx, http.StatusNotFound, err)
   393  		return
   394  	}
   395  
   396  	resp := createPackageVersionsResponse(pvs)
   397  
   398  	ctx.JSON(http.StatusOK, resp)
   399  }
   400  
   401  // https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec
   402  // https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
   403  func DownloadPackageFile(ctx *context.Context) {
   404  	packageName := ctx.Params("id")
   405  	packageVersion := ctx.Params("version")
   406  	filename := ctx.Params("filename")
   407  
   408  	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
   409  		ctx,
   410  		&packages_service.PackageInfo{
   411  			Owner:       ctx.Package.Owner,
   412  			PackageType: packages_model.TypeNuGet,
   413  			Name:        packageName,
   414  			Version:     packageVersion,
   415  		},
   416  		&packages_service.PackageFileInfo{
   417  			Filename: filename,
   418  		},
   419  	)
   420  	if err != nil {
   421  		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
   422  			apiError(ctx, http.StatusNotFound, err)
   423  			return
   424  		}
   425  		apiError(ctx, http.StatusInternalServerError, err)
   426  		return
   427  	}
   428  
   429  	helper.ServePackageFile(ctx, s, u, pf)
   430  }
   431  
   432  // UploadPackage creates a new package with the metadata contained in the uploaded nupgk file
   433  // https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#push-a-package
   434  func UploadPackage(ctx *context.Context) {
   435  	np, buf, closables := processUploadedFile(ctx, nuget_module.DependencyPackage)
   436  	defer func() {
   437  		for _, c := range closables {
   438  			c.Close()
   439  		}
   440  	}()
   441  	if np == nil {
   442  		return
   443  	}
   444  
   445  	pv, _, err := packages_service.CreatePackageAndAddFile(
   446  		ctx,
   447  		&packages_service.PackageCreationInfo{
   448  			PackageInfo: packages_service.PackageInfo{
   449  				Owner:       ctx.Package.Owner,
   450  				PackageType: packages_model.TypeNuGet,
   451  				Name:        np.ID,
   452  				Version:     np.Version,
   453  			},
   454  			SemverCompatible: true,
   455  			Creator:          ctx.Doer,
   456  			Metadata:         np.Metadata,
   457  		},
   458  		&packages_service.PackageFileCreationInfo{
   459  			PackageFileInfo: packages_service.PackageFileInfo{
   460  				Filename: strings.ToLower(fmt.Sprintf("%s.%s.nupkg", np.ID, np.Version)),
   461  			},
   462  			Creator: ctx.Doer,
   463  			Data:    buf,
   464  			IsLead:  true,
   465  		},
   466  	)
   467  	if err != nil {
   468  		switch err {
   469  		case packages_model.ErrDuplicatePackageVersion:
   470  			apiError(ctx, http.StatusConflict, err)
   471  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   472  			apiError(ctx, http.StatusForbidden, err)
   473  		default:
   474  			apiError(ctx, http.StatusInternalServerError, err)
   475  		}
   476  		return
   477  	}
   478  
   479  	nuspecBuf, err := packages_module.CreateHashedBufferFromReaderWithSize(np.NuspecContent, np.NuspecContent.Len())
   480  	if err != nil {
   481  		apiError(ctx, http.StatusInternalServerError, err)
   482  		return
   483  	}
   484  	defer nuspecBuf.Close()
   485  
   486  	_, err = packages_service.AddFileToPackageVersionInternal(
   487  		ctx,
   488  		pv,
   489  		&packages_service.PackageFileCreationInfo{
   490  			PackageFileInfo: packages_service.PackageFileInfo{
   491  				Filename: strings.ToLower(fmt.Sprintf("%s.nuspec", np.ID)),
   492  			},
   493  			Data: nuspecBuf,
   494  		},
   495  	)
   496  	if err != nil {
   497  		switch err {
   498  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   499  			apiError(ctx, http.StatusForbidden, err)
   500  		default:
   501  			apiError(ctx, http.StatusInternalServerError, err)
   502  		}
   503  		return
   504  	}
   505  
   506  	ctx.Status(http.StatusCreated)
   507  }
   508  
   509  // UploadSymbolPackage adds a symbol package to an existing package
   510  // https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource
   511  func UploadSymbolPackage(ctx *context.Context) {
   512  	np, buf, closables := processUploadedFile(ctx, nuget_module.SymbolsPackage)
   513  	defer func() {
   514  		for _, c := range closables {
   515  			c.Close()
   516  		}
   517  	}()
   518  	if np == nil {
   519  		return
   520  	}
   521  
   522  	pdbs, err := nuget_module.ExtractPortablePdb(buf, buf.Size())
   523  	if err != nil {
   524  		if errors.Is(err, util.ErrInvalidArgument) {
   525  			apiError(ctx, http.StatusBadRequest, err)
   526  		} else {
   527  			apiError(ctx, http.StatusInternalServerError, err)
   528  		}
   529  		return
   530  	}
   531  	defer pdbs.Close()
   532  
   533  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   534  		apiError(ctx, http.StatusInternalServerError, err)
   535  		return
   536  	}
   537  
   538  	pi := &packages_service.PackageInfo{
   539  		Owner:       ctx.Package.Owner,
   540  		PackageType: packages_model.TypeNuGet,
   541  		Name:        np.ID,
   542  		Version:     np.Version,
   543  	}
   544  
   545  	_, err = packages_service.AddFileToExistingPackage(
   546  		ctx,
   547  		pi,
   548  		&packages_service.PackageFileCreationInfo{
   549  			PackageFileInfo: packages_service.PackageFileInfo{
   550  				Filename: strings.ToLower(fmt.Sprintf("%s.%s.snupkg", np.ID, np.Version)),
   551  			},
   552  			Creator: ctx.Doer,
   553  			Data:    buf,
   554  			IsLead:  false,
   555  		},
   556  	)
   557  	if err != nil {
   558  		switch err {
   559  		case packages_model.ErrPackageNotExist:
   560  			apiError(ctx, http.StatusNotFound, err)
   561  		case packages_model.ErrDuplicatePackageFile:
   562  			apiError(ctx, http.StatusConflict, err)
   563  		case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   564  			apiError(ctx, http.StatusForbidden, err)
   565  		default:
   566  			apiError(ctx, http.StatusInternalServerError, err)
   567  		}
   568  		return
   569  	}
   570  
   571  	for _, pdb := range pdbs {
   572  		_, err := packages_service.AddFileToExistingPackage(
   573  			ctx,
   574  			pi,
   575  			&packages_service.PackageFileCreationInfo{
   576  				PackageFileInfo: packages_service.PackageFileInfo{
   577  					Filename:     strings.ToLower(pdb.Name),
   578  					CompositeKey: strings.ToLower(pdb.ID),
   579  				},
   580  				Creator: ctx.Doer,
   581  				Data:    pdb.Content,
   582  				IsLead:  false,
   583  				Properties: map[string]string{
   584  					nuget_module.PropertySymbolID: strings.ToLower(pdb.ID),
   585  				},
   586  			},
   587  		)
   588  		if err != nil {
   589  			switch err {
   590  			case packages_model.ErrDuplicatePackageFile:
   591  				apiError(ctx, http.StatusConflict, err)
   592  			case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   593  				apiError(ctx, http.StatusForbidden, err)
   594  			default:
   595  				apiError(ctx, http.StatusInternalServerError, err)
   596  			}
   597  			return
   598  		}
   599  	}
   600  
   601  	ctx.Status(http.StatusCreated)
   602  }
   603  
   604  func processUploadedFile(ctx *context.Context, expectedType nuget_module.PackageType) (*nuget_module.Package, *packages_module.HashedBuffer, []io.Closer) {
   605  	closables := make([]io.Closer, 0, 2)
   606  
   607  	upload, needToClose, err := ctx.UploadStream()
   608  	if err != nil {
   609  		apiError(ctx, http.StatusBadRequest, err)
   610  		return nil, nil, closables
   611  	}
   612  
   613  	if needToClose {
   614  		closables = append(closables, upload)
   615  	}
   616  
   617  	buf, err := packages_module.CreateHashedBufferFromReader(upload)
   618  	if err != nil {
   619  		apiError(ctx, http.StatusInternalServerError, err)
   620  		return nil, nil, closables
   621  	}
   622  	closables = append(closables, buf)
   623  
   624  	np, err := nuget_module.ParsePackageMetaData(buf, buf.Size())
   625  	if err != nil {
   626  		if errors.Is(err, util.ErrInvalidArgument) {
   627  			apiError(ctx, http.StatusBadRequest, err)
   628  		} else {
   629  			apiError(ctx, http.StatusInternalServerError, err)
   630  		}
   631  		return nil, nil, closables
   632  	}
   633  	if np.PackageType != expectedType {
   634  		apiError(ctx, http.StatusBadRequest, errors.New("unexpected package type"))
   635  		return nil, nil, closables
   636  	}
   637  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   638  		apiError(ctx, http.StatusInternalServerError, err)
   639  		return nil, nil, closables
   640  	}
   641  	return np, buf, closables
   642  }
   643  
   644  // https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request
   645  func DownloadSymbolFile(ctx *context.Context) {
   646  	filename := ctx.Params("filename")
   647  	guid := ctx.Params("guid")[:32]
   648  	filename2 := ctx.Params("filename2")
   649  
   650  	if filename != filename2 {
   651  		apiError(ctx, http.StatusBadRequest, nil)
   652  		return
   653  	}
   654  
   655  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   656  		OwnerID:     ctx.Package.Owner.ID,
   657  		PackageType: packages_model.TypeNuGet,
   658  		Query:       filename,
   659  		Properties: map[string]string{
   660  			nuget_module.PropertySymbolID: strings.ToLower(guid),
   661  		},
   662  	})
   663  	if err != nil {
   664  		apiError(ctx, http.StatusInternalServerError, err)
   665  		return
   666  	}
   667  	if len(pfs) != 1 {
   668  		apiError(ctx, http.StatusNotFound, nil)
   669  		return
   670  	}
   671  
   672  	s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
   673  	if err != nil {
   674  		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
   675  			apiError(ctx, http.StatusNotFound, err)
   676  			return
   677  		}
   678  		apiError(ctx, http.StatusInternalServerError, err)
   679  		return
   680  	}
   681  
   682  	helper.ServePackageFile(ctx, s, u, pf)
   683  }
   684  
   685  // DeletePackage hard deletes the package
   686  // https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#delete-a-package
   687  func DeletePackage(ctx *context.Context) {
   688  	packageName := ctx.Params("id")
   689  	packageVersion := ctx.Params("version")
   690  
   691  	err := packages_service.RemovePackageVersionByNameAndVersion(
   692  		ctx,
   693  		ctx.Doer,
   694  		&packages_service.PackageInfo{
   695  			Owner:       ctx.Package.Owner,
   696  			PackageType: packages_model.TypeNuGet,
   697  			Name:        packageName,
   698  			Version:     packageVersion,
   699  		},
   700  	)
   701  	if err != nil {
   702  		if err == packages_model.ErrPackageNotExist {
   703  			apiError(ctx, http.StatusNotFound, err)
   704  			return
   705  		}
   706  		apiError(ctx, http.StatusInternalServerError, err)
   707  	}
   708  
   709  	ctx.Status(http.StatusNoContent)
   710  }