From a8cbfb711eb539f984cd6d4652c925f8f12c5400 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 4 Oct 2019 16:21:05 -0400 Subject: [PATCH] Added ability to install modules and skins at run-time directly from Nuget --- .../Modules/Admin/ModuleDefinitions/Add.razor | 48 ++++- Oqtane.Client/Modules/Admin/Themes/Add.razor | 49 ++++- Oqtane.Client/Modules/Controls/Pager.razor | 9 +- .../Interfaces/IModuleDefinitionService.cs | 2 +- .../Services/Interfaces/IPackageService.cs | 12 ++ .../Services/ModuleDefinitionService.cs | 2 +- Oqtane.Client/Services/PackageService.cs | 40 ++++ Oqtane.Client/Startup.cs | 1 + .../Controllers/PackageController.cs | 171 ++++++++++++++++++ Oqtane.Server/Controllers/SiteController.cs | 13 +- Oqtane.Server/Startup.cs | 1 + Oqtane.Shared/Models/Package.cs | 12 ++ 12 files changed, 341 insertions(+), 19 deletions(-) create mode 100644 Oqtane.Client/Services/Interfaces/IPackageService.cs create mode 100644 Oqtane.Client/Services/PackageService.cs create mode 100644 Oqtane.Server/Controllers/PackageController.cs create mode 100644 Oqtane.Shared/Models/Package.cs diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor index d5a7cdfe..c7122339 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor @@ -3,6 +3,7 @@ @inject NavigationManager NavigationManager @inject IFileService FileService @inject IModuleDefinitionService ModuleDefinitionService +@inject IPackageService PackageService @@ -14,31 +15,66 @@
+ + +@if (packages != null) +{ +
+

Available Modules

+ + +
+ Name + Version + +
+ + @context.Name + @context.Version + + + + +
+} + @if (uploaded) { - -} -else -{ - + } Cancel + @code { public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } } bool uploaded = false; + List packages; + + protected override async Task OnInitializedAsync() + { + packages = await PackageService.GetPackagesAsync("module"); + } private async Task UploadFile() { await FileService.UploadFilesAsync("Modules"); + ModuleInstance.AddModuleMessage("Module Uploaded Successfully. Click Install To Complete Installation.", MessageType.Success); uploaded = true; StateHasChanged(); } - private async Task InstallFile() + private async Task InstallModules() { await ModuleDefinitionService.InstallModulesAsync(); NavigationManager.NavigateTo(NavigateUrl(Reload.Application)); } + + private async Task DownloadModule(string moduledefinitionname, string version) + { + await PackageService.DownloadPackageAsync(moduledefinitionname, version, "Modules"); + ModuleInstance.AddModuleMessage("Module Downloaded Successfully. Click Install To Complete Installation.", MessageType.Success); + uploaded = true; + StateHasChanged(); + } } diff --git a/Oqtane.Client/Modules/Admin/Themes/Add.razor b/Oqtane.Client/Modules/Admin/Themes/Add.razor index 03093f16..51363077 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Add.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Add.razor @@ -3,6 +3,7 @@ @inject NavigationManager NavigationManager @inject IFileService FileService @inject IThemeService ThemeService +@inject IPackageService PackageService @@ -14,13 +15,32 @@
+ + +@if (packages != null) +{ +
+

Available Themes

+ + +
+ Name + Version + +
+ + @context.Name + @context.Version + + + + +
+} + @if (uploaded) { - -} -else -{ - + } Cancel @@ -28,17 +48,32 @@ else public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } } bool uploaded = false; + List packages; - private async Task UploadFile() + protected override async Task OnInitializedAsync() + { + packages = await PackageService.GetPackagesAsync("theme"); + } + + private async Task UploadTheme() { await FileService.UploadFilesAsync("Themes"); + ModuleInstance.AddModuleMessage("Theme Uploaded Successfully. Click Install To Complete Installation.", MessageType.Success); uploaded = true; StateHasChanged(); } - private async Task InstallFile() + private async Task InstallThemes() { await ThemeService.InstallThemesAsync(); NavigationManager.NavigateTo(NavigateUrl(Reload.Application)); } + + private async Task DownloadTheme(string packageid, string version) + { + await PackageService.DownloadPackageAsync(packageid, version, "Themes"); + ModuleInstance.AddModuleMessage("Theme Downloaded Successfully. Click Install To Complete Installation.", MessageType.Success); + uploaded = true; + StateHasChanged(); + } } diff --git a/Oqtane.Client/Modules/Controls/Pager.razor b/Oqtane.Client/Modules/Controls/Pager.razor index 700e8195..ec7b79f5 100644 --- a/Oqtane.Client/Modules/Controls/Pager.razor +++ b/Oqtane.Client/Modules/Controls/Pager.razor @@ -43,7 +43,7 @@

