diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index 40ed3917..03c706bf 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -71,43 +71,41 @@ namespace Oqtane.Client // 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(); + // asemblies and debug symbols are packaged in a zip file using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) { - var dlls = new Dictionary(); - var pdbs = new Dictionary(); - foreach (ZipArchiveEntry entry in archive.Entries) { - if (!assemblies.Contains(Path.GetFileNameWithoutExtension(entry.FullName))) + using (var memoryStream = new MemoryStream()) { - using (var memoryStream = new MemoryStream()) + entry.Open().CopyTo(memoryStream); + byte[] file = memoryStream.ToArray(); + switch (Path.GetExtension(entry.FullName)) { - entry.Open().CopyTo(memoryStream); - byte[] file = memoryStream.ToArray(); - switch (Path.GetExtension(entry.FullName)) - { - case ".dll": - dlls.Add(entry.FullName, file); - break; - case ".pdb": - pdbs.Add(entry.FullName, file); - break; - } + case ".dll": + dlls.Add(entry.FullName, file); + break; + case ".pdb": + pdbs.Add(entry.FullName, file); + break; } } } + } - foreach (var item in dlls) + // load assemblies into app domain + foreach (var item in dlls) + { + if (pdbs.ContainsKey(item.Key.Replace(".dll", ".pdb"))) { - if (pdbs.ContainsKey(item.Key)) - { - AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key])); - } - else - { - AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value)); - } + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key.Replace(".dll", ".pdb")])); + } + else + { + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value)); } } } diff --git a/Oqtane.Maui/MauiProgram.cs b/Oqtane.Maui/MauiProgram.cs index 8b911249..0dda050e 100644 --- a/Oqtane.Maui/MauiProgram.cs +++ b/Oqtane.Maui/MauiProgram.cs @@ -4,6 +4,8 @@ using System.Runtime.Loader; using System.Diagnostics; using Oqtane.Modules; using Oqtane.Services; +using System.Globalization; +using System.Text.Json; namespace Oqtane.Maui; @@ -61,49 +63,116 @@ public static class MauiProgram { try { - // 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 = Task.Run(() => http.GetByteArrayAsync("/api/Installation/load")).GetAwaiter().GetResult(); - - // asemblies and debug symbols are packaged in a zip file - using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) + // ensure local assembly folder exists + string folder = Path.Combine(FileSystem.Current.AppDataDirectory, "oqtane"); + if (!Directory.Exists(folder)) { - var dlls = new Dictionary(); - var pdbs = new Dictionary(); + Directory.CreateDirectory(folder); + } - foreach (ZipArchiveEntry entry in archive.Entries) + 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 + var json = Task.Run(() => http.GetStringAsync("/api/Installation/list")).GetAwaiter().GetResult(); + var assemblies = JsonSerializer.Deserialize>(json); + + // determine which assemblies need to be downloaded + foreach (var assembly in assemblies) { - if (!assemblies.Contains(Path.GetFileNameWithoutExtension(entry.FullName))) + var file = files.FirstOrDefault(item => item.Contains(assembly)); + if (file == null) + { + filter.Add(assembly); + } + else + { + // check if newer version available + if (GetFileDate(assembly) > GetFileDate(file)) + { + filter.Add(assembly); + } + } + } + + // get assemblies already downloaded + foreach (var file in files) + { + 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))) + { + pdbs.Add(pdb, File.ReadAllBytes(Path.Combine(folder, pdb))); + } + } + else // file is deprecated + { + File.Delete(Path.Combine(folder, file)); + } + } + } + else + { + filter.Add("*"); + } + + if (filter.Count != 0) + { + // get assemblies from server + var zip = Task.Run(() => http.GetByteArrayAsync("/api/Installation/load?list=" + string.Join(",", filter))).GetAwaiter().GetResult(); + + // 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(); - switch (Path.GetExtension(entry.FullName)) + + // save assembly to local folder + int subfolder = entry.FullName.IndexOf('/'); + if (subfolder != -1 && !Directory.Exists(Path.Combine(folder, entry.FullName.Substring(0, subfolder)))) { - case ".dll": - dlls.Add(entry.FullName, file); - break; - case ".pdb": - pdbs.Add(entry.FullName, file); - break; + 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); + + if (Path.GetExtension(entry.FullName) == ".dll") + { + dlls.Add(entry.FullName, file); + } + else + { + pdbs.Add(entry.FullName, file); } } } } + } - foreach (var item in dlls) + // load assemblies into app domain + foreach (var item in dlls) + { + if (pdbs.ContainsKey(item.Key.Replace(".dll", ".pdb"))) { - if (pdbs.ContainsKey(item.Key)) - { - AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key])); - } - else - { - AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value)); - } + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key.Replace(".dll", ".pdb")])); + } + else + { + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value)); } } } @@ -113,6 +182,12 @@ public static class MauiProgram } } + 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.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 206f7584..d5b069fb 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -18,6 +18,9 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Diagnostics; +using static System.Net.WebRequestMethods; namespace Oqtane.Controllers { @@ -49,7 +52,7 @@ namespace Oqtane.Controllers [HttpPost] public async Task Post([FromBody] InstallConfig config) { - var installation = new Installation {Success = false, Message = ""}; + var installation = new Installation { Success = false, Message = "" }; if (ModelState.IsValid && (User.IsInRole(RoleNames.Host) || string.IsNullOrEmpty(_configManager.GetSetting("ConnectionStrings:" + SettingKeys.ConnectionStringKey, "")))) { @@ -85,7 +88,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public Installation Upgrade() { - var installation = new Installation {Success = true, Message = ""}; + var installation = new Installation { Success = true, Message = "" }; _installationManager.UpgradeFramework(); return installation; } @@ -98,107 +101,169 @@ namespace Oqtane.Controllers _installationManager.RestartApplication(); } - // GET api//load - [HttpGet("load")] - public IActionResult Load() + // GET api//list + [HttpGet("list")] + public List List() { - return File(GetAssemblies(), System.Net.Mime.MediaTypeNames.Application.Octet, "oqtane.dll"); + return GetAssemblyList(); } - private byte[] GetAssemblies() - { - return _cache.GetOrCreate("assemblies", entry => + // GET api//load?list=x,y + [HttpGet("load")] + public IActionResult Load(string list = "*") + { + return File(GetAssemblies(list), System.Net.Mime.MediaTypeNames.Application.Octet, "oqtane.dll"); + } + + private List GetAssemblyList() + { + // get list of assemblies which should be downloaded to client + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies(); + var list = assemblies.Select(a => a.GetName().Name).ToList(); + + // include version numbers + for (int i = 0; i < list.Count; i++) { - // get list of assemblies which should be downloaded to client - var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies(); - var list = assemblies.Select(a => a.GetName().Name).ToList(); - var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + list[i] = Path.GetFileName(AddFileDate(Path.Combine(binFolder, list[i] + ".dll"))); + } - // insert satellite assemblies at beginning of list - foreach (var culture in _localizationManager.GetInstalledCultures()) + // insert satellite assemblies at beginning of list + foreach (var culture in _localizationManager.GetInstalledCultures()) + { + var assembliesFolderPath = Path.Combine(binFolder, culture); + if (culture == Constants.DefaultCulture) { - var assembliesFolderPath = Path.Combine(binFolder, culture); - if (culture == Constants.DefaultCulture) - { - continue; - } - - if (Directory.Exists(assembliesFolderPath)) - { - foreach (var resourceFile in Directory.EnumerateFiles(assembliesFolderPath)) - { - list.Insert(0, Path.Combine(culture, Path.GetFileNameWithoutExtension(resourceFile))); - } - } - else - { - _filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist")); - } + continue; } - // insert module and theme dependencies at beginning of list - foreach (var assembly in assemblies) + if (Directory.Exists(assembliesFolderPath)) { - foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IModule)))) + foreach (var resourceFile in Directory.EnumerateFiles(assembliesFolderPath)) { - var instance = Activator.CreateInstance(type) as IModule; - foreach (string name in instance.ModuleDefinition.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (System.IO.File.Exists(Path.Combine(binFolder, name + ".dll"))) - { - if (!list.Contains(name)) list.Insert(0, name); - } - else - { - _filelogger.LogError(Utilities.LogMessage(this, $"Module {instance.ModuleDefinition.ModuleDefinitionName} Dependency {name}.dll Does Not Exist")); - } - } + list.Insert(0, culture + "/" + Path.GetFileName(AddFileDate(resourceFile))); } - foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(ITheme)))) + } + else + { + _filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist")); + } + } + + // insert module and theme dependencies at beginning of list + foreach (var assembly in assemblies) + { + foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IModule)))) + { + var instance = Activator.CreateInstance(type) as IModule; + foreach (string name in instance.ModuleDefinition.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - var instance = Activator.CreateInstance(type) as ITheme; - foreach (string name in instance.Theme.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + var path = Path.Combine(binFolder, name + ".dll"); + if (System.IO.File.Exists(path)) { - if (System.IO.File.Exists(Path.Combine(binFolder, name + ".dll"))) - { - if (!list.Contains(name)) list.Insert(0, name); - } - else - { - _filelogger.LogError(Utilities.LogMessage(this, $"Theme {instance.Theme.ThemeName} Dependency {name}.dll Does Not Exist")); - } + path = Path.GetFileName(AddFileDate(path)); + if (!list.Contains(path)) list.Insert(0, path); + } + else + { + _filelogger.LogError(Utilities.LogMessage(this, $"Module {instance.ModuleDefinition.ModuleDefinitionName} Dependency {name}.dll Does Not Exist")); } } } - - // create zip file containing assemblies and debug symbols - using (var memoryStream = new MemoryStream()) + foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(ITheme)))) { - using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + var instance = Activator.CreateInstance(type) as ITheme; + foreach (string name in instance.Theme.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - foreach (string file in list) + var path = Path.Combine(binFolder, name + ".dll"); + if (System.IO.File.Exists(path)) { - using (var filestream = new FileStream(Path.Combine(binFolder, file + ".dll"), FileMode.Open, FileAccess.Read)) - using (var entrystream = archive.CreateEntry(file + ".dll").Open()) + path = Path.GetFileName(AddFileDate(path)); + if (!list.Contains(path)) list.Insert(0, path); + } + else + { + _filelogger.LogError(Utilities.LogMessage(this, $"Theme {instance.Theme.ThemeName} Dependency {name}.dll Does Not Exist")); + } + } + } + } + + return list; + } + + private byte[] GetAssemblies(string list) + { + if (list == "*") + { + return _cache.GetOrCreate("assemblies", entry => + { + return GetZIP(list); + }); + } + else + { + return GetZIP(list); + } + } + + private byte[] GetZIP(string list) + { + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + + // get list of assemblies which should be downloaded to client + List assemblies; + if (list == "*") + { + assemblies = GetAssemblyList(); + } + else + { + assemblies = list.Split(',').ToList(); + } + + // create zip file containing assemblies and debug symbols + using (var memoryStream = new MemoryStream()) + { + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + { + foreach (string file in assemblies) + { + var filename = RemoveFileDate(file); + if (System.IO.File.Exists(Path.Combine(binFolder, filename))) + { + using (var filestream = new FileStream(Path.Combine(binFolder, filename), FileMode.Open, FileAccess.Read)) + using (var entrystream = archive.CreateEntry(file).Open()) { filestream.CopyTo(entrystream); } - - // include debug symbols - if (System.IO.File.Exists(Path.Combine(binFolder, file + ".pdb"))) + } + filename = filename.Replace(".dll", ".pdb"); + if (System.IO.File.Exists(Path.Combine(binFolder, filename))) + { + using (var filestream = new FileStream(Path.Combine(binFolder, filename), FileMode.Open, FileAccess.Read)) + using (var entrystream = archive.CreateEntry(file.Replace(".dll", ".pdb")).Open()) { - using (var filestream = new FileStream(Path.Combine(binFolder, file + ".pdb"), FileMode.Open, FileAccess.Read)) - using (var entrystream = archive.CreateEntry(file + ".pdb").Open()) - { - filestream.CopyTo(entrystream); - } + filestream.CopyTo(entrystream); } } } - - return memoryStream.ToArray(); } - }); + + return memoryStream.ToArray(); + } + } + + private string AddFileDate(string filepath) + { + DateTime lastwritetime = System.IO.File.GetLastWriteTime(filepath); + return Path.GetFileNameWithoutExtension(filepath) + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath); + } + + private string RemoveFileDate(string filepath) + { + var segments = filepath.Split("."); + return string.Join(".", segments, 0, segments.Length - 2) + Path.GetExtension(filepath); } private async Task RegisterContact(string email)