From b8e2c729c164185b3eb2c69e7f9f94a7d3cb667e Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 12 Sep 2022 14:46:46 -0400 Subject: [PATCH] cache assemblies in IndexedDB on WebAssembly --- Oqtane.Client/Program.cs | 138 ++++++++++++++++++++++------ Oqtane.Client/UI/Interop.cs | 77 ++++++++++++++++ Oqtane.Maui/MauiProgram.cs | 44 ++++++--- Oqtane.Server/wwwroot/js/interop.js | 76 +++++++++++++++ 4 files changed, 296 insertions(+), 39 deletions(-) diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index 03c706bf..02b09fa7 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -7,16 +7,19 @@ using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.Loader; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; using Oqtane.Documentation; +using Oqtane.Models; using Oqtane.Modules; using Oqtane.Services; using Oqtane.Shared; using Oqtane.UI; +using static System.Net.WebRequestMethods; namespace Oqtane.Client { @@ -42,7 +45,9 @@ namespace Oqtane.Client // register scoped core services builder.Services.AddOqtaneScopedServices(); - await LoadClientAssemblies(httpClient); + var serviceProvider = builder.Services.BuildServiceProvider(); + + await LoadClientAssemblies(httpClient, serviceProvider); var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); foreach (var assembly in assemblies) @@ -54,43 +59,114 @@ namespace Oqtane.Client RegisterClientStartups(assembly, builder.Services); } - var host = builder.Build(); - - await SetCultureFromLocalizationCookie(host.Services); - - ServiceActivator.Configure(host.Services); - - await host.RunAsync(); + await builder.Build().RunAsync(); } - private static async Task LoadClientAssemblies(HttpClient http) + private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider) { - // get list of loaded assemblies on the client - var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList(); - - // get assemblies from server and load into client app domain - var zip = await http.GetByteArrayAsync($"/api/Installation/load"); - var dlls = new Dictionary(); var pdbs = new Dictionary(); + var filter = new List(); - // asemblies and debug symbols are packaged in a zip file - using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) + var jsRuntime = serviceProvider.GetRequiredService(); + var interop = new Interop(jsRuntime); + var files = await interop.GetIndexedDBKeys(".dll"); + + if (files.Count() != 0) { - foreach (ZipArchiveEntry entry in archive.Entries) + // get list of assemblies from server + var json = await http.GetStringAsync("/api/Installation/list"); + var assemblies = JsonSerializer.Deserialize>(json); + + // determine which assemblies need to be downloaded + foreach (var assembly in assemblies) { - using (var memoryStream = new MemoryStream()) + var file = files.FirstOrDefault(item => item.Contains(assembly)); + if (file == null) { - entry.Open().CopyTo(memoryStream); - byte[] file = memoryStream.ToArray(); - switch (Path.GetExtension(entry.FullName)) + filter.Add(assembly); + } + else + { + // check if newer version available + if (GetFileDate(assembly) > GetFileDate(file)) { - case ".dll": - dlls.Add(entry.FullName, file); - break; - case ".pdb": - pdbs.Add(entry.FullName, file); - break; + filter.Add(assembly); + } + } + } + + // get assemblies already downloaded + foreach (var file in files) + { + if (assemblies.Contains(file) && !filter.Contains(file)) + { + try + { + dlls.Add(file, await interop.GetIndexedDBItem(file)); + var pdb = file.Replace(".dll", ".pdb"); + if (files.Contains(pdb)) + { + pdbs.Add(pdb, await interop.GetIndexedDBItem(pdb)); + } + } + catch + { + // ignore + } + } + else // file is deprecated + { + try + { + await interop.RemoveIndexedDBItem(file); + } + catch + { + // ignore + } + } + } + } + else + { + filter.Add("*"); + } + + if (filter.Count != 0) + { + // get assemblies from server and load into client app domain + var zip = await http.GetByteArrayAsync($"/api/Installation/load?list=" + string.Join(",", filter)); + + // asemblies and debug symbols are packaged in a zip file + using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + using (var memoryStream = new MemoryStream()) + { + entry.Open().CopyTo(memoryStream); + byte[] file = memoryStream.ToArray(); + + // save assembly to indexeddb + try + { + await interop.SetIndexedDBItem(entry.FullName, file); + } + catch + { + // ignore + } + + switch (Path.GetExtension(entry.FullName)) + { + case ".dll": + dlls.Add(entry.FullName, file); + break; + case ".pdb": + pdbs.Add(entry.FullName, file); + break; + } } } } @@ -110,6 +186,12 @@ namespace Oqtane.Client } } + private static DateTime GetFileDate(string filepath) + { + var segments = filepath.Split('.'); + return DateTime.ParseExact(segments[segments.Length - 2], "yyyyMMddHHmmss", CultureInfo.InvariantCulture); + } + private static void RegisterModuleServices(Assembly assembly, IServiceCollection services) { // dynamically register module scoped services diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index be03b5c7..05b312a5 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -1,6 +1,12 @@ using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; +using System.Net; +using System; using System.Threading.Tasks; +using System.Text.Json; +using System.Xml.Linq; +using System.Collections.Generic; +using System.Linq; namespace Oqtane.UI { @@ -307,5 +313,76 @@ namespace Oqtane.UI return new ValueTask(-1); } } + + public Task SetIndexedDBItem(string key, object value) + { + try + { + _jsRuntime.InvokeVoidAsync( + "Oqtane.Interop.setIndexedDBItem", + key, value); + return Task.CompletedTask; + } + catch + { + return Task.CompletedTask; + } + } + + public async Task GetIndexedDBItem(string key) + { + try + { + return await _jsRuntime.InvokeAsync( + "Oqtane.Interop.getIndexedDBItem", + key); + } + catch + { + return default(T); + } + } + + public async Task> GetIndexedDBKeys() + { + return await GetIndexedDBKeys(""); + } + + public async Task> GetIndexedDBKeys(string contains) + { + try + { + var items = await _jsRuntime.InvokeAsync( + "Oqtane.Interop.getIndexedDBKeys"); + if (!string.IsNullOrEmpty(contains)) + { + return items.Deserialize>() + .Where(item => item.Contains(contains)).ToList(); + } + else + { + return items.Deserialize>(); + } + } + catch + { + return new List(); + } + } + + public Task RemoveIndexedDBItem(string key) + { + try + { + _jsRuntime.InvokeVoidAsync( + "Oqtane.Interop.removeIndexedDBItem", + key); + return Task.CompletedTask; + } + catch + { + return Task.CompletedTask; + } + } } } diff --git a/Oqtane.Maui/MauiProgram.cs b/Oqtane.Maui/MauiProgram.cs index 0dda050e..89618ff7 100644 --- a/Oqtane.Maui/MauiProgram.cs +++ b/Oqtane.Maui/MauiProgram.cs @@ -72,13 +72,14 @@ public static class MauiProgram var dlls = new Dictionary(); var pdbs = new Dictionary(); - var filter = new List(); + var files = new List(); foreach (var file in Directory.EnumerateFiles(folder, "*.dll", SearchOption.AllDirectories)) { files.Add(file.Substring(folder.Length + 1).Replace("\\", "/")); } + if (files.Count() != 0) { // get list of assemblies from server @@ -108,16 +109,30 @@ public static class MauiProgram { if (assemblies.Contains(file) && !filter.Contains(file)) { - dlls.Add(file, File.ReadAllBytes(Path.Combine(folder, file))); - var pdb = file.Replace(".dll", ".pdb"); - if (File.Exists(Path.Combine(folder, pdb))) + try { - pdbs.Add(pdb, File.ReadAllBytes(Path.Combine(folder, pdb))); + dlls.Add(file, File.ReadAllBytes(Path.Combine(folder, file))); + var pdb = file.Replace(".dll", ".pdb"); + if (File.Exists(Path.Combine(folder, pdb))) + { + pdbs.Add(pdb, File.ReadAllBytes(Path.Combine(folder, pdb))); + } + } + catch + { + // ignore } } else // file is deprecated { - File.Delete(Path.Combine(folder, file)); + try + { + File.Delete(Path.Combine(folder, file)); + } + catch + { + // ignore + } } } } @@ -142,13 +157,20 @@ public static class MauiProgram byte[] file = memoryStream.ToArray(); // save assembly to local folder - int subfolder = entry.FullName.IndexOf('/'); - if (subfolder != -1 && !Directory.Exists(Path.Combine(folder, entry.FullName.Substring(0, subfolder)))) + try { - Directory.CreateDirectory(Path.Combine(folder, entry.FullName.Substring(0, subfolder))); + int subfolder = entry.FullName.IndexOf('/'); + if (subfolder != -1 && !Directory.Exists(Path.Combine(folder, entry.FullName.Substring(0, subfolder)))) + { + Directory.CreateDirectory(Path.Combine(folder, entry.FullName.Substring(0, subfolder))); + } + using var stream = File.Create(Path.Combine(folder, entry.FullName)); + stream.Write(file, 0, file.Length); + } + catch + { + // ignore } - using var stream = File.Create(Path.Combine(folder, entry.FullName)); - stream.Write(file, 0, file.Length); if (Path.GetExtension(entry.FullName) == ".dll") { diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 96c9811b..5c33c2ea 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -395,5 +395,81 @@ Oqtane.Interop = { getCaretPosition: function (id) { var element = document.getElementById(id); return element.selectionStart; + }, + setIndexedDBItem: function (key, value) { + let idb = indexedDB.open("oqtane", 1); + + idb.onupgradeneeded = function () { + let db = idb.result; + db.createObjectStore("items"); + } + + idb.onsuccess = function () { + let transaction = idb.result.transaction("items", "readwrite"); + let collection = transaction.objectStore("items") + collection.put(value, key); + } + }, + getIndexedDBItem: async function (key) { + let request = new Promise((resolve) => { + let idb = indexedDB.open("oqtane", 1); + + idb.onupgradeneeded = function () { + let db = idb.result; + db.createObjectStore("items"); + } + + idb.onsuccess = function () { + let transaction = idb.result.transaction("items", "readonly"); + let collection = transaction.objectStore("items"); + let result = collection.get(key); + + result.onsuccess = function (e) { + resolve(result.result); + } + } + }); + + let result = await request; + + return result; + }, + getIndexedDBKeys: async function () { + let request = new Promise((resolve) => { + let idb = indexedDB.open("oqtane", 1); + + idb.onupgradeneeded = function () { + let db = idb.result; + db.createObjectStore("items"); + } + + idb.onsuccess = function () { + let transaction = idb.result.transaction("items", "readonly"); + let collection = transaction.objectStore("items"); + let result = collection.getAllKeys(); + + result.onsuccess = function (e) { + resolve(result.result); + } + } + }); + + let result = await request; + + return result; + }, + removeIndexedDBItem: function (key) { + let idb = indexedDB.open("oqtane", 1); + + idb.onupgradeneeded = function () { + let db = idb.result; + db.createObjectStore("items"); + } + + idb.onsuccess = function () { + let transaction = idb.result.transaction("items", "readwrite"); + let collection = transaction.objectStore("items"); + collection.delete(key); + } } };