@code { - int Pages; + int Pages = 0; int Page; int MaxItems; int MaxPages; @@ -87,8 +87,11 @@ } Page = 1; - ItemList = Items.Skip((Page - 1) * MaxItems).Take(MaxItems); - Pages = (int)Math.Ceiling(Items.Count() / (decimal)MaxItems); + if (Items != null) + { + ItemList = Items.Skip((Page - 1) * MaxItems).Take(MaxItems); + Pages = (int)Math.Ceiling(Items.Count() / (decimal)MaxItems); + } SetPagerSize("forward"); } diff --git a/Oqtane.Client/Services/Interfaces/IModuleDefinitionService.cs b/Oqtane.Client/Services/Interfaces/IModuleDefinitionService.cs index 47872218..548b246e 100644 --- a/Oqtane.Client/Services/Interfaces/IModuleDefinitionService.cs +++ b/Oqtane.Client/Services/Interfaces/IModuleDefinitionService.cs @@ -9,5 +9,5 @@ namespace Oqtane.Services Task> GetModuleDefinitionsAsync(int SiteId); Task UpdateModuleDefinitionAsync(ModuleDefinition ModuleDefinition); Task InstallModulesAsync(); - } + } } diff --git a/Oqtane.Client/Services/Interfaces/IPackageService.cs b/Oqtane.Client/Services/Interfaces/IPackageService.cs new file mode 100644 index 00000000..a40a8697 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/IPackageService.cs @@ -0,0 +1,12 @@ +using Oqtane.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + public interface IPackageService + { + Task> GetPackagesAsync(string Tag); + Task DownloadPackageAsync(string PackageId, string Version, string Folder); + } +} diff --git a/Oqtane.Client/Services/ModuleDefinitionService.cs b/Oqtane.Client/Services/ModuleDefinitionService.cs index 4432d90a..7c866a24 100644 --- a/Oqtane.Client/Services/ModuleDefinitionService.cs +++ b/Oqtane.Client/Services/ModuleDefinitionService.cs @@ -66,7 +66,7 @@ namespace Oqtane.Services public async Task UpdateModuleDefinitionAsync(ModuleDefinition ModuleDefinition) { - await http.PutJsonAsync(apiurl + "/" + ModuleDefinition.ModuleDefinitionId.ToString(), ModuleDefinition); + await http.PutJsonAsync(apiurl + "/" + ModuleDefinition.ModuleDefinitionId.ToString(), ModuleDefinition); } public async Task InstallModulesAsync() diff --git a/Oqtane.Client/Services/PackageService.cs b/Oqtane.Client/Services/PackageService.cs new file mode 100644 index 00000000..bd2c8b39 --- /dev/null +++ b/Oqtane.Client/Services/PackageService.cs @@ -0,0 +1,40 @@ +using Oqtane.Models; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Oqtane.Shared; +using System.Linq; + +namespace Oqtane.Services +{ + public class PackageService : ServiceBase, IPackageService + { + private readonly HttpClient http; + private readonly SiteState sitestate; + private readonly NavigationManager NavigationManager; + + public PackageService(HttpClient http, SiteState sitestate, NavigationManager NavigationManager) + { + this.http = http; + this.sitestate = sitestate; + this.NavigationManager = NavigationManager; + } + + private string apiurl + { + get { return CreateApiUrl(sitestate.Alias, NavigationManager.Uri, "Package"); } + } + + public async Task> GetPackagesAsync(string Tag) + { + List packages = await http.GetJsonAsync>(apiurl + "?tag=" + Tag); + return packages.OrderByDescending(item => item.Downloads).ToList(); + } + + public async Task DownloadPackageAsync(string PackageId, string Version, string Folder) + { + await http.PostJsonAsync(apiurl + "?packageid=" + PackageId + "&version=" + Version + "&folder=" + Folder, null); + } + } +} diff --git a/Oqtane.Client/Startup.cs b/Oqtane.Client/Startup.cs index 8698416e..75838511 100644 --- a/Oqtane.Client/Startup.cs +++ b/Oqtane.Client/Startup.cs @@ -52,6 +52,7 @@ namespace Oqtane.Client services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // dynamically register module contexts and repository services Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); diff --git a/Oqtane.Server/Controllers/PackageController.cs b/Oqtane.Server/Controllers/PackageController.cs new file mode 100644 index 00000000..d91ca1af --- /dev/null +++ b/Oqtane.Server/Controllers/PackageController.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Models; +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using System.Threading; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Hosting; + +namespace Oqtane.Controllers +{ + [Route("{site}/api/[controller]")] + public class PackageController : Controller + { + private readonly IWebHostEnvironment environment; + + public PackageController(IWebHostEnvironment environment) + { + this.environment = environment; + } + + // GET: api/?tag=x + [HttpGet] + public async Task> Get(string tag) + { + List packages = new List(); + + using (var httpClient = new HttpClient()) + { + CancellationToken token; + var searchResult = await GetJson(httpClient, "https://azuresearch-usnc.nuget.org/query?q=tags:oqtane", token); + foreach(Data data in searchResult.Data) + { + if (data.Tags.Contains(tag)) + { + Package package = new Package(); + package.PackageId = data.Id; + package.Name = data.Title; + package.Description = data.Description; + package.Owner = data.Authors[0]; + package.Version = data.Version; + package.Downloads = data.TotalDownloads; + packages.Add(package); + } + } + } + + return packages; + } + + [HttpPost] + public async Task Post(string packageid, string version, string folder) + { + using (var httpClient = new HttpClient()) + { + CancellationToken token; + folder = Path.Combine(environment.WebRootPath, folder); + var response = await httpClient.GetAsync("https://www.nuget.org/api/v2/package/" + packageid.ToLower() + "/" + version, token).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + string filename = packageid + "." + version + ".nupkg"; + using (var fileStream = new FileStream(Path.Combine(folder, filename), FileMode.Create, FileAccess.Write, FileShare.None)) + { + await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); + } + } + } + + private async Task GetJson(HttpClient httpClient, string url, CancellationToken token) + { + Uri uri = new Uri(url); + var response = await httpClient.GetAsync(uri, token).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var stream = await response.Content.ReadAsStreamAsync(); + using (var streamReader = new StreamReader(stream)) + { + using (var jsonTextReader = new JsonTextReader(streamReader)) + { + var serializer = new JsonSerializer(); + return serializer.Deserialize(jsonTextReader); + } + } + } + } + + public partial class SearchResult + { + [JsonProperty("@context")] + public Context Context { get; set; } + + [JsonProperty("totalHits")] + public long TotalHits { get; set; } + + [JsonProperty("data")] + public Data[] Data { get; set; } + } + + public partial class Context + { + [JsonProperty("@vocab")] + public Uri Vocab { get; set; } + + [JsonProperty("@base")] + public Uri Base { get; set; } + } + + public partial class Data + { + [JsonProperty("@id")] + public Uri Url { get; set; } + + [JsonProperty("@type")] + public string Type { get; set; } + + [JsonProperty("registration")] + public Uri Registration { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("summary")] + public string Summary { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("iconUrl")] + public Uri IconUrl { get; set; } + + [JsonProperty("licenseUrl")] + public Uri LicenseUrl { get; set; } + + [JsonProperty("projectUrl")] + public Uri ProjectUrl { get; set; } + + [JsonProperty("tags")] + public string[] Tags { get; set; } + + [JsonProperty("authors")] + public string[] Authors { get; set; } + + [JsonProperty("totalDownloads")] + public long TotalDownloads { get; set; } + + [JsonProperty("verified")] + public bool Verified { get; set; } + + [JsonProperty("versions")] + public Version[] Versions { get; set; } + } + + public partial class Version + { + [JsonProperty("version")] + public string Number { get; set; } + + [JsonProperty("downloads")] + public long Downloads { get; set; } + + [JsonProperty("@id")] + public Uri Url { get; set; } + } +} diff --git a/Oqtane.Server/Controllers/SiteController.cs b/Oqtane.Server/Controllers/SiteController.cs index 41d8cf01..03e28e8b 100644 --- a/Oqtane.Server/Controllers/SiteController.cs +++ b/Oqtane.Server/Controllers/SiteController.cs @@ -5,6 +5,8 @@ using Oqtane.Repository; using Oqtane.Models; using Oqtane.Shared; using System.Linq; +using System.IO; +using Microsoft.AspNetCore.Hosting; namespace Oqtane.Controllers { @@ -12,10 +14,14 @@ namespace Oqtane.Controllers public class SiteController : Controller { private readonly ISiteRepository Sites; + private readonly ITenantResolver Tenants; + private readonly IWebHostEnvironment environment; - public SiteController(ISiteRepository Sites) + public SiteController(ISiteRepository Sites, ITenantResolver Tenants, IWebHostEnvironment environment) { this.Sites = Sites; + this.Tenants = Tenants; + this.environment = environment; } // GET: api/ @@ -50,6 +56,11 @@ namespace Oqtane.Controllers if (authorized) { Site = Sites.AddSite(Site); + string folder = environment.WebRootPath + "\\Tenants\\" + Tenants.GetTenant().TenantId.ToString() + "\\Sites\\" + Site.SiteId.ToString(); + if (!Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } } } return Site; diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index ee6343c7..55482c24 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -98,6 +98,7 @@ namespace Oqtane.Server services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); diff --git a/Oqtane.Shared/Models/Package.cs b/Oqtane.Shared/Models/Package.cs new file mode 100644 index 00000000..b2e0b214 --- /dev/null +++ b/Oqtane.Shared/Models/Package.cs @@ -0,0 +1,12 @@ +namespace Oqtane.Models +{ + public class Package + { + public string PackageId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Owner { get; set; } + public string Version { get; set; } + public long Downloads { get; set; } + } +}