using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NuGet.Versioning; using nuget_host.Data; using nuget_host.Entities; namespace nuget_host.Controllers { [AllowAnonymous] public partial class PackagesController : Controller { private readonly ILogger logger; private readonly IDataProtector protector; private readonly NugetSettings nugetSettings; ApplicationDbContext dbContext; public PackagesController( ILoggerFactory loggerFactory, IDataProtectionProvider provider, IOptions nugetOptions, ApplicationDbContext dbContext) { logger = loggerFactory.CreateLogger(); nugetSettings = nugetOptions.Value; protector = provider.CreateProtector(nugetSettings.ProtectionTitle); this.dbContext = dbContext; } const string defaultSemVer = "2.0.0"; // dotnet add . package -s http://localhost:5000/packages applec // packages/FindPackagesById()?id='applec'&semVerLevel=2.0.0 // Search // GET {@id}?q={QUERY}&skip={SKIP}&take={TAKE}&prerelease={PRERELEASE}&semVerLevel={SEMVERLEVEL}&packageType={PACKAGETYPE} private readonly Resource[] ressources = { new Resource { id = "package/index.json", type ="SearchAutocompleteService/3.5.0", comment = "Auto complete service" }, new Resource { id = "package/index.json", type ="SearchQueryService/3.5.0", comment = "Search Query service" }, new Resource { id = "package", type ="PackagePublish/2.0.0", comment = "Package Publish service" }, new Resource { id = "package", type = "PackageBaseAddress/3.0.0", comment = "Package Base Address service" } }; const string _pkgRootPrefix = "~/package"; [HttpGet("~/index.json")] public IActionResult ApiIndex() { return Ok(ressources); } [HttpGet(_pkgRootPrefix + "/index.json")] public IActionResult Index( string q, string semVerLevel = defaultSemVer, bool prerelease = false, string packageType = null, int skip = 0, int take = 25) { if (string.IsNullOrEmpty(q)) { ModelState.AddModelError("q", "no value"); } if (take > maxTake) { ModelState.AddModelError("take", "Maximum exceeded"); } if (ModelState.IsValid) { var scope = dbContext.Packages .Include(p => p.Versions) .Where( p => (CamelCaseMatch(p.Id, q) || SeparatedByMinusMatch(p.Id, q)) && (prerelease || p.Versions.Any(v => !v.IsPrerelease)) && (packageType == null || p.Versions.Any(v => v.Type == packageType)) ); var result = new { totalHits = scope.Count(), data = scope.OrderBy(p => p.Id) .Skip(skip).Take(take).ToArray() }; return Ok(result); } return BadRequest(new { error = ModelState }); } protected static bool CamelCaseMatch(string id, string q) { // Assert.False (q==null); string query = q; if (query.Length == 0) return false; while (id.Length > 0) { int i = 0; while (id.Length > i && char.IsLower(id[i])) i++; if (i == 0) break; id = id.Substring(i); if (id.StartsWith(q, System.StringComparison.OrdinalIgnoreCase)) return true; } return false; } protected static bool SeparatedByMinusMatch(string id, string q) { foreach (var part in id.Split('-')) { if (part.StartsWith(q, System.StringComparison.OrdinalIgnoreCase)) return true; } return false; } const int maxTake = 100; // GET /autocomplete?id=nuget.protocol&prerelease=true [HttpGet(_pkgRootPrefix + "/autocomplete")] public IActionResult AutoComplete( string id, string semVerLevel = defaultSemVer, bool prerelease = false, string packageType = null, int skip = 0, int take = 25) { if (take > maxTake) { ModelState.AddModelError("take", "Maximum exceeded"); return BadRequest(ModelState); } var scope = dbContext.PackageVersions.Where( v => v.PackageId == id && (prerelease || !v.IsPrerelease) && (packageType == null || v.Type == packageType) ) .OrderBy(v => v.FullString); return Ok(new { data = scope.Select(v => v.FullString) .Skip(skip).Take(take).ToArray(), totalHits = scope.Count() }); } // TODO GET {@id}/{LOWER_ID}/index.json // LOWER_ID URL string yes The package ID, lowercased // response : versions array of strings yes The versions available [HttpGet(_pkgRootPrefix + "/{id}/{lower}/index.json")] public IActionResult GetVersions( string id, string lower, bool prerelease = false, string packageType = null, int skip = 0, int take = 25) { if (take > maxTake) { ModelState.AddModelError("take", "Maximum exceeded"); } // NugetVersion if (!NuGetVersion.TryParse(lower, out NuGetVersion parsedVersion)) { ModelState.AddModelError("lower", "invalid version string"); } if (!ModelState.IsValid) { return BadRequest(ModelState); } return Ok(new { // TODO stocker MetaData plutôt que FullString en base, // et en profiter pour corriger ce listing versions = dbContext.PackageVersions.Where( v => v.PackageId == id && (prerelease || !v.IsPrerelease) && (packageType == null || v.Type == packageType) && (parsedVersion.CompareTo(new SemanticVersion(v.Major, v.Minor, v.Patch)) < 0) ) .OrderBy(v => v.FullString) .Select(v => v.FullString) .Skip(skip).Take(take).ToArray() }); } // TODO GET GET {@id}/{LOWER_ID}/{LOWER_VERSION}/{LOWER_ID}.{LOWER_VERSION}.nupkg // LOWER_ID URL string yes The package ID, lowercase // LOWER_VERSION URL string yes The package version, normalized and lowercased // response 200 : the package [HttpGet(_pkgRootPrefix + "/{id}/{lower}/{idf}.{lowerf}.nupkg")] public IActionResult GetPackage( [FromRoute] string id, [FromRoute] string lower, [FromRoute] string idf, [FromRoute] string lowerf) { var pkgpath = Path.Combine(nugetSettings.PackagesRootDir, id, lower, $"{idf}.{lowerf}.nupkg" ); FileInfo pkgfi = new FileInfo(pkgpath); return File(pkgfi.OpenRead(), "application/zip; charset=binary"); } // TODO GET {@id}/{LOWER_ID}/{LOWER_VERSION}/{LOWER_ID}.nuspec // response 200 : the nuspec [HttpGet(_pkgRootPrefix + "/{id}/{lower}/{idf}.{lowerf}.nuspec")] public IActionResult GetNuspec( [FromRoute][SafeName][Required] string id, [FromRoute][SafeName][Required] string lower, [FromRoute][SafeName][Required] string idf, [FromRoute][SafeName][Required] string lowerf) { var pkgpath = Path.Combine(nugetSettings.PackagesRootDir, id, lower, $"{idf}.{lowerf}.nuspec"); FileInfo pkgfi = new FileInfo(pkgpath); return File(pkgfi.OpenRead(), "text/xml; charset=utf-8"); } } }