From 32ae79524e719739216691eab92140d6ea4d4147 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 15 Jun 2025 14:51:03 +0530 Subject: [PATCH 01/73] refactor: convert to file-scoped namespaces --- DocumentService/Pdf/Models/ContentMetaData.cs | 19 +- DocumentService/Pdf/Models/DocumentData.cs | 21 +- DocumentService/Pdf/PdfDocumentGenerator.cs | 425 ++++++------ DocumentService/Word/Models/ContentData.cs | 59 +- DocumentService/Word/Models/DocumentData.cs | 47 +- DocumentService/Word/Models/Enums.cs | 71 +- DocumentService/Word/Models/TableData.cs | 45 +- DocumentService/Word/WordDocumentGenerator.cs | 617 +++++++++--------- 8 files changed, 648 insertions(+), 656 deletions(-) diff --git a/DocumentService/Pdf/Models/ContentMetaData.cs b/DocumentService/Pdf/Models/ContentMetaData.cs index 1f9cb3e..fd8d22e 100644 --- a/DocumentService/Pdf/Models/ContentMetaData.cs +++ b/DocumentService/Pdf/Models/ContentMetaData.cs @@ -1,10 +1,9 @@ -namespace DocumentService.Pdf.Models -{ - public class ContentMetaData - { - public string Placeholder { get; set; } - public string Content { get; set; } - } -} - - +namespace DocumentService.Pdf.Models; + +public class ContentMetaData +{ + public string Placeholder { get; set; } + public string Content { get; set; } +} + + diff --git a/DocumentService/Pdf/Models/DocumentData.cs b/DocumentService/Pdf/Models/DocumentData.cs index 9a93c7a..3427bca 100644 --- a/DocumentService/Pdf/Models/DocumentData.cs +++ b/DocumentService/Pdf/Models/DocumentData.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DocumentService.Pdf.Models -{ - public class DocumentData - { - public List Placeholders { get; set; } - } -} +using System; +using System.Collections.Generic; +using System.Text; + +namespace DocumentService.Pdf.Models; + +public class DocumentData +{ + public List Placeholders { get; set; } +} diff --git a/DocumentService/Pdf/PdfDocumentGenerator.cs b/DocumentService/Pdf/PdfDocumentGenerator.cs index a3cb308..d293c4b 100644 --- a/DocumentService/Pdf/PdfDocumentGenerator.cs +++ b/DocumentService/Pdf/PdfDocumentGenerator.cs @@ -1,213 +1,212 @@ -using DocumentService.Pdf.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; - -namespace DocumentService.Pdf -{ - public class PdfDocumentGenerator - { - public static void GeneratePdf(string toolFolderAbsolutePath, string templatePath, List metaDataList, string outputFilePath, bool isEjsTemplate, string serializedEjsDataJson) - { - try - { - if (!File.Exists(templatePath)) - { - throw new Exception("The file path you provided is not valid."); - } - - if (isEjsTemplate) - { - // Validate if template in file path is an ejs file - if (Path.GetExtension(templatePath).ToLower() != ".ejs") - { - throw new Exception("Input template should be a valid EJS file"); - } - - // Convert ejs file to an equivalent html - templatePath = ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); - } - - // Modify html template with content data and generate pdf - string modifiedHtmlFilePath = ReplaceFileElementsWithMetaData(templatePath, metaDataList, outputFilePath); - ConvertHtmlToPdf(toolFolderAbsolutePath, modifiedHtmlFilePath, outputFilePath); - - if (isEjsTemplate) - { - // If input template was an ejs file, then the template path contains path to html converted from ejs - if (Path.GetExtension(templatePath).ToLower() == ".html") - { - // If template path contains path to converted html template then delete it - File.Delete(templatePath); - } - } - } - catch (Exception e) - { - throw e; - } - } - - private static string ReplaceFileElementsWithMetaData(string templatePath, List metaDataList, string outputFilePath) - { - string htmlContent = File.ReadAllText(templatePath); - - foreach (ContentMetaData metaData in metaDataList) - { - htmlContent = htmlContent.Replace($"{{{metaData.Placeholder}}}", metaData.Content); - } - - string directoryPath = Path.GetDirectoryName(outputFilePath); - string tempHtmlFilePath = Path.Combine(directoryPath, "Modified"); - string tempHtmlFile = Path.Combine(tempHtmlFilePath, "modifiedHtml.html"); - - if (!Directory.Exists(tempHtmlFilePath)) - { - Directory.CreateDirectory(tempHtmlFilePath); - } - - File.WriteAllText(tempHtmlFile, htmlContent); - return tempHtmlFile; - } - - private static void ConvertHtmlToPdf(string toolFolderAbsolutePath, string modifiedHtmlFilePath, string outputFilePath) - { - string wkHtmlToPdfPath = "cmd.exe"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - wkHtmlToPdfPath = "wkhtmltopdf"; - } - - /* - * FIXME: Issue if tools file path has spaces in between - */ - string arguments = HtmlToPdfArgumentsBasedOnOS(toolFolderAbsolutePath, modifiedHtmlFilePath, outputFilePath); - - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = wkHtmlToPdfPath, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (Process process = new Process()) - { - process.StartInfo = psi; - process.Start(); - process.WaitForExit(); - string output = process.StandardOutput.ReadToEnd(); - string errors = process.StandardError.ReadToEnd(); - } - - File.Delete(modifiedHtmlFilePath); - } - - private static string ConvertEjsToHTML(string ejsFilePath, string outputFilePath, string ejsDataJson) - { - // Generate directory - string directoryPath = Path.GetDirectoryName(outputFilePath); - string tempDirectoryFilePath = Path.Combine(directoryPath, "Temp"); - - if (!Directory.Exists(tempDirectoryFilePath)) - { - Directory.CreateDirectory(tempDirectoryFilePath); - } - - // Generate file path to converted html template - string tempHtmlFilePath = Path.Combine(tempDirectoryFilePath, "htmlTemplate.html"); - - // If the ejs data json is invalid then throw exception - if (!string.IsNullOrWhiteSpace(ejsDataJson) && !IsValidJSON(ejsDataJson)) - { - throw new Exception("Received invalid JSON data for EJS template"); - } - - // Write json data string to json file - string ejsDataJsonFilePath = Path.Combine(tempDirectoryFilePath, "ejsData.json"); - File.WriteAllText(ejsDataJsonFilePath, ejsDataJson); - - string commandLine = "cmd.exe"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - commandLine = "ejs"; - } - string arguments = EjsToHtmlArgumentsBasedOnOS(ejsFilePath, ejsDataJsonFilePath, tempHtmlFilePath); - - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = commandLine, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (Process process = new Process()) - { - process.StartInfo = psi; - process.Start(); - process.WaitForExit(); - string output = process.StandardOutput.ReadToEnd(); - string errors = process.StandardError.ReadToEnd(); - } - - // Delete json data file - File.Delete(ejsDataJsonFilePath); - - return tempHtmlFilePath; - } - - private static bool IsValidJSON(string json) - { - try - { - JToken.Parse(json); - return true; - } - catch (JsonReaderException) - { - return false; - } - } - - private static string HtmlToPdfArgumentsBasedOnOS(string toolFolderAbsolutePath, string modifiedHtmlFilePath, string outputFilePath) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return $"/C {toolFolderAbsolutePath} \"{modifiedHtmlFilePath}\" \"{outputFilePath}\""; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return $"{modifiedHtmlFilePath} {outputFilePath}"; - } - else - { - throw new Exception("Unknown operating system"); - } - } - - private static string EjsToHtmlArgumentsBasedOnOS(string ejsFilePath, string ejsDataJsonFilePath, string tempHtmlFilePath) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return $"/C ejs \"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return $"{ejsFilePath} -f {ejsDataJsonFilePath} -o {tempHtmlFilePath}"; - } - else - { - throw new Exception("Unknown operating system"); - } - } - } -} +using DocumentService.Pdf.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace DocumentService.Pdf; + +public class PdfDocumentGenerator +{ + public static void GeneratePdf(string toolFolderAbsolutePath, string templatePath, List metaDataList, string outputFilePath, bool isEjsTemplate, string serializedEjsDataJson) + { + try + { + if (!File.Exists(templatePath)) + { + throw new Exception("The file path you provided is not valid."); + } + + if (isEjsTemplate) + { + // Validate if template in file path is an ejs file + if (Path.GetExtension(templatePath).ToLower() != ".ejs") + { + throw new Exception("Input template should be a valid EJS file"); + } + + // Convert ejs file to an equivalent html + templatePath = ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); + } + + // Modify html template with content data and generate pdf + string modifiedHtmlFilePath = ReplaceFileElementsWithMetaData(templatePath, metaDataList, outputFilePath); + ConvertHtmlToPdf(toolFolderAbsolutePath, modifiedHtmlFilePath, outputFilePath); + + if (isEjsTemplate) + { + // If input template was an ejs file, then the template path contains path to html converted from ejs + if (Path.GetExtension(templatePath).ToLower() == ".html") + { + // If template path contains path to converted html template then delete it + File.Delete(templatePath); + } + } + } + catch (Exception e) + { + throw; + } + } + + private static string ReplaceFileElementsWithMetaData(string templatePath, List metaDataList, string outputFilePath) + { + string htmlContent = File.ReadAllText(templatePath); + + foreach (ContentMetaData metaData in metaDataList) + { + htmlContent = htmlContent.Replace($"{{{metaData.Placeholder}}}", metaData.Content); + } + + string directoryPath = Path.GetDirectoryName(outputFilePath); + string tempHtmlFilePath = Path.Combine(directoryPath, "Modified"); + string tempHtmlFile = Path.Combine(tempHtmlFilePath, "modifiedHtml.html"); + + if (!Directory.Exists(tempHtmlFilePath)) + { + Directory.CreateDirectory(tempHtmlFilePath); + } + + File.WriteAllText(tempHtmlFile, htmlContent); + return tempHtmlFile; + } + + private static void ConvertHtmlToPdf(string toolFolderAbsolutePath, string modifiedHtmlFilePath, string outputFilePath) + { + string wkHtmlToPdfPath = "cmd.exe"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + wkHtmlToPdfPath = "wkhtmltopdf"; + } + + /* + * FIXME: Issue if tools file path has spaces in between + */ + string arguments = HtmlToPdfArgumentsBasedOnOS(toolFolderAbsolutePath, modifiedHtmlFilePath, outputFilePath); + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = wkHtmlToPdfPath, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = new Process()) + { + process.StartInfo = psi; + process.Start(); + process.WaitForExit(); + string output = process.StandardOutput.ReadToEnd(); + string errors = process.StandardError.ReadToEnd(); + } + + File.Delete(modifiedHtmlFilePath); + } + + private static string ConvertEjsToHTML(string ejsFilePath, string outputFilePath, string ejsDataJson) + { + // Generate directory + string directoryPath = Path.GetDirectoryName(outputFilePath); + string tempDirectoryFilePath = Path.Combine(directoryPath, "Temp"); + + if (!Directory.Exists(tempDirectoryFilePath)) + { + Directory.CreateDirectory(tempDirectoryFilePath); + } + + // Generate file path to converted html template + string tempHtmlFilePath = Path.Combine(tempDirectoryFilePath, "htmlTemplate.html"); + + // If the ejs data json is invalid then throw exception + if (!string.IsNullOrWhiteSpace(ejsDataJson) && !IsValidJSON(ejsDataJson)) + { + throw new Exception("Received invalid JSON data for EJS template"); + } + + // Write json data string to json file + string ejsDataJsonFilePath = Path.Combine(tempDirectoryFilePath, "ejsData.json"); + File.WriteAllText(ejsDataJsonFilePath, ejsDataJson); + + string commandLine = "cmd.exe"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + commandLine = "ejs"; + } + string arguments = EjsToHtmlArgumentsBasedOnOS(ejsFilePath, ejsDataJsonFilePath, tempHtmlFilePath); + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = commandLine, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = new Process()) + { + process.StartInfo = psi; + process.Start(); + process.WaitForExit(); + string output = process.StandardOutput.ReadToEnd(); + string errors = process.StandardError.ReadToEnd(); + } + + // Delete json data file + File.Delete(ejsDataJsonFilePath); + + return tempHtmlFilePath; + } + + private static bool IsValidJSON(string json) + { + try + { + JToken.Parse(json); + return true; + } + catch (JsonReaderException) + { + return false; + } + } + + private static string HtmlToPdfArgumentsBasedOnOS(string toolFolderAbsolutePath, string modifiedHtmlFilePath, string outputFilePath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return $"/C {toolFolderAbsolutePath} \"{modifiedHtmlFilePath}\" \"{outputFilePath}\""; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return $"{modifiedHtmlFilePath} {outputFilePath}"; + } + else + { + throw new Exception("Unknown operating system"); + } + } + + private static string EjsToHtmlArgumentsBasedOnOS(string ejsFilePath, string ejsDataJsonFilePath, string tempHtmlFilePath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return $"/C ejs \"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return $"{ejsFilePath} -f {ejsDataJsonFilePath} -o {tempHtmlFilePath}"; + } + else + { + throw new Exception("Unknown operating system"); + } + } +} diff --git a/DocumentService/Word/Models/ContentData.cs b/DocumentService/Word/Models/ContentData.cs index c6f4ae2..38391ba 100644 --- a/DocumentService/Word/Models/ContentData.cs +++ b/DocumentService/Word/Models/ContentData.cs @@ -1,30 +1,29 @@ -namespace DocumentService.Word.Models -{ - - /// - /// Represents the data for a content placeholder in a Word document. - /// - public class ContentData - { - /// - /// Gets or sets the placeholder name. - /// - public string Placeholder { get; set; } - - /// - /// Gets or sets the content to replace the placeholder with. - /// - public string Content { get; set; } - - /// - /// Gets or sets the content type of the placeholder (text or image). - /// - public ContentType ContentType { get; set; } - - /// - /// Gets or sets the parent body of the placeholder (none or table). - /// - - public ParentBody ParentBody { get; set; } - } -} +namespace DocumentService.Word.Models; + + +/// +/// Represents the data for a content placeholder in a Word document. +/// +public class ContentData +{ + /// + /// Gets or sets the placeholder name. + /// + public string Placeholder { get; set; } + + /// + /// Gets or sets the content to replace the placeholder with. + /// + public string Content { get; set; } + + /// + /// Gets or sets the content type of the placeholder (text or image). + /// + public ContentType ContentType { get; set; } + + /// + /// Gets or sets the parent body of the placeholder (none or table). + /// + + public ParentBody ParentBody { get; set; } +} diff --git a/DocumentService/Word/Models/DocumentData.cs b/DocumentService/Word/Models/DocumentData.cs index 1ca80fe..5b56a8e 100644 --- a/DocumentService/Word/Models/DocumentData.cs +++ b/DocumentService/Word/Models/DocumentData.cs @@ -1,24 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DocumentService.Word.Models -{ - - /// - /// Represents the data for a Word document, including content placeholders and table data. - /// - public class DocumentData - { - /// - /// Gets or sets the list of content placeholders in the document. - /// - public List Placeholders { get; set; } - - /// - /// Gets or sets the list of table data in the document. - /// - - public List TablesData { get; set; } - } -} +using System; +using System.Collections.Generic; +using System.Text; + +namespace DocumentService.Word.Models; + + +/// +/// Represents the data for a Word document, including content placeholders and table data. +/// +public class DocumentData +{ + /// + /// Gets or sets the list of content placeholders in the document. + /// + public List Placeholders { get; set; } + + /// + /// Gets or sets the list of table data in the document. + /// + + public List TablesData { get; set; } +} diff --git a/DocumentService/Word/Models/Enums.cs b/DocumentService/Word/Models/Enums.cs index dff7e01..31fedc5 100644 --- a/DocumentService/Word/Models/Enums.cs +++ b/DocumentService/Word/Models/Enums.cs @@ -1,36 +1,35 @@ -namespace DocumentService.Word.Models -{ - - /// - /// Represents the content type of a placeholder in a Word document. - /// - public enum ContentType - { - /// - /// The placeholder represents text content. - /// - Text = 0, - - /// - /// The placeholder represents an image. - /// - Image = 1 - } - - /// - /// Represents the parent body of a placeholder in a Word document. - /// - public enum ParentBody - { - /// - /// The placeholder does not have a parent body. - /// - None = 0, - - /// - /// The placeholder belongs to a table. - /// - - Table = 1 - } -} +namespace DocumentService.Word.Models; + + +/// +/// Represents the content type of a placeholder in a Word document. +/// +public enum ContentType +{ + /// + /// The placeholder represents text content. + /// + Text = 0, + + /// + /// The placeholder represents an image. + /// + Image = 1 +} + +/// +/// Represents the parent body of a placeholder in a Word document. +/// +public enum ParentBody +{ + /// + /// The placeholder does not have a parent body. + /// + None = 0, + + /// + /// The placeholder belongs to a table. + /// + + Table = 1 +} diff --git a/DocumentService/Word/Models/TableData.cs b/DocumentService/Word/Models/TableData.cs index f5e8a2a..dba7a72 100644 --- a/DocumentService/Word/Models/TableData.cs +++ b/DocumentService/Word/Models/TableData.cs @@ -1,23 +1,22 @@ -using System.Collections.Generic; - -namespace DocumentService.Word.Models -{ - - /// - /// Represents the data for a table in a Word document. - /// - public class TableData - { - /// - /// Gets or sets the position of the table in the document. - /// - public int TablePos { get; set; } - - /// - /// Gets or sets the list of dictionaries representing the data for each row in the table. - /// Each dictionary contains column header-value pairs. - /// - - public List> Data { get; set; } - } -} +using System.Collections.Generic; + +namespace DocumentService.Word.Models; + + +/// +/// Represents the data for a table in a Word document. +/// +public class TableData +{ + /// + /// Gets or sets the position of the table in the document. + /// + public int TablePos { get; set; } + + /// + /// Gets or sets the list of dictionaries representing the data for each row in the table. + /// Each dictionary contains column header-value pairs. + /// + + public List> Data { get; set; } +} diff --git a/DocumentService/Word/WordDocumentGenerator.cs b/DocumentService/Word/WordDocumentGenerator.cs index 659d55e..83198f5 100644 --- a/DocumentService/Word/WordDocumentGenerator.cs +++ b/DocumentService/Word/WordDocumentGenerator.cs @@ -1,310 +1,309 @@ -using DocumentFormat.OpenXml.Drawing; -using DocumentFormat.OpenXml.Drawing.Wordprocessing; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Wordprocessing; -using DocumentService.Word.Models; -using NPOI.XWPF.UserModel; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; - -namespace DocumentService.Word -{ - /// - /// Provides functionality to generate Word documents based on templates and data. - /// - public static class WordDocumentGenerator - { - /// - /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. - /// - /// The file path of the template document. - /// The data to replace the placeholders in the template. - /// The file path to save the generated document. - public static void GenerateDocumentByTemplate(string templateFilePath, DocumentData documentData, string outputFilePath) - { - try - { - List contentData = documentData.Placeholders; - List tablesData = documentData.TablesData; - - // Creating dictionaries for each type of placeholders - Dictionary textPlaceholders = new Dictionary(); - Dictionary tableContentPlaceholders = new Dictionary(); - Dictionary imagePlaceholders = new Dictionary(); - - foreach (ContentData content in contentData) - { - if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) - { - string placeholder = "{" + content.Placeholder + "}"; - textPlaceholders.Add(placeholder, content.Content); - } - else if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Image) - { - string placeholder = content.Placeholder; - imagePlaceholders.Add(placeholder, content.Content); - } - else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) - { - string placeholder = "{" + content.Placeholder + "}"; - tableContentPlaceholders.Add(placeholder, content.Content); - } - } - - // Create document of the template - XWPFDocument document = GetXWPFDocument(templateFilePath); - - // For each element in the document - foreach (IBodyElement element in document.BodyElements) - { - if (element.ElementType == BodyElementType.PARAGRAPH) - { - // If element is a paragraph - XWPFParagraph paragraph = (XWPFParagraph)element; - - // If the paragraph is empty string or the placeholder regex does not match then continue - if (paragraph.ParagraphText == string.Empty || !new Regex(@"{[a-zA-Z]+}").IsMatch(paragraph.ParagraphText)) - { - continue; - } - - // Replace placeholders in paragraph with values - paragraph = ReplacePlaceholdersOnBody(paragraph, textPlaceholders); - } - else if (element.ElementType == BodyElementType.TABLE) - { - // If element is a table - XWPFTable table = (XWPFTable)element; - - // Replace placeholders in a table - table = ReplacePlaceholderOnTables(table, tableContentPlaceholders); - - // Populate the table with data if it is passed in tablesData list - foreach (TableData insertData in tablesData) - { - if (insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) - { - table = PopulateTable(table, insertData); - } - } - } - } - - // Write the document to output file path and close the document - WriteDocument(document, outputFilePath); - document.Close(); - - /* - * Image Replacement is done after writing the document here, - * because for Text Replacement, NPOI package is being used - * and for Image Replacement, OpeXML package is used. - * Since both the packages have different execution method, so they are handled separately - */ - // Replace all the image placeholders in the output file - ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); - } - catch (Exception ex) - { - throw ex; - } - } - - /// - /// Retrieves an instance of XWPFDocument from the specified document file path. - /// - /// The file path of the Word document. - /// An instance of XWPFDocument representing the Word document. - private static XWPFDocument GetXWPFDocument(string docFilePath) - { - FileStream readStream = File.OpenRead(docFilePath); - XWPFDocument document = new XWPFDocument(readStream); - readStream.Close(); - return document; - } - - /// - /// Writes the XWPFDocument to the specified file path. - /// - /// The XWPFDocument to write. - /// The file path to save the document. - private static void WriteDocument(XWPFDocument document, string filePath) - { - using (FileStream writeStream = File.Create(filePath)) - { - document.Write(writeStream); - } - } - - /// - /// Replaces the text placeholders in a paragraph with the specified values. - /// - /// The XWPFParagraph containing the placeholders. - /// The dictionary of text placeholders and their corresponding values. - /// The updated XWPFParagraph. - private static XWPFParagraph ReplacePlaceholdersOnBody(XWPFParagraph paragraph, Dictionary textPlaceholders) - { - // Get a list of all placeholders in the current paragraph - List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") - .Cast() - .Select(s => s.Groups[0].Value).ToList(); - - // For each placeholder in paragraph - foreach (string placeholder in placeholdersTobeReplaced) - { - // Replace text placeholders in paragraph with values - if (textPlaceholders.ContainsKey(placeholder)) - { - paragraph.ReplaceText(placeholder, textPlaceholders[placeholder]); - } - - paragraph.SpacingAfter = 0; - } - - return paragraph; - } - - /// - /// Replaces the text placeholders in a table with the specified values. - /// - /// The XWPFTable containing the placeholders. - /// The dictionary of table content placeholders and their corresponding values. - /// The updated XWPFTable. - private static XWPFTable ReplacePlaceholderOnTables(XWPFTable table, Dictionary tableContentPlaceholders) - { - // Loop through each cell of the table - foreach (XWPFTableRow row in table.Rows) - { - foreach (XWPFTableCell cell in row.GetTableCells()) - { - foreach (XWPFParagraph paragraph in cell.Paragraphs) - { - // Get a list of all placeholders in the current cell - List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") - .Cast() - .Select(s => s.Groups[0].Value).ToList(); - - // For each placeholder in the cell - foreach (string placeholder in placeholdersTobeReplaced) - { - // replace the placeholder with its value - if (tableContentPlaceholders.ContainsKey(placeholder)) - { - paragraph.ReplaceText(placeholder, tableContentPlaceholders[placeholder]); - } - } - } - } - } - - return table; - } - - /// - /// Populates a table with the specified data. - /// - /// The XWPFTable to populate. - /// The data to populate the table. - /// The updated XWPFTable. - private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) - { - // Get the header row - XWPFTableRow headerRow = table.GetRow(0); - - // Return if no header row found or if it does not have any column - if (headerRow == null || headerRow.GetTableCells() == null || headerRow.GetTableCells().Count <= 0) - { - return table; - } - - // For each row's data stored in table data - foreach (Dictionary rowData in tableData.Data) - { - // Create a new row and its columns - XWPFTableRow row = table.CreateRow(); - - // For each cell in row - for (int cellNumber = 0; cellNumber < row.GetTableCells().Count; cellNumber++) - { - XWPFTableCell cell = row.GetCell(cellNumber); - - // Get the column header of this cell - string columnHeader = headerRow.GetCell(cellNumber).GetText(); - - // Add the cell's value - if (rowData.ContainsKey(columnHeader)) - { - cell.SetText(rowData[columnHeader]); - } - } - } - - return table; - } - - /// - /// Replaces the image placeholders in the output file with the specified images. - /// - /// The input file path containing the image placeholders. - /// The output file path where the updated document will be saved. - /// The dictionary of image placeholders and their corresponding image paths. - private static void ReplaceImagePlaceholders(string inputFilePath, string outputFilePath, Dictionary imagePlaceholders) - { - byte[] docBytes = File.ReadAllBytes(inputFilePath); - - // Write document bytes to memory - MemoryStream memoryStream = new MemoryStream(); - memoryStream.Write(docBytes, 0, docBytes.Length); - - using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) - { - MainDocumentPart mainDocumentPart = wordDocument.MainDocumentPart; - - // Get a list of drawings (images) - IEnumerable drawings = mainDocumentPart.Document.Descendants().ToList(); - - /* - * FIXME: Look on how we can improve this loop operation. - */ - foreach (Drawing drawing in drawings) - { - DocProperties docProperty = drawing.Descendants().FirstOrDefault(); - - // If drawing / image name is present in imagePlaceholders dictionary, then replace image - if (docProperty != null && imagePlaceholders.ContainsKey(docProperty.Name)) - { - List drawingBlips = drawing.Descendants().ToList(); - - foreach (Blip blip in drawingBlips) - { - OpenXmlPart imagePart = wordDocument.MainDocumentPart.GetPartById(blip.Embed); - - using (BinaryWriter writer = new BinaryWriter(imagePart.GetStream())) - { - string imagePath = imagePlaceholders[docProperty.Name]; - - /* - * WebClient has been deprecated and we need to use HTTPClient. - * This involves the methods to be asynchronous. - */ - using (WebClient webClient = new WebClient()) - { - writer.Write(webClient.DownloadData(imagePath)); - } - } - } - } - } - } - - // Overwrite the output file - FileStream fileStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); - memoryStream.WriteTo(fileStream); - fileStream.Close(); - memoryStream.Close(); - } - } +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Drawing.Wordprocessing; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using DocumentService.Word.Models; +using NPOI.XWPF.UserModel; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; + +namespace DocumentService.Word; + +/// +/// Provides functionality to generate Word documents based on templates and data. +/// +public static class WordDocumentGenerator +{ + /// + /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. + /// + /// The file path of the template document. + /// The data to replace the placeholders in the template. + /// The file path to save the generated document. + public static void GenerateDocumentByTemplate(string templateFilePath, DocumentData documentData, string outputFilePath) + { + try + { + List contentData = documentData.Placeholders; + List tablesData = documentData.TablesData; + + // Creating dictionaries for each type of placeholders + Dictionary textPlaceholders = new Dictionary(); + Dictionary tableContentPlaceholders = new Dictionary(); + Dictionary imagePlaceholders = new Dictionary(); + + foreach (ContentData content in contentData) + { + if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) + { + string placeholder = "{" + content.Placeholder + "}"; + textPlaceholders.Add(placeholder, content.Content); + } + else if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Image) + { + string placeholder = content.Placeholder; + imagePlaceholders.Add(placeholder, content.Content); + } + else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) + { + string placeholder = "{" + content.Placeholder + "}"; + tableContentPlaceholders.Add(placeholder, content.Content); + } + } + + // Create document of the template + XWPFDocument document = GetXWPFDocument(templateFilePath); + + // For each element in the document + foreach (IBodyElement element in document.BodyElements) + { + if (element.ElementType == BodyElementType.PARAGRAPH) + { + // If element is a paragraph + XWPFParagraph paragraph = (XWPFParagraph)element; + + // If the paragraph is empty string or the placeholder regex does not match then continue + if (paragraph.ParagraphText == string.Empty || !new Regex(@"{[a-zA-Z]+}").IsMatch(paragraph.ParagraphText)) + { + continue; + } + + // Replace placeholders in paragraph with values + paragraph = ReplacePlaceholdersOnBody(paragraph, textPlaceholders); + } + else if (element.ElementType == BodyElementType.TABLE) + { + // If element is a table + XWPFTable table = (XWPFTable)element; + + // Replace placeholders in a table + table = ReplacePlaceholderOnTables(table, tableContentPlaceholders); + + // Populate the table with data if it is passed in tablesData list + foreach (TableData insertData in tablesData) + { + if (insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) + { + table = PopulateTable(table, insertData); + } + } + } + } + + // Write the document to output file path and close the document + WriteDocument(document, outputFilePath); + document.Close(); + + /* + * Image Replacement is done after writing the document here, + * because for Text Replacement, NPOI package is being used + * and for Image Replacement, OpeXML package is used. + * Since both the packages have different execution method, so they are handled separately + */ + // Replace all the image placeholders in the output file + ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); + } + catch (Exception ex) + { + throw; + } + } + + /// + /// Retrieves an instance of XWPFDocument from the specified document file path. + /// + /// The file path of the Word document. + /// An instance of XWPFDocument representing the Word document. + private static XWPFDocument GetXWPFDocument(string docFilePath) + { + FileStream readStream = File.OpenRead(docFilePath); + XWPFDocument document = new XWPFDocument(readStream); + readStream.Close(); + return document; + } + + /// + /// Writes the XWPFDocument to the specified file path. + /// + /// The XWPFDocument to write. + /// The file path to save the document. + private static void WriteDocument(XWPFDocument document, string filePath) + { + using (FileStream writeStream = File.Create(filePath)) + { + document.Write(writeStream); + } + } + + /// + /// Replaces the text placeholders in a paragraph with the specified values. + /// + /// The XWPFParagraph containing the placeholders. + /// The dictionary of text placeholders and their corresponding values. + /// The updated XWPFParagraph. + private static XWPFParagraph ReplacePlaceholdersOnBody(XWPFParagraph paragraph, Dictionary textPlaceholders) + { + // Get a list of all placeholders in the current paragraph + List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") + .Cast() + .Select(s => s.Groups[0].Value).ToList(); + + // For each placeholder in paragraph + foreach (string placeholder in placeholdersTobeReplaced) + { + // Replace text placeholders in paragraph with values + if (textPlaceholders.ContainsKey(placeholder)) + { + paragraph.ReplaceText(placeholder, textPlaceholders[placeholder]); + } + + paragraph.SpacingAfter = 0; + } + + return paragraph; + } + + /// + /// Replaces the text placeholders in a table with the specified values. + /// + /// The XWPFTable containing the placeholders. + /// The dictionary of table content placeholders and their corresponding values. + /// The updated XWPFTable. + private static XWPFTable ReplacePlaceholderOnTables(XWPFTable table, Dictionary tableContentPlaceholders) + { + // Loop through each cell of the table + foreach (XWPFTableRow row in table.Rows) + { + foreach (XWPFTableCell cell in row.GetTableCells()) + { + foreach (XWPFParagraph paragraph in cell.Paragraphs) + { + // Get a list of all placeholders in the current cell + List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") + .Cast() + .Select(s => s.Groups[0].Value).ToList(); + + // For each placeholder in the cell + foreach (string placeholder in placeholdersTobeReplaced) + { + // replace the placeholder with its value + if (tableContentPlaceholders.ContainsKey(placeholder)) + { + paragraph.ReplaceText(placeholder, tableContentPlaceholders[placeholder]); + } + } + } + } + } + + return table; + } + + /// + /// Populates a table with the specified data. + /// + /// The XWPFTable to populate. + /// The data to populate the table. + /// The updated XWPFTable. + private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) + { + // Get the header row + XWPFTableRow headerRow = table.GetRow(0); + + // Return if no header row found or if it does not have any column + if (headerRow == null || headerRow.GetTableCells() == null || headerRow.GetTableCells().Count <= 0) + { + return table; + } + + // For each row's data stored in table data + foreach (Dictionary rowData in tableData.Data) + { + // Create a new row and its columns + XWPFTableRow row = table.CreateRow(); + + // For each cell in row + for (int cellNumber = 0; cellNumber < row.GetTableCells().Count; cellNumber++) + { + XWPFTableCell cell = row.GetCell(cellNumber); + + // Get the column header of this cell + string columnHeader = headerRow.GetCell(cellNumber).GetText(); + + // Add the cell's value + if (rowData.ContainsKey(columnHeader)) + { + cell.SetText(rowData[columnHeader]); + } + } + } + + return table; + } + + /// + /// Replaces the image placeholders in the output file with the specified images. + /// + /// The input file path containing the image placeholders. + /// The output file path where the updated document will be saved. + /// The dictionary of image placeholders and their corresponding image paths. + private static void ReplaceImagePlaceholders(string inputFilePath, string outputFilePath, Dictionary imagePlaceholders) + { + byte[] docBytes = File.ReadAllBytes(inputFilePath); + + // Write document bytes to memory + MemoryStream memoryStream = new MemoryStream(); + memoryStream.Write(docBytes, 0, docBytes.Length); + + using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) + { + MainDocumentPart mainDocumentPart = wordDocument.MainDocumentPart; + + // Get a list of drawings (images) + IEnumerable drawings = mainDocumentPart.Document.Descendants().ToList(); + + /* + * FIXME: Look on how we can improve this loop operation. + */ + foreach (Drawing drawing in drawings) + { + DocProperties docProperty = drawing.Descendants().FirstOrDefault(); + + // If drawing / image name is present in imagePlaceholders dictionary, then replace image + if (docProperty != null && imagePlaceholders.ContainsKey(docProperty.Name)) + { + List drawingBlips = drawing.Descendants().ToList(); + + foreach (Blip blip in drawingBlips) + { + OpenXmlPart imagePart = wordDocument.MainDocumentPart.GetPartById(blip.Embed); + + using (BinaryWriter writer = new BinaryWriter(imagePart.GetStream())) + { + string imagePath = imagePlaceholders[docProperty.Name]; + + /* + * WebClient has been deprecated and we need to use HTTPClient. + * This involves the methods to be asynchronous. + */ + using (WebClient webClient = new WebClient()) + { + writer.Write(webClient.DownloadData(imagePath)); + } + } + } + } + } + } + + // Overwrite the output file + FileStream fileStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + memoryStream.WriteTo(fileStream); + fileStream.Close(); + memoryStream.Close(); + } } \ No newline at end of file From 71073b1ece7335e1253baf5c14bd91f14ded481b Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 15 Jun 2025 14:52:09 +0530 Subject: [PATCH 02/73] chore: apply dotnet format changes --- .../Controllers/PdfController.cs | 414 +++++++++--------- .../Controllers/WordController.cs | 312 ++++++------- DocumentService.API/DotEnv.cs | 70 +-- .../Helpers/AuthenticationHelper.cs | 56 +-- .../Helpers/AutoMappingProfile.cs | 26 +- .../Helpers/Base64StringHelper.cs | 62 +-- .../Helpers/CommonMethodsHelper.cs | 52 +-- DocumentService.API/Models/BaseResponse.cs | 66 +-- .../Models/PdfGenerationRequestDTO.cs | 26 +- .../Models/WordGenerationRequestDTO.cs | 48 +- DocumentService.API/Program.cs | 274 ++++++------ 11 files changed, 703 insertions(+), 703 deletions(-) diff --git a/DocumentService.API/Controllers/PdfController.cs b/DocumentService.API/Controllers/PdfController.cs index 0413daa..1436eff 100644 --- a/DocumentService.API/Controllers/PdfController.cs +++ b/DocumentService.API/Controllers/PdfController.cs @@ -1,207 +1,207 @@ -using DocumentService.Pdf; -using DocumentService.API.Helpers; -using DocumentService.API.Models; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; - -namespace DocumentService.API.Controllers; - -[Route("api")] -[ApiController] -public class PdfController : ControllerBase -{ - private readonly IConfiguration _configuration; - private readonly IWebHostEnvironment _hostingEnvironment; - private readonly ILogger _logger; - - public PdfController(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, ILogger logger) - { - this._configuration = configuration; - this._hostingEnvironment = hostingEnvironment; - this._logger = logger; - } - - [HttpPost] - [Authorize] - [Route("pdf/GeneratePdfUsingHtml")] - public async Task> GeneratePdf(PdfGenerationRequestDTO request) - { - BaseResponse response = new BaseResponse(ResponseStatus.Fail); - - try - { - // Generate filepath to save base64 html template - string htmlTemplateFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:HTML").Value, - CommonMethodsHelper.GenerateRandomFileName("html") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(htmlTemplateFilePath); - - // Save base64 html template to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, htmlTemplateFilePath, this._configuration); - - // Initialize tools and output filepaths - string htmlToPDfToolsFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value - ); - - string outputFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, - CommonMethodsHelper.GenerateRandomFileName("pdf") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); - - // Generate and save pdf in output directory - PdfDocumentGenerator.GeneratePdf( - htmlToPDfToolsFilePath, - htmlTemplateFilePath, - request.DocumentData.Placeholders, - outputFilePath, - isEjsTemplate: false, - serializedEjsDataJson: null - ); - - // Convert pdf file in output directory to base64 string - string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); - - // Return response - response.Status = ResponseStatus.Success; - response.Base64 = outputBase64String; - response.Message = "PDF generated successfully"; - return this.Ok(response); - } - catch (BadHttpRequestException ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FormatException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Error converting base64 string to file"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FileNotFoundException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Unable to load file saved from base64 string"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - catch (Exception ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - } - - [HttpPost] - [Authorize] - [Route("pdf/GeneratePdfUsingEjs")] - public async Task> GeneratePdfUsingEjs(PdfGenerationRequestDTO request) - { - BaseResponse response = new BaseResponse(ResponseStatus.Fail); - - try - { - // Generate filepath to save base64 html template - string ejsTemplateFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:EJS").Value, - CommonMethodsHelper.GenerateRandomFileName("ejs") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(ejsTemplateFilePath); - - // Save base64 html template to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, ejsTemplateFilePath, this._configuration); - - // Initialize tools and output filepaths - string ejsToPDfToolsFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value - ); - - string outputFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, - CommonMethodsHelper.GenerateRandomFileName("pdf") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); - - // Generate and save pdf in output directory - PdfDocumentGenerator.GeneratePdf( - ejsToPDfToolsFilePath, - ejsTemplateFilePath, - request.DocumentData?.Placeholders, - outputFilePath, - isEjsTemplate: true, - serializedEjsDataJson: request.SerializedEjsDataJson - ); - - // Convert pdf file in output directory to base64 string - string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); - - // Return response - response.Status = ResponseStatus.Success; - response.Base64 = outputBase64String; - response.Message = "PDF generated successfully"; - return this.Ok(response); - } - catch (BadHttpRequestException ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FormatException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Error converting base64 string to file"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FileNotFoundException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Unable to load file saved from base64 string"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - catch (Exception ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - } -} +using DocumentService.Pdf; +using DocumentService.API.Helpers; +using DocumentService.API.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +namespace DocumentService.API.Controllers; + +[Route("api")] +[ApiController] +public class PdfController : ControllerBase +{ + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _hostingEnvironment; + private readonly ILogger _logger; + + public PdfController(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, ILogger logger) + { + this._configuration = configuration; + this._hostingEnvironment = hostingEnvironment; + this._logger = logger; + } + + [HttpPost] + [Authorize] + [Route("pdf/GeneratePdfUsingHtml")] + public async Task> GeneratePdf(PdfGenerationRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + + try + { + // Generate filepath to save base64 html template + string htmlTemplateFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:HTML").Value, + CommonMethodsHelper.GenerateRandomFileName("html") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(htmlTemplateFilePath); + + // Save base64 html template to inputs directory + await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, htmlTemplateFilePath, this._configuration); + + // Initialize tools and output filepaths + string htmlToPDfToolsFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value + ); + + string outputFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, + CommonMethodsHelper.GenerateRandomFileName("pdf") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); + + // Generate and save pdf in output directory + PdfDocumentGenerator.GeneratePdf( + htmlToPDfToolsFilePath, + htmlTemplateFilePath, + request.DocumentData.Placeholders, + outputFilePath, + isEjsTemplate: false, + serializedEjsDataJson: null + ); + + // Convert pdf file in output directory to base64 string + string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); + + // Return response + response.Status = ResponseStatus.Success; + response.Base64 = outputBase64String; + response.Message = "PDF generated successfully"; + return this.Ok(response); + } + catch (BadHttpRequestException ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FormatException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Error converting base64 string to file"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FileNotFoundException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Unable to load file saved from base64 string"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } + + [HttpPost] + [Authorize] + [Route("pdf/GeneratePdfUsingEjs")] + public async Task> GeneratePdfUsingEjs(PdfGenerationRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + + try + { + // Generate filepath to save base64 html template + string ejsTemplateFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:EJS").Value, + CommonMethodsHelper.GenerateRandomFileName("ejs") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(ejsTemplateFilePath); + + // Save base64 html template to inputs directory + await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, ejsTemplateFilePath, this._configuration); + + // Initialize tools and output filepaths + string ejsToPDfToolsFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value + ); + + string outputFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, + CommonMethodsHelper.GenerateRandomFileName("pdf") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); + + // Generate and save pdf in output directory + PdfDocumentGenerator.GeneratePdf( + ejsToPDfToolsFilePath, + ejsTemplateFilePath, + request.DocumentData?.Placeholders, + outputFilePath, + isEjsTemplate: true, + serializedEjsDataJson: request.SerializedEjsDataJson + ); + + // Convert pdf file in output directory to base64 string + string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); + + // Return response + response.Status = ResponseStatus.Success; + response.Base64 = outputBase64String; + response.Message = "PDF generated successfully"; + return this.Ok(response); + } + catch (BadHttpRequestException ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FormatException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Error converting base64 string to file"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FileNotFoundException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Unable to load file saved from base64 string"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } +} diff --git a/DocumentService.API/Controllers/WordController.cs b/DocumentService.API/Controllers/WordController.cs index 33601a8..c888b91 100644 --- a/DocumentService.API/Controllers/WordController.cs +++ b/DocumentService.API/Controllers/WordController.cs @@ -1,156 +1,156 @@ -using AutoMapper; -using DocumentService.Word; -using DocumentService.Word.Models; -using DocumentService.API.Helpers; -using DocumentService.API.Models; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; - -namespace DocumentService.API.Controllers; - -[Route("api")] -[ApiController] -public class WordController : ControllerBase -{ - private readonly IConfiguration _configuration; - private readonly IWebHostEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly IMapper _mapper; - - public WordController(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, ILogger logger, IMapper mapper) - { - this._configuration = configuration; - this._hostingEnvironment = hostingEnvironment; - this._logger = logger; - this._mapper = mapper; - } - - [HttpPost] - [Authorize] - [Route("word/GenerateWordDocument")] - public async Task> GenerateWord(WordGenerationRequestDTO request) - { - BaseResponse response = new BaseResponse(ResponseStatus.Fail); - - try - { - // Generate filepath to save base64 docx template - string docxTemplateFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, - CommonMethodsHelper.GenerateRandomFileName("docx") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(docxTemplateFilePath); - - // Save docx template to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, docxTemplateFilePath, this._configuration); - - // Initialize output filepath - string outputFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, - CommonMethodsHelper.GenerateRandomFileName("docx") - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); - - // Handle image placeholder data in request - foreach (WordContentDataRequestDTO placeholder in request.DocumentData.Placeholders) - { - if (placeholder.ContentType == ContentType.Image) - { - if (string.IsNullOrWhiteSpace(placeholder.ImageExtension)) - { - throw new BadHttpRequestException("Image extension is required for image content data"); - } - - if (string.IsNullOrWhiteSpace(placeholder.Content)) - { - throw new BadHttpRequestException("Image content data is required"); - } - - // Remove '.' from image extension if present - placeholder.ImageExtension = placeholder.ImageExtension.Replace(".", string.Empty); - - // Generate a random image file name and its path - string imageFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:IMAGES").Value, - CommonMethodsHelper.GenerateRandomFileName(placeholder.ImageExtension) - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(imageFilePath); - - // Save image content base64 string to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(placeholder.Content, imageFilePath, this._configuration); - - // Replace placeholder content with image file path - placeholder.Content = imageFilePath; - } - } - - // Map document data in request to word library model class - DocumentData documentData = new DocumentData - { - Placeholders = this._mapper.Map>(request.DocumentData.Placeholders), - TablesData = request.DocumentData.TablesData - }; - - // Generate and save output docx in output directory - WordDocumentGenerator.GenerateDocumentByTemplate( - docxTemplateFilePath, - documentData, - outputFilePath - ); - - // Convert docx file in output directory to base64 string - string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); - - // Return response - response.Status = ResponseStatus.Success; - response.Base64 = outputBase64String; - response.Message = "Word document generated successfully"; - return this.Ok(response); - } - catch (BadHttpRequestException ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FormatException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Error converting base64 string to file"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.BadRequest(response); - } - catch (FileNotFoundException ex) - { - response.Status = ResponseStatus.Error; - response.Message = "Unable to load file saved from base64 string"; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - catch (Exception ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - } -} +using AutoMapper; +using DocumentService.Word; +using DocumentService.Word.Models; +using DocumentService.API.Helpers; +using DocumentService.API.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +namespace DocumentService.API.Controllers; + +[Route("api")] +[ApiController] +public class WordController : ControllerBase +{ + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public WordController(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, ILogger logger, IMapper mapper) + { + this._configuration = configuration; + this._hostingEnvironment = hostingEnvironment; + this._logger = logger; + this._mapper = mapper; + } + + [HttpPost] + [Authorize] + [Route("word/GenerateWordDocument")] + public async Task> GenerateWord(WordGenerationRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + + try + { + // Generate filepath to save base64 docx template + string docxTemplateFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, + CommonMethodsHelper.GenerateRandomFileName("docx") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(docxTemplateFilePath); + + // Save docx template to inputs directory + await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, docxTemplateFilePath, this._configuration); + + // Initialize output filepath + string outputFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, + CommonMethodsHelper.GenerateRandomFileName("docx") + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); + + // Handle image placeholder data in request + foreach (WordContentDataRequestDTO placeholder in request.DocumentData.Placeholders) + { + if (placeholder.ContentType == ContentType.Image) + { + if (string.IsNullOrWhiteSpace(placeholder.ImageExtension)) + { + throw new BadHttpRequestException("Image extension is required for image content data"); + } + + if (string.IsNullOrWhiteSpace(placeholder.Content)) + { + throw new BadHttpRequestException("Image content data is required"); + } + + // Remove '.' from image extension if present + placeholder.ImageExtension = placeholder.ImageExtension.Replace(".", string.Empty); + + // Generate a random image file name and its path + string imageFilePath = Path.Combine( + this._hostingEnvironment.WebRootPath, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, + this._configuration.GetSection("TEMPORARY_FILE_PATHS:IMAGES").Value, + CommonMethodsHelper.GenerateRandomFileName(placeholder.ImageExtension) + ); + + CommonMethodsHelper.CreateDirectoryIfNotExists(imageFilePath); + + // Save image content base64 string to inputs directory + await Base64StringHelper.SaveBase64StringToFilePath(placeholder.Content, imageFilePath, this._configuration); + + // Replace placeholder content with image file path + placeholder.Content = imageFilePath; + } + } + + // Map document data in request to word library model class + DocumentData documentData = new DocumentData + { + Placeholders = this._mapper.Map>(request.DocumentData.Placeholders), + TablesData = request.DocumentData.TablesData + }; + + // Generate and save output docx in output directory + WordDocumentGenerator.GenerateDocumentByTemplate( + docxTemplateFilePath, + documentData, + outputFilePath + ); + + // Convert docx file in output directory to base64 string + string outputBase64String = await Base64StringHelper.ConvertFileToBase64String(outputFilePath); + + // Return response + response.Status = ResponseStatus.Success; + response.Base64 = outputBase64String; + response.Message = "Word document generated successfully"; + return this.Ok(response); + } + catch (BadHttpRequestException ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FormatException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Error converting base64 string to file"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.BadRequest(response); + } + catch (FileNotFoundException ex) + { + response.Status = ResponseStatus.Error; + response.Message = "Unable to load file saved from base64 string"; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } +} diff --git a/DocumentService.API/DotEnv.cs b/DocumentService.API/DotEnv.cs index 2b5b4ce..45e2a4a 100644 --- a/DocumentService.API/DotEnv.cs +++ b/DocumentService.API/DotEnv.cs @@ -1,35 +1,35 @@ -namespace DocumentService.API; - -public static class DotEnv -{ - public static void Load(string filePath) - { - if (!File.Exists(filePath)) - { - return; - } - - foreach (string line in File.ReadAllLines(filePath)) - { - // Check if the line contains '=' - int equalsIndex = line.IndexOf('='); - if (equalsIndex == -1) - { - continue; // Skip lines without '=' - } - - string key = line.Substring(0, equalsIndex).Trim(); - string value = line.Substring(equalsIndex + 1).Trim(); - - // Check if the value starts and ends with double quotation marks - if (value.StartsWith("\"") && value.EndsWith("\"")) - { - // Remove the double quotation marks - value = value[1..^1]; - } - - Environment.SetEnvironmentVariable(key, value); - } - } - -} +namespace DocumentService.API; + +public static class DotEnv +{ + public static void Load(string filePath) + { + if (!File.Exists(filePath)) + { + return; + } + + foreach (string line in File.ReadAllLines(filePath)) + { + // Check if the line contains '=' + int equalsIndex = line.IndexOf('='); + if (equalsIndex == -1) + { + continue; // Skip lines without '=' + } + + string key = line.Substring(0, equalsIndex).Trim(); + string value = line.Substring(equalsIndex + 1).Trim(); + + // Check if the value starts and ends with double quotation marks + if (value.StartsWith("\"") && value.EndsWith("\"")) + { + // Remove the double quotation marks + value = value[1..^1]; + } + + Environment.SetEnvironmentVariable(key, value); + } + } + +} diff --git a/DocumentService.API/Helpers/AuthenticationHelper.cs b/DocumentService.API/Helpers/AuthenticationHelper.cs index ed0dee9..5104e65 100644 --- a/DocumentService.API/Helpers/AuthenticationHelper.cs +++ b/DocumentService.API/Helpers/AuthenticationHelper.cs @@ -1,28 +1,28 @@ -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Text; -using System.Security.Claims; - -namespace DocumentService.API.Helpers; - -public class AuthenticationHelper -{ - // Function to generate non expiry jwt token based on email - public static string JwtTokenGenerator(string LoginEmail) - { - SymmetricSecurityKey secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( - Environment.GetEnvironmentVariable("JWT_KEY") ?? throw new InvalidOperationException("No JWT key specified") - )); - SigningCredentials signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); - - JwtSecurityToken tokenOptions = new JwtSecurityToken( - claims: new List() - { - new(ClaimTypes.Email, LoginEmail), - }, - signingCredentials: signinCredentials - ); - - return new JwtSecurityTokenHandler().WriteToken(tokenOptions); - } -} +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using System.Security.Claims; + +namespace DocumentService.API.Helpers; + +public class AuthenticationHelper +{ + // Function to generate non expiry jwt token based on email + public static string JwtTokenGenerator(string LoginEmail) + { + SymmetricSecurityKey secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( + Environment.GetEnvironmentVariable("JWT_KEY") ?? throw new InvalidOperationException("No JWT key specified") + )); + SigningCredentials signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + + JwtSecurityToken tokenOptions = new JwtSecurityToken( + claims: new List() + { + new(ClaimTypes.Email, LoginEmail), + }, + signingCredentials: signinCredentials + ); + + return new JwtSecurityTokenHandler().WriteToken(tokenOptions); + } +} diff --git a/DocumentService.API/Helpers/AutoMappingProfile.cs b/DocumentService.API/Helpers/AutoMappingProfile.cs index a9cef5c..d6ddaae 100644 --- a/DocumentService.API/Helpers/AutoMappingProfile.cs +++ b/DocumentService.API/Helpers/AutoMappingProfile.cs @@ -1,13 +1,13 @@ -using AutoMapper; -using DocumentService.Word.Models; -using DocumentService.API.Models; - -namespace DocumentService.API.Helpers; - -public class AutoMappingProfile : Profile -{ - public AutoMappingProfile() - { - this.CreateMap(); - } -} +using AutoMapper; +using DocumentService.Word.Models; +using DocumentService.API.Models; + +namespace DocumentService.API.Helpers; + +public class AutoMappingProfile : Profile +{ + public AutoMappingProfile() + { + this.CreateMap(); + } +} diff --git a/DocumentService.API/Helpers/Base64StringHelper.cs b/DocumentService.API/Helpers/Base64StringHelper.cs index 9984908..9d0368d 100644 --- a/DocumentService.API/Helpers/Base64StringHelper.cs +++ b/DocumentService.API/Helpers/Base64StringHelper.cs @@ -1,31 +1,31 @@ -namespace DocumentService.API.Helpers; - -public static class Base64StringHelper -{ - public static async Task SaveBase64StringToFilePath(string base64String, string filePath, IConfiguration configuration) - { - byte[] data = Convert.FromBase64String(base64String); - - long uploadFileSizeLimitBytes = Convert.ToInt64(configuration.GetSection("CONFIG:UPLOAD_FILE_SIZE_LIMIT_BYTES").Value); - - if (data.LongLength > uploadFileSizeLimitBytes) - { - throw new BadHttpRequestException("Uploaded file is too large"); - } - - await File.WriteAllBytesAsync(filePath, data); - } - - public static async Task ConvertFileToBase64String(string filePath) - { - if (File.Exists(filePath)) - { - byte[] fileData = await File.ReadAllBytesAsync(filePath); - return Convert.ToBase64String(fileData); - } - else - { - throw new FileNotFoundException("The file does not exist: " + filePath); - } - } -} +namespace DocumentService.API.Helpers; + +public static class Base64StringHelper +{ + public static async Task SaveBase64StringToFilePath(string base64String, string filePath, IConfiguration configuration) + { + byte[] data = Convert.FromBase64String(base64String); + + long uploadFileSizeLimitBytes = Convert.ToInt64(configuration.GetSection("CONFIG:UPLOAD_FILE_SIZE_LIMIT_BYTES").Value); + + if (data.LongLength > uploadFileSizeLimitBytes) + { + throw new BadHttpRequestException("Uploaded file is too large"); + } + + await File.WriteAllBytesAsync(filePath, data); + } + + public static async Task ConvertFileToBase64String(string filePath) + { + if (File.Exists(filePath)) + { + byte[] fileData = await File.ReadAllBytesAsync(filePath); + return Convert.ToBase64String(fileData); + } + else + { + throw new FileNotFoundException("The file does not exist: " + filePath); + } + } +} diff --git a/DocumentService.API/Helpers/CommonMethodsHelper.cs b/DocumentService.API/Helpers/CommonMethodsHelper.cs index dc9006d..eaac7f5 100644 --- a/DocumentService.API/Helpers/CommonMethodsHelper.cs +++ b/DocumentService.API/Helpers/CommonMethodsHelper.cs @@ -1,26 +1,26 @@ -namespace DocumentService.API.Helpers; - -public static class CommonMethodsHelper -{ - public static void CreateDirectoryIfNotExists(string filePath) - { - // Get directory name of the file - // If path is a file name only, directory name will be an empty string - string directoryName = Path.GetDirectoryName(filePath); - - if (!string.IsNullOrWhiteSpace(directoryName)) - { - if (!Directory.Exists(directoryName)) - { - // Create all directories on the path that don't already exist - Directory.CreateDirectory(directoryName); - } - } - } - - public static string GenerateRandomFileName(string fileExtension) - { - string randomFileName = Path.GetRandomFileName().Replace(".", string.Empty); - return $"{randomFileName}-{Guid.NewGuid()}.{fileExtension}"; - } -} +namespace DocumentService.API.Helpers; + +public static class CommonMethodsHelper +{ + public static void CreateDirectoryIfNotExists(string filePath) + { + // Get directory name of the file + // If path is a file name only, directory name will be an empty string + string directoryName = Path.GetDirectoryName(filePath); + + if (!string.IsNullOrWhiteSpace(directoryName)) + { + if (!Directory.Exists(directoryName)) + { + // Create all directories on the path that don't already exist + Directory.CreateDirectory(directoryName); + } + } + } + + public static string GenerateRandomFileName(string fileExtension) + { + string randomFileName = Path.GetRandomFileName().Replace(".", string.Empty); + return $"{randomFileName}-{Guid.NewGuid()}.{fileExtension}"; + } +} diff --git a/DocumentService.API/Models/BaseResponse.cs b/DocumentService.API/Models/BaseResponse.cs index 0422bbe..e6d5954 100644 --- a/DocumentService.API/Models/BaseResponse.cs +++ b/DocumentService.API/Models/BaseResponse.cs @@ -1,33 +1,33 @@ -using Microsoft.AspNetCore.Mvc; - -namespace DocumentService.API.Models; - -public enum ResponseStatus -{ - Success, - Fail, - Error -} - -public class BaseResponse -{ - public BaseResponse(ResponseStatus status) => this.Status = status; - public ResponseStatus? Status { get; set; } - public string? Base64 { get; set; } - public string? AuthToken { get; set; } - public string? Message { get; set; } - public string? StackTrace { get; set; } -} - -public class ModelValidationBadRequest -{ - public static BadRequestObjectResult ModelValidationErrorResponse(ActionContext actionContext) - { - return new BadRequestObjectResult(actionContext.ModelState - .Where(modelError => modelError.Value.Errors.Any()) - .Select(modelError => new BaseResponse(ResponseStatus.Error) - { - Message = modelError.Value.Errors.FirstOrDefault().ErrorMessage - }).FirstOrDefault()); - } -} +using Microsoft.AspNetCore.Mvc; + +namespace DocumentService.API.Models; + +public enum ResponseStatus +{ + Success, + Fail, + Error +} + +public class BaseResponse +{ + public BaseResponse(ResponseStatus status) => this.Status = status; + public ResponseStatus? Status { get; set; } + public string? Base64 { get; set; } + public string? AuthToken { get; set; } + public string? Message { get; set; } + public string? StackTrace { get; set; } +} + +public class ModelValidationBadRequest +{ + public static BadRequestObjectResult ModelValidationErrorResponse(ActionContext actionContext) + { + return new BadRequestObjectResult(actionContext.ModelState + .Where(modelError => modelError.Value.Errors.Any()) + .Select(modelError => new BaseResponse(ResponseStatus.Error) + { + Message = modelError.Value.Errors.FirstOrDefault().ErrorMessage + }).FirstOrDefault()); + } +} diff --git a/DocumentService.API/Models/PdfGenerationRequestDTO.cs b/DocumentService.API/Models/PdfGenerationRequestDTO.cs index 8408128..7b0b602 100644 --- a/DocumentService.API/Models/PdfGenerationRequestDTO.cs +++ b/DocumentService.API/Models/PdfGenerationRequestDTO.cs @@ -1,13 +1,13 @@ -using DocumentService.Pdf.Models; -using System.ComponentModel.DataAnnotations; - -namespace DocumentService.API.Models; - -public class PdfGenerationRequestDTO -{ - [Required(ErrorMessage = "Base64 string for PDF template is required")] - public string? Base64 { get; set; } - [Required(ErrorMessage = "Data to be modified in PDF is required")] - public DocumentData? DocumentData { get; set; } - public string? SerializedEjsDataJson { get; set; } -} +using DocumentService.Pdf.Models; +using System.ComponentModel.DataAnnotations; + +namespace DocumentService.API.Models; + +public class PdfGenerationRequestDTO +{ + [Required(ErrorMessage = "Base64 string for PDF template is required")] + public string? Base64 { get; set; } + [Required(ErrorMessage = "Data to be modified in PDF is required")] + public DocumentData? DocumentData { get; set; } + public string? SerializedEjsDataJson { get; set; } +} diff --git a/DocumentService.API/Models/WordGenerationRequestDTO.cs b/DocumentService.API/Models/WordGenerationRequestDTO.cs index c2462a1..1fd1bca 100644 --- a/DocumentService.API/Models/WordGenerationRequestDTO.cs +++ b/DocumentService.API/Models/WordGenerationRequestDTO.cs @@ -1,24 +1,24 @@ -using DocumentService.Word.Models; -using System.ComponentModel.DataAnnotations; - -namespace DocumentService.API.Models; - - -public class WordGenerationRequestDTO -{ - [Required(ErrorMessage = "Base64 string for Word template is required")] - public string? Base64 { get; set; } - [Required(ErrorMessage = "Data to be modified in Word file is required")] - public WordDocumentDataRequestDTO? DocumentData { get; set; } -} - -public class WordContentDataRequestDTO : ContentData -{ - public string? ImageExtension { get; set; } -} - -public class WordDocumentDataRequestDTO -{ - public List Placeholders { get; set; } - public List TablesData { get; set; } -} +using DocumentService.Word.Models; +using System.ComponentModel.DataAnnotations; + +namespace DocumentService.API.Models; + + +public class WordGenerationRequestDTO +{ + [Required(ErrorMessage = "Base64 string for Word template is required")] + public string? Base64 { get; set; } + [Required(ErrorMessage = "Data to be modified in Word file is required")] + public WordDocumentDataRequestDTO? DocumentData { get; set; } +} + +public class WordContentDataRequestDTO : ContentData +{ + public string? ImageExtension { get; set; } +} + +public class WordDocumentDataRequestDTO +{ + public List Placeholders { get; set; } + public List TablesData { get; set; } +} diff --git a/DocumentService.API/Program.cs b/DocumentService.API/Program.cs index 728b1ac..6c4ec49 100644 --- a/DocumentService.API/Program.cs +++ b/DocumentService.API/Program.cs @@ -1,137 +1,137 @@ -using DocumentService.API.Models; -using Microsoft.AspNetCore.Mvc; -using Serilog.Events; -using Serilog; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.OpenApi.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; -using System.Text; -using Swashbuckle.AspNetCore.Filters; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -// Controller Services -builder.Services.AddControllers(options => options.Filters.Add(new ProducesAttribute("application/json"))) - .AddJsonOptions(options => - { - options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); - -// Load .env file -string root = Directory.GetCurrentDirectory(); -string dotenv = Path.GetFullPath(Path.Combine(root, "..", ".env")); -DocumentService.API.DotEnv.Load(dotenv); - -// Configure request size limit -long requestBodySizeLimitBytes = Convert.ToInt64(builder.Configuration.GetSection("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES").Value); - -// Configure request size for Kestrel server - ASP.NET Core project templates use Kestrel by default when not hosted with IIS -builder.Services.Configure(options => -{ - options.Limits.MaxRequestBodySize = requestBodySizeLimitBytes; -}); - -// Configure request size for IIS server -builder.Services.Configure(options => -{ - options.MaxRequestBodySize = requestBodySizeLimitBytes; -}); - -// AutoMapper Services -builder.Services.AddAutoMapper(typeof(Program)); - -// Swagger UI Services -builder.Services.AddSwaggerGen(options => -{ - options.SwaggerDoc("v1", new OpenApiInfo { Title = "DocumentService API", Version = "v1" }); - - options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme - { - Name = "Authorization", - Description = "Standard Authorization header using the Bearer Scheme (\"bearer {token}\")", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey - }); - - options.OperationFilter(); -}); - -// Authentication -builder.Services.AddAuthentication(options => -{ - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}) -.AddJwtBearer(options => -{ - string JWT_KEY = Environment.GetEnvironmentVariable("JWT_KEY") ?? throw new InvalidOperationException("No JWT key specified"); - - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)), - ValidateIssuer = false, - ValidateAudience = false, - // Following code is to allow us to custom handle expiry - // Here check expiry as nullable - ClockSkew = TimeSpan.Zero, - ValidateLifetime = true, - LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) => - { - // Clone the validation parameters, and remove the defult lifetime validator - TokenValidationParameters clonedParameters = validationParameters.Clone(); - clonedParameters.LifetimeValidator = null; - - // If token expiry time is not null, then validate lifetime with skewed clock - if (expires != null) - { - Validators.ValidateLifetime(notBefore, expires, securityToken, clonedParameters); - } - - return true; - } - }; -}); - -// Configure Error Response from Model Validations -builder.Services.AddMvc().ConfigureApiBehaviorOptions(options => -{ - options.InvalidModelStateResponseFactory = actionContext => - { - return ModelValidationBadRequest.ModelValidationErrorResponse(actionContext); - }; -}); - -// Logging service Serilogs -builder.Logging.AddSerilog(); -Log.Logger = new LoggerConfiguration() - .WriteTo.File( - path: "wwwroot/logs/log-.txt", - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}{NewLine}{NewLine}", - rollingInterval: RollingInterval.Day, - restrictedToMinimumLevel: LogEventLevel.Information - ).CreateLogger(); - -WebApplication app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseStaticFiles(); - -app.UseHttpsRedirection(); - -app.UseAuthentication(); - -app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); +using DocumentService.API.Models; +using Microsoft.AspNetCore.Mvc; +using Serilog.Events; +using Serilog; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.OpenApi.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using Swashbuckle.AspNetCore.Filters; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// Controller Services +builder.Services.AddControllers(options => options.Filters.Add(new ProducesAttribute("application/json"))) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + +// Load .env file +string root = Directory.GetCurrentDirectory(); +string dotenv = Path.GetFullPath(Path.Combine(root, "..", ".env")); +DocumentService.API.DotEnv.Load(dotenv); + +// Configure request size limit +long requestBodySizeLimitBytes = Convert.ToInt64(builder.Configuration.GetSection("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES").Value); + +// Configure request size for Kestrel server - ASP.NET Core project templates use Kestrel by default when not hosted with IIS +builder.Services.Configure(options => +{ + options.Limits.MaxRequestBodySize = requestBodySizeLimitBytes; +}); + +// Configure request size for IIS server +builder.Services.Configure(options => +{ + options.MaxRequestBodySize = requestBodySizeLimitBytes; +}); + +// AutoMapper Services +builder.Services.AddAutoMapper(typeof(Program)); + +// Swagger UI Services +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo { Title = "DocumentService API", Version = "v1" }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Name = "Authorization", + Description = "Standard Authorization header using the Bearer Scheme (\"bearer {token}\")", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey + }); + + options.OperationFilter(); +}); + +// Authentication +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddJwtBearer(options => +{ + string JWT_KEY = Environment.GetEnvironmentVariable("JWT_KEY") ?? throw new InvalidOperationException("No JWT key specified"); + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)), + ValidateIssuer = false, + ValidateAudience = false, + // Following code is to allow us to custom handle expiry + // Here check expiry as nullable + ClockSkew = TimeSpan.Zero, + ValidateLifetime = true, + LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) => + { + // Clone the validation parameters, and remove the defult lifetime validator + TokenValidationParameters clonedParameters = validationParameters.Clone(); + clonedParameters.LifetimeValidator = null; + + // If token expiry time is not null, then validate lifetime with skewed clock + if (expires != null) + { + Validators.ValidateLifetime(notBefore, expires, securityToken, clonedParameters); + } + + return true; + } + }; +}); + +// Configure Error Response from Model Validations +builder.Services.AddMvc().ConfigureApiBehaviorOptions(options => +{ + options.InvalidModelStateResponseFactory = actionContext => + { + return ModelValidationBadRequest.ModelValidationErrorResponse(actionContext); + }; +}); + +// Logging service Serilogs +builder.Logging.AddSerilog(); +Log.Logger = new LoggerConfiguration() + .WriteTo.File( + path: "wwwroot/logs/log-.txt", + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}{NewLine}{NewLine}", + rollingInterval: RollingInterval.Day, + restrictedToMinimumLevel: LogEventLevel.Information + ).CreateLogger(); + +WebApplication app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseStaticFiles(); + +app.UseHttpsRedirection(); + +app.UseAuthentication(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); From 9d714d615a16d0c865a7441c3a0e4538aecc4431 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 15 Jun 2025 15:21:17 +0530 Subject: [PATCH 03/73] refactor: renamed project from DocumentService to OsmoDoc and updated all related namespaces --- .../Controllers/PdfController.cs | 8 ++++---- .../Controllers/WordController.cs | 10 +++++----- {DocumentService.API => OsmoDoc.API}/DotEnv.cs | 2 +- .../Helpers/AuthenticationHelper.cs | 2 +- .../Helpers/AutoMappingProfile.cs | 6 +++--- .../Helpers/Base64StringHelper.cs | 2 +- .../Helpers/CommonMethodsHelper.cs | 2 +- .../Models/BaseResponse.cs | 2 +- .../Models/PdfGenerationRequestDTO.cs | 4 ++-- .../Models/WordGenerationRequestDTO.cs | 4 ++-- .../OsmoDoc.API.csproj | 2 +- .../OsmoDoc.API.sln | 2 +- {DocumentService.API => OsmoDoc.API}/Program.cs | 6 +++--- .../Properties/launchSettings.json | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 .../wwwroot/Tools/wkhtmltopdf.exe | Bin DocumentService.sln => OsmoDoc.sln | 4 ++-- .../OsmoDoc.csproj | 0 .../Pdf/Models/ContentMetaData.cs | 2 +- .../Pdf/Models/DocumentData.cs | 2 +- .../Pdf/PdfDocumentGenerator.cs | 4 ++-- .../Word/Models/ContentData.cs | 2 +- .../Word/Models/DocumentData.cs | 2 +- {DocumentService => OsmoDoc}/Word/Models/Enums.cs | 2 +- .../Word/Models/TableData.cs | 2 +- .../Word/WordDocumentGenerator.cs | 4 ++-- 27 files changed, 38 insertions(+), 38 deletions(-) rename {DocumentService.API => OsmoDoc.API}/Controllers/PdfController.cs (96%) rename {DocumentService.API => OsmoDoc.API}/Controllers/WordController.cs (95%) rename {DocumentService.API => OsmoDoc.API}/DotEnv.cs (92%) rename {DocumentService.API => OsmoDoc.API}/Helpers/AuthenticationHelper.cs (93%) rename {DocumentService.API => OsmoDoc.API}/Helpers/AutoMappingProfile.cs (59%) rename {DocumentService.API => OsmoDoc.API}/Helpers/Base64StringHelper.cs (93%) rename {DocumentService.API => OsmoDoc.API}/Helpers/CommonMethodsHelper.cs (92%) rename {DocumentService.API => OsmoDoc.API}/Models/BaseResponse.cs (92%) rename {DocumentService.API => OsmoDoc.API}/Models/PdfGenerationRequestDTO.cs (81%) rename {DocumentService.API => OsmoDoc.API}/Models/WordGenerationRequestDTO.cs (86%) rename DocumentService.API/DocumentService.API.csproj => OsmoDoc.API/OsmoDoc.API.csproj (90%) rename DocumentService.API/DocumentService.API.sln => OsmoDoc.API/OsmoDoc.API.sln (86%) rename {DocumentService.API => OsmoDoc.API}/Program.cs (93%) rename {DocumentService.API => OsmoDoc.API}/Properties/launchSettings.json (100%) rename {DocumentService.API => OsmoDoc.API}/appsettings.Development.json (100%) rename {DocumentService.API => OsmoDoc.API}/appsettings.json (100%) rename {DocumentService.API => OsmoDoc.API}/wwwroot/Tools/wkhtmltopdf.exe (100%) rename DocumentService.sln => OsmoDoc.sln (80%) rename DocumentService/DocumentService.csproj => OsmoDoc/OsmoDoc.csproj (100%) rename {DocumentService => OsmoDoc}/Pdf/Models/ContentMetaData.cs (70%) rename {DocumentService => OsmoDoc}/Pdf/Models/DocumentData.cs (77%) rename {DocumentService => OsmoDoc}/Pdf/PdfDocumentGenerator.cs (96%) rename {DocumentService => OsmoDoc}/Word/Models/ContentData.cs (91%) rename {DocumentService => OsmoDoc}/Word/Models/DocumentData.cs (89%) rename {DocumentService => OsmoDoc}/Word/Models/Enums.cs (89%) rename {DocumentService => OsmoDoc}/Word/Models/TableData.cs (89%) rename {DocumentService => OsmoDoc}/Word/WordDocumentGenerator.cs (97%) diff --git a/DocumentService.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs similarity index 96% rename from DocumentService.API/Controllers/PdfController.cs rename to OsmoDoc.API/Controllers/PdfController.cs index 1436eff..de7f1e2 100644 --- a/DocumentService.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -1,10 +1,10 @@ -using DocumentService.Pdf; -using DocumentService.API.Helpers; -using DocumentService.API.Models; +using OsmoDoc.Pdf; +using OsmoDoc.API.Helpers; +using OsmoDoc.API.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; -namespace DocumentService.API.Controllers; +namespace OsmoDoc.API.Controllers; [Route("api")] [ApiController] diff --git a/DocumentService.API/Controllers/WordController.cs b/OsmoDoc.API/Controllers/WordController.cs similarity index 95% rename from DocumentService.API/Controllers/WordController.cs rename to OsmoDoc.API/Controllers/WordController.cs index c888b91..e32095f 100644 --- a/DocumentService.API/Controllers/WordController.cs +++ b/OsmoDoc.API/Controllers/WordController.cs @@ -1,12 +1,12 @@ using AutoMapper; -using DocumentService.Word; -using DocumentService.Word.Models; -using DocumentService.API.Helpers; -using DocumentService.API.Models; +using OsmoDoc.Word; +using OsmoDoc.Word.Models; +using OsmoDoc.API.Helpers; +using OsmoDoc.API.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; -namespace DocumentService.API.Controllers; +namespace OsmoDoc.API.Controllers; [Route("api")] [ApiController] diff --git a/DocumentService.API/DotEnv.cs b/OsmoDoc.API/DotEnv.cs similarity index 92% rename from DocumentService.API/DotEnv.cs rename to OsmoDoc.API/DotEnv.cs index 45e2a4a..e0801f2 100644 --- a/DocumentService.API/DotEnv.cs +++ b/OsmoDoc.API/DotEnv.cs @@ -1,4 +1,4 @@ -namespace DocumentService.API; +namespace OsmoDoc.API; public static class DotEnv { diff --git a/DocumentService.API/Helpers/AuthenticationHelper.cs b/OsmoDoc.API/Helpers/AuthenticationHelper.cs similarity index 93% rename from DocumentService.API/Helpers/AuthenticationHelper.cs rename to OsmoDoc.API/Helpers/AuthenticationHelper.cs index 5104e65..04dc325 100644 --- a/DocumentService.API/Helpers/AuthenticationHelper.cs +++ b/OsmoDoc.API/Helpers/AuthenticationHelper.cs @@ -3,7 +3,7 @@ using System.Text; using System.Security.Claims; -namespace DocumentService.API.Helpers; +namespace OsmoDoc.API.Helpers; public class AuthenticationHelper { diff --git a/DocumentService.API/Helpers/AutoMappingProfile.cs b/OsmoDoc.API/Helpers/AutoMappingProfile.cs similarity index 59% rename from DocumentService.API/Helpers/AutoMappingProfile.cs rename to OsmoDoc.API/Helpers/AutoMappingProfile.cs index d6ddaae..269ba7b 100644 --- a/DocumentService.API/Helpers/AutoMappingProfile.cs +++ b/OsmoDoc.API/Helpers/AutoMappingProfile.cs @@ -1,8 +1,8 @@ using AutoMapper; -using DocumentService.Word.Models; -using DocumentService.API.Models; +using OsmoDoc.Word.Models; +using OsmoDoc.API.Models; -namespace DocumentService.API.Helpers; +namespace OsmoDoc.API.Helpers; public class AutoMappingProfile : Profile { diff --git a/DocumentService.API/Helpers/Base64StringHelper.cs b/OsmoDoc.API/Helpers/Base64StringHelper.cs similarity index 93% rename from DocumentService.API/Helpers/Base64StringHelper.cs rename to OsmoDoc.API/Helpers/Base64StringHelper.cs index 9d0368d..df48427 100644 --- a/DocumentService.API/Helpers/Base64StringHelper.cs +++ b/OsmoDoc.API/Helpers/Base64StringHelper.cs @@ -1,4 +1,4 @@ -namespace DocumentService.API.Helpers; +namespace OsmoDoc.API.Helpers; public static class Base64StringHelper { diff --git a/DocumentService.API/Helpers/CommonMethodsHelper.cs b/OsmoDoc.API/Helpers/CommonMethodsHelper.cs similarity index 92% rename from DocumentService.API/Helpers/CommonMethodsHelper.cs rename to OsmoDoc.API/Helpers/CommonMethodsHelper.cs index eaac7f5..fe3de2b 100644 --- a/DocumentService.API/Helpers/CommonMethodsHelper.cs +++ b/OsmoDoc.API/Helpers/CommonMethodsHelper.cs @@ -1,4 +1,4 @@ -namespace DocumentService.API.Helpers; +namespace OsmoDoc.API.Helpers; public static class CommonMethodsHelper { diff --git a/DocumentService.API/Models/BaseResponse.cs b/OsmoDoc.API/Models/BaseResponse.cs similarity index 92% rename from DocumentService.API/Models/BaseResponse.cs rename to OsmoDoc.API/Models/BaseResponse.cs index e6d5954..a2df3cc 100644 --- a/DocumentService.API/Models/BaseResponse.cs +++ b/OsmoDoc.API/Models/BaseResponse.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace DocumentService.API.Models; +namespace OsmoDoc.API.Models; public enum ResponseStatus { diff --git a/DocumentService.API/Models/PdfGenerationRequestDTO.cs b/OsmoDoc.API/Models/PdfGenerationRequestDTO.cs similarity index 81% rename from DocumentService.API/Models/PdfGenerationRequestDTO.cs rename to OsmoDoc.API/Models/PdfGenerationRequestDTO.cs index 7b0b602..f88a849 100644 --- a/DocumentService.API/Models/PdfGenerationRequestDTO.cs +++ b/OsmoDoc.API/Models/PdfGenerationRequestDTO.cs @@ -1,7 +1,7 @@ -using DocumentService.Pdf.Models; +using OsmoDoc.Pdf.Models; using System.ComponentModel.DataAnnotations; -namespace DocumentService.API.Models; +namespace OsmoDoc.API.Models; public class PdfGenerationRequestDTO { diff --git a/DocumentService.API/Models/WordGenerationRequestDTO.cs b/OsmoDoc.API/Models/WordGenerationRequestDTO.cs similarity index 86% rename from DocumentService.API/Models/WordGenerationRequestDTO.cs rename to OsmoDoc.API/Models/WordGenerationRequestDTO.cs index 1fd1bca..35acf13 100644 --- a/DocumentService.API/Models/WordGenerationRequestDTO.cs +++ b/OsmoDoc.API/Models/WordGenerationRequestDTO.cs @@ -1,7 +1,7 @@ -using DocumentService.Word.Models; +using OsmoDoc.Word.Models; using System.ComponentModel.DataAnnotations; -namespace DocumentService.API.Models; +namespace OsmoDoc.API.Models; public class WordGenerationRequestDTO diff --git a/DocumentService.API/DocumentService.API.csproj b/OsmoDoc.API/OsmoDoc.API.csproj similarity index 90% rename from DocumentService.API/DocumentService.API.csproj rename to OsmoDoc.API/OsmoDoc.API.csproj index bcd5563..b09bf6b 100644 --- a/DocumentService.API/DocumentService.API.csproj +++ b/OsmoDoc.API/OsmoDoc.API.csproj @@ -12,6 +12,6 @@ - + \ No newline at end of file diff --git a/DocumentService.API/DocumentService.API.sln b/OsmoDoc.API/OsmoDoc.API.sln similarity index 86% rename from DocumentService.API/DocumentService.API.sln rename to OsmoDoc.API/OsmoDoc.API.sln index ee22b37..94f94c8 100644 --- a/DocumentService.API/DocumentService.API.sln +++ b/OsmoDoc.API/OsmoDoc.API.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34031.279 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.API", "DocumentService.API.csproj", "{A99B82C1-0758-4FDA-8D3B-5F11E99896F1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OsmoDoc.API", "OsmoDoc.API.csproj", "{A99B82C1-0758-4FDA-8D3B-5F11E99896F1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/DocumentService.API/Program.cs b/OsmoDoc.API/Program.cs similarity index 93% rename from DocumentService.API/Program.cs rename to OsmoDoc.API/Program.cs index 6c4ec49..bf4266b 100644 --- a/DocumentService.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -1,4 +1,4 @@ -using DocumentService.API.Models; +using OsmoDoc.API.Models; using Microsoft.AspNetCore.Mvc; using Serilog.Events; using Serilog; @@ -23,7 +23,7 @@ // Load .env file string root = Directory.GetCurrentDirectory(); string dotenv = Path.GetFullPath(Path.Combine(root, "..", ".env")); -DocumentService.API.DotEnv.Load(dotenv); +OsmoDoc.API.DotEnv.Load(dotenv); // Configure request size limit long requestBodySizeLimitBytes = Convert.ToInt64(builder.Configuration.GetSection("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES").Value); @@ -46,7 +46,7 @@ // Swagger UI Services builder.Services.AddSwaggerGen(options => { - options.SwaggerDoc("v1", new OpenApiInfo { Title = "DocumentService API", Version = "v1" }); + options.SwaggerDoc("v1", new OpenApiInfo { Title = "OsmoDoc API", Version = "v1" }); options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { diff --git a/DocumentService.API/Properties/launchSettings.json b/OsmoDoc.API/Properties/launchSettings.json similarity index 100% rename from DocumentService.API/Properties/launchSettings.json rename to OsmoDoc.API/Properties/launchSettings.json diff --git a/DocumentService.API/appsettings.Development.json b/OsmoDoc.API/appsettings.Development.json similarity index 100% rename from DocumentService.API/appsettings.Development.json rename to OsmoDoc.API/appsettings.Development.json diff --git a/DocumentService.API/appsettings.json b/OsmoDoc.API/appsettings.json similarity index 100% rename from DocumentService.API/appsettings.json rename to OsmoDoc.API/appsettings.json diff --git a/DocumentService.API/wwwroot/Tools/wkhtmltopdf.exe b/OsmoDoc.API/wwwroot/Tools/wkhtmltopdf.exe similarity index 100% rename from DocumentService.API/wwwroot/Tools/wkhtmltopdf.exe rename to OsmoDoc.API/wwwroot/Tools/wkhtmltopdf.exe diff --git a/DocumentService.sln b/OsmoDoc.sln similarity index 80% rename from DocumentService.sln rename to OsmoDoc.sln index 2a3de81..d4c665f 100644 --- a/DocumentService.sln +++ b/OsmoDoc.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentService", "DocumentService\DocumentService.csproj", "{AC14A26A-220C-487E-9D7B-BB0548D86318}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OsmoDoc", "OsmoDoc\OsmoDoc.csproj", "{AC14A26A-220C-487E-9D7B-BB0548D86318}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentService.API", "DocumentService.API\DocumentService.API.csproj", "{2D2738C1-032B-441A-9298-1722A4915BD4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OsmoDoc.API", "OsmoDoc.API\OsmoDoc.API.csproj", "{2D2738C1-032B-441A-9298-1722A4915BD4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/DocumentService/DocumentService.csproj b/OsmoDoc/OsmoDoc.csproj similarity index 100% rename from DocumentService/DocumentService.csproj rename to OsmoDoc/OsmoDoc.csproj diff --git a/DocumentService/Pdf/Models/ContentMetaData.cs b/OsmoDoc/Pdf/Models/ContentMetaData.cs similarity index 70% rename from DocumentService/Pdf/Models/ContentMetaData.cs rename to OsmoDoc/Pdf/Models/ContentMetaData.cs index fd8d22e..549f187 100644 --- a/DocumentService/Pdf/Models/ContentMetaData.cs +++ b/OsmoDoc/Pdf/Models/ContentMetaData.cs @@ -1,4 +1,4 @@ -namespace DocumentService.Pdf.Models; +namespace OsmoDoc.Pdf.Models; public class ContentMetaData { diff --git a/DocumentService/Pdf/Models/DocumentData.cs b/OsmoDoc/Pdf/Models/DocumentData.cs similarity index 77% rename from DocumentService/Pdf/Models/DocumentData.cs rename to OsmoDoc/Pdf/Models/DocumentData.cs index 3427bca..7aa919b 100644 --- a/DocumentService/Pdf/Models/DocumentData.cs +++ b/OsmoDoc/Pdf/Models/DocumentData.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace DocumentService.Pdf.Models; +namespace OsmoDoc.Pdf.Models; public class DocumentData { diff --git a/DocumentService/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs similarity index 96% rename from DocumentService/Pdf/PdfDocumentGenerator.cs rename to OsmoDoc/Pdf/PdfDocumentGenerator.cs index d293c4b..370b094 100644 --- a/DocumentService/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -1,4 +1,4 @@ -using DocumentService.Pdf.Models; +using OsmoDoc.Pdf.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -7,7 +7,7 @@ using System.IO; using System.Runtime.InteropServices; -namespace DocumentService.Pdf; +namespace OsmoDoc.Pdf; public class PdfDocumentGenerator { diff --git a/DocumentService/Word/Models/ContentData.cs b/OsmoDoc/Word/Models/ContentData.cs similarity index 91% rename from DocumentService/Word/Models/ContentData.cs rename to OsmoDoc/Word/Models/ContentData.cs index 38391ba..512eb79 100644 --- a/DocumentService/Word/Models/ContentData.cs +++ b/OsmoDoc/Word/Models/ContentData.cs @@ -1,4 +1,4 @@ -namespace DocumentService.Word.Models; +namespace OsmoDoc.Word.Models; /// diff --git a/DocumentService/Word/Models/DocumentData.cs b/OsmoDoc/Word/Models/DocumentData.cs similarity index 89% rename from DocumentService/Word/Models/DocumentData.cs rename to OsmoDoc/Word/Models/DocumentData.cs index 5b56a8e..ea0e978 100644 --- a/DocumentService/Word/Models/DocumentData.cs +++ b/OsmoDoc/Word/Models/DocumentData.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace DocumentService.Word.Models; +namespace OsmoDoc.Word.Models; /// diff --git a/DocumentService/Word/Models/Enums.cs b/OsmoDoc/Word/Models/Enums.cs similarity index 89% rename from DocumentService/Word/Models/Enums.cs rename to OsmoDoc/Word/Models/Enums.cs index 31fedc5..d957dad 100644 --- a/DocumentService/Word/Models/Enums.cs +++ b/OsmoDoc/Word/Models/Enums.cs @@ -1,4 +1,4 @@ -namespace DocumentService.Word.Models; +namespace OsmoDoc.Word.Models; /// diff --git a/DocumentService/Word/Models/TableData.cs b/OsmoDoc/Word/Models/TableData.cs similarity index 89% rename from DocumentService/Word/Models/TableData.cs rename to OsmoDoc/Word/Models/TableData.cs index dba7a72..6e47f98 100644 --- a/DocumentService/Word/Models/TableData.cs +++ b/OsmoDoc/Word/Models/TableData.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace DocumentService.Word.Models; +namespace OsmoDoc.Word.Models; /// diff --git a/DocumentService/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs similarity index 97% rename from DocumentService/Word/WordDocumentGenerator.cs rename to OsmoDoc/Word/WordDocumentGenerator.cs index 83198f5..ca7d2e4 100644 --- a/DocumentService/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -2,7 +2,7 @@ using DocumentFormat.OpenXml.Drawing.Wordprocessing; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; -using DocumentService.Word.Models; +using OsmoDoc.Word.Models; using NPOI.XWPF.UserModel; using System; using System.Collections.Generic; @@ -11,7 +11,7 @@ using System.Net; using System.Text.RegularExpressions; -namespace DocumentService.Word; +namespace OsmoDoc.Word; /// /// Provides functionality to generate Word documents based on templates and data. From 513819c074e1fe01817af4449039441c74646045 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 15 Jun 2025 15:23:49 +0530 Subject: [PATCH 04/73] refactor: update Dockerfile and docker-compose for OsmoDoc rename --- Dockerfile | 14 +++++++------- docker-compose.yaml | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8deee30..e4f3a8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,19 +10,19 @@ EXPOSE 5000 ENV BUILD_CONFIGURATION=Debug # Copy data -COPY ["DocumentService.API/DocumentService.API.csproj", "DocumentService.API/"] -COPY ["DocumentService/DocumentService.csproj", "DocumentService/"] +COPY ["OsmoDoc.API/OsmoDoc.API.csproj", "OsmoDoc.API/"] +COPY ["OsmoDoc/OsmoDoc.csproj", "OsmoDoc/"] # Restore the project dependencies -RUN dotnet restore "./DocumentService.API/./DocumentService.API.csproj" -RUN dotnet restore "./DocumentService/./DocumentService.csproj" +RUN dotnet restore "./OsmoDoc.API/./OsmoDoc.API.csproj" +RUN dotnet restore "./OsmoDoc/./OsmoDoc.csproj" # Copy the rest of the data COPY . . -WORKDIR "/app/DocumentService.API" +WORKDIR "/app/OsmoDoc.API" # Build the project and store artifacts in /out folder -RUN dotnet publish "./DocumentService.API.csproj" -c BUILD_CONFIGURATION -o /app/out +RUN dotnet publish "./OsmoDoc.API.csproj" -c BUILD_CONFIGURATION -o /app/out # Use the official ASP.NET runtime image as the base image FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base @@ -45,4 +45,4 @@ RUN chmod 755 /usr/bin/wkhtmltopdf RUN npm install -g --only=prod ejs # Set the entry point for the container -ENTRYPOINT ["dotnet", "DocumentService.API.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "OsmoDoc.API.dll"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 8df18d6..957757b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,10 +1,10 @@ services: - document-service: + osmodoc: build: context: . dockerfile: Dockerfile - image: document-service-docker - container_name: document-service-api + image: osmodoc-docker + container_name: osmodoc-api env_file: - .env ports: From 570d94f69446dc26073ecaae8fa46ba502eb8a63 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 15 Jun 2025 15:33:08 +0530 Subject: [PATCH 05/73] refactor: update docs directory for OsmoDoc rename --- ...l => OsmoDoc.Word.Models.ContentData.html} | 30 +-- ...l => OsmoDoc.Word.Models.ContentType.html} | 14 +- ... => OsmoDoc.Word.Models.DocumentData.html} | 22 +- ...ml => OsmoDoc.Word.Models.ParentBody.html} | 14 +- ...tml => OsmoDoc.Word.Models.TableData.html} | 18 +- ...d.Models.html => OsmoDoc.Word.Models.html} | 18 +- ...> OsmoDoc.Word.WordDocumentGenerator.html} | 16 +- ...mentService.Word.html => OsmoDocWord.html} | 10 +- docs/site/10.0.2/api/toc.html | 16 +- docs/site/manifest.json | 34 +-- docs/site/xrefmap.yml | 250 +++++++++--------- 11 files changed, 221 insertions(+), 221 deletions(-) rename docs/site/10.0.2/api/{DocumentService.Word.Models.ContentData.html => OsmoDoc.Word.Models.ContentData.html} (81%) rename docs/site/10.0.2/api/{DocumentService.Word.Models.ContentType.html => OsmoDoc.Word.Models.ContentType.html} (86%) rename docs/site/10.0.2/api/{DocumentService.Word.Models.DocumentData.html => OsmoDoc.Word.Models.DocumentData.html} (84%) rename docs/site/10.0.2/api/{DocumentService.Word.Models.ParentBody.html => OsmoDoc.Word.Models.ParentBody.html} (86%) rename docs/site/10.0.2/api/{DocumentService.Word.Models.TableData.html => OsmoDoc.Word.Models.TableData.html} (87%) rename docs/site/10.0.2/api/{DocumentService.Word.Models.html => OsmoDoc.Word.Models.html} (84%) rename docs/site/10.0.2/api/{DocumentService.Word.WordDocumentGenerator.html => OsmoDoc.Word.WordDocumentGenerator.html} (85%) rename docs/site/10.0.2/api/{DocumentService.Word.html => OsmoDocWord.html} (91%) diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.ContentData.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentData.html similarity index 81% rename from docs/site/10.0.2/api/DocumentService.Word.Models.ContentData.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentData.html index 240969e..c7f4db0 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.ContentData.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentData.html @@ -67,11 +67,11 @@
-
+
-

Class ContentData +

Class ContentData

Represents the data for a content placeholder in a Word document.
@@ -104,16 +104,16 @@
Inherited Members
object.MemberwiseClone()
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public class ContentData

Properties

- -

Content

+ +

Content

Gets or sets the content to replace the placeholder with.
Declaration
@@ -135,8 +135,8 @@
Property Value
- -

ContentType

+ +

ContentType

Gets or sets the content type of the placeholder (text or image).
Declaration
@@ -153,13 +153,13 @@
Property Value
- ContentType + ContentType - -

ParentBody

+ +

ParentBody

Gets or sets the parent body of the placeholder (none or table).
Declaration
@@ -176,13 +176,13 @@
Property Value
- ParentBody + ParentBody - -

Placeholder

+ +

Placeholder

Gets or sets the placeholder name.
Declaration
diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.ContentType.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentType.html similarity index 86% rename from docs/site/10.0.2/api/DocumentService.Word.Models.ContentType.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentType.html index 976b450..53b1147 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.ContentType.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ContentType.html @@ -67,18 +67,18 @@
-
+
-

Enum ContentType +

Enum ContentType

Represents the content type of a placeholder in a Word document.
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public enum ContentType
@@ -93,11 +93,11 @@

Fields - Image + Image The placeholder represents an image. - Text + Text The placeholder represents text content. diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.DocumentData.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.DocumentData.html similarity index 84% rename from docs/site/10.0.2/api/DocumentService.Word.Models.DocumentData.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.DocumentData.html index 22bee61..e7bde9c 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.DocumentData.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.DocumentData.html @@ -67,11 +67,11 @@

-
+
-

Class DocumentData +

Class DocumentData

Represents the data for a Word document, including content placeholders and table data.
@@ -104,16 +104,16 @@
Inherited Members
object.MemberwiseClone()
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public class DocumentData

Properties

- -

Placeholders

+ +

Placeholders

Gets or sets the list of content placeholders in the document.
Declaration
@@ -130,13 +130,13 @@
Property Value
- List<ContentData> + List<ContentData> - -

TablesData

+ +

TablesData

Gets or sets the list of table data in the document.
Declaration
@@ -153,7 +153,7 @@
Property Value
- List<TableData> + List<TableData> diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.ParentBody.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ParentBody.html similarity index 86% rename from docs/site/10.0.2/api/DocumentService.Word.Models.ParentBody.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.ParentBody.html index 30027df..89ec062 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.ParentBody.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.ParentBody.html @@ -67,18 +67,18 @@
-
+
-

Enum ParentBody +

Enum ParentBody

Represents the parent body of a placeholder in a Word document.
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public enum ParentBody
@@ -93,11 +93,11 @@

Fields - None + None The placeholder does not have a parent body. - Table + Table The placeholder belongs to a table. diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.TableData.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.TableData.html similarity index 87% rename from docs/site/10.0.2/api/DocumentService.Word.Models.TableData.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.TableData.html index 2f6f1bf..8a48600 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.TableData.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.TableData.html @@ -67,11 +67,11 @@

-
+
-

Class TableData +

Class TableData

Represents the data for a table in a Word document.
@@ -104,16 +104,16 @@
Inherited Members
object.MemberwiseClone()
-
Namespace: DocumentService.Word.Models
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word.Models
+
Assembly: OsmoDoc.dll
+
Syntax
public class TableData

Properties

- -

Data

+ +

Data

Gets or sets the list of dictionaries representing the data for each row in the table. Each dictionary contains column header-value pairs.
@@ -136,8 +136,8 @@
Property Value
- -

TablePos

+ +

TablePos

Gets or sets the position of the table in the document.
Declaration
diff --git a/docs/site/10.0.2/api/DocumentService.Word.Models.html b/docs/site/10.0.2/api/OsmoDoc.Word.Models.html similarity index 84% rename from docs/site/10.0.2/api/DocumentService.Word.Models.html rename to docs/site/10.0.2/api/OsmoDoc.Word.Models.html index bdb06e8..7d574b8 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.Models.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.Models.html @@ -5,10 +5,10 @@ - Namespace DocumentService.Word.Models + <title>Namespace OsmoDoc.Word.Models | Some Documentation - @@ -67,9 +67,9 @@
-
+
-

Namespace DocumentService.Word.Models +

Namespace OsmoDoc.Word.Models

@@ -77,18 +77,18 @@

Classes

-

ContentData

+

ContentData

Represents the data for a content placeholder in a Word document.
-

DocumentData

+

DocumentData

Represents the data for a Word document, including content placeholders and table data.
-

TableData

+

TableData

Represents the data for a table in a Word document.

Enums

-

ContentType

+

ContentType

Represents the content type of a placeholder in a Word document.
-

ParentBody

+

ParentBody

Represents the parent body of a placeholder in a Word document.
diff --git a/docs/site/10.0.2/api/DocumentService.Word.WordDocumentGenerator.html b/docs/site/10.0.2/api/OsmoDoc.Word.WordDocumentGenerator.html similarity index 85% rename from docs/site/10.0.2/api/DocumentService.Word.WordDocumentGenerator.html rename to docs/site/10.0.2/api/OsmoDoc.Word.WordDocumentGenerator.html index 68e403b..3bf7445 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.WordDocumentGenerator.html +++ b/docs/site/10.0.2/api/OsmoDoc.Word.WordDocumentGenerator.html @@ -67,11 +67,11 @@
-
+
-

Class WordDocumentGenerator +

Class WordDocumentGenerator

Provides functionality to generate Word documents based on templates and data.
@@ -104,16 +104,16 @@
Inherited Members
object.MemberwiseClone()
-
Namespace: DocumentService.Word
-
Assembly: DocumentService.dll
-
Syntax
+
Namespace: OsmoDoc.Word
+
Assembly: OsmoDoc.dll
+
Syntax
public static class WordDocumentGenerator

Methods

- -

GenerateDocumentByTemplate(string, DocumentData, string)

+ +

GenerateDocumentByTemplate(string, DocumentData, string)

Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path.
Declaration
@@ -136,7 +136,7 @@
Parameters
The file path of the template document. - DocumentData + DocumentData documentData The data to replace the placeholders in the template. diff --git a/docs/site/10.0.2/api/DocumentService.Word.html b/docs/site/10.0.2/api/OsmoDocWord.html similarity index 91% rename from docs/site/10.0.2/api/DocumentService.Word.html rename to docs/site/10.0.2/api/OsmoDocWord.html index b4b7697..2e1c7cb 100644 --- a/docs/site/10.0.2/api/DocumentService.Word.html +++ b/docs/site/10.0.2/api/OsmoDocWord.html @@ -5,10 +5,10 @@ - Namespace DocumentService.Word + <title>Namespace OsmoDoc.Word | Some Documentation - @@ -67,9 +67,9 @@
-
+
-

Namespace DocumentService.Word +

Namespace OsmoDoc.Word

@@ -77,7 +77,7 @@

Classes

-

WordDocumentGenerator

+

WordDocumentGenerator

Provides functionality to generate Word documents based on templates and data.
diff --git a/docs/site/10.0.2/api/toc.html b/docs/site/10.0.2/api/toc.html index 005aae4..74aad52 100644 --- a/docs/site/10.0.2/api/toc.html +++ b/docs/site/10.0.2/api/toc.html @@ -14,33 +14,33 @@
public static class WordDocumentGenerator { + private static readonly HttpClient _httpClient = new HttpClient(); + /// /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. /// /// The file path of the template document. /// The data to replace the placeholders in the template. /// The file path to save the generated document. - public static void GenerateDocumentByTemplate(string templateFilePath, DocumentData documentData, string outputFilePath) + public async static Task GenerateDocumentByTemplate(string templateFilePath, DocumentData documentData, string outputFilePath) { try { @@ -41,22 +45,22 @@ public static void GenerateDocumentByTemplate(string templateFilePath, DocumentD if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) { string placeholder = "{" + content.Placeholder + "}"; - textPlaceholders.Add(placeholder, content.Content); + textPlaceholders.TryAdd(placeholder, content.Content); } else if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Image) { string placeholder = content.Placeholder; - imagePlaceholders.Add(placeholder, content.Content); + imagePlaceholders.TryAdd(placeholder, content.Content); } else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) { string placeholder = "{" + content.Placeholder + "}"; - tableContentPlaceholders.Add(placeholder, content.Content); + tableContentPlaceholders.TryAdd(placeholder, content.Content); } } // Create document of the template - XWPFDocument document = GetXWPFDocument(templateFilePath); + XWPFDocument document = await GetXWPFDocument(templateFilePath); // For each element in the document foreach (IBodyElement element in document.BodyElements) @@ -95,7 +99,7 @@ public static void GenerateDocumentByTemplate(string templateFilePath, DocumentD } // Write the document to output file path and close the document - WriteDocument(document, outputFilePath); + await WriteDocument(document, outputFilePath); document.Close(); /* @@ -105,9 +109,9 @@ public static void GenerateDocumentByTemplate(string templateFilePath, DocumentD * Since both the packages have different execution method, so they are handled separately */ // Replace all the image placeholders in the output file - ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); + await ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); } - catch (Exception ex) + catch (Exception) { throw; } @@ -118,12 +122,16 @@ public static void GenerateDocumentByTemplate(string templateFilePath, DocumentD ///
/// The file path of the Word document. /// An instance of XWPFDocument representing the Word document. - private static XWPFDocument GetXWPFDocument(string docFilePath) + private async static Task GetXWPFDocument(string docFilePath) { - FileStream readStream = File.OpenRead(docFilePath); - XWPFDocument document = new XWPFDocument(readStream); - readStream.Close(); - return document; + return await Task.Run(() => + { + using (FileStream readStream = File.OpenRead(docFilePath)) + { + XWPFDocument document = new XWPFDocument(readStream); + return document; + } + }); } /// @@ -131,12 +139,15 @@ private static XWPFDocument GetXWPFDocument(string docFilePath) /// /// The XWPFDocument to write. /// The file path to save the document. - private static void WriteDocument(XWPFDocument document, string filePath) + private async static Task WriteDocument(XWPFDocument document, string filePath) { - using (FileStream writeStream = File.Create(filePath)) + await Task.Run(() => { - document.Write(writeStream); - } + using (FileStream writeStream = File.Create(filePath)) + { + document.Write(writeStream); + } + }); } /// @@ -251,59 +262,65 @@ private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) /// The input file path containing the image placeholders. /// The output file path where the updated document will be saved. /// The dictionary of image placeholders and their corresponding image paths. - private static void ReplaceImagePlaceholders(string inputFilePath, string outputFilePath, Dictionary imagePlaceholders) + private async static Task ReplaceImagePlaceholders(string inputFilePath, string outputFilePath, Dictionary imagePlaceholders) { - byte[] docBytes = File.ReadAllBytes(inputFilePath); - - // Write document bytes to memory - MemoryStream memoryStream = new MemoryStream(); - memoryStream.Write(docBytes, 0, docBytes.Length); + byte[] docBytes = await File.ReadAllBytesAsync(inputFilePath); - using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) + // Write document bytes to memory asynchronously + using (MemoryStream memoryStream = new MemoryStream()) { - MainDocumentPart mainDocumentPart = wordDocument.MainDocumentPart; + await memoryStream.WriteAsync(docBytes, 0, docBytes.Length); + memoryStream.Position = 0; - // Get a list of drawings (images) - IEnumerable drawings = mainDocumentPart.Document.Descendants().ToList(); - - /* - * FIXME: Look on how we can improve this loop operation. - */ - foreach (Drawing drawing in drawings) + using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) { - DocProperties docProperty = drawing.Descendants().FirstOrDefault(); + MainDocumentPart? mainDocumentPart = wordDocument.MainDocumentPart; - // If drawing / image name is present in imagePlaceholders dictionary, then replace image - if (docProperty != null && imagePlaceholders.ContainsKey(docProperty.Name)) + // Get a list of drawings (images) + IEnumerable drawings = new List(); + if (mainDocumentPart != null) { - List drawingBlips = drawing.Descendants().ToList(); + drawings = mainDocumentPart.Document.Descendants().ToList(); + } + + /* + * FIXME: Look on how we can improve this loop operation. + */ + foreach (Drawing drawing in drawings) + { + DocProperties? docProperty = drawing.Descendants().FirstOrDefault(); - foreach (Blip blip in drawingBlips) + // If drawing / image name is present in imagePlaceholders dictionary, then replace image + if (docProperty != null && imagePlaceholders.ContainsKey(docProperty.Name)) { - OpenXmlPart imagePart = wordDocument.MainDocumentPart.GetPartById(blip.Embed); + List drawingBlips = drawing.Descendants().ToList(); - using (BinaryWriter writer = new BinaryWriter(imagePart.GetStream())) + foreach (Blip blip in drawingBlips) { + OpenXmlPart imagePart = wordDocument.MainDocumentPart.GetPartById(blip.Embed); + string imagePath = imagePlaceholders[docProperty.Name]; - /* - * WebClient has been deprecated and we need to use HTTPClient. - * This involves the methods to be asynchronous. - */ - using (WebClient webClient = new WebClient()) + // Asynchronously download image data using HttpClient + byte[] imageData = await _httpClient.GetByteArrayAsync(imagePath); + + using (Stream partStream = imagePart.GetStream(FileMode.OpenOrCreate, FileAccess.Write)) { - writer.Write(webClient.DownloadData(imagePath)); + // Asynchronously write image data to the part stream + await partStream.WriteAsync(imageData, 0, imageData.Length); + partStream.SetLength(imageData.Length); // Ensure the stream is truncated if new data is smaller } } } } } + // Overwrite the output file asynchronously + using (FileStream fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) + { + // Reset MemoryStream position before writing to fileStream + memoryStream.Position = 0; + await memoryStream.CopyToAsync(fileStream); + } } - - // Overwrite the output file - FileStream fileStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); - memoryStream.WriteTo(fileStream); - fileStream.Close(); - memoryStream.Close(); } } \ No newline at end of file From 548252ea403d42ac581b559c19be363359effab8 Mon Sep 17 00:00:00 2001 From: Jatin Date: Wed, 18 Jun 2025 20:05:51 +0530 Subject: [PATCH 18/73] refactor: remove using block in GetXWPFDocument function --- OsmoDoc/Word/WordDocumentGenerator.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index f4b12aa..f270105 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -90,7 +90,7 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc // Populate the table with data if it is passed in tablesData list foreach (TableData insertData in tablesData) { - if (insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) + if (insertData.TablePos >= 1 && insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) { table = PopulateTable(table, insertData); } @@ -126,11 +126,8 @@ private async static Task GetXWPFDocument(string docFilePath) { return await Task.Run(() => { - using (FileStream readStream = File.OpenRead(docFilePath)) - { - XWPFDocument document = new XWPFDocument(readStream); - return document; - } + FileStream readStream = File.OpenRead(docFilePath); + return new XWPFDocument(readStream); }); } From d1892765918dedb9c05c49be95e9a4838ee1207d Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 09:11:45 +0530 Subject: [PATCH 19/73] feat: add null checks for ejsData and function parameters --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 20 ++++++++++++++++++-- OsmoDoc/Word/WordDocumentGenerator.cs | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index 0455a77..22f6fe0 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -24,6 +24,21 @@ public async static Task GeneratePdf(string templatePath, List { try { + if (metaDataList is null) + { + throw new ArgumentNullException(nameof(metaDataList)); + } + + if (string.IsNullOrWhiteSpace(templatePath)) + { + throw new ArgumentNullException(nameof(templatePath)); + } + + if (string.IsNullOrWhiteSpace(outputFilePath)) + { + throw new ArgumentNullException(nameof(outputFilePath)); + } + if (string.IsNullOrWhiteSpace(OsmoDocPdfConfig.WkhtmltopdfPath) && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { throw new Exception("WkhtmltopdfPath is not set in OsmoDocPdfConfig."); @@ -180,7 +195,8 @@ private async static Task ConvertEjsToHTML(string ejsFilePath, string ou // Write json data string to json file string ejsDataJsonFilePath = Path.Combine(tempDirectoryFilePath, "ejsData.json"); - File.WriteAllText(ejsDataJsonFilePath, ejsDataJson); + string contentToWrite = ejsDataJson ?? "{}"; + File.WriteAllText(ejsDataJsonFilePath, contentToWrite); string commandLine = "cmd.exe"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -243,7 +259,7 @@ private static string EjsToHtmlArgumentsBasedOnOS(string ejsFilePath, string ejs } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return $"{ejsFilePath} -f {ejsDataJsonFilePath} -o {tempHtmlFilePath}"; + return $"\"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; } else { diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index f270105..910d13c 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; +using IOPath = System.IO.Path; using System.Linq; using System.Net; using System.Text.RegularExpressions; @@ -32,6 +33,16 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc { try { + if (string.IsNullOrWhiteSpace(templateFilePath)) + { + throw new ArgumentNullException(nameof(templateFilePath)); + } + + if (string.IsNullOrWhiteSpace(outputFilePath)) + { + throw new ArgumentNullException(nameof(outputFilePath)); + } + List contentData = documentData.Placeholders; List tablesData = documentData.TablesData; @@ -138,6 +149,12 @@ private async static Task GetXWPFDocument(string docFilePath) /// The file path to save the document. private async static Task WriteDocument(XWPFDocument document, string filePath) { + string? directory = IOPath.GetDirectoryName(filePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + await Task.Run(() => { using (FileStream writeStream = File.Create(filePath)) From 325426ac5af51f6a2c022d303e1cce819a660fb7 Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 09:53:13 +0530 Subject: [PATCH 20/73] feat: avoid temporary path collisions under concurrent requests by adding unique id in the filename --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index 22f6fe0..a045d16 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -95,7 +95,8 @@ private static string ReplaceFileElementsWithMetaData(string templatePath, List< { throw new Exception($"No directory found for the path: {outputFilePath}"); } - string tempHtmlFilePath = Path.Combine(directoryPath, "Modified"); + string uniqueId = Guid.NewGuid().ToString("N"); + string tempHtmlFilePath = Path.Combine(directoryPath, $"Modified_{uniqueId}"); string tempHtmlFile = Path.Combine(tempHtmlFilePath, "modifiedHtml.html"); if (!Directory.Exists(tempHtmlFilePath)) @@ -177,7 +178,8 @@ private async static Task ConvertEjsToHTML(string ejsFilePath, string ou { throw new Exception($"No directory found for the path: {outputFilePath}"); } - string tempDirectoryFilePath = Path.Combine(directoryPath, "Temp"); + string uniqueId = Guid.NewGuid().ToString("N"); + string tempDirectoryFilePath = Path.Combine(directoryPath, $"Temp_{uniqueId}"); if (!Directory.Exists(tempDirectoryFilePath)) { From 4950f3ecb9c23b1c457371da3536ace85e583f08 Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 10:04:25 +0530 Subject: [PATCH 21/73] refactor: initialize PDF tool path once at startup --- OsmoDoc.API/Controllers/PdfController.cs | 12 ------------ OsmoDoc.API/Program.cs | 7 +++++++ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/OsmoDoc.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs index 987d9c6..2e11b2a 100644 --- a/OsmoDoc.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -44,12 +44,6 @@ public async Task> GeneratePdf(PdfGenerationRequestDT // Save base64 html template to inputs directory await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, htmlTemplateFilePath, this._configuration); - // Initialize tools and output filepaths - OsmoDocPdfConfig.WkhtmltopdfPath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value - ); - string outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, @@ -135,12 +129,6 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR // Save base64 html template to inputs directory await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, ejsTemplateFilePath, this._configuration); - // Initialize tools and output filepaths - OsmoDocPdfConfig.WkhtmltopdfPath = Path.Combine( - this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value - ); - string outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, diff --git a/OsmoDoc.API/Program.cs b/OsmoDoc.API/Program.cs index bf4266b..3636ece 100644 --- a/OsmoDoc.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -9,6 +9,7 @@ using Microsoft.IdentityModel.Tokens; using System.Text; using Swashbuckle.AspNetCore.Filters; +using OsmoDoc.Pdf; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -25,6 +26,12 @@ string dotenv = Path.GetFullPath(Path.Combine(root, "..", ".env")); OsmoDoc.API.DotEnv.Load(dotenv); +// Initialize PDF tool path once at startup +OsmoDocPdfConfig.WkhtmltopdfPath = Path.Combine( + builder.Environment.WebRootPath, + builder.Configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value! +); + // Configure request size limit long requestBodySizeLimitBytes = Convert.ToInt64(builder.Configuration.GetSection("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES").Value); From 3f786502841280ac5f3601764a5e3e9066a193cd Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 13:22:28 +0530 Subject: [PATCH 22/73] feat: handle row cell-count mismatch when populating tables --- OsmoDoc/Word/WordDocumentGenerator.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 910d13c..5e58b62 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -248,18 +248,20 @@ private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) // For each row's data stored in table data foreach (Dictionary rowData in tableData.Data) { - // Create a new row and its columns - XWPFTableRow row = table.CreateRow(); - - // For each cell in row - for (int cellNumber = 0; cellNumber < row.GetTableCells().Count; cellNumber++) + XWPFTableRow row = table.CreateRow(); // This is a DATA row, not header + + int columnCount = headerRow.GetTableCells().Count; // Read from header + for (int cellNumber = 0; cellNumber < columnCount; cellNumber++) { - XWPFTableCell cell = row.GetCell(cellNumber); + // Ensure THIS data row has enough cells + while (row.GetTableCells().Count <= cellNumber) + { + row.AddNewTableCell(); + } - // Get the column header of this cell + // Now populate the cell in this data row + XWPFTableCell cell = row.GetCell(cellNumber); string columnHeader = headerRow.GetCell(cellNumber).GetText(); - - // Add the cell's value if (rowData.ContainsKey(columnHeader)) { cell.SetText(rowData[columnHeader]); From 5f0e3bf4967ae7096c1a6d097eddfc704f55df90 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 20 Jun 2025 05:01:41 +0530 Subject: [PATCH 23/73] feat: avoid Task.Run for I/O operations and fix resource disposal --- OsmoDoc/Word/WordDocumentGenerator.cs | 30 +++++++++++---------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 5e58b62..49c1588 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -20,9 +20,7 @@ namespace OsmoDoc.Word; /// Provides functionality to generate Word documents based on templates and data. /// public static class WordDocumentGenerator -{ - private static readonly HttpClient _httpClient = new HttpClient(); - +{ /// /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. /// @@ -110,7 +108,7 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc } // Write the document to output file path and close the document - await WriteDocument(document, outputFilePath); + WriteDocument(document, outputFilePath); document.Close(); /* @@ -135,11 +133,9 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc /// An instance of XWPFDocument representing the Word document. private async static Task GetXWPFDocument(string docFilePath) { - return await Task.Run(() => - { - FileStream readStream = File.OpenRead(docFilePath); - return new XWPFDocument(readStream); - }); + byte[] fileBytes = await File.ReadAllBytesAsync(docFilePath); + using MemoryStream memoryStream = new MemoryStream(fileBytes); + return new XWPFDocument(memoryStream); } /// @@ -147,21 +143,18 @@ private async static Task GetXWPFDocument(string docFilePath) /// /// The XWPFDocument to write. /// The file path to save the document. - private async static Task WriteDocument(XWPFDocument document, string filePath) + private static void WriteDocument(XWPFDocument document, string filePath) { string? directory = IOPath.GetDirectoryName(filePath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } - - await Task.Run(() => + + using (FileStream writeStream = File.Create(filePath)) { - using (FileStream writeStream = File.Create(filePath)) - { - document.Write(writeStream); - } - }); + document.Write(writeStream); + } } /// @@ -318,7 +311,8 @@ private async static Task ReplaceImagePlaceholders(string inputFilePath, string string imagePath = imagePlaceholders[docProperty.Name]; // Asynchronously download image data using HttpClient - byte[] imageData = await _httpClient.GetByteArrayAsync(imagePath); + using HttpClient httpClient = new HttpClient(); + byte[] imageData = await httpClient.GetByteArrayAsync(imagePath); using (Stream partStream = imagePart.GetStream(FileMode.OpenOrCreate, FileAccess.Write)) { From 4036fb7ed8aa8b4c31305e9a43272555d6bcd0af Mon Sep 17 00:00:00 2001 From: Jatin Date: Wed, 18 Jun 2025 11:07:35 +0530 Subject: [PATCH 24/73] docs: update example env --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 50a5a4e..8bbfe3b 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -JWT_KEY=xxx \ No newline at end of file +JWT_KEY=PLACEHOLDER_REPLACE_WITH_STRONG_KEY_MIN_32_CHARS_BEFORE_USE \ No newline at end of file From 1a4dcddc5173c677ca2371970ffca081813bc442 Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 15:06:32 +0530 Subject: [PATCH 25/73] feat: add login endpoint for generating jwt token --- OsmoDoc.API/Controllers/LoginController.cs | 43 ++++++++++++++++++++++ OsmoDoc.API/Models/LoginRequestDTO.cs | 10 +++++ 2 files changed, 53 insertions(+) create mode 100644 OsmoDoc.API/Controllers/LoginController.cs create mode 100644 OsmoDoc.API/Models/LoginRequestDTO.cs diff --git a/OsmoDoc.API/Controllers/LoginController.cs b/OsmoDoc.API/Controllers/LoginController.cs new file mode 100644 index 0000000..b592bb2 --- /dev/null +++ b/OsmoDoc.API/Controllers/LoginController.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc; +using OsmoDoc.API.Models; +using OsmoDoc.API.Helpers; +using Microsoft.AspNetCore.Authorization; + +namespace OsmoDoc.API.Controllers; + +[Route("api")] +[ApiController] +public class LoginController : ControllerBase +{ + private readonly ILogger _logger; + + public LoginController(ILogger logger) + { + this._logger = logger; + } + + [HttpPost] + [Route("login")] + [AllowAnonymous] + public ActionResult Login([FromBody] LoginRequestDTO loginRequest) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + try + { + string token = AuthenticationHelper.JwtTokenGenerator(loginRequest.Email); + + response.Status = ResponseStatus.Success; + response.AuthToken = token; + response.Message = "Token generated successfully"; + return this.Ok(response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } +} \ No newline at end of file diff --git a/OsmoDoc.API/Models/LoginRequestDTO.cs b/OsmoDoc.API/Models/LoginRequestDTO.cs new file mode 100644 index 0000000..bf7f680 --- /dev/null +++ b/OsmoDoc.API/Models/LoginRequestDTO.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.API.Models; + +public class LoginRequestDTO +{ + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Invalid email format")] + public string Email { get; set; } = string.Empty; +} \ No newline at end of file From a45e4950cbd1463a96426a3124e0de00ead94d41 Mon Sep 17 00:00:00 2001 From: Jatin Date: Wed, 18 Jun 2025 19:24:31 +0530 Subject: [PATCH 26/73] feat: add Redis service to docker-compose for token storage --- .env.example | 5 ++++- docker-compose.yaml | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 8bbfe3b..c18cc14 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,4 @@ -JWT_KEY=PLACEHOLDER_REPLACE_WITH_STRONG_KEY_MIN_32_CHARS_BEFORE_USE \ No newline at end of file +JWT_KEY=PLACEHOLDER_REPLACE_WITH_STRONG_KEY_MIN_32_CHARS_BEFORE_USE + +REDIS_URL=redis:6379 +REDIS_PORT=6379 diff --git a/docker-compose.yaml b/docker-compose.yaml index 957757b..6eb71ca 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,12 @@ services: + redis: + image: redis:7 + container_name: redis + env_file: + - .env + ports: + - ${REDIS_PORT}:6379 + osmodoc: build: context: . From be6ff1338812e5aa22640ff262cb68b934df3172 Mon Sep 17 00:00:00 2001 From: Jatin Date: Wed, 18 Jun 2025 19:25:07 +0530 Subject: [PATCH 27/73] chore: add StackExchange.Redis package for Redis integration --- OsmoDoc.API/OsmoDoc.API.csproj | 1 + OsmoDoc/OsmoDoc.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/OsmoDoc.API/OsmoDoc.API.csproj b/OsmoDoc.API/OsmoDoc.API.csproj index b09bf6b..a36d4ff 100644 --- a/OsmoDoc.API/OsmoDoc.API.csproj +++ b/OsmoDoc.API/OsmoDoc.API.csproj @@ -8,6 +8,7 @@ + diff --git a/OsmoDoc/OsmoDoc.csproj b/OsmoDoc/OsmoDoc.csproj index 6094181..f0b5443 100644 --- a/OsmoDoc/OsmoDoc.csproj +++ b/OsmoDoc/OsmoDoc.csproj @@ -22,5 +22,6 @@ + From 76fcfbf44716f4831797c4f359e9ce2e222c471d Mon Sep 17 00:00:00 2001 From: Jatin Date: Wed, 18 Jun 2025 19:35:35 +0530 Subject: [PATCH 28/73] feat: implement RedisTokenService for JWT token storage and validation --- OsmoDoc.API/Program.cs | 26 +++++++++++++- .../Interfaces/IRedisTokenStoreService.cs | 10 ++++++ OsmoDoc/Services/RedisTokenStoreService.cs | 35 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs create mode 100644 OsmoDoc/Services/RedisTokenStoreService.cs diff --git a/OsmoDoc.API/Program.cs b/OsmoDoc.API/Program.cs index 3636ece..f6f0d5c 100644 --- a/OsmoDoc.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -1,4 +1,3 @@ -using OsmoDoc.API.Models; using Microsoft.AspNetCore.Mvc; using Serilog.Events; using Serilog; @@ -10,6 +9,10 @@ using System.Text; using Swashbuckle.AspNetCore.Filters; using OsmoDoc.Pdf; +using StackExchange.Redis; +using OsmoDoc.API.Models; +using OsmoDoc.Services; +using System.IdentityModel.Tokens.Jwt; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -32,6 +35,12 @@ builder.Configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value! ); +// Register REDIS service +builder.Services.AddSingleton( + ConnectionMultiplexer.Connect(Environment.GetEnvironmentVariable("REDIS_URL") ?? throw new Exception("No REDIS URL specified")) +); +builder.Services.AddScoped(); + // Configure request size limit long requestBodySizeLimitBytes = Convert.ToInt64(builder.Configuration.GetSection("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES").Value); @@ -101,6 +110,21 @@ return true; } }; + + options.Events = new JwtBearerEvents + { + OnTokenValidated = async context => + { + IRedisTokenStoreService tokenStore = context.HttpContext.RequestServices.GetRequiredService(); + JwtSecurityToken? token = context.SecurityToken as JwtSecurityToken; + string tokenString = context.Request.Headers["Authorization"].ToString().Replace("bearer ", ""); + + if (!await tokenStore.IsTokenValidAsync(tokenString)) + { + context.Fail("Token has been revoked."); + } + } + }; }); // Configure Error Response from Model Validations diff --git a/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs b/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs new file mode 100644 index 0000000..498acbc --- /dev/null +++ b/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace OsmoDoc.Services; + +public interface IRedisTokenStoreService +{ + Task StoreTokenAsync(string token, string email); + Task IsTokenValidAsync(string token); + Task RevokeTokenAsync(string token); +} \ No newline at end of file diff --git a/OsmoDoc/Services/RedisTokenStoreService.cs b/OsmoDoc/Services/RedisTokenStoreService.cs new file mode 100644 index 0000000..a742733 --- /dev/null +++ b/OsmoDoc/Services/RedisTokenStoreService.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using StackExchange.Redis; + +namespace OsmoDoc.Services; + +public class RedisTokenStoreService : IRedisTokenStoreService +{ + private readonly IDatabase _db; + private const string KeyPrefix = "valid_token:"; + + public RedisTokenStoreService(IConnectionMultiplexer redis) + { + this._db = redis.GetDatabase(); + } + + public Task StoreTokenAsync(string token, string email) + { + return this._db.StringSetAsync($"{KeyPrefix}{token}", JsonConvert.SerializeObject(new { + issuedTo = email, + issuedAt = DateTime.UtcNow + })); + } + + public Task IsTokenValidAsync(string token) + { + return this._db.KeyExistsAsync($"{KeyPrefix}{token}"); + } + + public Task RevokeTokenAsync(string token) + { + return this._db.KeyDeleteAsync($"{KeyPrefix}{token}"); + } +} \ No newline at end of file From 1057e4d31042d63eb315d3daadf14f602142b1ad Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 15:12:17 +0530 Subject: [PATCH 29/73] feat: add endpoint for revoking token and update login endpoint to save generated token to redis --- OsmoDoc.API/Controllers/LoginController.cs | 33 +++++++++++++++++++-- OsmoDoc.API/Models/RevokeTokenRequestDTO.cs | 9 ++++++ 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 OsmoDoc.API/Models/RevokeTokenRequestDTO.cs diff --git a/OsmoDoc.API/Controllers/LoginController.cs b/OsmoDoc.API/Controllers/LoginController.cs index b592bb2..453dd4a 100644 --- a/OsmoDoc.API/Controllers/LoginController.cs +++ b/OsmoDoc.API/Controllers/LoginController.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; using OsmoDoc.API.Models; using OsmoDoc.API.Helpers; -using Microsoft.AspNetCore.Authorization; +using OsmoDoc.Services; namespace OsmoDoc.API.Controllers; @@ -9,22 +10,25 @@ namespace OsmoDoc.API.Controllers; [ApiController] public class LoginController : ControllerBase { + private readonly IRedisTokenStoreService _tokenStoreSerivce; private readonly ILogger _logger; - public LoginController(ILogger logger) + public LoginController(IRedisTokenStoreService tokenStoreService, ILogger logger) { + this._tokenStoreSerivce = tokenStoreService; this._logger = logger; } [HttpPost] [Route("login")] [AllowAnonymous] - public ActionResult Login([FromBody] LoginRequestDTO loginRequest) + public async Task> Login([FromBody] LoginRequestDTO loginRequest) { BaseResponse response = new BaseResponse(ResponseStatus.Fail); try { string token = AuthenticationHelper.JwtTokenGenerator(loginRequest.Email); + await this._tokenStoreSerivce.StoreTokenAsync(token, loginRequest.Email); response.Status = ResponseStatus.Success; response.AuthToken = token; @@ -40,4 +44,27 @@ public ActionResult Login([FromBody] LoginRequestDTO loginRequest) return this.StatusCode(StatusCodes.Status500InternalServerError, response); } } + + [HttpPost] + [Route("revoke")] + public async Task> RevokeToken([FromBody] RevokeTokenRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + try + { + await this._tokenStoreSerivce.RevokeTokenAsync(request.Token); + + response.Status = ResponseStatus.Success; + response.Message = "Token revoked"; + return this.Ok(response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } } \ No newline at end of file diff --git a/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs b/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs new file mode 100644 index 0000000..26d5c21 --- /dev/null +++ b/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.API.Models; + +public class RevokeTokenRequestDTO +{ + [Required(ErrorMessage = "Token is required")] + public string Token { get; set; } = string.Empty; +} \ No newline at end of file From 104a56a6947d24a039e82201faf84177d00952f9 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 20 Jun 2025 20:23:41 +0530 Subject: [PATCH 30/73] feat: make header parsing case-insensitive --- OsmoDoc.API/Program.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/OsmoDoc.API/Program.cs b/OsmoDoc.API/Program.cs index f6f0d5c..266ab1d 100644 --- a/OsmoDoc.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -117,7 +117,13 @@ { IRedisTokenStoreService tokenStore = context.HttpContext.RequestServices.GetRequiredService(); JwtSecurityToken? token = context.SecurityToken as JwtSecurityToken; - string tokenString = context.Request.Headers["Authorization"].ToString().Replace("bearer ", ""); + string tokenString = string.Empty; + + if (context.Request.Headers.TryGetValue("Authorization", out Microsoft.Extensions.Primitives.StringValues authHeader) && + authHeader.ToString().StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + tokenString = authHeader.ToString().Substring("Bearer ".Length).Trim(); + } if (!await tokenStore.IsTokenValidAsync(tokenString)) { From 2994eebd5d660d6393710b63cabb32117099bfda Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 20 Jun 2025 20:47:26 +0530 Subject: [PATCH 31/73] feat: add cancellation token for async operations and pass HttpContext.RequestAborted for automatic request cancellation --- OsmoDoc.API/Controllers/LoginController.cs | 4 ++-- OsmoDoc.API/Program.cs | 2 +- .../Interfaces/IRedisTokenStoreService.cs | 7 ++++--- OsmoDoc/Services/RedisTokenStoreService.cs | 15 +++++++++++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/OsmoDoc.API/Controllers/LoginController.cs b/OsmoDoc.API/Controllers/LoginController.cs index 453dd4a..0321d91 100644 --- a/OsmoDoc.API/Controllers/LoginController.cs +++ b/OsmoDoc.API/Controllers/LoginController.cs @@ -28,7 +28,7 @@ public async Task> Login([FromBody] LoginRequestDTO l try { string token = AuthenticationHelper.JwtTokenGenerator(loginRequest.Email); - await this._tokenStoreSerivce.StoreTokenAsync(token, loginRequest.Email); + await this._tokenStoreSerivce.StoreTokenAsync(token, loginRequest.Email, this.HttpContext.RequestAborted); response.Status = ResponseStatus.Success; response.AuthToken = token; @@ -52,7 +52,7 @@ public async Task> RevokeToken([FromBody] RevokeToken BaseResponse response = new BaseResponse(ResponseStatus.Fail); try { - await this._tokenStoreSerivce.RevokeTokenAsync(request.Token); + await this._tokenStoreSerivce.RevokeTokenAsync(request.Token, this.HttpContext.RequestAborted); response.Status = ResponseStatus.Success; response.Message = "Token revoked"; diff --git a/OsmoDoc.API/Program.cs b/OsmoDoc.API/Program.cs index 266ab1d..2f58b91 100644 --- a/OsmoDoc.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -125,7 +125,7 @@ tokenString = authHeader.ToString().Substring("Bearer ".Length).Trim(); } - if (!await tokenStore.IsTokenValidAsync(tokenString)) + if (!await tokenStore.IsTokenValidAsync(tokenString, context.HttpContext.RequestAborted)) { context.Fail("Token has been revoked."); } diff --git a/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs b/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs index 498acbc..ddbfde5 100644 --- a/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs +++ b/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs @@ -1,10 +1,11 @@ +using System.Threading; using System.Threading.Tasks; namespace OsmoDoc.Services; public interface IRedisTokenStoreService { - Task StoreTokenAsync(string token, string email); - Task IsTokenValidAsync(string token); - Task RevokeTokenAsync(string token); + Task StoreTokenAsync(string token, string email, CancellationToken cancellationToken = default); + Task IsTokenValidAsync(string token, CancellationToken cancellationToken = default); + Task RevokeTokenAsync(string token, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/OsmoDoc/Services/RedisTokenStoreService.cs b/OsmoDoc/Services/RedisTokenStoreService.cs index a742733..f54a688 100644 --- a/OsmoDoc/Services/RedisTokenStoreService.cs +++ b/OsmoDoc/Services/RedisTokenStoreService.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using StackExchange.Redis; @@ -15,21 +16,27 @@ public RedisTokenStoreService(IConnectionMultiplexer redis) this._db = redis.GetDatabase(); } - public Task StoreTokenAsync(string token, string email) + public Task StoreTokenAsync(string token, string email, CancellationToken cancellationToken = default) { - return this._db.StringSetAsync($"{KeyPrefix}{token}", JsonConvert.SerializeObject(new { + // Check if operation was cancelled before starting + cancellationToken.ThrowIfCancellationRequested(); + + return this._db.StringSetAsync($"{KeyPrefix}{token}", JsonConvert.SerializeObject(new + { issuedTo = email, issuedAt = DateTime.UtcNow })); } - public Task IsTokenValidAsync(string token) + public Task IsTokenValidAsync(string token, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); return this._db.KeyExistsAsync($"{KeyPrefix}{token}"); } - public Task RevokeTokenAsync(string token) + public Task RevokeTokenAsync(string token, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); return this._db.KeyDeleteAsync($"{KeyPrefix}{token}"); } } \ No newline at end of file From 67d91798604dc7f4c6f970da4fbb339b7ae97a60 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sat, 21 Jun 2025 22:24:17 +0530 Subject: [PATCH 32/73] fix: the typo in _tokenStoreService --- OsmoDoc.API/Controllers/LoginController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OsmoDoc.API/Controllers/LoginController.cs b/OsmoDoc.API/Controllers/LoginController.cs index 0321d91..a27d792 100644 --- a/OsmoDoc.API/Controllers/LoginController.cs +++ b/OsmoDoc.API/Controllers/LoginController.cs @@ -10,12 +10,12 @@ namespace OsmoDoc.API.Controllers; [ApiController] public class LoginController : ControllerBase { - private readonly IRedisTokenStoreService _tokenStoreSerivce; + private readonly IRedisTokenStoreService _tokenStoreService; private readonly ILogger _logger; public LoginController(IRedisTokenStoreService tokenStoreService, ILogger logger) { - this._tokenStoreSerivce = tokenStoreService; + this._tokenStoreService = tokenStoreService; this._logger = logger; } @@ -28,7 +28,7 @@ public async Task> Login([FromBody] LoginRequestDTO l try { string token = AuthenticationHelper.JwtTokenGenerator(loginRequest.Email); - await this._tokenStoreSerivce.StoreTokenAsync(token, loginRequest.Email, this.HttpContext.RequestAborted); + await this._tokenStoreService.StoreTokenAsync(token, loginRequest.Email, this.HttpContext.RequestAborted); response.Status = ResponseStatus.Success; response.AuthToken = token; @@ -52,7 +52,7 @@ public async Task> RevokeToken([FromBody] RevokeToken BaseResponse response = new BaseResponse(ResponseStatus.Fail); try { - await this._tokenStoreSerivce.RevokeTokenAsync(request.Token, this.HttpContext.RequestAborted); + await this._tokenStoreService.RevokeTokenAsync(request.Token, this.HttpContext.RequestAborted); response.Status = ResponseStatus.Success; response.Message = "Token revoked"; From 865ddedc3bbab4a1e56bca7946a8b752067dc125 Mon Sep 17 00:00:00 2001 From: Jatin Date: Wed, 18 Jun 2025 16:29:02 +0530 Subject: [PATCH 33/73] fix: initialise non-nullable properties in DTOs --- OsmoDoc.API/Models/PdfGenerationRequestDTO.cs | 5 ++--- OsmoDoc.API/Models/WordGenerationRequestDTO.cs | 4 ++-- OsmoDoc/Pdf/Models/ContentMetaData.cs | 4 ++-- OsmoDoc/Word/Models/ContentData.cs | 4 ++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/OsmoDoc.API/Models/PdfGenerationRequestDTO.cs b/OsmoDoc.API/Models/PdfGenerationRequestDTO.cs index f88a849..52f8fc1 100644 --- a/OsmoDoc.API/Models/PdfGenerationRequestDTO.cs +++ b/OsmoDoc.API/Models/PdfGenerationRequestDTO.cs @@ -6,8 +6,7 @@ namespace OsmoDoc.API.Models; public class PdfGenerationRequestDTO { [Required(ErrorMessage = "Base64 string for PDF template is required")] - public string? Base64 { get; set; } - [Required(ErrorMessage = "Data to be modified in PDF is required")] - public DocumentData? DocumentData { get; set; } + public required string Base64 { get; set; } + public DocumentData DocumentData { get; set; } = new(); public string? SerializedEjsDataJson { get; set; } } diff --git a/OsmoDoc.API/Models/WordGenerationRequestDTO.cs b/OsmoDoc.API/Models/WordGenerationRequestDTO.cs index 41d1914..75111c1 100644 --- a/OsmoDoc.API/Models/WordGenerationRequestDTO.cs +++ b/OsmoDoc.API/Models/WordGenerationRequestDTO.cs @@ -7,9 +7,9 @@ namespace OsmoDoc.API.Models; public class WordGenerationRequestDTO { [Required(ErrorMessage = "Base64 string for Word template is required")] - public string? Base64 { get; set; } + public required string Base64 { get; set; } [Required(ErrorMessage = "Data to be modified in Word file is required")] - public WordDocumentDataRequestDTO? DocumentData { get; set; } + public WordDocumentDataRequestDTO DocumentData { get; set; } = new(); } public class WordContentDataRequestDTO : ContentData diff --git a/OsmoDoc/Pdf/Models/ContentMetaData.cs b/OsmoDoc/Pdf/Models/ContentMetaData.cs index 549f187..c7fe88f 100644 --- a/OsmoDoc/Pdf/Models/ContentMetaData.cs +++ b/OsmoDoc/Pdf/Models/ContentMetaData.cs @@ -2,8 +2,8 @@ public class ContentMetaData { - public string Placeholder { get; set; } - public string Content { get; set; } + public string Placeholder { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; } diff --git a/OsmoDoc/Word/Models/ContentData.cs b/OsmoDoc/Word/Models/ContentData.cs index 512eb79..dc27d31 100644 --- a/OsmoDoc/Word/Models/ContentData.cs +++ b/OsmoDoc/Word/Models/ContentData.cs @@ -9,12 +9,12 @@ public class ContentData /// /// Gets or sets the placeholder name. /// - public string Placeholder { get; set; } + public string Placeholder { get; set; } = string.Empty; /// /// Gets or sets the content to replace the placeholder with. /// - public string Content { get; set; } + public string Content { get; set; } = string.Empty; /// /// Gets or sets the content type of the placeholder (text or image). From 6c069811e766ae0e60f15f330c45c7cee0c8cceb Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 09:18:03 +0530 Subject: [PATCH 34/73] refactor: handle nulls safely while extracting model validation error message --- OsmoDoc.API/Models/BaseResponse.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/OsmoDoc.API/Models/BaseResponse.cs b/OsmoDoc.API/Models/BaseResponse.cs index a2df3cc..5bf6cbf 100644 --- a/OsmoDoc.API/Models/BaseResponse.cs +++ b/OsmoDoc.API/Models/BaseResponse.cs @@ -23,11 +23,16 @@ public class ModelValidationBadRequest { public static BadRequestObjectResult ModelValidationErrorResponse(ActionContext actionContext) { - return new BadRequestObjectResult(actionContext.ModelState - .Where(modelError => modelError.Value.Errors.Any()) - .Select(modelError => new BaseResponse(ResponseStatus.Error) - { - Message = modelError.Value.Errors.FirstOrDefault().ErrorMessage - }).FirstOrDefault()); + string? firstError = actionContext.ModelState + .Where(ms => ms.Value != null && ms.Value.Errors.Any()) + .Select(ms => ms.Value!.Errors.FirstOrDefault()?.ErrorMessage) + .FirstOrDefault(msg => !string.IsNullOrWhiteSpace(msg)); + + BaseResponse response = new BaseResponse(ResponseStatus.Error) + { + Message = firstError ?? "Validation failed" + }; + + return new BadRequestObjectResult(response); } } From ef48c02b3762e982b0a6e2e67e5220da03994ed0 Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 09:18:18 +0530 Subject: [PATCH 35/73] feat: add validation check for empty filePath --- OsmoDoc.API/Helpers/CommonMethodsHelper.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/OsmoDoc.API/Helpers/CommonMethodsHelper.cs b/OsmoDoc.API/Helpers/CommonMethodsHelper.cs index fe3de2b..8c729bd 100644 --- a/OsmoDoc.API/Helpers/CommonMethodsHelper.cs +++ b/OsmoDoc.API/Helpers/CommonMethodsHelper.cs @@ -1,12 +1,19 @@ -namespace OsmoDoc.API.Helpers; +using System.IO; + +namespace OsmoDoc.API.Helpers; public static class CommonMethodsHelper { public static void CreateDirectoryIfNotExists(string filePath) { + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentException("File path cannot be null or empty.", nameof(filePath)); + } + // Get directory name of the file // If path is a file name only, directory name will be an empty string - string directoryName = Path.GetDirectoryName(filePath); + string? directoryName = Path.GetDirectoryName(filePath); if (!string.IsNullOrWhiteSpace(directoryName)) { From bd29e6182212d971ac3cc360ebf5bde1eb0904bc Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 09:18:29 +0530 Subject: [PATCH 36/73] chore: add project metadata to OsmoDoc.csproj file --- OsmoDoc.API/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OsmoDoc.API/Program.cs b/OsmoDoc.API/Program.cs index 2f58b91..abb3550 100644 --- a/OsmoDoc.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -41,8 +41,8 @@ ); builder.Services.AddScoped(); -// Configure request size limit -long requestBodySizeLimitBytes = Convert.ToInt64(builder.Configuration.GetSection("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES").Value); +// Configure request size limit (50 MB default) +long requestBodySizeLimitBytes = builder.Configuration.GetValue("CONFIG:REQUEST_BODY_SIZE_LIMIT_BYTES", 50 * 1024 * 1024); // Configure request size for Kestrel server - ASP.NET Core project templates use Kestrel by default when not hosted with IIS builder.Services.Configure(options => From 3c537e3f161e56f1ae164fc8f099335c1381adfd Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 09:22:04 +0530 Subject: [PATCH 37/73] feat: add null checks for configuration path values and reques body --- OsmoDoc.API/Controllers/PdfController.cs | 54 +++++++++++++++++------ OsmoDoc.API/Controllers/WordController.cs | 42 +++++++++++++----- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/OsmoDoc.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs index 2e11b2a..3d89b48 100644 --- a/OsmoDoc.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -30,12 +30,29 @@ public async Task> GeneratePdf(PdfGenerationRequestDT try { + if (request == null) + { + throw new BadHttpRequestException("Request body cannot be null"); + } + + string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); + string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:INPUT is missing."); + string htmlPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:HTML").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:HTML is missing."); + string outputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:OUTPUT is missing."); + string pdfPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:PDF is missing."); + + // Generate filepath to save base64 html template string htmlTemplateFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:HTML").Value, + tempPath, + inputPath, + htmlPath, CommonMethodsHelper.GenerateRandomFileName("html") ); @@ -46,9 +63,9 @@ public async Task> GeneratePdf(PdfGenerationRequestDT string outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, + tempPath, + outputPath, + pdfPath, CommonMethodsHelper.GenerateRandomFileName("pdf") ); @@ -115,12 +132,23 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR try { + string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); + string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:INPUT is missing."); + string ejsPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:EJS").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:EJS is missing."); + string outputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:OUTPUT is missing."); + string pdfPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:PDF is missing."); + // Generate filepath to save base64 html template string ejsTemplateFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:EJS").Value, + tempPath, + inputPath, + ejsPath, CommonMethodsHelper.GenerateRandomFileName("ejs") ); @@ -131,9 +159,9 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR string outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value, + tempPath, + inputPath, + pdfPath, CommonMethodsHelper.GenerateRandomFileName("pdf") ); @@ -142,7 +170,7 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR // Generate and save pdf in output directory await PdfDocumentGenerator.GeneratePdf( ejsTemplateFilePath, - request.DocumentData?.Placeholders, + request.DocumentData.Placeholders, outputFilePath, isEjsTemplate: true, serializedEjsDataJson: request.SerializedEjsDataJson diff --git a/OsmoDoc.API/Controllers/WordController.cs b/OsmoDoc.API/Controllers/WordController.cs index 03ca47c..0461d28 100644 --- a/OsmoDoc.API/Controllers/WordController.cs +++ b/OsmoDoc.API/Controllers/WordController.cs @@ -34,12 +34,34 @@ public async Task> GenerateWord(WordGenerationRequest try { + if (request == null) + { + throw new BadHttpRequestException("Request body cannot be null"); + } + + if (request.DocumentData == null) + { + throw new BadHttpRequestException("Document data is required"); + } + + string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); + string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:INPUT is missing."); + string wordPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:WORD is missing."); + string outputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:OUTPUT is missing."); + string imagesPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:IMAGES").Value + ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:IMAGES is missing."); + + // Generate filepath to save base64 docx template string docxTemplateFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, + tempPath, + inputPath, + wordPath, CommonMethodsHelper.GenerateRandomFileName("docx") ); @@ -51,9 +73,9 @@ public async Task> GenerateWord(WordGenerationRequest // Initialize output filepath string outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, + tempPath, + outputPath, + wordPath, CommonMethodsHelper.GenerateRandomFileName("docx") ); @@ -80,10 +102,10 @@ public async Task> GenerateWord(WordGenerationRequest // Generate a random image file name and its path string imageFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value, - this._configuration.GetSection("TEMPORARY_FILE_PATHS:IMAGES").Value, + tempPath, + inputPath, + wordPath, + imagesPath, CommonMethodsHelper.GenerateRandomFileName(placeholder.ImageExtension) ); From 539b0e12d9db696caf9d1e1268ee4d78427024af Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 09:32:47 +0530 Subject: [PATCH 38/73] refactor: suppress nullable compliler warnigns --- OsmoDoc/Word/Models/DocumentData.cs | 4 ++-- OsmoDoc/Word/Models/TableData.cs | 2 +- OsmoDoc/Word/WordDocumentGenerator.cs | 11 ++++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/OsmoDoc/Word/Models/DocumentData.cs b/OsmoDoc/Word/Models/DocumentData.cs index ea0e978..787b5e1 100644 --- a/OsmoDoc/Word/Models/DocumentData.cs +++ b/OsmoDoc/Word/Models/DocumentData.cs @@ -13,11 +13,11 @@ public class DocumentData /// /// Gets or sets the list of content placeholders in the document. /// - public List Placeholders { get; set; } + public List Placeholders { get; set; } = new(); /// /// Gets or sets the list of table data in the document. /// - public List TablesData { get; set; } + public List TablesData { get; set; } = new(); } diff --git a/OsmoDoc/Word/Models/TableData.cs b/OsmoDoc/Word/Models/TableData.cs index 6e47f98..924c190 100644 --- a/OsmoDoc/Word/Models/TableData.cs +++ b/OsmoDoc/Word/Models/TableData.cs @@ -18,5 +18,5 @@ public class TableData /// Each dictionary contains column header-value pairs. /// - public List> Data { get; set; } + public List> Data { get; set; } = new(); } diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 49c1588..c9191e8 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -300,15 +300,20 @@ private async static Task ReplaceImagePlaceholders(string inputFilePath, string DocProperties? docProperty = drawing.Descendants().FirstOrDefault(); // If drawing / image name is present in imagePlaceholders dictionary, then replace image - if (docProperty != null && imagePlaceholders.ContainsKey(docProperty.Name)) + if (docProperty != null && docProperty.Name != null && imagePlaceholders.ContainsKey(docProperty.Name!)) { List drawingBlips = drawing.Descendants().ToList(); foreach (Blip blip in drawingBlips) { - OpenXmlPart imagePart = wordDocument.MainDocumentPart.GetPartById(blip.Embed); + if (blip.Embed == null) + { + continue; + } + + OpenXmlPart imagePart = mainDocumentPart!.GetPartById(blip.Embed!); - string imagePath = imagePlaceholders[docProperty.Name]; + string imagePath = imagePlaceholders[docProperty.Name!]; // Asynchronously download image data using HttpClient using HttpClient httpClient = new HttpClient(); From 2147849b3e348e4854be2f530b4dcaa745c151ef Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 13:31:44 +0530 Subject: [PATCH 39/73] feat: add image URL validation and handle nullable Uri safely --- OsmoDoc/Word/WordDocumentGenerator.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index c9191e8..3a9f061 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -315,9 +315,16 @@ private async static Task ReplaceImagePlaceholders(string inputFilePath, string string imagePath = imagePlaceholders[docProperty.Name!]; + // Validate URL before downloading + if (!Uri.TryCreate(imagePath, UriKind.Absolute, out Uri? tempUri)) + { + throw new ArgumentException($"Invalid image URL: {imagePath}"); + } + // Asynchronously download image data using HttpClient using HttpClient httpClient = new HttpClient(); - byte[] imageData = await httpClient.GetByteArrayAsync(imagePath); + Uri imageUri = tempUri!; + byte[] imageData = await httpClient.GetByteArrayAsync(imageUri); using (Stream partStream = imagePart.GetStream(FileMode.OpenOrCreate, FileAccess.Write)) { From 32b31f9963f08f50dc772f4698d3d1bb1b846622 Mon Sep 17 00:00:00 2001 From: Jatin Date: Thu, 19 Jun 2025 14:06:39 +0530 Subject: [PATCH 40/73] feat: add constant for Placeholder regex pattern --- OsmoDoc/Word/WordDocumentGenerator.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 3a9f061..1343383 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -20,7 +20,9 @@ namespace OsmoDoc.Word; /// Provides functionality to generate Word documents based on templates and data. /// public static class WordDocumentGenerator -{ +{ + private const string PlaceholderPattern = @"{[a-zA-Z]+}"; + /// /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. /// @@ -80,7 +82,7 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc XWPFParagraph paragraph = (XWPFParagraph)element; // If the paragraph is empty string or the placeholder regex does not match then continue - if (paragraph.ParagraphText == string.Empty || !new Regex(@"{[a-zA-Z]+}").IsMatch(paragraph.ParagraphText)) + if (paragraph.ParagraphText == string.Empty || !new Regex(PlaceholderPattern).IsMatch(paragraph.ParagraphText)) { continue; } @@ -166,7 +168,7 @@ private static void WriteDocument(XWPFDocument document, string filePath) private static XWPFParagraph ReplacePlaceholdersOnBody(XWPFParagraph paragraph, Dictionary textPlaceholders) { // Get a list of all placeholders in the current paragraph - List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") + List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, PlaceholderPattern) .Cast() .Select(s => s.Groups[0].Value).ToList(); @@ -201,7 +203,7 @@ private static XWPFTable ReplacePlaceholderOnTables(XWPFTable table, Dictionary< foreach (XWPFParagraph paragraph in cell.Paragraphs) { // Get a list of all placeholders in the current cell - List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, @"{[a-zA-Z]+}") + List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, PlaceholderPattern) .Cast() .Select(s => s.Groups[0].Value).ToList(); From 928a94e191568f30cc8c295e9a4ce347e9159c1c Mon Sep 17 00:00:00 2001 From: Jatin Date: Sat, 21 Jun 2025 22:30:26 +0530 Subject: [PATCH 41/73] feat: remove the redundant try catch blocks --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 81 +++++++-------- OsmoDoc/Word/WordDocumentGenerator.cs | 143 ++++++++++++-------------- 2 files changed, 105 insertions(+), 119 deletions(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index a045d16..4b8b3ed 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -22,63 +22,56 @@ public class PdfDocumentGenerator /// JSON string containing data for EJS template rendering. Required if isEjsTemplate is true. public async static Task GeneratePdf(string templatePath, List metaDataList, string outputFilePath, bool isEjsTemplate, string? serializedEjsDataJson) { - try + if (metaDataList is null) { - if (metaDataList is null) - { - throw new ArgumentNullException(nameof(metaDataList)); - } + throw new ArgumentNullException(nameof(metaDataList)); + } - if (string.IsNullOrWhiteSpace(templatePath)) - { - throw new ArgumentNullException(nameof(templatePath)); - } + if (string.IsNullOrWhiteSpace(templatePath)) + { + throw new ArgumentNullException(nameof(templatePath)); + } - if (string.IsNullOrWhiteSpace(outputFilePath)) - { - throw new ArgumentNullException(nameof(outputFilePath)); - } + if (string.IsNullOrWhiteSpace(outputFilePath)) + { + throw new ArgumentNullException(nameof(outputFilePath)); + } - if (string.IsNullOrWhiteSpace(OsmoDocPdfConfig.WkhtmltopdfPath) && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - throw new Exception("WkhtmltopdfPath is not set in OsmoDocPdfConfig."); - } + if (string.IsNullOrWhiteSpace(OsmoDocPdfConfig.WkhtmltopdfPath) && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + throw new Exception("WkhtmltopdfPath is not set in OsmoDocPdfConfig."); + } - if (!File.Exists(templatePath)) - { - throw new Exception("The file path you provided is not valid."); - } + if (!File.Exists(templatePath)) + { + throw new Exception("The file path you provided is not valid."); + } - if (isEjsTemplate) + if (isEjsTemplate) + { + // Validate if template in file path is an ejs file + if (Path.GetExtension(templatePath).ToLower() != ".ejs") { - // Validate if template in file path is an ejs file - if (Path.GetExtension(templatePath).ToLower() != ".ejs") - { - throw new Exception("Input template should be a valid EJS file"); - } - - // Convert ejs file to an equivalent html - templatePath = await ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); + throw new Exception("Input template should be a valid EJS file"); } - // Modify html template with content data and generate pdf - string modifiedHtmlFilePath = ReplaceFileElementsWithMetaData(templatePath, metaDataList, outputFilePath); - await ConvertHtmlToPdf(OsmoDocPdfConfig.WkhtmltopdfPath, modifiedHtmlFilePath, outputFilePath); + // Convert ejs file to an equivalent html + templatePath = await ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); + } + + // Modify html template with content data and generate pdf + string modifiedHtmlFilePath = ReplaceFileElementsWithMetaData(templatePath, metaDataList, outputFilePath); + await ConvertHtmlToPdf(OsmoDocPdfConfig.WkhtmltopdfPath, modifiedHtmlFilePath, outputFilePath); - if (isEjsTemplate) + if (isEjsTemplate) + { + // If input template was an ejs file, then the template path contains path to html converted from ejs + if (File.Exists(templatePath) && Path.GetExtension(templatePath).ToLower() == ".html") { - // If input template was an ejs file, then the template path contains path to html converted from ejs - if (File.Exists(templatePath) && Path.GetExtension(templatePath).ToLower() == ".html") - { - // If template path contains path to converted html template then delete it - File.Delete(templatePath); - } + // If template path contains path to converted html template then delete it + File.Delete(templatePath); } } - catch (Exception) - { - throw; - } } private static string ReplaceFileElementsWithMetaData(string templatePath, List metaDataList, string outputFilePath) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 1343383..78e25d5 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -31,101 +31,94 @@ public static class WordDocumentGenerator /// The file path to save the generated document. public async static Task GenerateDocumentByTemplate(string templateFilePath, DocumentData documentData, string outputFilePath) { - try + if (string.IsNullOrWhiteSpace(templateFilePath)) { - if (string.IsNullOrWhiteSpace(templateFilePath)) + throw new ArgumentNullException(nameof(templateFilePath)); + } + + if (string.IsNullOrWhiteSpace(outputFilePath)) + { + throw new ArgumentNullException(nameof(outputFilePath)); + } + + List contentData = documentData.Placeholders; + List tablesData = documentData.TablesData; + + // Creating dictionaries for each type of placeholders + Dictionary textPlaceholders = new Dictionary(); + Dictionary tableContentPlaceholders = new Dictionary(); + Dictionary imagePlaceholders = new Dictionary(); + + foreach (ContentData content in contentData) + { + if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) { - throw new ArgumentNullException(nameof(templateFilePath)); + string placeholder = "{" + content.Placeholder + "}"; + textPlaceholders.TryAdd(placeholder, content.Content); } - - if (string.IsNullOrWhiteSpace(outputFilePath)) + else if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Image) { - throw new ArgumentNullException(nameof(outputFilePath)); + string placeholder = content.Placeholder; + imagePlaceholders.TryAdd(placeholder, content.Content); } + else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) + { + string placeholder = "{" + content.Placeholder + "}"; + tableContentPlaceholders.TryAdd(placeholder, content.Content); + } + } - List contentData = documentData.Placeholders; - List tablesData = documentData.TablesData; - - // Creating dictionaries for each type of placeholders - Dictionary textPlaceholders = new Dictionary(); - Dictionary tableContentPlaceholders = new Dictionary(); - Dictionary imagePlaceholders = new Dictionary(); + // Create document of the template + XWPFDocument document = await GetXWPFDocument(templateFilePath); - foreach (ContentData content in contentData) + // For each element in the document + foreach (IBodyElement element in document.BodyElements) + { + if (element.ElementType == BodyElementType.PARAGRAPH) { - if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) - { - string placeholder = "{" + content.Placeholder + "}"; - textPlaceholders.TryAdd(placeholder, content.Content); - } - else if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Image) - { - string placeholder = content.Placeholder; - imagePlaceholders.TryAdd(placeholder, content.Content); - } - else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) + // If element is a paragraph + XWPFParagraph paragraph = (XWPFParagraph)element; + + // If the paragraph is empty string or the placeholder regex does not match then continue + if (paragraph.ParagraphText == string.Empty || !new Regex(PlaceholderPattern).IsMatch(paragraph.ParagraphText)) { - string placeholder = "{" + content.Placeholder + "}"; - tableContentPlaceholders.TryAdd(placeholder, content.Content); + continue; } - } - // Create document of the template - XWPFDocument document = await GetXWPFDocument(templateFilePath); - - // For each element in the document - foreach (IBodyElement element in document.BodyElements) + // Replace placeholders in paragraph with values + paragraph = ReplacePlaceholdersOnBody(paragraph, textPlaceholders); + } + else if (element.ElementType == BodyElementType.TABLE) { - if (element.ElementType == BodyElementType.PARAGRAPH) - { - // If element is a paragraph - XWPFParagraph paragraph = (XWPFParagraph)element; + // If element is a table + XWPFTable table = (XWPFTable)element; - // If the paragraph is empty string or the placeholder regex does not match then continue - if (paragraph.ParagraphText == string.Empty || !new Regex(PlaceholderPattern).IsMatch(paragraph.ParagraphText)) - { - continue; - } + // Replace placeholders in a table + table = ReplacePlaceholderOnTables(table, tableContentPlaceholders); - // Replace placeholders in paragraph with values - paragraph = ReplacePlaceholdersOnBody(paragraph, textPlaceholders); - } - else if (element.ElementType == BodyElementType.TABLE) + // Populate the table with data if it is passed in tablesData list + foreach (TableData insertData in tablesData) { - // If element is a table - XWPFTable table = (XWPFTable)element; - - // Replace placeholders in a table - table = ReplacePlaceholderOnTables(table, tableContentPlaceholders); - - // Populate the table with data if it is passed in tablesData list - foreach (TableData insertData in tablesData) + if (insertData.TablePos >= 1 && insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) { - if (insertData.TablePos >= 1 && insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) - { - table = PopulateTable(table, insertData); - } + table = PopulateTable(table, insertData); } } } - - // Write the document to output file path and close the document - WriteDocument(document, outputFilePath); - document.Close(); - - /* - * Image Replacement is done after writing the document here, - * because for Text Replacement, NPOI package is being used - * and for Image Replacement, OpeXML package is used. - * Since both the packages have different execution method, so they are handled separately - */ - // Replace all the image placeholders in the output file - await ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); - } - catch (Exception) - { - throw; } + + // Write the document to output file path and close the document + WriteDocument(document, outputFilePath); + document.Close(); + + /* + * Image Replacement is done after writing the document here, + * because for Text Replacement, NPOI package is being used + * and for Image Replacement, OpeXML package is used. + * Since both the packages have different execution method, so they are handled separately + */ + // Replace all the image placeholders in the output file + await ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); } /// From 880ac8b1365b713e60c00520d5b9bf6a9a04711d Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 22 Jun 2025 00:48:17 +0530 Subject: [PATCH 42/73] feat: add separate ImageData class --- OsmoDoc/Word/Models/DocumentData.cs | 5 +++++ OsmoDoc/Word/Models/Enums.cs | 7 +------ OsmoDoc/Word/Models/ImageData.cs | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 OsmoDoc/Word/Models/ImageData.cs diff --git a/OsmoDoc/Word/Models/DocumentData.cs b/OsmoDoc/Word/Models/DocumentData.cs index 787b5e1..943d7d2 100644 --- a/OsmoDoc/Word/Models/DocumentData.cs +++ b/OsmoDoc/Word/Models/DocumentData.cs @@ -20,4 +20,9 @@ public class DocumentData /// public List TablesData { get; set; } = new(); + + /// + /// Gets or sets the list of images in the document. + /// + public List Images { get; set; } = new(); } diff --git a/OsmoDoc/Word/Models/Enums.cs b/OsmoDoc/Word/Models/Enums.cs index d957dad..47d7ada 100644 --- a/OsmoDoc/Word/Models/Enums.cs +++ b/OsmoDoc/Word/Models/Enums.cs @@ -9,12 +9,7 @@ public enum ContentType /// /// The placeholder represents text content. /// - Text = 0, - - /// - /// The placeholder represents an image. - /// - Image = 1 + Text = 0 } /// diff --git a/OsmoDoc/Word/Models/ImageData.cs b/OsmoDoc/Word/Models/ImageData.cs new file mode 100644 index 0000000..56fe2fc --- /dev/null +++ b/OsmoDoc/Word/Models/ImageData.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +public enum ImageSourceType +{ + Base64, + LocalFile, + Url +} + +public class ImageData +{ + [Required(ErrorMessage = "Placeholder name is required")] + public string PlaceholderName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Image source type is required")] + public ImageSourceType SourceType { get; set; } + + [Required(ErrorMessage = "Image data is required")] + public string Data { get; set; } = string.Empty; // Can be base64, file path, or URL + + [Required(ErrorMessage = "Image extension is required")] + public string? ImageExtension { get; set; } // Required for Base64 +} \ No newline at end of file From 1b1c4da7913e8b015f3e31f1cab9bff06106ed31 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 22 Jun 2025 00:48:59 +0530 Subject: [PATCH 43/73] feat: add the feature of handling different types of images --- OsmoDoc/Word/WordDocumentGenerator.cs | 168 +++++++++++++++++--------- 1 file changed, 109 insertions(+), 59 deletions(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 78e25d5..4ca4d9e 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -13,6 +13,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Net.Http; +using System.Diagnostics; namespace OsmoDoc.Word; @@ -22,7 +23,7 @@ namespace OsmoDoc.Word; public static class WordDocumentGenerator { private const string PlaceholderPattern = @"{[a-zA-Z]+}"; - + /// /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. /// @@ -56,11 +57,6 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc string placeholder = "{" + content.Placeholder + "}"; textPlaceholders.TryAdd(placeholder, content.Content); } - else if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Image) - { - string placeholder = content.Placeholder; - imagePlaceholders.TryAdd(placeholder, content.Content); - } else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) { string placeholder = "{" + content.Placeholder + "}"; @@ -118,7 +114,7 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc * Since both the packages have different execution method, so they are handled separately */ // Replace all the image placeholders in the output file - await ReplaceImagePlaceholders(outputFilePath, outputFilePath, imagePlaceholders); + await ProcessImagePlaceholders(outputFilePath, documentData.Images); } /// @@ -237,7 +233,7 @@ private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) foreach (Dictionary rowData in tableData.Data) { XWPFTableRow row = table.CreateRow(); // This is a DATA row, not header - + int columnCount = headerRow.GetTableCells().Count; // Read from header for (int cellNumber = 0; cellNumber < columnCount; cellNumber++) { @@ -263,81 +259,135 @@ private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) /// /// Replaces the image placeholders in the output file with the specified images. /// - /// The input file path containing the image placeholders. - /// The output file path where the updated document will be saved. - /// The dictionary of image placeholders and their corresponding image paths. - private async static Task ReplaceImagePlaceholders(string inputFilePath, string outputFilePath, Dictionary imagePlaceholders) + /// The ile path where the updated document will be saved. + /// The data structure for holding the images details. + + private static async Task ProcessImagePlaceholders( + string documentPath, + List images) { - byte[] docBytes = await File.ReadAllBytesAsync(inputFilePath); + if (images == null || !images.Any()) + { + return; + } + + List tempFiles = new List(); - // Write document bytes to memory asynchronously - using (MemoryStream memoryStream = new MemoryStream()) + try { - await memoryStream.WriteAsync(docBytes, 0, docBytes.Length); - memoryStream.Position = 0; + byte[] docBytes = await File.ReadAllBytesAsync(documentPath); - using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) + using (MemoryStream memoryStream = new MemoryStream()) { - MainDocumentPart? mainDocumentPart = wordDocument.MainDocumentPart; + await memoryStream.WriteAsync(docBytes); + memoryStream.Position = 0; - // Get a list of drawings (images) - IEnumerable drawings = new List(); - if (mainDocumentPart != null) + using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) { - drawings = mainDocumentPart.Document.Descendants().ToList(); - } + MainDocumentPart? mainPart = wordDocument.MainDocumentPart; + if (mainPart == null) + { + return; + } - /* - * FIXME: Look on how we can improve this loop operation. - */ - foreach (Drawing drawing in drawings) - { - DocProperties? docProperty = drawing.Descendants().FirstOrDefault(); + List drawings = mainPart.Document.Descendants().ToList(); - // If drawing / image name is present in imagePlaceholders dictionary, then replace image - if (docProperty != null && docProperty.Name != null && imagePlaceholders.ContainsKey(docProperty.Name!)) + foreach (ImageData img in images) { - List drawingBlips = drawing.Descendants().ToList(); - - foreach (Blip blip in drawingBlips) + try { - if (blip.Embed == null) - { - continue; - } - - OpenXmlPart imagePart = mainDocumentPart!.GetPartById(blip.Embed!); + string tempFilePath = await PrepareImageFile(img); + tempFiles.Add(tempFilePath); - string imagePath = imagePlaceholders[docProperty.Name!]; + Drawing? drawing = drawings.FirstOrDefault(d => + d.Descendants() + .Any(dp => dp.Name == img.PlaceholderName)); - // Validate URL before downloading - if (!Uri.TryCreate(imagePath, UriKind.Absolute, out Uri? tempUri)) + if (drawing == null) { - throw new ArgumentException($"Invalid image URL: {imagePath}"); + continue; } - // Asynchronously download image data using HttpClient - using HttpClient httpClient = new HttpClient(); - Uri imageUri = tempUri!; - byte[] imageData = await httpClient.GetByteArrayAsync(imageUri); - - using (Stream partStream = imagePart.GetStream(FileMode.OpenOrCreate, FileAccess.Write)) + foreach (Blip blip in drawing.Descendants()) { - // Asynchronously write image data to the part stream - await partStream.WriteAsync(imageData, 0, imageData.Length); - partStream.SetLength(imageData.Length); // Ensure the stream is truncated if new data is smaller + if (blip.Embed?.Value == null) + { + continue; + } + + OpenXmlPart imagePart = mainPart.GetPartById(blip.Embed!); + using (Stream partStream = imagePart.GetStream(FileMode.Create)) + { + await using FileStream fileStream = File.OpenRead(tempFilePath); + await fileStream.CopyToAsync(partStream); + } } } + catch (Exception ex) + { + // Log error but continue with other images + Debug.WriteLine($"Failed to process image {img.PlaceholderName}: {ex.Message}"); + } } } + + // Save the modified document + memoryStream.Position = 0; + using (FileStream fileStream = new FileStream(documentPath, FileMode.Create)) + { + await memoryStream.CopyToAsync(fileStream); + } } - // Overwrite the output file asynchronously - using (FileStream fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) + } + finally + { + // Clean up temp files + foreach (string file in tempFiles) { - // Reset MemoryStream position before writing to fileStream - memoryStream.Position = 0; - await memoryStream.CopyToAsync(fileStream); + try { File.Delete(file); } + catch { /* Ignore cleanup errors */ } } } } + + private static async Task PrepareImageFile(ImageData imageData) + { + string tempFilePath = System.IO.Path.GetTempFileName(); + + if (!string.IsNullOrEmpty(imageData.ImageExtension)) + { + tempFilePath = System.IO.Path.ChangeExtension(tempFilePath, imageData.ImageExtension); + } + + switch (imageData.SourceType) + { + case ImageSourceType.Base64: + await File.WriteAllBytesAsync( + tempFilePath, + Convert.FromBase64String(imageData.Data)); + break; + + case ImageSourceType.LocalFile: + if (!File.Exists(imageData.Data)) + { + throw new FileNotFoundException("Image file not found", imageData.Data); + } + + File.Copy(imageData.Data, tempFilePath, true); + break; + + case ImageSourceType.Url: + using (HttpClient httpClient = new HttpClient()) + { + byte[] bytes = await httpClient.GetByteArrayAsync(imageData.Data); + await File.WriteAllBytesAsync(tempFilePath, bytes); + } + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + return tempFilePath; + } } \ No newline at end of file From d565e1557f81d9152403a2fb004a96aa6a737a09 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 22 Jun 2025 00:49:43 +0530 Subject: [PATCH 44/73] refactor: update the controller according to the separate handling of images --- OsmoDoc.API/Controllers/WordController.cs | 41 +------------------ OsmoDoc.API/Helpers/AutoMappingProfile.cs | 3 +- .../Models/WordGenerationRequestDTO.cs | 8 +--- 3 files changed, 5 insertions(+), 47 deletions(-) diff --git a/OsmoDoc.API/Controllers/WordController.cs b/OsmoDoc.API/Controllers/WordController.cs index 0461d28..feaf46f 100644 --- a/OsmoDoc.API/Controllers/WordController.cs +++ b/OsmoDoc.API/Controllers/WordController.cs @@ -81,49 +81,12 @@ public async Task> GenerateWord(WordGenerationRequest CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); - // Handle image placeholder data in request - foreach (WordContentDataRequestDTO placeholder in request.DocumentData.Placeholders) - { - if (placeholder.ContentType == ContentType.Image) - { - if (string.IsNullOrWhiteSpace(placeholder.ImageExtension)) - { - throw new BadHttpRequestException("Image extension is required for image content data"); - } - - if (string.IsNullOrWhiteSpace(placeholder.Content)) - { - throw new BadHttpRequestException("Image content data is required"); - } - - // Remove '.' from image extension if present - placeholder.ImageExtension = placeholder.ImageExtension.Replace(".", string.Empty); - - // Generate a random image file name and its path - string imageFilePath = Path.Combine( - this._hostingEnvironment.WebRootPath, - tempPath, - inputPath, - wordPath, - imagesPath, - CommonMethodsHelper.GenerateRandomFileName(placeholder.ImageExtension) - ); - - CommonMethodsHelper.CreateDirectoryIfNotExists(imageFilePath); - - // Save image content base64 string to inputs directory - await Base64StringHelper.SaveBase64StringToFilePath(placeholder.Content, imageFilePath, this._configuration); - - // Replace placeholder content with image file path - placeholder.Content = imageFilePath; - } - } - // Map document data in request to word library model class DocumentData documentData = new DocumentData { Placeholders = this._mapper.Map>(request.DocumentData.Placeholders), - TablesData = request.DocumentData.TablesData + TablesData = request.DocumentData.TablesData, + Images = request.DocumentData.ImagesData }; // Generate and save output docx in output directory diff --git a/OsmoDoc.API/Helpers/AutoMappingProfile.cs b/OsmoDoc.API/Helpers/AutoMappingProfile.cs index 269ba7b..9730c3e 100644 --- a/OsmoDoc.API/Helpers/AutoMappingProfile.cs +++ b/OsmoDoc.API/Helpers/AutoMappingProfile.cs @@ -7,7 +7,6 @@ namespace OsmoDoc.API.Helpers; public class AutoMappingProfile : Profile { public AutoMappingProfile() - { - this.CreateMap(); + { } } diff --git a/OsmoDoc.API/Models/WordGenerationRequestDTO.cs b/OsmoDoc.API/Models/WordGenerationRequestDTO.cs index 75111c1..89b9d2f 100644 --- a/OsmoDoc.API/Models/WordGenerationRequestDTO.cs +++ b/OsmoDoc.API/Models/WordGenerationRequestDTO.cs @@ -12,13 +12,9 @@ public class WordGenerationRequestDTO public WordDocumentDataRequestDTO DocumentData { get; set; } = new(); } -public class WordContentDataRequestDTO : ContentData -{ - public string? ImageExtension { get; set; } -} - public class WordDocumentDataRequestDTO { - public List Placeholders { get; set; } = new List(); + public List Placeholders { get; set; } = new List(); public List TablesData { get; set; } = new List(); + public List ImagesData { get; set; } = new List(); } From ccbff551d46dbcd4a571fc48b61826aa65980d90 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 08:39:33 +0530 Subject: [PATCH 45/73] feat: add namespace in ImagrData --- OsmoDoc/Word/Models/ImageData.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/OsmoDoc/Word/Models/ImageData.cs b/OsmoDoc/Word/Models/ImageData.cs index 56fe2fc..24c3d55 100644 --- a/OsmoDoc/Word/Models/ImageData.cs +++ b/OsmoDoc/Word/Models/ImageData.cs @@ -1,10 +1,12 @@ using System.ComponentModel.DataAnnotations; +namespace OsmoDoc.Word.Models; + public enum ImageSourceType { - Base64, - LocalFile, - Url + Base64 = 0, + LocalFile = 1, + Url = 1 } public class ImageData @@ -18,6 +20,5 @@ public class ImageData [Required(ErrorMessage = "Image data is required")] public string Data { get; set; } = string.Empty; // Can be base64, file path, or URL - [Required(ErrorMessage = "Image extension is required")] public string? ImageExtension { get; set; } // Required for Base64 } \ No newline at end of file From f6650e1f9a54c752066ca6a43cc9b2e3007a5497 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 08:39:57 +0530 Subject: [PATCH 46/73] fix: the typo in output file path --- OsmoDoc.API/Controllers/PdfController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OsmoDoc.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs index 3d89b48..aff002e 100644 --- a/OsmoDoc.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -160,7 +160,7 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR string outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, tempPath, - inputPath, + outputPath, pdfPath, CommonMethodsHelper.GenerateRandomFileName("pdf") ); From b0ad8b8f92ebf3b1eb7908d4a599c957f7ac426b Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 08:44:14 +0530 Subject: [PATCH 47/73] feat: add null check for documentData parameter --- OsmoDoc/Word/WordDocumentGenerator.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 4ca4d9e..9e38982 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -37,6 +37,11 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc throw new ArgumentNullException(nameof(templateFilePath)); } + if (documentData == null) + { + throw new ArgumentNullException(nameof(documentData)); + } + if (string.IsNullOrWhiteSpace(outputFilePath)) { throw new ArgumentNullException(nameof(outputFilePath)); From ae617b43534fa01f7f2e4f0db37bcf0955346889 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 08:46:34 +0530 Subject: [PATCH 48/73] refactor: make the PdfDocumentGenerator class static --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index 4b8b3ed..99cb6fb 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -10,7 +10,7 @@ namespace OsmoDoc.Pdf; -public class PdfDocumentGenerator +public static class PdfDocumentGenerator { /// /// Generates a PDF document from an HTML or EJS template. From 411bc5f41ebdfac592b34cf906a9d7b06ecf8a6b Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 08:46:52 +0530 Subject: [PATCH 49/73] fix; the enum values in ImageSourceType --- OsmoDoc/Word/Models/ImageData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OsmoDoc/Word/Models/ImageData.cs b/OsmoDoc/Word/Models/ImageData.cs index 24c3d55..a28bde3 100644 --- a/OsmoDoc/Word/Models/ImageData.cs +++ b/OsmoDoc/Word/Models/ImageData.cs @@ -6,7 +6,7 @@ public enum ImageSourceType { Base64 = 0, LocalFile = 1, - Url = 1 + Url = 2 } public class ImageData From 95c914beb234447a07160df92d1067ee63e52667 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 08:58:47 +0530 Subject: [PATCH 50/73] feat: add null check for request parameter --- OsmoDoc.API/Controllers/PdfController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OsmoDoc.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs index aff002e..963ab5b 100644 --- a/OsmoDoc.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -132,6 +132,11 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR try { + if (request == null) + { + throw new BadHttpRequestException("Request body cannot be null"); + } + string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value From 628e09393e9da99294add063a8cd19a7bb5fad9a Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 09:12:53 +0530 Subject: [PATCH 51/73] feat: validate images data before mapping --- OsmoDoc.API/Controllers/WordController.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/OsmoDoc.API/Controllers/WordController.cs b/OsmoDoc.API/Controllers/WordController.cs index feaf46f..ed9939d 100644 --- a/OsmoDoc.API/Controllers/WordController.cs +++ b/OsmoDoc.API/Controllers/WordController.cs @@ -81,6 +81,12 @@ public async Task> GenerateWord(WordGenerationRequest CommonMethodsHelper.CreateDirectoryIfNotExists(outputFilePath); + // Validate images data + if (request.DocumentData.ImagesData.Any(img => string.IsNullOrEmpty(img.Data)) == true) + { + throw new BadHttpRequestException("Invalid image data: Image content is required"); + } + // Map document data in request to word library model class DocumentData documentData = new DocumentData { @@ -92,7 +98,7 @@ public async Task> GenerateWord(WordGenerationRequest // Generate and save output docx in output directory await WordDocumentGenerator.GenerateDocumentByTemplate( docxTemplateFilePath, - documentData, + documentData, outputFilePath ); From 298771198ef8dcc352515f8b1963f88f10882724 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 09:17:15 +0530 Subject: [PATCH 52/73] feat: add authorization to the revoke endpoint --- OsmoDoc.API/Controllers/LoginController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OsmoDoc.API/Controllers/LoginController.cs b/OsmoDoc.API/Controllers/LoginController.cs index a27d792..fc34b1c 100644 --- a/OsmoDoc.API/Controllers/LoginController.cs +++ b/OsmoDoc.API/Controllers/LoginController.cs @@ -47,6 +47,7 @@ public async Task> Login([FromBody] LoginRequestDTO l [HttpPost] [Route("revoke")] + [Authorize] public async Task> RevokeToken([FromBody] RevokeTokenRequestDTO request) { BaseResponse response = new BaseResponse(ResponseStatus.Fail); From fff1e6922caa38909bc241f1aeb0cdfc6bea7409 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 09:36:54 +0530 Subject: [PATCH 53/73] refactor: handle all JSON parsing exceptions --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index 99cb6fb..edd9f0b 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -240,7 +240,7 @@ private static bool IsValidJSON(string json) JToken.Parse(json); return true; } - catch (JsonReaderException) + catch (Exception) { return false; } From ce8a1d910570bc8e24de736bb72f194e031ff7fd Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 15:23:01 +0530 Subject: [PATCH 54/73] feat: enable Swagger in Production environment --- OsmoDoc.API/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OsmoDoc.API/Program.cs b/OsmoDoc.API/Program.cs index abb3550..9ccfba5 100644 --- a/OsmoDoc.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -155,7 +155,7 @@ WebApplication app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) +if (app.Environment.IsDevelopment() || app.Environment.IsProduction()) { app.UseSwagger(); app.UseSwaggerUI(); From b2c59c52db2b605553d3e054af2ad42d460f6bc0 Mon Sep 17 00:00:00 2001 From: Jatin Date: Fri, 4 Jul 2025 15:26:18 +0530 Subject: [PATCH 55/73] feat: update docker files and env values --- .env.example | 6 ++++++ .gitignore | 3 +++ Dockerfile | 48 ++++++++++++++++++++++++--------------------- docker-compose.yaml | 25 +++++++++++++++-------- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/.env.example b/.env.example index c18cc14..b061ea4 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,10 @@ JWT_KEY=PLACEHOLDER_REPLACE_WITH_STRONG_KEY_MIN_32_CHARS_BEFORE_USE REDIS_URL=redis:6379 + +REDIS_HOST=redis REDIS_PORT=6379 + +SERVER_PORT=5000 + +COMPOSE_PROJECT_NAME=osmodoc diff --git a/.gitignore b/.gitignore index 3b1832a..7d5e33a 100644 --- a/.gitignore +++ b/.gitignore @@ -352,3 +352,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Docker mount volumes +Temp/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e4f3a8d..0a54882 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,47 +2,51 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app -# Expose the desired port -ENV ASPNETCORE_ENVIRONMENT=Development -EXPOSE 5000 - -# Build configuration variable. Debug as default -ENV BUILD_CONFIGURATION=Debug +# Build configuration variable. Debug for development, Release for production +ENV BUILD_CONFIGURATION=Release +ENV ASPNETCORE_ENVIRONMENT=Production -# Copy data -COPY ["OsmoDoc.API/OsmoDoc.API.csproj", "OsmoDoc.API/"] -COPY ["OsmoDoc/OsmoDoc.csproj", "OsmoDoc/"] +# Copy the solution and project files +COPY *.sln ./ +COPY ./OsmoDoc.API/OsmoDoc.API.csproj OsmoDoc.API/ +COPY ./OsmoDoc/OsmoDoc.csproj OsmoDoc/ -# Restore the project dependencies -RUN dotnet restore "./OsmoDoc.API/./OsmoDoc.API.csproj" -RUN dotnet restore "./OsmoDoc/./OsmoDoc.csproj" +# Restore dependencies +RUN dotnet restore OsmoDoc.API/OsmoDoc.API.csproj -# Copy the rest of the data -COPY . . -WORKDIR "/app/OsmoDoc.API" +# Copy required project files +COPY ./OsmoDoc.API/ OsmoDoc.API/ +COPY ./OsmoDoc/ OsmoDoc/ +COPY .env . # Build the project and store artifacts in /out folder -RUN dotnet publish "./OsmoDoc.API.csproj" -c BUILD_CONFIGURATION -o /app/out +RUN dotnet publish OsmoDoc.API/OsmoDoc.API.csproj -c $BUILD_CONFIGURATION -o out # Use the official ASP.NET runtime image as the base image FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base + WORKDIR /app +ENV ASPNETCORE_URLS=http://+:5000 + # Copy the published artifacts from the build stage COPY --from=build /app/out . # Install only necessary dependencies for wkhtmltopdf, Node.js and npm -RUN apt-get update \ - && apt-get install -y --fix-missing wkhtmltopdf \ - && apt-get install -y --fix-missing nodejs \ - && apt-get install -y --no-install-recommends npm \ - && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends \ + wkhtmltopdf \ + nodejs \ + npm && \ + rm -rf /var/lib/apt/lists/* # Install wkhtmltopdf and allow execute access RUN chmod 755 /usr/bin/wkhtmltopdf -# Install ejs globally without unnecessary dependencies +# Install EJS globally RUN npm install -g --only=prod ejs +# Expose the API port +EXPOSE 5000 + # Set the entry point for the container ENTRYPOINT ["dotnet", "OsmoDoc.API.dll"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 6eb71ca..d83f6fa 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,23 +1,32 @@ services: redis: image: redis:7 - container_name: redis + container_name: ${COMPOSE_PROJECT_NAME}-redis env_file: - .env ports: - ${REDIS_PORT}:6379 + networks: + - osmodoc-net - osmodoc: + osmodoc-api: build: context: . dockerfile: Dockerfile - image: osmodoc-docker - container_name: osmodoc-api + container_name: ${COMPOSE_PROJECT_NAME}-api env_file: - .env ports: - - 5000:5000 + - ${SERVER_PORT}:5000 environment: - - ASPNETCORE_URLS=http://+:5000 - - ASPNETCORE_ENVIRONMENT=Development - - BUILD_CONFIGURATION=Release + - REDIS_URL=${REDIS_HOST}:${REDIS_PORT} + depends_on: + - redis + volumes: + - ./Temp:/app/wwwroot/Temp + networks: + - osmodoc-net + +networks: + osmodoc-net: + driver: bridge \ No newline at end of file From 01a0b7eadad070327cd3651bd0b2e12953626120 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sat, 5 Jul 2025 16:57:50 +0530 Subject: [PATCH 56/73] refactor: gracefully handle missing .env file --- OsmoDoc.API/Program.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/OsmoDoc.API/Program.cs b/OsmoDoc.API/Program.cs index 9ccfba5..b1bbdc7 100644 --- a/OsmoDoc.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -27,7 +27,14 @@ // Load .env file string root = Directory.GetCurrentDirectory(); string dotenv = Path.GetFullPath(Path.Combine(root, "..", ".env")); -OsmoDoc.API.DotEnv.Load(dotenv); +if (File.Exists(dotenv)) +{ + OsmoDoc.API.DotEnv.Load(dotenv); +} +else +{ + throw new FileNotFoundException($".env file not found at path: {dotenv}"); +} // Initialize PDF tool path once at startup OsmoDocPdfConfig.WkhtmltopdfPath = Path.Combine( From 1a5f74908868bf47661e78d2fb43792f578c218d Mon Sep 17 00:00:00 2001 From: Jatin Date: Sat, 5 Jul 2025 12:15:13 +0530 Subject: [PATCH 57/73] refactor: format the code --- OsmoDoc.API/Controllers/LoginController.cs | 140 +++++++++--------- OsmoDoc.API/Controllers/PdfController.cs | 4 +- OsmoDoc.API/Controllers/WordController.cs | 2 +- OsmoDoc.API/Helpers/AutoMappingProfile.cs | 2 +- OsmoDoc.API/Models/BaseResponse.cs | 2 +- OsmoDoc.API/Models/LoginRequestDTO.cs | 18 +-- OsmoDoc.API/Models/RevokeTokenRequestDTO.cs | 16 +- OsmoDoc/Pdf/Models/OsmoDocPdfConfig.cs | 14 +- .../Interfaces/IRedisTokenStoreService.cs | 20 +-- OsmoDoc/Services/RedisTokenStoreService.cs | 82 +++++----- OsmoDoc/Word/Models/ImageData.cs | 46 +++--- OsmoDoc/Word/WordDocumentGenerator.cs | 12 +- 12 files changed, 179 insertions(+), 179 deletions(-) diff --git a/OsmoDoc.API/Controllers/LoginController.cs b/OsmoDoc.API/Controllers/LoginController.cs index fc34b1c..6d6a4f8 100644 --- a/OsmoDoc.API/Controllers/LoginController.cs +++ b/OsmoDoc.API/Controllers/LoginController.cs @@ -1,71 +1,71 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using OsmoDoc.API.Models; -using OsmoDoc.API.Helpers; -using OsmoDoc.Services; - -namespace OsmoDoc.API.Controllers; - -[Route("api")] -[ApiController] -public class LoginController : ControllerBase -{ - private readonly IRedisTokenStoreService _tokenStoreService; - private readonly ILogger _logger; - - public LoginController(IRedisTokenStoreService tokenStoreService, ILogger logger) - { - this._tokenStoreService = tokenStoreService; - this._logger = logger; - } - - [HttpPost] - [Route("login")] - [AllowAnonymous] - public async Task> Login([FromBody] LoginRequestDTO loginRequest) - { - BaseResponse response = new BaseResponse(ResponseStatus.Fail); - try - { - string token = AuthenticationHelper.JwtTokenGenerator(loginRequest.Email); - await this._tokenStoreService.StoreTokenAsync(token, loginRequest.Email, this.HttpContext.RequestAborted); - - response.Status = ResponseStatus.Success; - response.AuthToken = token; - response.Message = "Token generated successfully"; - return this.Ok(response); - } - catch (Exception ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - } - - [HttpPost] - [Route("revoke")] - [Authorize] - public async Task> RevokeToken([FromBody] RevokeTokenRequestDTO request) - { - BaseResponse response = new BaseResponse(ResponseStatus.Fail); - try - { - await this._tokenStoreService.RevokeTokenAsync(request.Token, this.HttpContext.RequestAborted); - - response.Status = ResponseStatus.Success; - response.Message = "Token revoked"; - return this.Ok(response); - } - catch (Exception ex) - { - response.Status = ResponseStatus.Error; - response.Message = ex.Message; - this._logger.LogError(ex.Message); - this._logger.LogError(ex.StackTrace); - return this.StatusCode(StatusCodes.Status500InternalServerError, response); - } - } +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using OsmoDoc.API.Models; +using OsmoDoc.API.Helpers; +using OsmoDoc.Services; + +namespace OsmoDoc.API.Controllers; + +[Route("api")] +[ApiController] +public class LoginController : ControllerBase +{ + private readonly IRedisTokenStoreService _tokenStoreService; + private readonly ILogger _logger; + + public LoginController(IRedisTokenStoreService tokenStoreService, ILogger logger) + { + this._tokenStoreService = tokenStoreService; + this._logger = logger; + } + + [HttpPost] + [Route("login")] + [AllowAnonymous] + public async Task> Login([FromBody] LoginRequestDTO loginRequest) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + try + { + string token = AuthenticationHelper.JwtTokenGenerator(loginRequest.Email); + await this._tokenStoreService.StoreTokenAsync(token, loginRequest.Email, this.HttpContext.RequestAborted); + + response.Status = ResponseStatus.Success; + response.AuthToken = token; + response.Message = "Token generated successfully"; + return this.Ok(response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } + + [HttpPost] + [Route("revoke")] + [Authorize] + public async Task> RevokeToken([FromBody] RevokeTokenRequestDTO request) + { + BaseResponse response = new BaseResponse(ResponseStatus.Fail); + try + { + await this._tokenStoreService.RevokeTokenAsync(request.Token, this.HttpContext.RequestAborted); + + response.Status = ResponseStatus.Success; + response.Message = "Token revoked"; + return this.Ok(response); + } + catch (Exception ex) + { + response.Status = ResponseStatus.Error; + response.Message = ex.Message; + this._logger.LogError(ex.Message); + this._logger.LogError(ex.StackTrace); + return this.StatusCode(StatusCodes.Status500InternalServerError, response); + } + } } \ No newline at end of file diff --git a/OsmoDoc.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs index 963ab5b..d1bc714 100644 --- a/OsmoDoc.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -34,7 +34,7 @@ public async Task> GeneratePdf(PdfGenerationRequestDT { throw new BadHttpRequestException("Request body cannot be null"); } - + string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value @@ -136,7 +136,7 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR { throw new BadHttpRequestException("Request body cannot be null"); } - + string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value diff --git a/OsmoDoc.API/Controllers/WordController.cs b/OsmoDoc.API/Controllers/WordController.cs index ed9939d..d759d19 100644 --- a/OsmoDoc.API/Controllers/WordController.cs +++ b/OsmoDoc.API/Controllers/WordController.cs @@ -98,7 +98,7 @@ public async Task> GenerateWord(WordGenerationRequest // Generate and save output docx in output directory await WordDocumentGenerator.GenerateDocumentByTemplate( docxTemplateFilePath, - documentData, + documentData, outputFilePath ); diff --git a/OsmoDoc.API/Helpers/AutoMappingProfile.cs b/OsmoDoc.API/Helpers/AutoMappingProfile.cs index 9730c3e..8869305 100644 --- a/OsmoDoc.API/Helpers/AutoMappingProfile.cs +++ b/OsmoDoc.API/Helpers/AutoMappingProfile.cs @@ -7,6 +7,6 @@ namespace OsmoDoc.API.Helpers; public class AutoMappingProfile : Profile { public AutoMappingProfile() - { + { } } diff --git a/OsmoDoc.API/Models/BaseResponse.cs b/OsmoDoc.API/Models/BaseResponse.cs index 5bf6cbf..fb72663 100644 --- a/OsmoDoc.API/Models/BaseResponse.cs +++ b/OsmoDoc.API/Models/BaseResponse.cs @@ -32,7 +32,7 @@ public static BadRequestObjectResult ModelValidationErrorResponse(ActionContext { Message = firstError ?? "Validation failed" }; - + return new BadRequestObjectResult(response); } } diff --git a/OsmoDoc.API/Models/LoginRequestDTO.cs b/OsmoDoc.API/Models/LoginRequestDTO.cs index bf7f680..0afbba8 100644 --- a/OsmoDoc.API/Models/LoginRequestDTO.cs +++ b/OsmoDoc.API/Models/LoginRequestDTO.cs @@ -1,10 +1,10 @@ -using System.ComponentModel.DataAnnotations; - -namespace OsmoDoc.API.Models; - -public class LoginRequestDTO -{ - [Required(ErrorMessage = "Email is required")] - [EmailAddress(ErrorMessage = "Invalid email format")] - public string Email { get; set; } = string.Empty; +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.API.Models; + +public class LoginRequestDTO +{ + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Invalid email format")] + public string Email { get; set; } = string.Empty; } \ No newline at end of file diff --git a/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs b/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs index 26d5c21..d01a328 100644 --- a/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs +++ b/OsmoDoc.API/Models/RevokeTokenRequestDTO.cs @@ -1,9 +1,9 @@ -using System.ComponentModel.DataAnnotations; - -namespace OsmoDoc.API.Models; - -public class RevokeTokenRequestDTO -{ - [Required(ErrorMessage = "Token is required")] - public string Token { get; set; } = string.Empty; +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.API.Models; + +public class RevokeTokenRequestDTO +{ + [Required(ErrorMessage = "Token is required")] + public string Token { get; set; } = string.Empty; } \ No newline at end of file diff --git a/OsmoDoc/Pdf/Models/OsmoDocPdfConfig.cs b/OsmoDoc/Pdf/Models/OsmoDocPdfConfig.cs index 92ae174..e401677 100644 --- a/OsmoDoc/Pdf/Models/OsmoDocPdfConfig.cs +++ b/OsmoDoc/Pdf/Models/OsmoDocPdfConfig.cs @@ -1,7 +1,7 @@ -namespace OsmoDoc.Pdf; - -public static class OsmoDocPdfConfig -{ - /// Required path to wkhtmltopdf binary, e.g. "wkhtmltopdf" on Linux - public static string? WkhtmltopdfPath { get; set; } -} +namespace OsmoDoc.Pdf; + +public static class OsmoDocPdfConfig +{ + /// Required path to wkhtmltopdf binary, e.g. "wkhtmltopdf" on Linux + public static string? WkhtmltopdfPath { get; set; } +} diff --git a/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs b/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs index ddbfde5..28eb8f0 100644 --- a/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs +++ b/OsmoDoc/Services/Interfaces/IRedisTokenStoreService.cs @@ -1,11 +1,11 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace OsmoDoc.Services; - -public interface IRedisTokenStoreService -{ - Task StoreTokenAsync(string token, string email, CancellationToken cancellationToken = default); - Task IsTokenValidAsync(string token, CancellationToken cancellationToken = default); - Task RevokeTokenAsync(string token, CancellationToken cancellationToken = default); +using System.Threading; +using System.Threading.Tasks; + +namespace OsmoDoc.Services; + +public interface IRedisTokenStoreService +{ + Task StoreTokenAsync(string token, string email, CancellationToken cancellationToken = default); + Task IsTokenValidAsync(string token, CancellationToken cancellationToken = default); + Task RevokeTokenAsync(string token, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/OsmoDoc/Services/RedisTokenStoreService.cs b/OsmoDoc/Services/RedisTokenStoreService.cs index f54a688..3c19ad5 100644 --- a/OsmoDoc/Services/RedisTokenStoreService.cs +++ b/OsmoDoc/Services/RedisTokenStoreService.cs @@ -1,42 +1,42 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using StackExchange.Redis; - -namespace OsmoDoc.Services; - -public class RedisTokenStoreService : IRedisTokenStoreService -{ - private readonly IDatabase _db; - private const string KeyPrefix = "valid_token:"; - - public RedisTokenStoreService(IConnectionMultiplexer redis) - { - this._db = redis.GetDatabase(); - } - - public Task StoreTokenAsync(string token, string email, CancellationToken cancellationToken = default) - { - // Check if operation was cancelled before starting - cancellationToken.ThrowIfCancellationRequested(); - - return this._db.StringSetAsync($"{KeyPrefix}{token}", JsonConvert.SerializeObject(new - { - issuedTo = email, - issuedAt = DateTime.UtcNow - })); - } - - public Task IsTokenValidAsync(string token, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - return this._db.KeyExistsAsync($"{KeyPrefix}{token}"); - } - - public Task RevokeTokenAsync(string token, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - return this._db.KeyDeleteAsync($"{KeyPrefix}{token}"); - } +using System; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using StackExchange.Redis; + +namespace OsmoDoc.Services; + +public class RedisTokenStoreService : IRedisTokenStoreService +{ + private readonly IDatabase _db; + private const string KeyPrefix = "valid_token:"; + + public RedisTokenStoreService(IConnectionMultiplexer redis) + { + this._db = redis.GetDatabase(); + } + + public Task StoreTokenAsync(string token, string email, CancellationToken cancellationToken = default) + { + // Check if operation was cancelled before starting + cancellationToken.ThrowIfCancellationRequested(); + + return this._db.StringSetAsync($"{KeyPrefix}{token}", JsonConvert.SerializeObject(new + { + issuedTo = email, + issuedAt = DateTime.UtcNow + })); + } + + public Task IsTokenValidAsync(string token, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return this._db.KeyExistsAsync($"{KeyPrefix}{token}"); + } + + public Task RevokeTokenAsync(string token, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return this._db.KeyDeleteAsync($"{KeyPrefix}{token}"); + } } \ No newline at end of file diff --git a/OsmoDoc/Word/Models/ImageData.cs b/OsmoDoc/Word/Models/ImageData.cs index a28bde3..c3428c0 100644 --- a/OsmoDoc/Word/Models/ImageData.cs +++ b/OsmoDoc/Word/Models/ImageData.cs @@ -1,24 +1,24 @@ -using System.ComponentModel.DataAnnotations; - -namespace OsmoDoc.Word.Models; - -public enum ImageSourceType -{ - Base64 = 0, - LocalFile = 1, - Url = 2 -} - -public class ImageData -{ - [Required(ErrorMessage = "Placeholder name is required")] - public string PlaceholderName { get; set; } = string.Empty; - - [Required(ErrorMessage = "Image source type is required")] - public ImageSourceType SourceType { get; set; } - - [Required(ErrorMessage = "Image data is required")] - public string Data { get; set; } = string.Empty; // Can be base64, file path, or URL - - public string? ImageExtension { get; set; } // Required for Base64 +using System.ComponentModel.DataAnnotations; + +namespace OsmoDoc.Word.Models; + +public enum ImageSourceType +{ + Base64 = 0, + LocalFile = 1, + Url = 2 +} + +public class ImageData +{ + [Required(ErrorMessage = "Placeholder name is required")] + public string PlaceholderName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Image source type is required")] + public ImageSourceType SourceType { get; set; } + + [Required(ErrorMessage = "Image data is required")] + public string Data { get; set; } = string.Empty; // Can be base64, file path, or URL + + public string? ImageExtension { get; set; } // Required for Base64 } \ No newline at end of file diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 9e38982..7dd6791 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -266,7 +266,7 @@ private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) /// /// The ile path where the updated document will be saved. /// The data structure for holding the images details. - + private static async Task ProcessImagePlaceholders( string documentPath, List images) @@ -358,7 +358,7 @@ private static async Task ProcessImagePlaceholders( private static async Task PrepareImageFile(ImageData imageData) { string tempFilePath = System.IO.Path.GetTempFileName(); - + if (!string.IsNullOrEmpty(imageData.ImageExtension)) { tempFilePath = System.IO.Path.ChangeExtension(tempFilePath, imageData.ImageExtension); @@ -368,10 +368,10 @@ private static async Task PrepareImageFile(ImageData imageData) { case ImageSourceType.Base64: await File.WriteAllBytesAsync( - tempFilePath, + tempFilePath, Convert.FromBase64String(imageData.Data)); break; - + case ImageSourceType.LocalFile: if (!File.Exists(imageData.Data)) { @@ -380,7 +380,7 @@ await File.WriteAllBytesAsync( File.Copy(imageData.Data, tempFilePath, true); break; - + case ImageSourceType.Url: using (HttpClient httpClient = new HttpClient()) { @@ -388,7 +388,7 @@ await File.WriteAllBytesAsync( await File.WriteAllBytesAsync(tempFilePath, bytes); } break; - + default: throw new ArgumentOutOfRangeException(); } From ea8787d0a087ab319804679a53fbdc5b937070db Mon Sep 17 00:00:00 2001 From: Jatin Date: Sat, 5 Jul 2025 12:15:54 +0530 Subject: [PATCH 58/73] feat: add function for resource cleanup in PdfDocumentGenerator --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 87 ++++++++++++++++++----------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index edd9f0b..1800cd1 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -47,34 +47,38 @@ public async static Task GeneratePdf(string templatePath, List throw new Exception("The file path you provided is not valid."); } - if (isEjsTemplate) + string? ejsConvertedHtmlPath = null; + string? tempModifiedHtmlDirectory = null; + + try { - // Validate if template in file path is an ejs file - if (Path.GetExtension(templatePath).ToLower() != ".ejs") + if (isEjsTemplate) { - throw new Exception("Input template should be a valid EJS file"); + // Validate if template in file path is an ejs file + if (Path.GetExtension(templatePath).ToLower() != ".ejs") + { + throw new Exception("Input template should be a valid EJS file"); + } + + // Convert ejs file to an equivalent html + ejsConvertedHtmlPath = await ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); + templatePath = ejsConvertedHtmlPath; } - // Convert ejs file to an equivalent html - templatePath = await ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); - } - - // Modify html template with content data and generate pdf - string modifiedHtmlFilePath = ReplaceFileElementsWithMetaData(templatePath, metaDataList, outputFilePath); - await ConvertHtmlToPdf(OsmoDocPdfConfig.WkhtmltopdfPath, modifiedHtmlFilePath, outputFilePath); + // Modify html template with content data and generate pdf + (string modifiedHtmlFilePath, string tempDirectory) = ReplaceFileElementsWithMetaData(templatePath, metaDataList, outputFilePath); + tempModifiedHtmlDirectory = tempDirectory; - if (isEjsTemplate) + await ConvertHtmlToPdf(OsmoDocPdfConfig.WkhtmltopdfPath, modifiedHtmlFilePath, outputFilePath); + } + finally { - // If input template was an ejs file, then the template path contains path to html converted from ejs - if (File.Exists(templatePath) && Path.GetExtension(templatePath).ToLower() == ".html") - { - // If template path contains path to converted html template then delete it - File.Delete(templatePath); - } + // Cleanup temporary directories and files + CleanupTemporaryResources(ejsConvertedHtmlPath, tempModifiedHtmlDirectory); } } - private static string ReplaceFileElementsWithMetaData(string templatePath, List metaDataList, string outputFilePath) + private static (string modifiedHtmlFilePath, string tempDirectory) ReplaceFileElementsWithMetaData(string templatePath, List metaDataList, string outputFilePath) { string htmlContent = File.ReadAllText(templatePath); @@ -98,7 +102,7 @@ private static string ReplaceFileElementsWithMetaData(string templatePath, List< } File.WriteAllText(tempHtmlFile, htmlContent); - return tempHtmlFile; + return (tempHtmlFile, tempHtmlFilePath); } private async static Task ConvertHtmlToPdf(string? wkhtmltopdfPath, string modifiedHtmlFilePath, string outputFilePath) @@ -155,12 +159,6 @@ private async static Task ConvertHtmlToPdf(string? wkhtmltopdfPath, string modif throw new Exception($"Error during PDF generation: {errors} (Exit Code: {process.ExitCode})"); } } - - // Delete the temporary modified HTML file - if (File.Exists(modifiedHtmlFilePath)) - { - File.Delete(modifiedHtmlFilePath); - } } private async static Task ConvertEjsToHTML(string ejsFilePath, string outputFilePath, string? ejsDataJson) @@ -224,12 +222,6 @@ private async static Task ConvertEjsToHTML(string ejsFilePath, string ou } } - // Delete json data file - if (File.Exists(ejsDataJsonFilePath)) - { - File.Delete(ejsDataJsonFilePath); - } - return tempHtmlFilePath; } @@ -261,4 +253,35 @@ private static string EjsToHtmlArgumentsBasedOnOS(string ejsFilePath, string ejs throw new Exception("Unknown operating system"); } } + + private static void CleanupTemporaryResources(string? ejsConvertedHtmlPath, string? tempModifiedHtmlDirectory) + { + // Clean up EJS converted HTML file + if (!string.IsNullOrEmpty(ejsConvertedHtmlPath) && File.Exists(ejsConvertedHtmlPath)) + { + try + { + File.Delete(ejsConvertedHtmlPath); + } + catch (Exception ex) + { + // Log the exception but don't throw to avoid masking original exceptions + Console.WriteLine($"Warning: Could not delete EJS converted HTML file {ejsConvertedHtmlPath}: {ex.Message}"); + } + } + + // Clean up temporary modified HTML directory and its contents + if (!string.IsNullOrEmpty(tempModifiedHtmlDirectory) && Directory.Exists(tempModifiedHtmlDirectory)) + { + try + { + Directory.Delete(tempModifiedHtmlDirectory, recursive: true); + } + catch (Exception ex) + { + // Log the exception but don't throw to avoid masking original exceptions + Console.WriteLine($"Warning: Could not delete temporary directory {tempModifiedHtmlDirectory}: {ex.Message}"); + } + } + } } From 723f4c115096f15998b68df96cbaba59966a956b Mon Sep 17 00:00:00 2001 From: Jatin Date: Sat, 5 Jul 2025 17:30:43 +0530 Subject: [PATCH 59/73] refactor: avoid leaking local absolute paths in build artefacts --- docs/site/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/site/manifest.json b/docs/site/manifest.json index 456a586..54626c9 100644 --- a/docs/site/manifest.json +++ b/docs/site/manifest.json @@ -1,6 +1,6 @@ { "homepages": [], - "source_base_path": "C:/Users/user/Desktop/osmodoc", + "source_base_path": "./", "xrefmap": "xrefmap.yml", "files": [ { From 0f25da9b1ccc59adb859bd82b5d419688ba3a476 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sat, 5 Jul 2025 20:36:03 +0530 Subject: [PATCH 60/73] feat: add conditional validation for Base64 images --- OsmoDoc/Word/Models/ImageData.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/OsmoDoc/Word/Models/ImageData.cs b/OsmoDoc/Word/Models/ImageData.cs index c3428c0..636a8e4 100644 --- a/OsmoDoc/Word/Models/ImageData.cs +++ b/OsmoDoc/Word/Models/ImageData.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace OsmoDoc.Word.Models; @@ -9,7 +10,7 @@ public enum ImageSourceType Url = 2 } -public class ImageData +public class ImageData : IValidatableObject { [Required(ErrorMessage = "Placeholder name is required")] public string PlaceholderName { get; set; } = string.Empty; @@ -21,4 +22,18 @@ public class ImageData public string Data { get; set; } = string.Empty; // Can be base64, file path, or URL public string? ImageExtension { get; set; } // Required for Base64 + + public IEnumerable Validate(ValidationContext validationContext) + { + if (this.SourceType == ImageSourceType.Base64) + { + if (string.IsNullOrWhiteSpace(this.ImageExtension)) + { + yield return new ValidationResult( + "Image extension is required for Base64 source type", + new[] { nameof(this.ImageExtension) } + ); + } + } + } } \ No newline at end of file From 7f21e5297c368497d4f55cd838eeaec64f38246c Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 6 Jul 2025 00:14:29 +0530 Subject: [PATCH 61/73] feat: handle cleanup of files generated by apis in PdfController --- OsmoDoc.API/Controllers/PdfController.cs | 62 ++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/OsmoDoc.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs index d1bc714..45e494b 100644 --- a/OsmoDoc.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -27,6 +27,8 @@ public PdfController(IConfiguration configuration, IWebHostEnvironment hostingEn public async Task> GeneratePdf(PdfGenerationRequestDTO request) { BaseResponse response = new BaseResponse(ResponseStatus.Fail); + string? htmlTemplateFilePath = null; + string? outputFilePath = null; try { @@ -48,7 +50,7 @@ public async Task> GeneratePdf(PdfGenerationRequestDT // Generate filepath to save base64 html template - string htmlTemplateFilePath = Path.Combine( + htmlTemplateFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, tempPath, inputPath, @@ -61,7 +63,7 @@ public async Task> GeneratePdf(PdfGenerationRequestDT // Save base64 html template to inputs directory await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, htmlTemplateFilePath, this._configuration); - string outputFilePath = Path.Combine( + outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, tempPath, outputPath, @@ -121,6 +123,31 @@ await PdfDocumentGenerator.GeneratePdf( this._logger.LogError(ex.StackTrace); return this.StatusCode(StatusCodes.Status500InternalServerError, response); } + finally + { + if (htmlTemplateFilePath != null && System.IO.File.Exists(htmlTemplateFilePath)) + { + try + { + System.IO.File.Delete(htmlTemplateFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {htmlTemplateFilePath}: {ex.Message}"); + } + } + if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) + { + try + { + System.IO.File.Delete(outputFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + } + } + } } [HttpPost] @@ -129,6 +156,8 @@ await PdfDocumentGenerator.GeneratePdf( public async Task> GeneratePdfUsingEjs(PdfGenerationRequestDTO request) { BaseResponse response = new BaseResponse(ResponseStatus.Fail); + string? ejsTemplateFilePath = null; + string? outputFilePath = null; try { @@ -149,7 +178,7 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:PDF is missing."); // Generate filepath to save base64 html template - string ejsTemplateFilePath = Path.Combine( + ejsTemplateFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, tempPath, inputPath, @@ -162,7 +191,7 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR // Save base64 html template to inputs directory await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, ejsTemplateFilePath, this._configuration); - string outputFilePath = Path.Combine( + outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, tempPath, outputPath, @@ -222,5 +251,30 @@ await PdfDocumentGenerator.GeneratePdf( this._logger.LogError(ex.StackTrace); return this.StatusCode(StatusCodes.Status500InternalServerError, response); } + finally + { + if (ejsTemplateFilePath != null && System.IO.File.Exists(ejsTemplateFilePath)) + { + try + { + System.IO.File.Delete(ejsTemplateFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {ejsTemplateFilePath}: {ex.Message}"); + } + } + if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) + { + try + { + System.IO.File.Delete(outputFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + } + } + } } } From d4c09b31de337cbed79f9a41a3bc08d66c9ba921 Mon Sep 17 00:00:00 2001 From: Jatin Date: Sun, 6 Jul 2025 00:18:33 +0530 Subject: [PATCH 62/73] feat: handle cleanup of files generated by apis in WordController --- OsmoDoc.API/Controllers/WordController.cs | 31 +++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/OsmoDoc.API/Controllers/WordController.cs b/OsmoDoc.API/Controllers/WordController.cs index d759d19..28e04e8 100644 --- a/OsmoDoc.API/Controllers/WordController.cs +++ b/OsmoDoc.API/Controllers/WordController.cs @@ -31,6 +31,8 @@ public WordController(IConfiguration configuration, IWebHostEnvironment hostingE public async Task> GenerateWord(WordGenerationRequestDTO request) { BaseResponse response = new BaseResponse(ResponseStatus.Fail); + string? docxTemplateFilePath = null; + string? outputFilePath = null; try { @@ -57,7 +59,7 @@ public async Task> GenerateWord(WordGenerationRequest // Generate filepath to save base64 docx template - string docxTemplateFilePath = Path.Combine( + docxTemplateFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, tempPath, inputPath, @@ -71,7 +73,7 @@ public async Task> GenerateWord(WordGenerationRequest await Base64StringHelper.SaveBase64StringToFilePath(request.Base64, docxTemplateFilePath, this._configuration); // Initialize output filepath - string outputFilePath = Path.Combine( + outputFilePath = Path.Combine( this._hostingEnvironment.WebRootPath, tempPath, outputPath, @@ -143,5 +145,30 @@ await WordDocumentGenerator.GenerateDocumentByTemplate( this._logger.LogError(ex.StackTrace); return this.StatusCode(StatusCodes.Status500InternalServerError, response); } + finally + { + if (docxTemplateFilePath != null && System.IO.File.Exists(docxTemplateFilePath)) + { + try + { + System.IO.File.Delete(docxTemplateFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {docxTemplateFilePath}: {ex.Message}"); + } + } + if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) + { + try + { + System.IO.File.Delete(outputFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + } + } + } } } From c524efdf7132751ad64cd8e076c90d330fd0ede9 Mon Sep 17 00:00:00 2001 From: Jatin Date: Mon, 7 Jul 2025 10:47:02 +0530 Subject: [PATCH 63/73] chore: update README file --- README.md | 183 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 131 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 1ada5b7..38ef022 100644 --- a/README.md +++ b/README.md @@ -30,22 +30,22 @@ Setting up the app in a Docker-based environment enables developers of non-Windo 6. Copy data from [example template](.env.example) into `.env`. Then set suitable JWT key. 7. Set `environment` variables `ASPNETCORE_ENVIRONMENT` and `BUILD_CONFIGURATION` as per requirement in [docker-compose.yaml](./docker-compose.yaml). Ensure correct formatting: -#### Development (Default Configuration) +#### Development ```yaml - - ASPNETCORE_ENVIRONMENT=Development - BUILD_CONFIGURATION=Debug + - ASPNETCORE_ENVIRONMENT=Development ``` #### Testing/Staging ```yaml - - ASPNETCORE_ENVIRONMENT=Development - BUILD_CONFIGURATION=Release + - ASPNETCORE_ENVIRONMENT=Development ``` #### Production ```yaml - - ASPNETCORE_ENVIRONMENT=Production - BUILD_CONFIGURATION=Release + - ASPNETCORE_ENVIRONMENT=Production ``` 8. Ensure Docker is running. @@ -108,7 +108,12 @@ Note: We use a Temp folder to temporarily hold the modified HTML file before con # Basic usage ## PDF generation + +#### HTML TO PDF ```csharp +string htmlTemplateFilePath = @"C:\Path\To\Template.html"; +string outputFilePath = @"C:\Path\To\GeneratedOutput.pdf"; + List contentList = new List { new ContentMetaData { Placeholder = "Incident UID", Content = "I-20230822-001" }, @@ -116,70 +121,143 @@ List contentList = new List new ContentMetaData { Placeholder = "Site", Content = "Headquarters" } }; -// Tools\\index.html - The path of the html template file in which changes are to be made. -// Tools\\OutputFile.pdf - The path of the final pdf output file. -PdfDocumentGenerator.GeneratePdfByTemplate("Tools\\index.html", contentList, "Tools\\OutputFile.pdf"); +await PdfDocumentGenerator.GeneratePdf(htmlTemplateFilePath, contentList, outputFilePath, isEjsTemplate: false, serializedEjsDataJson: null); +``` + +#### EJS TO PDF +```csharp +string htmlTemplateFilePath = @"C:\Path\To\Template.ejs"; +string outputFilePath = @"C:\Path\To\GeneratedOutput.pdf"; +string serializedEjsDataJson = "{\"title\": \"EJS Test\", \"user\": {\"name\": \"Jane\"}}" + +List contentList = new List{}; + +await PdfDocumentGenerator.GeneratePdf(htmlTemplateFilePath, contentList, outputFilePath, isEjsTemplate: true, serializedEjsDataJson: serializedEjsDataJson); ``` ## Word document generation ```csharp -string templateFilePath = @"C:\Users\Admin\Desktop\Osmosys\Work\Projects\OsmoDoc Component\Testing\Document.docx"; -string outputFilePath = @"C:\Users\Admin\Desktop\Osmosys\Work\Projects\OsmoDoc Service Component\Testing\Test_Output.docx"; +string templateFilePath = @"C:\Path\To\Template.docx"; +string outputFilePath = @"C:\Path\To\GeneratedOutput.docx"; + +// Text placeholders (optional) +List placeholders = new List() +{ + new ContentData + { + Placeholder = "InvoiceNo", + Content = "INV-20250618", + ContentType = ContentType.Text, + ParentBody = ParentBody.None + }, + new ContentData + { + Placeholder = "InvoiceDate", + Content = "18 June 2025", + ContentType = ContentType.Text, + ParentBody = ParentBody.None + }, + new ContentData + { + Placeholder = "TableCellNote", + Content = "Thanks for using OsmoDoc", + ContentType = ContentType.Text, + ParentBody = ParentBody.Table + }, + new ContentData + { + Placeholder = "CustomerName", + Content = "John Doe", + ContentType = ContentType.Text, + ParentBody = ParentBody.None + } +}; +// Table data example List tablesData = new List() +{ + new TableData() { - new TableData() + TablePos = 1, + Data = new List>() + { + new Dictionary() + { + { "Item", "Laptop" }, + { "Oty", "2" }, + { "Price", "60000" } + }, + new Dictionary() { - TablePos = 5, - Data = new List>() - { - new Dictionary() - { - { "Item Name", "1st med" }, - { "Dosage", "1" }, - { "Quantity", "1" }, - { "Precautions", "Take care" } - }, - new Dictionary() - { - { "Item Name", "2nd med" }, - { "Dosage", "1" }, - { "Quantity", "1" }, - } - } + { "Item", "Mouse" }, + { "Oty", "5" }, + { "Price", "500" } } - }; - - List contents = new List() + } + }, + new TableData() { - new ContentData + TablePos = 2, + Data = new List>() { - Placeholder = "Picture 1", - Content = @"../testImage1.jpg", - ContentType = ContentType.Image, - ParentBody = ParentBody.None - }, - new ContentData - { - Placeholder = "Picture 2", - Content = @"../testImage2.jpg", - ContentType = ContentType.Image, - ParentBody = ParentBody.None - }, - }; - - DocumentData documentData = new DocumentData() + new Dictionary() + { + { "TaxType", "CGST" }, + { "Amount", "900" } + }, + new Dictionary() + { + { "TaxType", "SGST" }, + { "Amount", "600" } + } + } + } +}; + + +// Image data for different source types +List images = new List() +{ + // Local file + new ImageData + { + PlaceholderName = "Picture 1", // Alt text of image placeholder in Word + SourceType = ImageSourceType.LocalFile, + Data = @"C:\Images\logo.png" + }, + + // URL + new ImageData + { + PlaceholderName = "Picture 2", + SourceType = ImageSourceType.Url, + Data = "https://example.com/image.jpg" + }, + + // Base64 (ImageExtension is required when SourceType is Base64) + new ImageData { - Placeholders = contents, - TablesData = tablesData - }; + PlaceholderName = "Picture 3", + SourceType = ImageSourceType.Base64, + Data = "", + ImageExtension = ".jpg" + } +}; - WordDocumentGenerator.GenerateDocumentByTemplate(templateFilePath, documentData, outputFilePath); +// Combine all document parts +DocumentData documentData = new DocumentData +{ + Placeholders = placeholders, + TablesData = tablesData, + Images = images +}; + +// Generate final Word document +await WordDocumentGenerator.GenerateDocumentByTemplate(templateFilePath, documentData, outputFilePath); ``` # Targeted frameworks -1. .NET Framework 4.5.2 -2. .NET Standard 2.0 - Can be installed as a dependency in applications running on .NET Framework 4.8 and modern .NET (Core, v5, v6 and later). +1. .NET Framework 8.0 # Citations - [NPOI](https://github.com/nissl-lab/npoi) @@ -188,6 +266,7 @@ List tablesData = new List() # License The OsmoDoc is licensed under the [MIT](https://github.com/OsmosysSoftware/osmodoc/blob/main/LICENSE) license. + ## 👏 Big Thanks to Our Contributors From 00f803c03f04b66c5f72dda4c83ad9c0db80e29b Mon Sep 17 00:00:00 2001 From: Jatin Date: Mon, 7 Jul 2025 18:36:26 +0530 Subject: [PATCH 64/73] refactor: use complete OpenXML Implementation for WordDocumentGenerator --- OsmoDoc/Word/WordDocumentGenerator.cs | 450 ++++++++++++++------------ 1 file changed, 244 insertions(+), 206 deletions(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 7dd6791..ecaa6a6 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -3,17 +3,22 @@ using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using OsmoDoc.Word.Models; -using NPOI.XWPF.UserModel; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; -using IOPath = System.IO.Path; using System.Linq; -using System.Net; +using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; -using System.Net.Http; -using System.Diagnostics; +using IOPath = System.IO.Path; +using Paragraph = DocumentFormat.OpenXml.Wordprocessing.Paragraph; +using Run = DocumentFormat.OpenXml.Wordprocessing.Run; +using Table = DocumentFormat.OpenXml.Wordprocessing.Table; +using TableCell = DocumentFormat.OpenXml.Wordprocessing.TableCell; +using TableCellProperties = DocumentFormat.OpenXml.Wordprocessing.TableCellProperties; +using TableRow = DocumentFormat.OpenXml.Wordprocessing.TableRow; +using Text = DocumentFormat.OpenXml.Wordprocessing.Text; namespace OsmoDoc.Word; @@ -22,7 +27,7 @@ namespace OsmoDoc.Word; /// public static class WordDocumentGenerator { - private const string PlaceholderPattern = @"{[a-zA-Z]+}"; + private const string PlaceholderPattern = @"{[a-zA-Z][a-zA-Z0-9_-]*}"; /// /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. @@ -47,229 +52,265 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc throw new ArgumentNullException(nameof(outputFilePath)); } - List contentData = documentData.Placeholders; - List tablesData = documentData.TablesData; - - // Creating dictionaries for each type of placeholders - Dictionary textPlaceholders = new Dictionary(); - Dictionary tableContentPlaceholders = new Dictionary(); - Dictionary imagePlaceholders = new Dictionary(); + // Copy template to output location + File.Copy(templateFilePath, outputFilePath, true); - foreach (ContentData content in contentData) + using (WordprocessingDocument document = WordprocessingDocument.Open(outputFilePath, true)) { - if (content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) + if (document.MainDocumentPart == null) { - string placeholder = "{" + content.Placeholder + "}"; - textPlaceholders.TryAdd(placeholder, content.Content); + throw new InvalidOperationException("Document does not contain a main document part."); } - else if (content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) - { - string placeholder = "{" + content.Placeholder + "}"; - tableContentPlaceholders.TryAdd(placeholder, content.Content); - } - } - // Create document of the template - XWPFDocument document = await GetXWPFDocument(templateFilePath); + // Create dictionaries for each type of placeholders + Dictionary textPlaceholders = documentData.Placeholders + .Where(content => content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) + .ToDictionary(content => "{" + content.Placeholder + "}", content => content.Content); - // For each element in the document - foreach (IBodyElement element in document.BodyElements) - { - if (element.ElementType == BodyElementType.PARAGRAPH) - { - // If element is a paragraph - XWPFParagraph paragraph = (XWPFParagraph)element; + Dictionary tableContentPlaceholders = documentData.Placeholders + .Where(content => content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) + .ToDictionary(content => "{" + content.Placeholder + "}", content => content.Content); - // If the paragraph is empty string or the placeholder regex does not match then continue - if (paragraph.ParagraphText == string.Empty || !new Regex(PlaceholderPattern).IsMatch(paragraph.ParagraphText)) - { - continue; - } + // Replace text placeholders in main document + ReplaceTextPlaceholders(document.MainDocumentPart.Document, textPlaceholders); - // Replace placeholders in paragraph with values - paragraph = ReplacePlaceholdersOnBody(paragraph, textPlaceholders); - } - else if (element.ElementType == BodyElementType.TABLE) - { - // If element is a table - XWPFTable table = (XWPFTable)element; + // Replace table placeholders and populate tables + ProcessTables(document.MainDocumentPart.Document, tableContentPlaceholders, documentData.TablesData); - // Replace placeholders in a table - table = ReplacePlaceholderOnTables(table, tableContentPlaceholders); + // Process images + await ProcessImagePlaceholders(document, documentData.Images); - // Populate the table with data if it is passed in tablesData list - foreach (TableData insertData in tablesData) - { - if (insertData.TablePos >= 1 && insertData.TablePos <= document.Tables.Count && table == document.Tables[insertData.TablePos - 1]) - { - table = PopulateTable(table, insertData); - } - } - } + // Save the document + document.Save(); } + } - // Write the document to output file path and close the document - WriteDocument(document, outputFilePath); - document.Close(); - - /* - * Image Replacement is done after writing the document here, - * because for Text Replacement, NPOI package is being used - * and for Image Replacement, OpeXML package is used. - * Since both the packages have different execution method, so they are handled separately - */ - // Replace all the image placeholders in the output file - await ProcessImagePlaceholders(outputFilePath, documentData.Images); + /// + /// Replaces text placeholders in the document. + /// + /// The document to process. + /// Dictionary of placeholders and their replacement values. + private static void ReplaceTextPlaceholders(Document document, Dictionary textPlaceholders) + { + if (textPlaceholders.Count == 0) + { + return; + } + + // Process all paragraphs in the document + List paragraphs = document.Descendants().ToList(); + + foreach (Paragraph paragraph in paragraphs) + { + // Get paragraph text to check for placeholders + string paragraphText = GetParagraphText(paragraph); + + if (string.IsNullOrEmpty(paragraphText) || !Regex.IsMatch(paragraphText, PlaceholderPattern)) + { + continue; + } + + // Replace placeholders in this paragraph + ReplacePlaceholdersInParagraph(paragraph, textPlaceholders); + } } /// - /// Retrieves an instance of XWPFDocument from the specified document file path. + /// Gets the text content of a paragraph. /// - /// The file path of the Word document. - /// An instance of XWPFDocument representing the Word document. - private async static Task GetXWPFDocument(string docFilePath) + /// The paragraph to get text from. + /// The text content of the paragraph. + private static string GetParagraphText(Paragraph paragraph) { - byte[] fileBytes = await File.ReadAllBytesAsync(docFilePath); - using MemoryStream memoryStream = new MemoryStream(fileBytes); - return new XWPFDocument(memoryStream); + return string.Join("", paragraph.Descendants().Select(t => t.Text)); } /// - /// Writes the XWPFDocument to the specified file path. + /// Replaces placeholders in a specific paragraph. /// - /// The XWPFDocument to write. - /// The file path to save the document. - private static void WriteDocument(XWPFDocument document, string filePath) + /// The paragraph to process. + /// Dictionary of placeholders and their replacement values. + private static void ReplacePlaceholdersInParagraph(Paragraph paragraph, Dictionary placeholders) { - string? directory = IOPath.GetDirectoryName(filePath); - if (!string.IsNullOrWhiteSpace(directory)) + if (placeholders.Count == 0) + { + return; + } + + // Get all text elements from the paragraph + List textElements = paragraph.Descendants().ToList(); + if (textElements.Count == 0) + { + return; + } + + // Concatenate all text to get the full paragraph content + string fullText = string.Join("", textElements.Select(t => t.Text)); + + // Check if any placeholders exist in the full text + bool hasPlaceholders = placeholders.Keys.Any(placeholder => fullText.Contains(placeholder)); + if (!hasPlaceholders) + { + return; + } + + // Replace placeholders in the full text + string replacedText = fullText; + foreach (KeyValuePair placeholder in placeholders) { - Directory.CreateDirectory(directory); + replacedText = replacedText.Replace(placeholder.Key, placeholder.Value); } - using (FileStream writeStream = File.Create(filePath)) + // If no changes were made, return + if (replacedText == fullText) { - document.Write(writeStream); + return; + } + + // Clear existing text elements and create a single new one + // This preserves the paragraph structure while ensuring text continuity + foreach (Text textElement in textElements) + { + textElement.Text = ""; + } + + // Use the first text element to hold all the replaced content + if (textElements.Count > 0) + { + textElements[0].Text = replacedText; } } /// - /// Replaces the text placeholders in a paragraph with the specified values. + /// Processes tables for placeholder replacement and data population. /// - /// The XWPFParagraph containing the placeholders. - /// The dictionary of text placeholders and their corresponding values. - /// The updated XWPFParagraph. - private static XWPFParagraph ReplacePlaceholdersOnBody(XWPFParagraph paragraph, Dictionary textPlaceholders) + /// The document containing tables. + /// Dictionary of table placeholders and their replacement values. + /// List of table data to populate. + private static void ProcessTables(Document document, Dictionary tableContentPlaceholders, List tablesData) { - // Get a list of all placeholders in the current paragraph - List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, PlaceholderPattern) - .Cast() - .Select(s => s.Groups[0].Value).ToList(); + List tables = document.Descendants
().ToList(); - // For each placeholder in paragraph - foreach (string placeholder in placeholdersTobeReplaced) + foreach (Table table in tables) { - // Replace text placeholders in paragraph with values - if (textPlaceholders.ContainsKey(placeholder)) + // Replace placeholders in table cells + ReplaceTablePlaceholders(table, tableContentPlaceholders); + + // Populate table with data if applicable + int tableIndex = tables.IndexOf(table); + TableData? tableData = tablesData.FirstOrDefault(td => td.TablePos == tableIndex + 1); + + if (tableData != null) { - paragraph.ReplaceText(placeholder, textPlaceholders[placeholder]); + PopulateTable(table, tableData); } - - paragraph.SpacingAfter = 0; } - - return paragraph; } /// - /// Replaces the text placeholders in a table with the specified values. + /// Replaces placeholders in table cells. /// - /// The XWPFTable containing the placeholders. - /// The dictionary of table content placeholders and their corresponding values. - /// The updated XWPFTable. - private static XWPFTable ReplacePlaceholderOnTables(XWPFTable table, Dictionary tableContentPlaceholders) + /// The table to process. + /// Dictionary of placeholders and their replacement values. + private static void ReplaceTablePlaceholders(Table table, Dictionary tableContentPlaceholders) { - // Loop through each cell of the table - foreach (XWPFTableRow row in table.Rows) + if (tableContentPlaceholders.Count == 0) + { + return; + } + + List tableRows = table.Elements().ToList(); + + foreach (TableRow row in tableRows) { - foreach (XWPFTableCell cell in row.GetTableCells()) + List cells = row.Elements().ToList(); + + foreach (TableCell cell in cells) { - foreach (XWPFParagraph paragraph in cell.Paragraphs) + List paragraphs = cell.Elements().ToList(); + + foreach (Paragraph paragraph in paragraphs) { - // Get a list of all placeholders in the current cell - List placeholdersTobeReplaced = Regex.Matches(paragraph.ParagraphText, PlaceholderPattern) - .Cast() - .Select(s => s.Groups[0].Value).ToList(); - - // For each placeholder in the cell - foreach (string placeholder in placeholdersTobeReplaced) + string paragraphText = GetParagraphText(paragraph); + + if (string.IsNullOrEmpty(paragraphText) || !Regex.IsMatch(paragraphText, PlaceholderPattern)) { - // replace the placeholder with its value - if (tableContentPlaceholders.ContainsKey(placeholder)) - { - paragraph.ReplaceText(placeholder, tableContentPlaceholders[placeholder]); - } + continue; } + + ReplacePlaceholdersInParagraph(paragraph, tableContentPlaceholders); } } } - - return table; } /// - /// Populates a table with the specified data. + /// Populates a table with data rows. /// - /// The XWPFTable to populate. - /// The data to populate the table. - /// The updated XWPFTable. - private static XWPFTable PopulateTable(XWPFTable table, TableData tableData) + /// The table to populate. + /// The data to populate the table with. + private static void PopulateTable(Table table, TableData tableData) { - // Get the header row - XWPFTableRow headerRow = table.GetRow(0); + TableRow? headerRow = table.Elements().FirstOrDefault(); + if (headerRow == null) + { + return; + } - // Return if no header row found or if it does not have any column - if (headerRow == null || headerRow.GetTableCells() == null || headerRow.GetTableCells().Count <= 0) + List headerCells = headerRow.Elements().ToList(); + if (headerCells.Count == 0) { - return table; + return; } - // For each row's data stored in table data + // Get column headers + List columnHeaders = headerCells.Select(cell => + { + Paragraph? firstParagraph = cell.Elements().FirstOrDefault(); + if (firstParagraph != null) + { + return string.Join("", firstParagraph.Descendants().Select(t => t.Text)); + } + return ""; + }).ToList(); + + // Add data rows foreach (Dictionary rowData in tableData.Data) { - XWPFTableRow row = table.CreateRow(); // This is a DATA row, not header + TableRow newRow = new TableRow(); - int columnCount = headerRow.GetTableCells().Count; // Read from header - for (int cellNumber = 0; cellNumber < columnCount; cellNumber++) + for (int i = 0; i < columnHeaders.Count; i++) { - // Ensure THIS data row has enough cells - while (row.GetTableCells().Count <= cellNumber) - { - row.AddNewTableCell(); - } + string cellValue = rowData.ContainsKey(columnHeaders[i]) ? rowData[columnHeaders[i]] : ""; - // Now populate the cell in this data row - XWPFTableCell cell = row.GetCell(cellNumber); - string columnHeader = headerRow.GetCell(cellNumber).GetText(); - if (rowData.ContainsKey(columnHeader)) + TableCell cell = new TableCell( + new Paragraph( + new Run( + new Text(cellValue)))); + + // Copy formatting from header cell if available + if (i < headerCells.Count) { - cell.SetText(rowData[columnHeader]); + TableCellProperties? headerProps = headerCells[i].TableCellProperties; + if (headerProps != null) + { + cell.TableCellProperties = (TableCellProperties)headerProps.CloneNode(true); + } } + + newRow.Append(cell); } - } - return table; + table.Append(newRow); + } } /// - /// Replaces the image placeholders in the output file with the specified images. + /// Processes image placeholders in the document. /// - /// The ile path where the updated document will be saved. - /// The data structure for holding the images details. - - private static async Task ProcessImagePlaceholders( - string documentPath, - List images) + /// The Word document. + /// List of image data to process. + private static async Task ProcessImagePlaceholders(WordprocessingDocument document, List images) { if (images == null || !images.Any()) { @@ -280,67 +321,48 @@ private static async Task ProcessImagePlaceholders( try { - byte[] docBytes = await File.ReadAllBytesAsync(documentPath); - - using (MemoryStream memoryStream = new MemoryStream()) + MainDocumentPart? mainPart = document.MainDocumentPart; + if (mainPart == null) { - await memoryStream.WriteAsync(docBytes); - memoryStream.Position = 0; + return; + } - using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(memoryStream, true)) + List drawings = mainPart.Document.Descendants().ToList(); + + foreach (ImageData img in images) + { + try { - MainDocumentPart? mainPart = wordDocument.MainDocumentPart; - if (mainPart == null) + string tempFilePath = await PrepareImageFile(img); + tempFiles.Add(tempFilePath); + + Drawing? drawing = drawings.FirstOrDefault(d => + d.Descendants() + .Any(dp => dp.Description == img.PlaceholderName)); + + if (drawing == null) { - return; + continue; } - List drawings = mainPart.Document.Descendants().ToList(); - - foreach (ImageData img in images) + foreach (Blip blip in drawing.Descendants()) { - try + if (blip.Embed?.Value == null) { - string tempFilePath = await PrepareImageFile(img); - tempFiles.Add(tempFilePath); - - Drawing? drawing = drawings.FirstOrDefault(d => - d.Descendants() - .Any(dp => dp.Name == img.PlaceholderName)); - - if (drawing == null) - { - continue; - } - - foreach (Blip blip in drawing.Descendants()) - { - if (blip.Embed?.Value == null) - { - continue; - } - - OpenXmlPart imagePart = mainPart.GetPartById(blip.Embed!); - using (Stream partStream = imagePart.GetStream(FileMode.Create)) - { - await using FileStream fileStream = File.OpenRead(tempFilePath); - await fileStream.CopyToAsync(partStream); - } - } + continue; } - catch (Exception ex) + + OpenXmlPart imagePart = mainPart.GetPartById(blip.Embed!); + using (Stream partStream = imagePart.GetStream(FileMode.Create)) + using (FileStream fileStream = File.OpenRead(tempFilePath)) { - // Log error but continue with other images - Debug.WriteLine($"Failed to process image {img.PlaceholderName}: {ex.Message}"); + await fileStream.CopyToAsync(partStream); } } } - - // Save the modified document - memoryStream.Position = 0; - using (FileStream fileStream = new FileStream(documentPath, FileMode.Create)) + catch (Exception ex) { - await memoryStream.CopyToAsync(fileStream); + Debug.WriteLine($"Failed to process image {img.PlaceholderName}: {ex.Message}"); } } } @@ -355,13 +377,29 @@ private static async Task ProcessImagePlaceholders( } } + /// + /// Prepares an image file from various sources (Base64, local file, URL). + /// + /// The image data containing source information. + /// Path to the prepared temporary image file. private static async Task PrepareImageFile(ImageData imageData) { - string tempFilePath = System.IO.Path.GetTempFileName(); + string tempFilePath = IOPath.GetTempFileName(); if (!string.IsNullOrEmpty(imageData.ImageExtension)) { - tempFilePath = System.IO.Path.ChangeExtension(tempFilePath, imageData.ImageExtension); + // Define allowed image extensions + string[] allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg" }; + string extension = imageData.ImageExtension.StartsWith(".") + ? imageData.ImageExtension + : "." + imageData.ImageExtension; + + if (!allowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + throw new ArgumentException($"Invalid image extension: {imageData.ImageExtension}"); + } + + tempFilePath = IOPath.ChangeExtension(tempFilePath, extension); } switch (imageData.SourceType) From af768e13dc4093e4d12960079a80b92633db74f4 Mon Sep 17 00:00:00 2001 From: Jatin Date: Mon, 7 Jul 2025 18:49:01 +0530 Subject: [PATCH 65/73] feat: update EJS execution to use npx for cross-platform compatibility --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index 1800cd1..0bfd646 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -191,11 +191,9 @@ private async static Task ConvertEjsToHTML(string ejsFilePath, string ou string contentToWrite = ejsDataJson ?? "{}"; File.WriteAllText(ejsDataJsonFilePath, contentToWrite); - string commandLine = "cmd.exe"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - commandLine = "ejs"; - } + + // string commandLine = "cmd.exe"; + string commandLine = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "npx"; string arguments = EjsToHtmlArgumentsBasedOnOS(ejsFilePath, ejsDataJsonFilePath, tempHtmlFilePath); ProcessStartInfo psi = new ProcessStartInfo @@ -242,11 +240,11 @@ private static string EjsToHtmlArgumentsBasedOnOS(string ejsFilePath, string ejs { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return $"/C ejs \"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; + return $"/C npx ejs \"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return $"\"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; + return $"ejs \"{ejsFilePath}\" -f \"{ejsDataJsonFilePath}\" -o \"{tempHtmlFilePath}\""; } else { From 9082d3c3f64b56a35581bb5ce9da6417e9935d5a Mon Sep 17 00:00:00 2001 From: Jatin Date: Mon, 7 Jul 2025 19:40:17 +0530 Subject: [PATCH 66/73] feat: update WordDocumentGenerator with optional logging --- OsmoDoc/OsmoDoc.csproj | 1 + OsmoDoc/Word/WordDocumentGenerator.cs | 99 +++++++++++++++++---------- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/OsmoDoc/OsmoDoc.csproj b/OsmoDoc/OsmoDoc.csproj index f0b5443..628090e 100644 --- a/OsmoDoc/OsmoDoc.csproj +++ b/OsmoDoc/OsmoDoc.csproj @@ -23,5 +23,6 @@ + diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index ecaa6a6..4ba6889 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -2,10 +2,11 @@ using DocumentFormat.OpenXml.Drawing.Wordprocessing; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using OsmoDoc.Word.Models; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; @@ -28,6 +29,16 @@ namespace OsmoDoc.Word; public static class WordDocumentGenerator { private const string PlaceholderPattern = @"{[a-zA-Z][a-zA-Z0-9_-]*}"; + private static ILogger _logger = NullLogger.Instance; + + /// + /// Configures logging for the WordDocumentGenerator + /// + /// Logger instance to use + public static void ConfigureLogging(ILogger logger) + { + _logger = logger ?? NullLogger.Instance; + } /// /// Generates a Word document based on a template, replaces placeholders with data, and saves it to the specified output file path. @@ -52,36 +63,44 @@ public async static Task GenerateDocumentByTemplate(string templateFilePath, Doc throw new ArgumentNullException(nameof(outputFilePath)); } - // Copy template to output location - File.Copy(templateFilePath, outputFilePath, true); - - using (WordprocessingDocument document = WordprocessingDocument.Open(outputFilePath, true)) + try { - if (document.MainDocumentPart == null) + // Copy template to output location + File.Copy(templateFilePath, outputFilePath, true); + + using (WordprocessingDocument document = WordprocessingDocument.Open(outputFilePath, true)) { - throw new InvalidOperationException("Document does not contain a main document part."); - } + if (document.MainDocumentPart == null) + { + throw new InvalidOperationException("Document does not contain a main document part."); + } - // Create dictionaries for each type of placeholders - Dictionary textPlaceholders = documentData.Placeholders - .Where(content => content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) - .ToDictionary(content => "{" + content.Placeholder + "}", content => content.Content); + // Create dictionaries for each type of placeholders + Dictionary textPlaceholders = documentData.Placeholders + .Where(content => content.ParentBody == ParentBody.None && content.ContentType == ContentType.Text) + .ToDictionary(content => "{" + content.Placeholder + "}", content => content.Content); - Dictionary tableContentPlaceholders = documentData.Placeholders - .Where(content => content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) - .ToDictionary(content => "{" + content.Placeholder + "}", content => content.Content); + Dictionary tableContentPlaceholders = documentData.Placeholders + .Where(content => content.ParentBody == ParentBody.Table && content.ContentType == ContentType.Text) + .ToDictionary(content => "{" + content.Placeholder + "}", content => content.Content); - // Replace text placeholders in main document - ReplaceTextPlaceholders(document.MainDocumentPart.Document, textPlaceholders); + // Replace text placeholders in main document + ReplaceTextPlaceholders(document.MainDocumentPart.Document, textPlaceholders); - // Replace table placeholders and populate tables - ProcessTables(document.MainDocumentPart.Document, tableContentPlaceholders, documentData.TablesData); + // Replace table placeholders and populate tables + ProcessTables(document.MainDocumentPart.Document, tableContentPlaceholders, documentData.TablesData); - // Process images - await ProcessImagePlaceholders(document, documentData.Images); + // Process images + await ProcessImagePlaceholders(document, documentData.Images); - // Save the document - document.Save(); + // Save the document + document.Save(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to generate document from template: {templateFilePath}"); + throw; // Re-throw for consumers to handle } } @@ -99,12 +118,12 @@ private static void ReplaceTextPlaceholders(Document document, Dictionary paragraphs = document.Descendants().ToList(); - + foreach (Paragraph paragraph in paragraphs) { // Get paragraph text to check for placeholders string paragraphText = GetParagraphText(paragraph); - + if (string.IsNullOrEmpty(paragraphText) || !Regex.IsMatch(paragraphText, PlaceholderPattern)) { continue; @@ -146,7 +165,7 @@ private static void ReplacePlaceholdersInParagraph(Paragraph paragraph, Dictiona // Concatenate all text to get the full paragraph content string fullText = string.Join("", textElements.Select(t => t.Text)); - + // Check if any placeholders exist in the full text bool hasPlaceholders = placeholders.Keys.Any(placeholder => fullText.Contains(placeholder)); if (!hasPlaceholders) @@ -220,19 +239,19 @@ private static void ReplaceTablePlaceholders(Table table, Dictionary tableRows = table.Elements().ToList(); - + foreach (TableRow row in tableRows) { List cells = row.Elements().ToList(); - + foreach (TableCell cell in cells) { List paragraphs = cell.Elements().ToList(); - + foreach (Paragraph paragraph in paragraphs) { string paragraphText = GetParagraphText(paragraph); - + if (string.IsNullOrEmpty(paragraphText) || !Regex.IsMatch(paragraphText, PlaceholderPattern)) { continue; @@ -264,7 +283,7 @@ private static void PopulateTable(Table table, TableData tableData) } // Get column headers - List columnHeaders = headerCells.Select(cell => + List columnHeaders = headerCells.Select(cell => { Paragraph? firstParagraph = cell.Elements().FirstOrDefault(); if (firstParagraph != null) @@ -362,7 +381,7 @@ private static async Task ProcessImagePlaceholders(WordprocessingDocument docume } catch (Exception ex) { - Debug.WriteLine($"Failed to process image {img.PlaceholderName}: {ex.Message}"); + _logger.LogWarning(ex, $"Failed to process image placeholder: {img.PlaceholderName}"); } } } @@ -371,8 +390,14 @@ private static async Task ProcessImagePlaceholders(WordprocessingDocument docume // Clean up temp files foreach (string file in tempFiles) { - try { File.Delete(file); } - catch { /* Ignore cleanup errors */ } + try + { + File.Delete(file); + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Failed to delete temporary file: {file}."); + } } } } @@ -390,10 +415,10 @@ private static async Task PrepareImageFile(ImageData imageData) { // Define allowed image extensions string[] allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg" }; - string extension = imageData.ImageExtension.StartsWith(".") - ? imageData.ImageExtension + string extension = imageData.ImageExtension.StartsWith(".") + ? imageData.ImageExtension : "." + imageData.ImageExtension; - + if (!allowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { throw new ArgumentException($"Invalid image extension: {imageData.ImageExtension}"); From fd00589b8232f199f8207cb8c60ee527978eec27 Mon Sep 17 00:00:00 2001 From: Jatin Date: Mon, 7 Jul 2025 20:19:03 +0530 Subject: [PATCH 67/73] feat: update PdfDocumentGenerator with optional logging --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index 0bfd646..8ff668b 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -1,5 +1,6 @@ using OsmoDoc.Pdf.Models; -using Newtonsoft.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; @@ -12,6 +13,17 @@ namespace OsmoDoc.Pdf; public static class PdfDocumentGenerator { + private static ILogger _logger = NullLogger.Instance; + + /// + /// Configures logging for the WordDocumentGenerator + /// + /// Logger instance to use + public static void ConfigureLogging(ILogger logger) + { + _logger = logger ?? NullLogger.Instance; + } + /// /// Generates a PDF document from an HTML or EJS template. /// @@ -264,7 +276,7 @@ private static void CleanupTemporaryResources(string? ejsConvertedHtmlPath, stri catch (Exception ex) { // Log the exception but don't throw to avoid masking original exceptions - Console.WriteLine($"Warning: Could not delete EJS converted HTML file {ejsConvertedHtmlPath}: {ex.Message}"); + _logger.LogWarning(ex, $"Failed to delete EJS converted HTML file {ejsConvertedHtmlPath}"); } } @@ -278,7 +290,7 @@ private static void CleanupTemporaryResources(string? ejsConvertedHtmlPath, stri catch (Exception ex) { // Log the exception but don't throw to avoid masking original exceptions - Console.WriteLine($"Warning: Could not delete temporary directory {tempModifiedHtmlDirectory}: {ex.Message}"); + _logger.LogWarning(ex, $"Failed to delete temporary directory {tempModifiedHtmlDirectory}"); } } } From 303b9ffb6acf98b96d324569a9027cc644bcbeea Mon Sep 17 00:00:00 2001 From: Jatin Date: Mon, 7 Jul 2025 20:41:03 +0530 Subject: [PATCH 68/73] chore: remove NPOI package reference --- OsmoDoc/OsmoDoc.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/OsmoDoc/OsmoDoc.csproj b/OsmoDoc/OsmoDoc.csproj index 628090e..26c0182 100644 --- a/OsmoDoc/OsmoDoc.csproj +++ b/OsmoDoc/OsmoDoc.csproj @@ -21,7 +21,6 @@ - From 1df9fd383b46878426574b13d135c06b9eee5042 Mon Sep 17 00:00:00 2001 From: Jatin Date: Mon, 7 Jul 2025 23:34:34 +0530 Subject: [PATCH 69/73] fix: infer image extension from file path or URL if not provided --- OsmoDoc/Word/WordDocumentGenerator.cs | 32 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 4ba6889..0d2be4b 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -411,22 +411,40 @@ private static async Task PrepareImageFile(ImageData imageData) { string tempFilePath = IOPath.GetTempFileName(); + // Determine image extension + string extension = ".jpg"; // Default fallback + if (!string.IsNullOrEmpty(imageData.ImageExtension)) { - // Define allowed image extensions - string[] allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg" }; - string extension = imageData.ImageExtension.StartsWith(".") + extension = imageData.ImageExtension.StartsWith(".") ? imageData.ImageExtension : "." + imageData.ImageExtension; - - if (!allowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + } + else if (imageData.SourceType == ImageSourceType.LocalFile || imageData.SourceType == ImageSourceType.Url) + { + try { - throw new ArgumentException($"Invalid image extension: {imageData.ImageExtension}"); + extension = System.IO.Path.GetExtension(imageData.Data); + if (string.IsNullOrEmpty(extension)) + { + extension = ".jpg"; // fallback if no extension in path/URL + } } + catch + { + extension = ".jpg"; // safe fallback on exception + } + } - tempFilePath = IOPath.ChangeExtension(tempFilePath, extension); + // Define allowed image extensions + string[] allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg" }; + if (!allowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + throw new ArgumentException($"Invalid image extension: {imageData.ImageExtension}"); } + tempFilePath = IOPath.ChangeExtension(tempFilePath, extension); + switch (imageData.SourceType) { case ImageSourceType.Base64: From 2298678d9ee23ef4453b6f30f841d30f22dc4469 Mon Sep 17 00:00:00 2001 From: Jatin Date: Mon, 7 Jul 2025 23:50:38 +0530 Subject: [PATCH 70/73] feat: add flag for deciding cleaning up of input and output files --- OsmoDoc.API/Controllers/PdfController.cs | 92 ++++++++++++----------- OsmoDoc.API/Controllers/WordController.cs | 47 ++++++------ OsmoDoc.API/appsettings.json | 3 +- 3 files changed, 76 insertions(+), 66 deletions(-) diff --git a/OsmoDoc.API/Controllers/PdfController.cs b/OsmoDoc.API/Controllers/PdfController.cs index 45e494b..a89035d 100644 --- a/OsmoDoc.API/Controllers/PdfController.cs +++ b/OsmoDoc.API/Controllers/PdfController.cs @@ -29,6 +29,7 @@ public async Task> GeneratePdf(PdfGenerationRequestDT BaseResponse response = new BaseResponse(ResponseStatus.Fail); string? htmlTemplateFilePath = null; string? outputFilePath = null; + bool cleanupResources = this._configuration.GetValue("CONFIG:CLEAN_RESOURCES_GENERATED_BY_BASE64_STRINGS", false); try { @@ -37,15 +38,15 @@ public async Task> GeneratePdf(PdfGenerationRequestDT throw new BadHttpRequestException("Request body cannot be null"); } - string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value + string tempPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:TEMP") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); - string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value + string inputPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:INPUT") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:INPUT is missing."); - string htmlPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:HTML").Value + string htmlPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:HTML") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:HTML is missing."); - string outputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value + string outputPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:OUTPUT") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:OUTPUT is missing."); - string pdfPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value + string pdfPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:PDF") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:PDF is missing."); @@ -125,26 +126,29 @@ await PdfDocumentGenerator.GeneratePdf( } finally { - if (htmlTemplateFilePath != null && System.IO.File.Exists(htmlTemplateFilePath)) + if (cleanupResources) { - try + if (htmlTemplateFilePath != null && System.IO.File.Exists(htmlTemplateFilePath)) { - System.IO.File.Delete(htmlTemplateFilePath); + try + { + System.IO.File.Delete(htmlTemplateFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {htmlTemplateFilePath}: {ex.Message}"); + } } - catch (Exception ex) + if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) { - this._logger.LogError($"Error in deleting file at path {htmlTemplateFilePath}: {ex.Message}"); - } - } - if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) - { - try - { - System.IO.File.Delete(outputFilePath); - } - catch (Exception ex) - { - this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + try + { + System.IO.File.Delete(outputFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + } } } } @@ -158,6 +162,7 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR BaseResponse response = new BaseResponse(ResponseStatus.Fail); string? ejsTemplateFilePath = null; string? outputFilePath = null; + bool cleanupResources = this._configuration.GetValue("CONFIG:CLEAN_RESOURCES_GENERATED_BY_BASE64_STRINGS", false); try { @@ -166,15 +171,15 @@ public async Task> GeneratePdfUsingEjs(PdfGenerationR throw new BadHttpRequestException("Request body cannot be null"); } - string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value + string tempPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:TEMP") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); - string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value + string inputPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:INPUT") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:INPUT is missing."); - string ejsPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:EJS").Value + string ejsPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:EJS") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:EJS is missing."); - string outputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value + string outputPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:OUTPUT") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:OUTPUT is missing."); - string pdfPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:PDF").Value + string pdfPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:PDF") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:PDF is missing."); // Generate filepath to save base64 html template @@ -253,26 +258,29 @@ await PdfDocumentGenerator.GeneratePdf( } finally { - if (ejsTemplateFilePath != null && System.IO.File.Exists(ejsTemplateFilePath)) - { - try - { - System.IO.File.Delete(ejsTemplateFilePath); - } - catch (Exception ex) - { - this._logger.LogError($"Error in deleting file at path {ejsTemplateFilePath}: {ex.Message}"); - } - } - if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) + if (cleanupResources) { - try + if (ejsTemplateFilePath != null && System.IO.File.Exists(ejsTemplateFilePath)) { - System.IO.File.Delete(outputFilePath); + try + { + System.IO.File.Delete(ejsTemplateFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {ejsTemplateFilePath}: {ex.Message}"); + } } - catch (Exception ex) + if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) { - this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + try + { + System.IO.File.Delete(outputFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + } } } } diff --git a/OsmoDoc.API/Controllers/WordController.cs b/OsmoDoc.API/Controllers/WordController.cs index 28e04e8..df9a1d2 100644 --- a/OsmoDoc.API/Controllers/WordController.cs +++ b/OsmoDoc.API/Controllers/WordController.cs @@ -33,6 +33,7 @@ public async Task> GenerateWord(WordGenerationRequest BaseResponse response = new BaseResponse(ResponseStatus.Fail); string? docxTemplateFilePath = null; string? outputFilePath = null; + bool cleanupResources = this._configuration.GetValue("CONFIG:CLEAN_RESOURCES_GENERATED_BY_BASE64_STRINGS", false); try { @@ -46,17 +47,14 @@ public async Task> GenerateWord(WordGenerationRequest throw new BadHttpRequestException("Document data is required"); } - string tempPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:TEMP").Value + string tempPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:TEMP") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:TEMP is missing."); - string inputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:INPUT").Value + string inputPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:INPUT") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:INPUT is missing."); - string wordPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:WORD").Value + string wordPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:WORD") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:WORD is missing."); - string outputPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:OUTPUT").Value + string outputPath = this._configuration.GetValue("TEMPORARY_FILE_PATHS:OUTPUT") ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:OUTPUT is missing."); - string imagesPath = this._configuration.GetSection("TEMPORARY_FILE_PATHS:IMAGES").Value - ?? throw new InvalidOperationException("Configuration TEMPORARY_FILE_PATHS:IMAGES is missing."); - // Generate filepath to save base64 docx template docxTemplateFilePath = Path.Combine( @@ -147,26 +145,29 @@ await WordDocumentGenerator.GenerateDocumentByTemplate( } finally { - if (docxTemplateFilePath != null && System.IO.File.Exists(docxTemplateFilePath)) - { - try - { - System.IO.File.Delete(docxTemplateFilePath); - } - catch (Exception ex) - { - this._logger.LogError($"Error in deleting file at path {docxTemplateFilePath}: {ex.Message}"); - } - } - if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) + if (cleanupResources) { - try + if (docxTemplateFilePath != null && System.IO.File.Exists(docxTemplateFilePath)) { - System.IO.File.Delete(outputFilePath); + try + { + System.IO.File.Delete(docxTemplateFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {docxTemplateFilePath}: {ex.Message}"); + } } - catch (Exception ex) + if (outputFilePath != null && System.IO.File.Exists(outputFilePath)) { - this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + try + { + System.IO.File.Delete(outputFilePath); + } + catch (Exception ex) + { + this._logger.LogError($"Error in deleting file at path {outputFilePath}: {ex.Message}"); + } } } } diff --git a/OsmoDoc.API/appsettings.json b/OsmoDoc.API/appsettings.json index 90dfbb6..a76df53 100644 --- a/OsmoDoc.API/appsettings.json +++ b/OsmoDoc.API/appsettings.json @@ -26,7 +26,8 @@ }, "CONFIG": { "REQUEST_BODY_SIZE_LIMIT_BYTES": 52428800, - "UPLOAD_FILE_SIZE_LIMIT_BYTES": 5242880 + "UPLOAD_FILE_SIZE_LIMIT_BYTES": 5242880, + "CLEAN_RESOURCES_GENERATED_BY_BASE64_STRINGS": false }, "AllowedHosts": "*" } From e275e9093eb8c674a717815f650ad3b9789c04ed Mon Sep 17 00:00:00 2001 From: Jatin Date: Tue, 8 Jul 2025 00:37:00 +0530 Subject: [PATCH 71/73] feat: update DotEnv.cs to lookup for .env file in nearest parent folder --- OsmoDoc.API/DotEnv.cs | 21 +++++++++++++++++---- OsmoDoc.API/Program.cs | 23 +++++++++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/OsmoDoc.API/DotEnv.cs b/OsmoDoc.API/DotEnv.cs index e0801f2..a574d2f 100644 --- a/OsmoDoc.API/DotEnv.cs +++ b/OsmoDoc.API/DotEnv.cs @@ -2,13 +2,27 @@ public static class DotEnv { - public static void Load(string filePath) + public static void LoadEnvFile(string fileName = ".env") { - if (!File.Exists(filePath)) + DirectoryInfo? dir = new DirectoryInfo(Directory.GetCurrentDirectory()); + + while (dir != null) { - return; + string envPath = Path.Combine(dir.FullName, fileName); + if (File.Exists(envPath)) + { + Load(envPath); + return; + } + + dir = dir.Parent; } + throw new FileNotFoundException($"{fileName} file not found"); + } + + public static void Load(string filePath) + { foreach (string line in File.ReadAllLines(filePath)) { // Check if the line contains '=' @@ -31,5 +45,4 @@ public static void Load(string filePath) Environment.SetEnvironmentVariable(key, value); } } - } diff --git a/OsmoDoc.API/Program.cs b/OsmoDoc.API/Program.cs index b1bbdc7..798f6b2 100644 --- a/OsmoDoc.API/Program.cs +++ b/OsmoDoc.API/Program.cs @@ -13,6 +13,7 @@ using OsmoDoc.API.Models; using OsmoDoc.Services; using System.IdentityModel.Tokens.Jwt; +using System.Runtime.InteropServices; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -25,23 +26,17 @@ }); // Load .env file -string root = Directory.GetCurrentDirectory(); -string dotenv = Path.GetFullPath(Path.Combine(root, "..", ".env")); -if (File.Exists(dotenv)) -{ - OsmoDoc.API.DotEnv.Load(dotenv); -} -else +OsmoDoc.API.DotEnv.LoadEnvFile(); + +// Initialize PDF tool path once at startup (on Windows) +if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - throw new FileNotFoundException($".env file not found at path: {dotenv}"); + OsmoDocPdfConfig.WkhtmltopdfPath = Path.Combine( + builder.Environment.WebRootPath, + builder.Configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value! + ); } -// Initialize PDF tool path once at startup -OsmoDocPdfConfig.WkhtmltopdfPath = Path.Combine( - builder.Environment.WebRootPath, - builder.Configuration.GetSection("STATIC_FILE_PATHS:HTML_TO_PDF_TOOL").Value! -); - // Register REDIS service builder.Services.AddSingleton( ConnectionMultiplexer.Connect(Environment.GetEnvironmentVariable("REDIS_URL") ?? throw new Exception("No REDIS URL specified")) From 7aea316e84f1598f9db652ddf22aa23a73c4b46c Mon Sep 17 00:00:00 2001 From: Jatin Date: Tue, 8 Jul 2025 00:45:16 +0530 Subject: [PATCH 72/73] refactor: improve image extension validation security --- OsmoDoc/Word/WordDocumentGenerator.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/OsmoDoc/Word/WordDocumentGenerator.cs b/OsmoDoc/Word/WordDocumentGenerator.cs index 0d2be4b..4b418c2 100644 --- a/OsmoDoc/Word/WordDocumentGenerator.cs +++ b/OsmoDoc/Word/WordDocumentGenerator.cs @@ -430,8 +430,9 @@ private static async Task PrepareImageFile(ImageData imageData) extension = ".jpg"; // fallback if no extension in path/URL } } - catch + catch (Exception ex) { + _logger.LogWarning(ex, $"Failed to determine image extension from path: {imageData.Data}"); extension = ".jpg"; // safe fallback on exception } } @@ -440,7 +441,7 @@ private static async Task PrepareImageFile(ImageData imageData) string[] allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg" }; if (!allowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - throw new ArgumentException($"Invalid image extension: {imageData.ImageExtension}"); + throw new ArgumentException($"Invalid image extension: {extension}"); } tempFilePath = IOPath.ChangeExtension(tempFilePath, extension); @@ -454,12 +455,12 @@ await File.WriteAllBytesAsync( break; case ImageSourceType.LocalFile: - if (!File.Exists(imageData.Data)) + string fullPath = IOPath.GetFullPath(imageData.Data); + if (!File.Exists(fullPath)) { - throw new FileNotFoundException("Image file not found", imageData.Data); + throw new FileNotFoundException("Image file not found", fullPath); } - - File.Copy(imageData.Data, tempFilePath, true); + File.Copy(fullPath, tempFilePath, true); break; case ImageSourceType.Url: From 15157cce87f3ee3850d91ed62d91bbc57ddb8974 Mon Sep 17 00:00:00 2001 From: Jatin Gupta Date: Tue, 8 Jul 2025 03:23:27 +0530 Subject: [PATCH 73/73] fix: ensure EJS temp directory is cleaned up after PDF generation --- OsmoDoc/Pdf/PdfDocumentGenerator.cs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/OsmoDoc/Pdf/PdfDocumentGenerator.cs b/OsmoDoc/Pdf/PdfDocumentGenerator.cs index 8ff668b..d1f5c71 100644 --- a/OsmoDoc/Pdf/PdfDocumentGenerator.cs +++ b/OsmoDoc/Pdf/PdfDocumentGenerator.cs @@ -60,6 +60,7 @@ public async static Task GeneratePdf(string templatePath, List } string? ejsConvertedHtmlPath = null; + string? tempEjsDirectory = null; string? tempModifiedHtmlDirectory = null; try @@ -73,7 +74,7 @@ public async static Task GeneratePdf(string templatePath, List } // Convert ejs file to an equivalent html - ejsConvertedHtmlPath = await ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); + (ejsConvertedHtmlPath, tempEjsDirectory) = await ConvertEjsToHTML(templatePath, outputFilePath, serializedEjsDataJson); templatePath = ejsConvertedHtmlPath; } @@ -86,7 +87,7 @@ public async static Task GeneratePdf(string templatePath, List finally { // Cleanup temporary directories and files - CleanupTemporaryResources(ejsConvertedHtmlPath, tempModifiedHtmlDirectory); + CleanupTemporaryResources(ejsConvertedHtmlPath, tempEjsDirectory, tempModifiedHtmlDirectory); } } @@ -173,7 +174,7 @@ private async static Task ConvertHtmlToPdf(string? wkhtmltopdfPath, string modif } } - private async static Task ConvertEjsToHTML(string ejsFilePath, string outputFilePath, string? ejsDataJson) + private async static Task<(string htmlPath, string tempDirectory)> ConvertEjsToHTML(string ejsFilePath, string outputFilePath, string? ejsDataJson) { // Generate directory string? directoryPath = Path.GetDirectoryName(outputFilePath); @@ -232,7 +233,7 @@ private async static Task ConvertEjsToHTML(string ejsFilePath, string ou } } - return tempHtmlFilePath; + return (tempHtmlFilePath, tempDirectoryFilePath); } private static bool IsValidJSON(string json) @@ -264,7 +265,7 @@ private static string EjsToHtmlArgumentsBasedOnOS(string ejsFilePath, string ejs } } - private static void CleanupTemporaryResources(string? ejsConvertedHtmlPath, string? tempModifiedHtmlDirectory) + private static void CleanupTemporaryResources(string? ejsConvertedHtmlPath, string? tempEjsDirectory, string? tempModifiedHtmlDirectory) { // Clean up EJS converted HTML file if (!string.IsNullOrEmpty(ejsConvertedHtmlPath) && File.Exists(ejsConvertedHtmlPath)) @@ -280,6 +281,19 @@ private static void CleanupTemporaryResources(string? ejsConvertedHtmlPath, stri } } + // Clean up temp EJS directory + if (!string.IsNullOrEmpty(tempEjsDirectory) && Directory.Exists(tempEjsDirectory)) + { + try + { + Directory.Delete(tempEjsDirectory, recursive: true); + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Failed to delete temporary EJS directory {tempEjsDirectory}"); + } + } + // Clean up temporary modified HTML directory and its contents if (!string.IsNullOrEmpty(tempModifiedHtmlDirectory) && Directory.Exists(tempModifiedHtmlDirectory)) {