using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using isn.abst; using isn.Abstract; using isnd.Data; using isnd.Data.Catalog; using isnd.Data.Packages; using isnd.Entities; using isnd.Interfaces; using isnd.ViewModels; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using NuGet.Versioning; using Unleash; namespace isnd.Services { public class PackageManager : IPackageManager { public const string BASE_API_LEVEL = "3.0.0"; readonly ApplicationDbContext dbContext; public PackageManager(ApplicationDbContext dbContext, IOptions siteConfigOptionsOptions) { this.dbContext = dbContext; isndSettings = siteConfigOptionsOptions.Value; apiBase = isndSettings.ExternalUrl + Constants.ApiVersionPrefix; } public IEnumerable GetResources(IUnleash unleashClient) { var res = new List { new Resource(apiBase + ApiConfig.Package, "PackagePublish/2.0.0") { Comment = "Package Publish service" }, // under dev, only leash in release mode new Resource(apiBase + ApiConfig.Package + "/{id}/{version}", "PackageDetailsUriTemplate/5.1.0") { Comment = "URI template used by NuGet Client to construct details URL for packages" }, new Resource(apiBase + ApiConfig.Nuget, "PackageBaseAddress/3.0.0") { Comment = "Package Base Address service - " + "Base URL of where NuGet packages are stored, in the format " + "https:///nupkg/{id-lower}/{version-lower}/{id-lower}.{version-lower}.nupkg" }, new Resource(apiBase + ApiConfig.AutoComplete, "SearchAutocompleteService/" + BASE_API_LEVEL) { Comment = "Auto complete service" }, new Resource(apiBase + ApiConfig.Search, "SearchQueryService/" + BASE_API_LEVEL) { Comment = "Search Query service" }, new Resource(apiBase + ApiConfig.Registration, "RegistrationsBaseUrl/Versioned") { Comment = "Base URL of storage where package registration info is stored. " + "This base URL includes SemVer 2.0.0 packages." } }; // TODO ? RegistrationsBaseUrl RegistrationsBaseUrl/v* Catalog return res; } public AutoCompleteResult AutoComplete(string id, int skip, int take, bool prerelease = false, string packageType = null) { var scope = dbContext.PackageVersions.Where( v => v.PackageId == id && (prerelease || !v.IsPrerelease) && (packageType == null || v.Type == packageType) ) .OrderBy(v => v.FullString); return new AutoCompleteResult { totalHits = scope.Count(), data = scope.Select(v => v.FullString) .Skip(skip).Take(take).ToArray() }; } public string[] GetVersions( string id, NuGetVersion parsedVersion, bool prerelease = false, string packageType = null, int skip = 0, int take = 25) { return 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.NugetVersion) .Select(v => v.FullString) .Skip(skip).Take(take).ToArray(); } public string CatalogBaseUrl => apiBase; private IsndSettings isndSettings; private readonly string apiBase; public virtual async Task GetCatalogIndexAsync() { return await ÛpdateCatalogForAsync(null); } public async Task ÛpdateCatalogForAsync (Commit reason = null) { int i = 0; string baseid = apiBase + ApiConfig.Catalog; string bidreg = $"{apiBase}{ApiConfig.Registration}"; PackageRegistration index = new PackageRegistration(baseid); var scope = await dbContext.Commits.OrderBy(c => c.TimeStamp).ToArrayAsync(); i = isndSettings.CatalogPageLen; foreach (var commit in scope) { var validPkgs = (await dbContext.Packages .Include(po => po.Owner) .Include(pkg => pkg.Versions) .Include(pkg => pkg.LatestVersion) .ToArrayAsync()) .GroupBy((q) => q.Id); foreach (var pkgid in validPkgs) { CatalogPage page = index.Items.FirstOrDefault (p => p.GetPackageId() == pkgid.Key); if (page == null) { page = new CatalogPage(pkgid.Key, apiBase); index.Items.Add(page); } foreach (var pkgv in pkgid) { page.AddVersionRange(pkgv.Versions); } } reason = commit; i++; } if (reason != null) { index.CommitId = reason.CommitId; index.CommitTimeStamp = reason.CommitTimeStamp; } else { // From a fresh db index.CommitId = "none"; } return index; } public async Task DeletePackageAsync (string pkgid, string version, string type) { // TODO deletion on disk var commit = new Commit { Action = PackageAction.DeletePackage, TimeStamp = DateTime.Now }; dbContext.Commits.Add(commit); var pkg = await dbContext.PackageVersions.SingleOrDefaultAsync( v => v.PackageId == pkgid && v.FullString == version && v.Type == type ); if (pkg == null) { return new PackageDeletionReport { Deleted = false }; } dbContext.PackageVersions.Remove(pkg); await dbContext.SaveChangesAsync(); await ÛpdateCatalogForAsync(commit); return new PackageDeletionReport { Deleted = true, DeletedVersion = pkg }; } public async Task GetPackageAsync (string pkgid, string version, string type) { return await dbContext.PackageVersions.SingleOrDefaultAsync( v => v.PackageId == pkgid && v.FullString == version && v.Type == type ); } public async Task GetCatalogEntryAsync (string pkgId, string semver = null, string pkgType = null) { return (await dbContext.PackageVersions .Include(v => v.Package).Include(v => v.Package.Owner) .Where(v => v.PackageId == pkgId && v.FullString == semver).SingleOrDefaultAsync()).ToPackage( apiBase); } public async Task UserAskForPackageDeletionAsync (string uid, string id, string lower, string type) { PackageVersion packageVersion = await dbContext.PackageVersions .Include(pv => pv.Package) .FirstOrDefaultAsync(m => m.PackageId == id && m.FullString == lower && m.Type == type); if (packageVersion == null) return null; if (packageVersion.Package.OwnerId != uid) return null; return new PackageDeletionReport { Deleted = true, DeletedVersion = packageVersion }; } public IEnumerable SearchCatalogEntriesById (string pkgId, string semver, string pkgType) { return dbContext.PackageVersions .Include(v => v.Package) .Include(v => v.Package.Owner) .Include(v => v.LatestCommit) .Where(v => v.PackageId == pkgId && semver == v.FullString && (pkgType == null || pkgType == v.Type)) .OrderByDescending(p => p.CommitNId) .Select(p => p.ToPackage(apiBase)) ; } public PackageVersion GetPackage(string pkgId, string semver, string pkgType) { return dbContext.PackageVersions .Include(v => v.Package) .Include(v => v.LatestCommit) .Single(v => v.PackageId == pkgId && semver == v.FullString && (pkgType == null || pkgType == v.Type)); } public async Task GetPackageRegistrationIndexAsync (PackageRegistrationQuery query) { // RegistrationPageIndexAndQuery if (string.IsNullOrWhiteSpace(query.Query)) return null; query.Query = query.Query.ToLower(); var scope = await dbContext.Packages.Include(p => p.Versions).Include(p => p.Owner) .Include(p=>p.LatestVersion) .Where(p => p.Id.ToLower() == query.Query).Skip(query.Skip).Take(query.Take).ToListAsync(); string bid = $"{apiBase}{ApiConfig.Registration}"; return new PackageRegistration(bid, query.Query, apiBase, scope); } public async Task SearchPackageAsync(PackageRegistrationQuery query) { string bid = $"{apiBase}{ApiConfig.Registration}"; if (query.Query == null) query.Query = ""; var scope = (await dbContext.Packages .Include(p => p.Versions) .Include(p => p.Owner) .Include(p=>p.LatestVersion) .ToListAsync()) .Where(p => Matching(p, query)) ; var total = scope.Count(); var pkgs = scope.Skip(query.Skip).Take(query.Take); return new PackageRegistration(bid, query.Query, apiBase, pkgs); } private static bool MatchingExact(Data.Packages.Package p, PackageRegistrationQuery query) { return p.Id == query.Query && (query.Prerelease || p.Versions.Any(v => !v.IsPrerelease)); } private static bool Matching(Data.Packages.Package p, PackageRegistrationQuery query) { return p.Id.StartsWith(query.Query) && (query.Prerelease || p.Versions.Any(v => !v.IsPrerelease)); } } }