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
+
+
+@if (packages != null)
+{
+
+ Available Modules
+
+
+
+
+ @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
+
+
+@if (packages != null)
+{
+
+ Available Themes
+
+
+
+
+ @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; }
+ }
+}