You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

nuget.go 19KB

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