Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for package repackaging #768

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/BaGet.Core/IUrlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,13 @@ public interface IUrlGenerator
/// <param name="id">The package's ID</param>
/// <param name="version">The package's version</param>
string GetPackageIconDownloadUrl(string id, NuGetVersion version);

/// <summary>
/// Get the package repackage url with a replacement token for the new version {newVersion}
/// </summary>
/// <param name="id">The package ID</param>
/// <param name="version">The package version</param>
/// <param name="newVersion">The package new version</param>
string GetPackageRepackageUrl(string id, NuGetVersion version, NuGetVersion newVersion);
}
}
7 changes: 7 additions & 0 deletions src/BaGet.Web/BaGetEndpointBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions src/BaGet.Web/BaGetUrlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
}
}
89 changes: 89 additions & 0 deletions src/BaGet.Web/Controllers/PackagePublishController.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<BaGetOptions> _options;
private readonly ILogger<PackagePublishController> _logger;

Expand All @@ -23,13 +31,15 @@ public PackagePublishController(
IPackageIndexingService indexer,
IPackageDatabase packages,
IPackageDeletionService deletionService,
IPackageStorageService storageService,
IOptionsSnapshot<BaGetOptions> options,
ILogger<PackagePublishController> logger)
{
_authentication = authentication ?? throw new ArgumentNullException(nameof(authentication));
_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));
}
Expand Down Expand Up @@ -135,5 +145,84 @@ public async Task<IActionResult> Relist(string id, string version, CancellationT
return NotFound();
}
}

[HttpPost]
public async Task<IActionResult> 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();
}
}
}
}
72 changes: 72 additions & 0 deletions src/BaGet.Web/Pages/Package.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ else
<i class="ms-Icon ms-Icon--CloudDownload"></i>
<a href="@Model.PackageDownloadUrl">Download package</a>
</li>

<li>
<i class="ms-Icon ms-Icon--Package"></i>
<a href="#" data-toggle="modal" data-target="#repackage">Repackage</a>
</li>
</ul>
</div>

Expand Down Expand Up @@ -268,6 +273,44 @@ else
}
</aside>
</div>

<!-- Modal -->
<form id="repackage-form">
<div class="modal fade" id="repackage" tabindex="-1" role="dialog" aria-labelledby="repackageLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="repackageLabel">Repackage Nuget Package</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="repackage-alert" class="alert alert-danger alert-dismissible fade hide" role="alert">
<div id="repackage-error"></div>
</div>
<div class="form-group">
<label>Package name</label>
<input type="text" readonly class="form-control" value="@Model.Package.Id" />
</div>
<div class="form-group">
<label>Package version</label>
<input type="text" readonly class="form-control" value="@Model.Package.NormalizedVersionString" />
</div>

<div class="form-group">
<label for="repackage-version">New Version</label>
<input type="text" class="form-control" id="repackage-version" value="@Model.Package.NormalizedVersionString">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" id="repackage-button" class="btn btn-primary">Repackage</button>
</div>
</div>
</div>
</div>
</form>
}

@if (Model.Found)
Expand Down Expand Up @@ -321,6 +364,35 @@ else
}
];
</script>

@section Scripts {
<script>
const repackageButton = $("#repackage-button");

repackageButton.click(() => {
const repackageForm = $("#repackage-form");
const repackageVersion = $("#repackage-version");

const postUrl = "@Model.RepackageUrl".replace("0.0.0", repackageVersion.val());

$.ajax({
type: "POST",
url: postUrl,
success: () => {
location.href = "@Url.Page("package", new{ id= Model.Package.Id, version = "0.0.0"})".replace("0.0.0", repackageVersion.val());
},
error: response => {
$("#repackage-error").text(response.responseText);
$("#repackage-alert").removeClass("fade").addClass("show")
},
headers: {
"X-NuGet-ApiKey": "@Model.ApiKey"
}
});
});

</script>
}
}

@functions {
Expand Down
9 changes: 9 additions & 0 deletions src/BaGet.Web/Pages/Package.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,6 +21,7 @@ public class PackageModel : PageModel
private readonly IPackageService _packages;
private readonly IPackageContentService _content;
private readonly ISearchService _search;
private readonly IOptionsSnapshot<BaGetOptions> _options;
private readonly IUrlGenerator _url;

static PackageModel()
Expand All @@ -33,11 +35,13 @@ public PackageModel(
IPackageService packages,
IPackageContentService content,
ISearchService search,
IOptionsSnapshot<BaGetOptions> 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));
}

Expand All @@ -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)
{
Expand All @@ -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;
Expand All @@ -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<DependencyGroupModel> ToDependencyGroups(Package package)
Expand Down
1 change: 1 addition & 0 deletions src/BaGet.Web/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/BaGet/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

"Storage": {
"Type": "FileSystem",
"Path": ""
"Path": "D:\\baget-packages"
},

"Search": {
Expand Down
6 changes: 5 additions & 1 deletion tests/BaGet.Web.Tests/Pages/PackageModelFacts.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,6 +18,7 @@ public class PackageModelFacts
private readonly Mock<IPackageService> _packages;
private readonly Mock<ISearchService> _search;
private readonly Mock<IUrlGenerator> _url;
private readonly Mock<IOptionsSnapshot<BaGetOptions>> _options;
private readonly PackageModel _target;

private readonly CancellationToken _cancellation = CancellationToken.None;
Expand All @@ -27,10 +29,12 @@ public PackageModelFacts()
_packages = new Mock<IPackageService>();
_search = new Mock<ISearchService>();
_url = new Mock<IUrlGenerator>();
_options = new Mock<IOptionsSnapshot<BaGetOptions>>();
_target = new PackageModel(
_packages.Object,
_content.Object,
_search.Object,
_options.Object,
_url.Object);

_search
Expand Down