From a49b8728fd3cc6ab1071a763816216f248d6d563 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 15 May 2025 08:56:21 -0400 Subject: [PATCH 1/4] improve module export so that content can be saved to a file --- .../Modules/Admin/Modules/Export.razor | 71 ++++++++++++++++--- .../Modules/Admin/Modules/Export.resx | 20 +++++- .../Services/Interfaces/IModuleService.cs | 12 +++- Oqtane.Client/Services/ModuleService.cs | 7 +- Oqtane.Server/Controllers/ModuleController.cs | 66 ++++++++++++++++- Oqtane.Shared/Models/Result.cs | 2 + 6 files changed, 164 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index d2e90193..ab714221 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -5,24 +5,45 @@ @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer -
-
- -
- + + +
+
+ +
+ +
+
-
-
+
+ + @SharedLocalizer["Cancel"] + + +
+
+ +
+ +
+
+
+
+ + @SharedLocalizer["Cancel"] +
+ + - -@SharedLocalizer["Cancel"] @code { private string _content = string.Empty; + private FileManager _filemanager; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Export Content"; - private async Task ExportModule() + private async Task ExportText() { try { @@ -35,4 +56,34 @@ AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); } } + + private async Task ExportFile() + { + try + { + var folderid = _filemanager.GetFolderId(); + if (folderid != -1) + { + var result = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid); + if (result.Success) + { + AddModuleMessage(string.Format(Localizer["Success.Export.File"], result.Message), MessageType.Success); + } + else + { + AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); + } + } + else + { + AddModuleMessage(Localizer["Message.Content.Export"], MessageType.Warning); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Exporting Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message); + AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); + } + } + } diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx index 3ed705f8..90e614c2 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx @@ -121,7 +121,7 @@ Export - The Exported Module Content + Select the Export option and you will be able to view the module content Content: @@ -135,4 +135,22 @@ Export Content + + Text + + + File + + + Folder: + + + Select a folder where you wish to save the exported content + + + Please Select A Folder Before Choosing Export + + + Content Was Successfully Exported To Specified Folder With Filename {0} + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IModuleService.cs b/Oqtane.Client/Services/Interfaces/IModuleService.cs index 46777fdf..34a9c1b1 100644 --- a/Oqtane.Client/Services/Interfaces/IModuleService.cs +++ b/Oqtane.Client/Services/Interfaces/IModuleService.cs @@ -56,7 +56,17 @@ namespace Oqtane.Services /// Exports a given module /// /// - /// module in JSON + /// + /// module content in JSON format Task ExportModuleAsync(int moduleId, int pageId); + + /// + /// Exports a given module + /// + /// + /// + /// + /// success/failure + Task ExportModuleAsync(int moduleId, int pageId, int folderId); } } diff --git a/Oqtane.Client/Services/ModuleService.cs b/Oqtane.Client/Services/ModuleService.cs index e1914004..1e688610 100644 --- a/Oqtane.Client/Services/ModuleService.cs +++ b/Oqtane.Client/Services/ModuleService.cs @@ -47,8 +47,13 @@ namespace Oqtane.Services } public async Task ExportModuleAsync(int moduleId, int pageId) -{ + { return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}"); } + + public async Task ExportModuleAsync(int moduleId, int pageId, int folderId) + { + return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}", null); + } } } diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index 02f6f8c6..78aae764 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -9,6 +9,10 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; using System.Net; +using System.IO; +using System; +using static System.Net.WebRequestMethods; +using System.Net.Http; namespace Oqtane.Controllers { @@ -20,18 +24,22 @@ namespace Oqtane.Controllers private readonly IPageRepository _pages; private readonly IModuleDefinitionRepository _moduleDefinitions; private readonly ISettingRepository _settings; + private readonly IFolderRepository _folders; + private readonly IFileRepository _files; private readonly IUserPermissions _userPermissions; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; - public ModuleController(IModuleRepository modules, IPageModuleRepository pageModules, IPageRepository pages, IModuleDefinitionRepository moduleDefinitions, ISettingRepository settings, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) + public ModuleController(IModuleRepository modules, IPageModuleRepository pageModules, IPageRepository pages, IModuleDefinitionRepository moduleDefinitions, ISettingRepository settings, IFolderRepository folders, IFileRepository files, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) { _modules = modules; _pageModules = pageModules; _pages = pages; _moduleDefinitions = moduleDefinitions; _settings = settings; + _folders = folders; + _files = files; _userPermissions = userPermissions; _syncManager = syncManager; _logger = logger; @@ -248,6 +256,62 @@ namespace Oqtane.Controllers return content; } + // POST api//export?moduleid=x&pageid=y&folderid=z + [HttpPost("export")] + [Authorize(Roles = RoleNames.Registered)] + public Result Export(int moduleid, int pageid, int folderid) + { + var result = new Result(false); + var module = _modules.GetModule(moduleid); + if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit) && + _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit)) + { + // get content + var content = _modules.ExportModule(moduleid); + + // get folder + var folder = _folders.GetFolder(folderid, false); + string folderPath = _folders.GetFolderPath(folder); + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + // create text file + var filename = Utilities.GetTypeNameLastSegment(module.ModuleDefinitionName, 0) + moduleid.ToString() + ".json"; + string filepath = Path.Combine(folderPath, filename); + if (System.IO.File.Exists(filepath)) + { + System.IO.File.Delete(filepath); + } + System.IO.File.WriteAllText(filepath, content); + + // register file + var file = _files.GetFile(folderid, filename); + if (file == null) + { + file = new Models.File { FolderId = folderid, Name = filename, Extension = "txt", Size = (int)new FileInfo(filepath).Length, ImageWidth = 0, ImageHeight = 0 }; + _files.AddFile(file); + } + else + { + file.Size = (int)new FileInfo(filepath).Length; + _files.UpdateFile(file); + } + + result.Success = true; + result.Message = filename; + + _logger.Log(LogLevel.Information, this, LogFunction.Read, "Content Exported For Module {ModuleId} To Folder {FolderId}", moduleid, folderid); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Export Attempt For Module {Module} To Folder {FolderId}", moduleid, folderid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + return result; + } + // POST api//import?moduleid=x&pageid=y [HttpPost("import")] [Authorize(Roles = RoleNames.Registered)] diff --git a/Oqtane.Shared/Models/Result.cs b/Oqtane.Shared/Models/Result.cs index d550f2c4..8500d3ad 100644 --- a/Oqtane.Shared/Models/Result.cs +++ b/Oqtane.Shared/Models/Result.cs @@ -6,6 +6,8 @@ namespace Oqtane.Models public string Message { get; set; } + public Result() {} + public Result(bool success) { Success = success; From 51ba3a01f5ce4eeae207c3d90e1707988a2c678e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 15 May 2025 09:34:19 -0400 Subject: [PATCH 2/4] allow module import from a file --- .../Modules/Admin/Modules/Import.razor | 19 ++++++++++++++++--- Oqtane.Server/Controllers/ModuleController.cs | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Import.razor b/Oqtane.Client/Modules/Admin/Modules/Import.razor index 04b2557a..eacec260 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Import.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Import.razor @@ -2,20 +2,27 @@ @inherits ModuleBase @inject NavigationManager NavigationManager @inject IModuleService ModuleService +@inject IFileService FileService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer
- + +
+ +
+
+
+
+
-
- +
@SharedLocalizer["Cancel"]
@@ -28,6 +35,12 @@ public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Import Content"; + private async Task OnSelectFile(int fileId) + { + var bytes = await FileService.DownloadFileAsync(fileId); + _content = System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + private async Task ImportModule() { validated = true; diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index 78aae764..e075e5e5 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -290,7 +290,7 @@ namespace Oqtane.Controllers var file = _files.GetFile(folderid, filename); if (file == null) { - file = new Models.File { FolderId = folderid, Name = filename, Extension = "txt", Size = (int)new FileInfo(filepath).Length, ImageWidth = 0, ImageHeight = 0 }; + file = new Models.File { FolderId = folderid, Name = filename, Extension = "json", Size = (int)new FileInfo(filepath).Length, ImageWidth = 0, ImageHeight = 0 }; _files.AddFile(file); } else From 5d077e843d2d8a0508509b8ac1fb6eaf30694ee6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 15 May 2025 10:58:55 -0400 Subject: [PATCH 3/4] allow filename to be provided during module export --- Oqtane.Client/Modules/Admin/Modules/Export.razor | 13 ++++++++++--- .../Resources/Modules/Admin/Modules/Export.resx | 9 ++++++--- Oqtane.Client/Services/Interfaces/IModuleService.cs | 3 ++- Oqtane.Client/Services/ModuleService.cs | 4 ++-- Oqtane.Server/Controllers/ModuleController.cs | 10 +++++----- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index ab714221..f7fd3a57 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -27,6 +27,12 @@
+
+ +
+ +
+

