diff --git a/src/BaGet.Core/IUrlGenerator.cs b/src/BaGet.Core/IUrlGenerator.cs
index 443dba796..f992e6287 100644
--- a/src/BaGet.Core/IUrlGenerator.cs
+++ b/src/BaGet.Core/IUrlGenerator.cs
@@ -101,5 +101,13 @@ public interface IUrlGenerator
/// The package's ID
/// The package's version
string GetPackageIconDownloadUrl(string id, NuGetVersion version);
+
+ ///
+ /// Get the package repackage url with a replacement token for the new version {newVersion}
+ ///
+ /// The package ID
+ /// The package version
+ /// The package new version
+ string GetPackageRepackageUrl(string id, NuGetVersion version, NuGetVersion newVersion);
}
}
diff --git a/src/BaGet.Web/BaGetEndpointBuilder.cs b/src/BaGet.Web/BaGetEndpointBuilder.cs
index d485eb40e..4e6b62dad 100644
--- a/src/BaGet.Web/BaGetEndpointBuilder.cs
+++ b/src/BaGet.Web/BaGetEndpointBuilder.cs
@@ -46,6 +46,13 @@ public void MapPackagePublishRoutes(IEndpointRouteBuilder endpoints)
pattern: "api/v2/package/{id}/{version}",
defaults: new { controller = "PackagePublish", action = "Relist" },
constraints: new { httpMethod = new HttpMethodRouteConstraint("POST") });
+
+ // This is an unofficial API to repackage a package as a new version
+ endpoints.MapControllerRoute(
+ name: Routes.RepackageRouteName,
+ pattern: "api/v2/package/{id}/{version}/repackage/{newVersion}",
+ defaults: new { controller = "PackagePublish", action = "Repackage" },
+ constraints: new { httpMethod = new HttpMethodRouteConstraint("POST") });
}
public void MapSymbolRoutes(IEndpointRouteBuilder endpoints)
diff --git a/src/BaGet.Web/BaGetUrlGenerator.cs b/src/BaGet.Web/BaGetUrlGenerator.cs
index 48b61bb57..d3c7a3f3c 100644
--- a/src/BaGet.Web/BaGetUrlGenerator.cs
+++ b/src/BaGet.Web/BaGetUrlGenerator.cs
@@ -161,5 +161,22 @@ private string AbsoluteUrl(string relativePath)
"/",
relativePath);
}
+
+ public string GetPackageRepackageUrl(string id, NuGetVersion version, NuGetVersion newVersion)
+ {
+ id = id.ToLowerInvariant();
+ var versionString = version.ToNormalizedString().ToLowerInvariant();
+ var newVersionString = newVersion.ToNormalizedString().ToLowerInvariant();
+
+ return _linkGenerator.GetUriByRouteValues(
+ _httpContextAccessor.HttpContext,
+ Routes.RepackageRouteName,
+ values: new
+ {
+ Id = id,
+ Version = versionString,
+ NewVersion = newVersionString
+ });
+ }
}
}
diff --git a/src/BaGet.Web/Controllers/PackagePublishController.cs b/src/BaGet.Web/Controllers/PackagePublishController.cs
index 8adb0b5f4..efa028c47 100644
--- a/src/BaGet.Web/Controllers/PackagePublishController.cs
+++ b/src/BaGet.Web/Controllers/PackagePublishController.cs
@@ -1,10 +1,17 @@
using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
using BaGet.Core;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using NuGet.Packaging;
using NuGet.Versioning;
namespace BaGet.Web
@@ -15,6 +22,7 @@ public class PackagePublishController : Controller
private readonly IPackageIndexingService _indexer;
private readonly IPackageDatabase _packages;
private readonly IPackageDeletionService _deleteService;
+ private readonly IPackageStorageService _storageService;
private readonly IOptionsSnapshot _options;
private readonly ILogger _logger;
@@ -23,6 +31,7 @@ public PackagePublishController(
IPackageIndexingService indexer,
IPackageDatabase packages,
IPackageDeletionService deletionService,
+ IPackageStorageService storageService,
IOptionsSnapshot options,
ILogger logger)
{
@@ -30,6 +39,7 @@ public PackagePublishController(
_indexer = indexer ?? throw new ArgumentNullException(nameof(indexer));
_packages = packages ?? throw new ArgumentNullException(nameof(packages));
_deleteService = deletionService ?? throw new ArgumentNullException(nameof(deletionService));
+ _storageService = storageService ?? throw new ArgumentNullException(nameof(storageService));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -135,5 +145,84 @@ public async Task Relist(string id, string version, CancellationT
return NotFound();
}
}
+
+ [HttpPost]
+ public async Task Repackage(string id, string version, string newVersion, CancellationToken cancellationToken)
+ {
+ if (_options.Value.IsReadOnlyMode)
+ {
+ return Unauthorized();
+ }
+
+ if (!NuGetVersion.TryParse(version, out var nugetVersion))
+ {
+ return NotFound();
+ }
+
+ if (!NuGetVersion.TryParse(newVersion, out var newNugetVersion))
+ {
+ return BadRequest("Invalid version");
+ }
+
+ if (!await _authentication.AuthenticateAsync(Request.GetApiKey(), cancellationToken))
+ {
+ return Unauthorized();
+ }
+
+ var packageStream = await _storageService.GetPackageStreamAsync(id, nugetVersion, cancellationToken);
+
+ if (packageStream == null)
+ {
+ return NotFound();
+ }
+
+ if(await _packages.ExistsAsync(id, newNugetVersion, cancellationToken))
+ {
+ return BadRequest("Package version already exists");
+ }
+
+ using (var ms = new MemoryStream())
+ {
+ await packageStream.CopyToAsync(ms);
+ using (var archive = new ZipArchive(ms, ZipArchiveMode.Update))
+ {
+ var nuspecEntry = archive.Entries.FirstOrDefault(x => x.Name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase));
+
+ if (nuspecEntry == null)
+ {
+ return BadRequest("Nuget file is missing nuspec");
+ }
+
+ string updatedNuspec;
+ using (var nuspecStream = nuspecEntry.Open())
+ {
+ var doc = XDocument.Load(nuspecStream);
+ var ns = XNamespace.Get("http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd");
+ doc.Descendants(ns + "version").First().Value = newNugetVersion.ToNormalizedString();
+ using(var docMs = new MemoryStream())
+ {
+ doc.Save(docMs);
+ updatedNuspec = Encoding.UTF8.GetString(docMs.ToArray());
+ }
+ }
+
+ nuspecEntry.Delete();
+ nuspecEntry = archive.CreateEntry($"{id}.nuspec");
+
+ using(var writer = new StreamWriter(nuspecEntry.Open()))
+ {
+ writer.Write(updatedNuspec);
+ }
+ }
+
+
+ using(var repackgedStream = new MemoryStream(ms.ToArray()))
+ {
+ await _indexer.IndexAsync(repackgedStream, cancellationToken);
+ }
+
+ return Ok();
+ }
+ }
}
}
diff --git a/src/BaGet.Web/Pages/Package.cshtml b/src/BaGet.Web/Pages/Package.cshtml
index 8e42cd6d3..ed6f24554 100644
--- a/src/BaGet.Web/Pages/Package.cshtml
+++ b/src/BaGet.Web/Pages/Package.cshtml
@@ -239,6 +239,11 @@ else
Download package
+
+
+
+ Repackage
+
@@ -268,6 +273,44 @@ else
}
+
+
+
}
@if (Model.Found)
@@ -321,6 +364,35 @@ else
}
];
+
+ @section Scripts {
+
+ }
}
@functions {
diff --git a/src/BaGet.Web/Pages/Package.cshtml.cs b/src/BaGet.Web/Pages/Package.cshtml.cs
index 8841648e7..eda6ead71 100644
--- a/src/BaGet.Web/Pages/Package.cshtml.cs
+++ b/src/BaGet.Web/Pages/Package.cshtml.cs
@@ -8,6 +8,7 @@
using Markdig;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Options;
using NuGet.Frameworks;
using NuGet.Versioning;
@@ -20,6 +21,7 @@ public class PackageModel : PageModel
private readonly IPackageService _packages;
private readonly IPackageContentService _content;
private readonly ISearchService _search;
+ private readonly IOptionsSnapshot _options;
private readonly IUrlGenerator _url;
static PackageModel()
@@ -33,11 +35,13 @@ public PackageModel(
IPackageService packages,
IPackageContentService content,
ISearchService search,
+ IOptionsSnapshot options,
IUrlGenerator url)
{
_packages = packages ?? throw new ArgumentNullException(nameof(packages));
_content = content ?? throw new ArgumentNullException(nameof(content));
_search = search ?? throw new ArgumentNullException(nameof(search));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
_url = url ?? throw new ArgumentNullException(nameof(url));
}
@@ -59,6 +63,8 @@ public PackageModel(
public string IconUrl { get; private set; }
public string LicenseUrl { get; private set; }
public string PackageDownloadUrl { get; private set; }
+ public string RepackageUrl { get; private set; }
+ public string ApiKey { get; private set; }
public async Task OnGetAsync(string id, string version, CancellationToken cancellationToken)
{
@@ -84,6 +90,8 @@ public async Task OnGetAsync(string id, string version, CancellationToken cancel
return;
}
+ ApiKey = _options.Value.ApiKey;
+
var packageVersion = Package.Version;
Found = true;
@@ -108,6 +116,7 @@ public async Task OnGetAsync(string id, string version, CancellationToken cancel
: Package.IconUrlString;
LicenseUrl = Package.LicenseUrlString;
PackageDownloadUrl = _url.GetPackageDownloadUrl(Package.Id, packageVersion);
+ RepackageUrl = _url.GetPackageRepackageUrl(Package.Id, packageVersion, NuGetVersion.Parse("0.0.0"));
}
private IReadOnlyList ToDependencyGroups(Package package)
diff --git a/src/BaGet.Web/Routes.cs b/src/BaGet.Web/Routes.cs
index ff31ad389..5907dabe0 100644
--- a/src/BaGet.Web/Routes.cs
+++ b/src/BaGet.Web/Routes.cs
@@ -8,6 +8,7 @@ public class Routes
public const string DeleteRouteName = "delete";
public const string RelistRouteName = "relist";
public const string SearchRouteName = "search";
+ public const string RepackageRouteName = "repackage";
public const string AutocompleteRouteName = "autocomplete";
public const string DependentsRouteName = "dependents";
public const string RegistrationIndexRouteName = "registration-index";
diff --git a/src/BaGet/appsettings.json b/src/BaGet/appsettings.json
index f431f367a..12976a840 100644
--- a/src/BaGet/appsettings.json
+++ b/src/BaGet/appsettings.json
@@ -10,7 +10,7 @@
"Storage": {
"Type": "FileSystem",
- "Path": ""
+ "Path": "D:\\baget-packages"
},
"Search": {
diff --git a/tests/BaGet.Web.Tests/Pages/PackageModelFacts.cs b/tests/BaGet.Web.Tests/Pages/PackageModelFacts.cs
index 51a7d1ccb..1baaebef7 100644
--- a/tests/BaGet.Web.Tests/Pages/PackageModelFacts.cs
+++ b/tests/BaGet.Web.Tests/Pages/PackageModelFacts.cs
@@ -1,10 +1,11 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BaGet.Core;
+using Microsoft.Extensions.Options;
using Moq;
using NuGet.Versioning;
using Xunit;
@@ -17,6 +18,7 @@ public class PackageModelFacts
private readonly Mock _packages;
private readonly Mock _search;
private readonly Mock _url;
+ private readonly Mock> _options;
private readonly PackageModel _target;
private readonly CancellationToken _cancellation = CancellationToken.None;
@@ -27,10 +29,12 @@ public PackageModelFacts()
_packages = new Mock();
_search = new Mock();
_url = new Mock();
+ _options = new Mock>();
_target = new PackageModel(
_packages.Object,
_content.Object,
_search.Object,
+ _options.Object,
_url.Object);
_search