From 18ed0534b744a11acf4b6c3cd1fa7c1a7a799578 Mon Sep 17 00:00:00 2001 From: Robert Morrison Date: Tue, 16 Jan 2024 11:00:35 +0000 Subject: [PATCH] refactor(EVERYTHING): Refactor all the things. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a little bit of OOP and a monster commit, I too can make a an ssg, Ka-chow... Changes: - ➕ added class to represent SiteFile - ➕ enabled conversion semi-automatic based on file type. - ➕ added template to Testing. - ➖ Removed awful code for dependency search - ➖ Removed awful code for enumerating directory - ➕ arguments to a class to allow for easier passing to other parts of the code. TODO: - 🐞Test and debug with a copy of a live site, - ✍️ Add handling for Pandoc errors on stderr - ❓Look into parallelising as much as possible. --- Program.cs | 394 +++++++++--------------- RuntimeSettings/RuntimeSettings.cs | 29 ++ SiteFile/SiteFile.ConverterFunctions.cs | 158 ++++++++++ SiteFile/SiteFile.cs | 138 +++++++++ TODO | 5 + Testing/src/.template | 92 ++++++ Utils/Utils.GetFiles.cs | 9 + Utils/Utils.PathSearch.cs | 52 ++++ csSiteGen.csproj | 8 +- 9 files changed, 633 insertions(+), 252 deletions(-) create mode 100644 RuntimeSettings/RuntimeSettings.cs create mode 100644 SiteFile/SiteFile.ConverterFunctions.cs create mode 100644 SiteFile/SiteFile.cs create mode 100644 TODO create mode 100755 Testing/src/.template create mode 100644 Utils/Utils.GetFiles.cs create mode 100644 Utils/Utils.PathSearch.cs diff --git a/Program.cs b/Program.cs index d4a9119..edfe1f8 100644 --- a/Program.cs +++ b/Program.cs @@ -16,12 +16,15 @@ * along with this program. If not, see . */ -using Serilog; using Serilog.Events; -using System.Reflection; -using System.Text.RegularExpressions; +using Serilog; +using System.CommandLine.Builder; +using System.CommandLine.Help; +using System.CommandLine.Parsing; +using System.CommandLine; using System.Diagnostics; -using System.Text.Json; +using System.Reflection; +using Spectre.Console; namespace csSiteGen; @@ -30,21 +33,6 @@ class Program static int Main(string[] args) { - // Default values - List _verbatimFileTypes = new List{ // file extenstions we want to have copied verbatim - "html", // Premade html pages - "css", // Style Sheets - // IMAGE TYPES TODO: add webp conversion for most images. - "jpg", - "png", - "webp", - "gif" - }; - // TODO: Make this a dictionary/tuple to show a FROM -> TO filetype relationship (Possibly with program) - List _conversionTypes = new List{ // file extenstions we want to convert to html - "md" - }; - // Get the current versiion number string? version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion; @@ -52,7 +40,6 @@ class Program // Configure logger Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() - .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information) .WriteTo.File("log.log") .CreateLogger(); @@ -64,264 +51,173 @@ class Program } else { - Log.Error("Cannot get Version Information"); - } - - // Exit on argument errors - if (args.Length < 2) - { - Log.Error("Too Few Args: Expected at least 2 but received {Count}",args.Length); - return 1; + Log.Warning("Cannot get Version Information"); } // It is very likely that this program will only work on linux. As such it is worth warning the user about this. if (!OperatingSystem.IsLinux()) { Log.Warning("This program has only been tested on linux and cannot be assumed to work on other Operating Systems"); + // AnsiConsole.MarkupLine("[[[yellow]Warning[/]]] This program is only tested on linux systems, it may not work on this Operating System."); } - // Get and check the directories passed to the program - string _inputDirectory = args[0]; - string _outputDirectory = args[1]; + Stopwatch TotalExecutionTime = Stopwatch.StartNew(); - if (!Directory.Exists(_inputDirectory)) - { - Log.Error("Input directory '{i}' Does not exist or is not a directory you have access to" , _inputDirectory); - Environment.Exit(1); - } - if (!Directory.Exists(_outputDirectory)) - { - Log.Error("Output directory '{o}' Does not exist or is not a directory you have access to" , _outputDirectory); - Environment.Exit(1); - } - - // Test for dependencies - List deps = new List{"pandoc"}; - var dep = CheckDeps(deps); - Log.Debug("CheckDeps result: {@dep}",dep); - - // Deal with the dependency test results - Dictionary depsDict = new(); - if (dep.Item2 is null) - { - Log.Debug("Ignoring dependency check as no dependencies listed"); - } - else - { - depsDict = dep.Item2; - } - if (!dep.Item1) - { - foreach (var dependency in depsDict) - { - Log.Error("Dependency {dependency} Cannot be found\nPlease install it or check it is in your PATH",dependency.Key); - } - Log.CloseAndFlush(); - Environment.Exit(1); - } - - List ConvertableInputFiles = new(); - foreach (var ext in _conversionTypes) - { - ConvertableInputFiles.AddRange(GetAllFilesMatching($".*{ext}",_inputDirectory)); - } - Log.Debug("Files matching conversiontypes: {@conversionType} found {count} \n {files} ",_conversionTypes,ConvertableInputFiles.Count(),ConvertableInputFiles); - - Dictionary? metadata = null; - if (File.Exists($"{_inputDirectory}/.files")) - { - Log.Information("Loading metadata for {src}",_inputDirectory); - metadata = new(); - string metaJSON = File.ReadAllText($"{_inputDirectory}/.files"); - try - { - Type metadataType = metadata.GetType(); - metadata = JsonSerializer.Deserialize>(metaJSON); - } - catch (JsonException e) - { - Log.Debug(e,"Cannot deserialize .files Json data"); - Log.Debug("As this is possibly caused by an internal interface change the file will be deleted"); - File.Delete($"{_inputDirectory}/.files"); - } - } - - if (metadata is not null) - { - // Use metadata to determine if files have been updated - // A shadow list is used here so we can iterate and modify at the same time - List shadow = new List(ConvertableInputFiles); - foreach (string file in shadow) - { - if (metadata[file] == new FileInfo(file).LastWriteTimeUtc) + var inputDirectoryOption = new Option( + name: "--input", + description: "The directory that contains the site source."); + inputDirectoryOption.IsRequired = true; + inputDirectoryOption.AddValidator(result => { - Log.Debug("File {file} has not been updated since last run. ignoring",file); - ConvertableInputFiles.Remove(file); - } - } - } - Log.Information("{count} Files need converting",ConvertableInputFiles.Count()); - Log.Debug("Files: {files}",ConvertableInputFiles); + if (!result.GetValueForOption(inputDirectoryOption)!.Exists) + { + result.ErrorMessage = $"Input directory {result.GetValueForOption(inputDirectoryOption)!.FullName} does not exist"; + } + }); + var outputDirectoryOption = new Option( + name: "--output", + description: "The directory that the site should be output to."); + outputDirectoryOption.IsRequired = true; + + var rootCommand = new RootCommand("csSiteGen"); + + var cleanCommand = new Command("clean", "Clean the output directory"); + cleanCommand.AddOption(outputDirectoryOption); + cleanCommand.SetHandler(async (directory) => + { + await Task.Run(() => + { + Clean(directory); + }); + } ,outputDirectoryOption); + + var convertCommand = new Command("convert", "Convert the input directory and place the files in the output directory."); + convertCommand.AddOption(inputDirectoryOption); + convertCommand.AddOption(outputDirectoryOption); + convertCommand.SetHandler(async (inputDir, outputDir) => + { + await Task.Run(() => + { + Convert(inputDir, outputDir); + }); + }, inputDirectoryOption, outputDirectoryOption); + + rootCommand.AddCommand(cleanCommand); + rootCommand.AddCommand(convertCommand); + + var parser = new CommandLineBuilder(rootCommand) + .UseDefaults() + .Build(); + + parser.Invoke(args); + + + TotalExecutionTime.Stop(); + Log.Information("TotalExecutionTime {time:000}ms", TotalExecutionTime.ElapsedMilliseconds); Log.CloseAndFlush(); return 0; } -/// -/// Returns a list of all the files in the given directory and all subdirectories recursive -/// -/// -/// Currently this function ignores reparse points (symlinks) so as to avoid infinite loops. -/// It is probably a good idea to make the function aware of loops so it can read symlinks. -/// -/// -/// The directory to find files in. -/// -/// -/// A string list of every file in the provided directory and all subdirectories -/// - static List GetAllFiles(string directory) + static int Convert(DirectoryInfo inputDir, DirectoryInfo outputDir) { - List res = new(); - Stack dirs = new(); + AnsiConsole.Console.Profile.Capabilities.Ansi = true; - dirs.Push(directory); - Log.Debug("Dirs Starting as: {DirStack}",dirs ); - Stopwatch timer = new(); - timer.Start(); - while (dirs.Count > 0) - { - var dir = dirs.Pop(); - res.AddRange(Directory.GetFiles(dir)); - foreach (string subdir in Directory.GetDirectories(dir)) - { - if(File.GetAttributes(subdir).HasFlag(FileAttributes.ReparsePoint)) + List siteFiles = new(); + + Utils.GetFiles(inputDir).ForEach(x => siteFiles.Add(new SiteFile(x))); + Log.Information("SiteFiles: {@sf} {count}", siteFiles, siteFiles.Count); + + Console.WriteLine($"Converting {siteFiles.Count} files from {inputDir.FullName} to {outputDir.FullName}"); + RuntimeSettings settings = new(inputDir,outputDir); + + + Dictionary fileStatus = new(); + Log.Debug("fileStatus {@fileStatus}",fileStatus); + + AnsiConsole.Progress() + .AutoRefresh(true) + .Columns(new ProgressColumn[] { - /* - * FIXME: This is only a temporary measure and should not be relied upon - * if possible there should be a depth detection system that allows for - * safe symlink following. - */ - Log.Debug("Directory {subdir} is a ReparsePoint(symlink) and has been ignored",subdir); - continue; + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new RemainingTimeColumn(), + new SpinnerColumn() + }) + .Start(ctx => + { + var tasks = siteFiles.Select(x => ctx.AddTask($"Converting {x.Name}")).ToList(); + var overallTask = ctx.AddTask("[bold]Converting Files[/]"); + overallTask.MaxValue = siteFiles.Count(); + + for (int i = 0; i < siteFiles.Count; i++) + { + tasks[i].MaxValue = 1; + Log.Information("Converting file {name} {i}/{count}",siteFiles[i].Name,i+1,siteFiles.Count()); + bool res = siteFiles[i].Convert(settings); + tasks[i].Increment(1); + tasks[i].StopTask(); + if (!res) + { + Log.Warning("{name} Failed...",siteFiles[i].FullName); + tasks[i].Description += " [red]FAILED[/]"; + } + Log.Information("adding {@siteFile} conversion status to fileStatus", siteFiles[i]); + Log.Debug("FileStatus {@fileStatus}",fileStatus); + fileStatus.Add(siteFiles[i].FullName,res); + overallTask.Increment(1); } - dirs.Push(subdir); - Log.Debug("Adding Directory {subdir} to dirs\nResult: {dirs}",subdir,dirs); - } - } - timer.Stop(); - Log.Information("Crawled {directory} in {time}ms with {number} results",directory,timer.ElapsedMilliseconds,res.Count()); - return res; - } + }); - static List GetAllFilesMatching(string pattern, string directory) - { - List res = new(); - List files = GetAllFiles(directory); + Log.Information("Conversion Status {@status}", fileStatus); - Regex expression = new Regex(pattern, - RegexOptions.Compiled); - - res = files.FindAll( x => expression.IsMatch(x)); // Here a lambda function is used as a predicate to do regex across the whole file list - - return res; - } - - void Usage() - { - /* - * This function will be called when argument errors happen. This will - * also be called if the user triggers the help flag currently this - * function throws a not implemented exception since it I cannot write - * the usage until I have other functionality - */ - throw new NotImplementedException(); - } - - static Tuple?> CheckDeps(List? dependencies) - { - bool success = true; - Stopwatch timer = new(); - if (! (dependencies?.Count() > 0)) // assume success as no dependencies + var Failed = fileStatus.Where(x => x.Value == false); + if ( Failed.Count() > 0) { - return new Tuple?>(true,null); - } - - - var path = System.Environment.GetEnvironmentVariable("PATH")?.Split(':'); - if (path is null) - { - Log.Error("Cannot read system path."); - Environment.Exit(1); - } - Log.Debug("Path is: {path}",path); - - List executables = new(); - timer = Stopwatch.StartNew(); - foreach (string dir in path) - { - if (Directory.Exists(dir)) // Sometimes People fuck up their path variables and include directories that don't exist. + foreach (var fail in Failed) { - executables.AddRange(GetAllFiles(dir)); - } - } - timer.Stop(); - Log.Verbose("Executables are: {executables}",executables); - Log.Information("Got executables list in {timer:000}ms",timer.ElapsedMilliseconds); - - // Do some regex magic to find an executable in the path. - Dictionary? result = new(); - Dictionary? failures = new(); - - foreach (string dependency in dependencies) - { - timer = Stopwatch.StartNew(); - string regex = $"/(.*/)*{dependency}"; - Regex expr = new Regex(regex,RegexOptions.Compiled); - Log.Debug("Compiled regex: {expr}",expr); - string? match = executables.Find( x => expr.IsMatch(x)); - if (match is not null && match != "") - { - timer.Stop(); - Log.Information("Dependency {dep} found at {path} in {timer:000}ms",dependency,match,timer.ElapsedMilliseconds); - result.Add(dependency,match); // Create a dictionary of paths to found executables - } - else - { - timer.Stop(); - Log.Information("Dependency {dep} cannot be found, search took {timer:000ms}",dependency,timer.ElapsedMilliseconds); - success = false; - failures.Add(dependency,dependency); + AnsiConsole.MarkupLineInterpolated( + $"[red]File [blue]\"{fail.Key}\"[/] failed to convert[/]"); + AnsiConsole.MarkupLine("[yellow]See log for more details[/]"); } } - if (success) - { - return new Tuple?>(success,result); - } - else - { - return new Tuple?>(success,failures); - } + return 0; } - static bool Convert(Dictionary files) + static int Clean(DirectoryInfo outputDir) { - - return false; + if (!outputDir.Exists) + { + Log.Warning("Not deleting {dir} as it doesn't exist",outputDir.FullName); + AnsiConsole.MarkupLineInterpolated($"[bold][[[yellow]Warning[/]]][/] Not cleaning [blue]\"{outputDir}\"[/] as it does not exist."); + return 0; // success because it doesn't exist. + } + try + { + Log.Information("Cleaning {dir}",outputDir.FullName); + AnsiConsole.MarkupInterpolated($"Cleaning [blue]\"{outputDir.FullName}\"[/]"); + outputDir.Delete(recursive: true); + outputDir.Create(); + AnsiConsole.MarkupLine(" [bold][[[green]OK[/]]][/]"); + AnsiConsole.MarkupLineInterpolated($"\t[grey]>>[/] [green]All files in {outputDir.FullName} purged successfully[/]"); + return 0; + } + catch (System.Security.SecurityException e) + { + AnsiConsole.MarkupLine(" [bold][[[red]Fail[/]]][/]"); + AnsiConsole.MarkupLine("[orangered1]See log for more details about what went wrong.[/]"); + Log.Error(e, "Failed to delete directory {dir} due to permission error.", outputDir.FullName); + return 1; + } + catch (Exception e) + { + AnsiConsole.MarkupLine(" [red][[[bold]Fail[/]]][/]"); + AnsiConsole.MarkupLine("[orangered1]See log for more details about what went wrong.[/]"); + Log.Error(e, "Failed to delete/create directory {dir}", outputDir.FullName); + return 1; + } } } - // if (!File.Exists($"{_inputDirectory}/.files")) - // { - // Log.Information("Creating metadata for {src}",_inputDirectory); - // metadata = new(); - // foreach (var file in ConvertableInputFiles) - // { - // FileInfo info = new FileInfo(file); - // DateTime LastMod = info.LastWriteTimeUtc; - // metadata.Add(file,LastMod); - // string metaJSON = JsonSerializer.Serialize(metadata); - // File.WriteAllText($"{_inputDirectory}/.files",metaJSON); - // } - // } diff --git a/RuntimeSettings/RuntimeSettings.cs b/RuntimeSettings/RuntimeSettings.cs new file mode 100644 index 0000000..92aff3e --- /dev/null +++ b/RuntimeSettings/RuntimeSettings.cs @@ -0,0 +1,29 @@ +namespace csSiteGen; + + +/// +/// Class RuntimeSettings

Contains all the settings that could be loaded from the commandline. +///
+public class RuntimeSettings { + public DirectoryInfo InputDirectory {get; private set;} + public DirectoryInfo OutputDirectory {get; private set;} + + + public RuntimeSettings(string inputDirectory, string outputDirectory){ + InputDirectory = new DirectoryInfo(inputDirectory); + OutputDirectory = new DirectoryInfo(outputDirectory); + + /* NOTE: it is the responisbility of the UI code to check the values passed are good. + */ + } + + + public RuntimeSettings(DirectoryInfo inputDirectory, DirectoryInfo outputDirectory){ + InputDirectory = inputDirectory; + OutputDirectory = outputDirectory; + + /* NOTE: it is the responisbility of the UI code to check the values passed are good. + */ + } + +} diff --git a/SiteFile/SiteFile.ConverterFunctions.cs b/SiteFile/SiteFile.ConverterFunctions.cs new file mode 100644 index 0000000..d15a414 --- /dev/null +++ b/SiteFile/SiteFile.ConverterFunctions.cs @@ -0,0 +1,158 @@ +using Serilog; +using Spectre.Console; +using System.Diagnostics; +namespace csSiteGen; + +public static class Conversions{ + + + public delegate bool ConvertFunc(FileInfo file, RuntimeSettings settings); + + /// + /// A Mapping of filetype to ConvertFunc. + /// + public static readonly Dictionary Mappings = new(){ + {".md", Pandoc}, + }; + + + /// + /// TEST FUNCTION. + /// + public static bool NoOp(FileInfo file, RuntimeSettings settings){ + Log.Information("Performing NoOp Conversion"); + string newName = GetNewName(file,settings,"NoOp"); + Log.Debug("{FullName} -> {newName}",file.FullName,newName); + Thread.Sleep(1500); + return true; + } + + /// + /// Copy the file verbatim + /// + public static bool RawCpy(FileInfo file, RuntimeSettings settings){ + FileInfo newPath = new FileInfo(GetNewName(file,settings,null)); + + Log.Information("RawCpy: Copying {file} to {dest}",file.FullName, newPath.FullName); + + if (!newPath.Directory!.Exists) + { + newPath.Directory.Create(); + } + + try { + file.CopyTo(newPath.FullName, overwrite: true); + } + catch (Exception e){ + Log.Fatal(e,"Copy Failed"); + return false; + } + return true; + } + + /// + /// Execute pandoc on the file, automatically detecting the template to use. + /// + public static bool Pandoc(FileInfo file, RuntimeSettings settings){ + Log.Information("Attempting to convert {file} using pandoc",file.Name); + + // Look for pandoc + string pandoc = Utils.PathSearch("pandoc"); + if (string.IsNullOrEmpty(pandoc)) + { + Console.WriteLine("Conversion failed due to dependency being unavailable."); + return false; + } + Log.Information("Located pandoc binary."); + + // Look for template file. + FileInfo? template = null; + DirectoryInfo? searchDir = file.Directory; + do + { + // This loop starts searching at the directory of the file, + // and if a template is not found works up to the InputDirectory. + // If no template is found at any level we simply run pandoc with no template. + + if (searchDir is null) // While it is unlikely this could happen the check is here to please the compiler. + { + break; + } + + var result = searchDir.GetFiles(".template"); + if (result.Length > 0) + { + template = result.First(); + Log.Information("Found template {template}", template); + break; + } + + // Go up the tree. + searchDir = searchDir.Parent; + } while (searchDir != settings.InputDirectory); // Check last as we want to search the InputDirectory + + string pandocArgs = $"{file.FullName} -o {GetNewName(file,settings,".html")}"; + + if (template is not null) + { + pandocArgs += $" --template={template.FullName}"; + } + else + { + AnsiConsole.MarkupLine("[bold][[[orange1] Warning [/]]][/] Pandoc Template was not located."); + Log.Warning("Pandoc template for {file} not found",file.Name); + } + + return RunExternalProgram(pandoc,pandocArgs); + + } + + private static bool RunExternalProgram(string program, string args) + { + + Log.Information("Executing {program}", program); + Log.Debug("Full arguments {args}",args); + + using (Process RunProgram = new()) + { + // Reading stderr and stdout needs to be done carefully. + string? stdout = null; + string? stderr = null; + + RunProgram.StartInfo.UseShellExecute = false; + RunProgram.StartInfo.FileName = program; + RunProgram.StartInfo.CreateNoWindow = true; + RunProgram.StartInfo.Arguments = args; + RunProgram.StartInfo.RedirectStandardOutput = true; + RunProgram.StartInfo.RedirectStandardError = true; + + // Add a handler to append stderr to the stderr string + RunProgram.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => + {stderr += e.Data;}); + RunProgram.OutputDataReceived += new DataReceivedEventHandler((sender, o) => + {stdout += o.Data;}); + + RunProgram.Start(); + RunProgram.BeginErrorReadLine(); + RunProgram.BeginOutputReadLine(); + + RunProgram.WaitForExit(); + + Log.Debug("{program} stdout {stdout}", program, stdout); + Log.Debug("{program} stderr {stderr}", program, stderr); + + if (RunProgram.ExitCode != 0) + { + Log.Error("{program} execution Failed. Check debug data for more information", program); + return false; + } + } + return true; + } + + private static string GetNewName(FileInfo file, RuntimeSettings settings, string? newExtension){ + return file.FullName + .Replace(settings.InputDirectory.FullName, settings.OutputDirectory.FullName) + .Replace(file.Extension,newExtension ?? file.Extension); + } +} diff --git a/SiteFile/SiteFile.cs b/SiteFile/SiteFile.cs new file mode 100644 index 0000000..8271bbe --- /dev/null +++ b/SiteFile/SiteFile.cs @@ -0,0 +1,138 @@ +using System.Text.Json; +using Serilog; +namespace csSiteGen; + +/// +/// A SiteFile represents an individual file to be converted for the static site. +/// +public partial class SiteFile +{ + FileInfo info; + Conversions.ConvertFunc ConverterFunction; + static Dictionary? Metadata = null; + + /// + /// The name of the file, Not Guaranteed to be unique. + /// Use only for output and logging, never file operations, + /// nor as a Dictionary key. + /// + public string Name => info.Name; + + /// + /// The FullName (or Path) of the file, This is unique as each file can only be found once. + /// Use for file operations and Dictionary keys, or anywhere else you need to avoid ambiguity. + /// + public string FullName => info.FullName; + + public SiteFile(FileInfo fileInfo) + { + info = fileInfo; + + Log.Information("{file} extension is {ext}",fileInfo.FullName, fileInfo.Extension); + // Using this Ensures that the ConverterFunction is Always set. + // ConverterFunctions ALWAYS accept just the FileInfo, and RuntimeSettings passed at convert time. + ConverterFunction = Conversions.Mappings.GetValueOrDefault(info.Extension, Conversions.RawCpy); + } + + /// + /// Convert the file, placing it in the correct place in the output directory. + /// If a filetype conversion is not needed, or specified, then the file is simply copied. + /// + public bool Convert(RuntimeSettings settings) + { + if (!NeedsUpdating(settings)) + { + Log.Information("Ignoring {name}, as it has not been changed since last run.", info.FullName); + return true; + } + bool res = ConverterFunction(info, settings); + + if (res) + { + Log.Information("Converted sucessfully, updating metadata"); + if (Metadata!.ContainsKey(info.FullName)) + { + Metadata[info.FullName] = info.LastWriteTimeUtc; + } + else + { + Metadata.Add(info.FullName,info.LastWriteTimeUtc); + } + Log.Debug("Metadata Dictionary now {@Metadata}", Metadata); + SaveMetadata(settings); + } + + return res; + } + + private bool NeedsUpdating(RuntimeSettings settings) + { + if (Metadata is null) + { + LoadMetadata(settings); + } + + // NOTE: By this point Metadata CANNOT be null as LoadMetadata would either have loaded the JSON or instantiated MetaData + if (Metadata!.GetValueOrDefault(info.FullName, DateTime.MinValue) == info.LastWriteTimeUtc) + { + return false; + } + + return true; + } + + + /* + * NOTE: It may seem wrong to save the metadata in the output directory. + * But that ensures that if you remove the output directory the site will be + * Fully recreated. + */ + private void LoadMetadata(RuntimeSettings settings) + { + string metaFile = $"{settings.OutputDirectory}/.files"; + Log.Information("Loading Metadata from {file}",metaFile); + try + { + string metaJson = File.ReadAllText(metaFile); + Log.Debug("Read Json {metaJson}", metaJson); + Metadata = JsonSerializer.Deserialize>(metaJson); + Log.Debug("Deserialized to {@Metadata}",Metadata); + } + catch (IOException e) + { + Log.Warning(e,"Error reading .files metafile"); + Metadata = new(); + return; + } + catch (JsonException e) + { + Log.Information(e, "Failed to deserialize .files Json data"); + Log.Information("The metadata file will be deleted as it is likely corrupted."); + File.Delete(metaFile); + Metadata = new(); + } + } + + private void SaveMetadata(RuntimeSettings settings) + { + if (Metadata is null) + { + Log.Warning("Attempted to save null Metadata"); + return; + } + string metaFile = $"{settings.OutputDirectory}/.files"; + string metaJson; + try + { + metaJson = JsonSerializer.Serialize>(Metadata); + Log.Debug("metaJson: {metaJson}",metaJson); + } + catch (JsonException e) + { + Log.Warning(e, "Failed to serialize .files Json data"); + return; + } + Log.Information("Writing metadata"); + File.WriteAllText(metaFile, metaJson); + } +} diff --git a/TODO b/TODO new file mode 100644 index 0000000..01c937f --- /dev/null +++ b/TODO @@ -0,0 +1,5 @@ +- Add a pre-commit or other type of hook to ensure that the Testing directory is properly cleaned and reset before commit. +- Double check that links are processed correctly by testing against a copy of my blog source. + - Also add the appropriate metadata to the markdown files to make sure that they are properly detected. + - Potentially host a local Test version to fully ensure that everything can be made to work properly. +- Look into the possiblity of using a TemplateTemplate, And generating the pandoc template on the fly making it possible to switch out domian/prefix stuff diff --git a/Testing/src/.template b/Testing/src/.template new file mode 100755 index 0000000..9f2ef31 --- /dev/null +++ b/Testing/src/.template @@ -0,0 +1,92 @@ + + + +$-- #### STATIC HEAD ELEMENTS + + + + + + + + +$-- #### VARIABLE HEAD ELEMENTS + $if(title-prefix)$$title-prefix$ – $endif$$pagetitle$ + + +$for(author-meta)$ $-- Include author information + +$endfor$ +$if(date-meta)$ $-- Include date information + +$endif$ +$if(keywords)$ $-- Include Keywords + +$endif$ +$if(description-meta)$ $-- Include description + + +$endif$ +$if(image)$ $-- OPTIONAL set image for link cards + +$else$ $-- Defaults to favicon + +$endif$ +$if(alt-text)$ $-- OPTIONAL Include alt text for twitter + +$endif$ + +$if(math)$ $-- I think this includes some kind of css + $math$ +$endif$ + +$for(header-includes)$ $-- Include any external data + $header-includes$ +$endfor$ + +$if(needs-syntax-highlighting)$ $-- If set then include css and JS for syntax highlighting + + +$endif$ +$if(style)$ $-- Include page specfic style (To reduce size of page load on other pages) + +$endif$ + + $-- default to sans-serif --$ + +$body$ + + + + diff --git a/Utils/Utils.GetFiles.cs b/Utils/Utils.GetFiles.cs new file mode 100644 index 0000000..a1ee635 --- /dev/null +++ b/Utils/Utils.GetFiles.cs @@ -0,0 +1,9 @@ +namespace csSiteGen; + +public static partial class Utils { + + // Abstract Directory.GetFiles to get a List as it will be easier to handle later. + public static List GetFiles(DirectoryInfo dir){ + return dir.GetFiles("*",SearchOption.AllDirectories).ToList(); + } +} diff --git a/Utils/Utils.PathSearch.cs b/Utils/Utils.PathSearch.cs new file mode 100644 index 0000000..fdb687b --- /dev/null +++ b/Utils/Utils.PathSearch.cs @@ -0,0 +1,52 @@ +using Serilog; +namespace csSiteGen; + +public static partial class Utils { + + static Dictionary PathSearchMemo = new(); + + /// + /// Find executable in path + /// + public static string PathSearch(string Program){ + + if (PathSearchMemo.TryGetValue(Program, out string? result)) + { + Log.Debug("Memo Hit for {program}", Program); + return result; + } + + var path = System.Environment.GetEnvironmentVariable("PATH")?.Split(':'); + if (path is null) + { + Log.Error("Failed to read PATH environment variable."); + return string.Empty; + } + + List candidateExecutables = new(); + + foreach (var dir in path) + { + // Directories do not need to exist for them to be in path + // This check avoids attempting to look in directories that + // Do not exist. + if (Directory.Exists(dir)) + { + Log.Information("Searching for {Program} in {dir}", Program, dir); + candidateExecutables.AddRange(Directory.GetFiles(dir,$"{Program}")); + } + } + + if (candidateExecutables.Count == 0) + { + Log.Warning("Dependency {Program} not found",Program); + return string.Empty; + } + + result = candidateExecutables.First(); + PathSearchMemo.Add(Program,result); + Log.Information("Found {program} at {path}",Program, result); + Log.Debug("Adding {@entry} to PathSearchMemo", PathSearchMemo.Last()); + return result; + } +} diff --git a/csSiteGen.csproj b/csSiteGen.csproj index 6e06db1..8f3ade6 100644 --- a/csSiteGen.csproj +++ b/csSiteGen.csproj @@ -10,15 +10,17 @@ bin\$(Configuration)\ - net6.0 + net7.0 enable enable - - + + + +