@@ -39,6 +45,7 @@ @code { private string _content = string.Empty; private FileManager _filemanager; + private string _filename = string.Empty; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Export Content"; @@ -62,12 +69,12 @@ try { var folderid = _filemanager.GetFolderId(); - if (folderid != -1) + if (folderid != -1 && !string.IsNullOrEmpty(_filename)) { - var result = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid); + var result = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename); if (result.Success) { - AddModuleMessage(string.Format(Localizer["Success.Export.File"], result.Message), MessageType.Success); + AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success); } else { diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx index 90e614c2..0a365dfd 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx @@ -148,9 +148,12 @@ Select a folder where you wish to save the exported content - Please Select A Folder Before Choosing Export + Please Select A Folder And Provide A Filename Before Choosing Export - - Content Was Successfully Exported To Specified Folder With Filename {0} + + Filename: + + + Specify a name for the file (without an extension) \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IModuleService.cs b/Oqtane.Client/Services/Interfaces/IModuleService.cs index 34a9c1b1..a2334a20 100644 --- a/Oqtane.Client/Services/Interfaces/IModuleService.cs +++ b/Oqtane.Client/Services/Interfaces/IModuleService.cs @@ -66,7 +66,8 @@ namespace Oqtane.Services /// /// /// + /// /// success/failure - Task ExportModuleAsync(int moduleId, int pageId, int folderId); + Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename); } } diff --git a/Oqtane.Client/Services/ModuleService.cs b/Oqtane.Client/Services/ModuleService.cs index 1e688610..68d1e4fd 100644 --- a/Oqtane.Client/Services/ModuleService.cs +++ b/Oqtane.Client/Services/ModuleService.cs @@ -51,9 +51,9 @@ namespace Oqtane.Services return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}"); } - public async Task ExportModuleAsync(int moduleId, int pageId, int folderId) + public async Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename) { - return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}", null); + return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}&filename={filename}", null); } } } diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index e075e5e5..012f2e11 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -256,15 +256,15 @@ namespace Oqtane.Controllers return content; } - // POST api//export?moduleid=x&pageid=y&folderid=z + // POST api//export?moduleid=x&pageid=y&folderid=z&filename=a [HttpPost("export")] [Authorize(Roles = RoleNames.Registered)] - public Result Export(int moduleid, int pageid, int folderid) + public Result Export(int moduleid, int pageid, int folderid, string filename) { var result = new Result(false); var module = _modules.GetModule(moduleid); if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit) && - _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit)) + _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename)) { // get content var content = _modules.ExportModule(moduleid); @@ -277,8 +277,8 @@ namespace Oqtane.Controllers Directory.CreateDirectory(folderPath); } - // create text file - var filename = Utilities.GetTypeNameLastSegment(module.ModuleDefinitionName, 0) + moduleid.ToString() + ".json"; + // create json file + filename = Path.GetFileNameWithoutExtension(filename) + ".json"; string filepath = Path.Combine(folderPath, filename); if (System.IO.File.Exists(filepath)) { From c57c6abb1b66acbaafe3ef48c2c83ab21df11f5e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 15 May 2025 11:06:04 -0400 Subject: [PATCH 4/4] update module export resource info --- Oqtane.Client/Modules/Admin/Modules/Export.razor | 2 +- Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index f7fd3a57..d72ec689 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -6,7 +6,7 @@ @inject IStringLocalizer SharedLocalizer - +
diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx index 0a365dfd..23378774 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx @@ -135,8 +135,8 @@ Export Content - - Text + + Content File