/* * csSiteGen - A static site generator written in c# * Copyright © 2022 Robert Morrison * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using Serilog.Events; using Serilog; using System.CommandLine.Builder; using System.CommandLine.Help; using System.CommandLine.Parsing; using System.CommandLine; using System.Diagnostics; using System.Reflection; using Spectre.Console; using System.Text.Json; namespace csSiteGen; class Program { static int Main(string[] args) { // Get the current version number string? version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion; // Only log to file // Logging to the console is BAD practice as it tends to be messy. // TODO: Log to a known location using environment to find the correct location. Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.File("log.log") .CreateLogger(); Log.Information("Starting New Instance of csSiteGen"); if (version is not null) { Log.Information("Version: {ver}", version); } else { 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. // With further testing and handling of any edge cases it _may_ be possible to have this work anywhere. 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."); } Stopwatch TotalExecutionTime = Stopwatch.StartNew(); /* !! IMPORTANT !! WARN: This code uses system.commandline which is still in pre-release the following section of code will contain comments to explain the intent of the programmer which may be useful if system.commandline has breaking changes */ // First the option for the project directory is created var ProjectDirectoryOption = new Option( name: "--project", description: "The Directory for the project"); ProjectDirectoryOption.IsRequired = false; // it is not required as not providing it infers that the current directory is the project directory // If the option is used then the input is validated before control passes to any of the actual code. ProjectDirectoryOption.AddValidator(result => { if (!result.GetValueForOption(ProjectDirectoryOption)!.Exists) { result.ErrorMessage = $"Project directory {result.GetValueForOption(ProjectDirectoryOption)} does not exist."; } } ); // The root command is the entry point for commandline but otherwise does nothing. var rootCommand = new RootCommand("csSiteGen"); // TODO: Verify if the use of async in these functions is necessary // This creates the command for cleaning a projects output directory. var cleanCommand = new Command("clean", "Clean the projects output directory"); cleanCommand.AddOption(ProjectDirectoryOption); // This command can use the project directory option we created earlier cleanCommand.SetHandler(async (ProjectDirectory) => { await Task.Run(() => { Clean(ProjectDirectory); }); },ProjectDirectoryOption); // This creates the command for actually converting the project. var convertCommand = new Command("convert", "Convert the projects input directory and place the files in the output directory."); convertCommand.AddOption(ProjectDirectoryOption); // This command can use the project directory option convertCommand.SetHandler(async (ProjectDirectory) => { await Task.Run(() => { Convert(ProjectDirectory); }); },ProjectDirectoryOption); // Adding the commands to the root command makes them actually callable on the commandline rootCommand.AddCommand(cleanCommand); rootCommand.AddCommand(convertCommand); // The parser is what actually handles the arguments and dispatches them to the appropriate commands. // This is used instead of the simpler method of just Invoking the root command as it automatically creates usage statements. // It also makes a user aware that a subcommand needs to be used. var parser = new CommandLineBuilder(rootCommand) .UseDefaults() .Build(); parser.Invoke(args); TotalExecutionTime.Stop(); Log.Information("TotalExecutionTime {time:000}ms", TotalExecutionTime.ElapsedMilliseconds); // Providing there were no early exits it is best to properly close the log before we exit. Log.CloseAndFlush(); return 0; } static int Convert(DirectoryInfo? ProjectDirectory) { Log.Information("Convert command was called, beginning conversion."); // WARN: This is only temporary as Spectre.Console does not recognise some Linux terminals // A better solution that checks the terminal value and sets this option should be added in the future. AnsiConsole.Console.Profile.Capabilities.Ansi = true; // NOTE: Future refactors may merge ProjectSettings and RuntimeSettings ProjectSettings projectSettings = GetProjectSettings(ProjectDirectory); DirectoryInfo inputDir = new(projectSettings.Source); DirectoryInfo outputDir = new(projectSettings.Destination); RuntimeSettings settings = new(inputDir,outputDir); settings.setBaseUrl(projectSettings.BaseUrl); 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}"); Dictionary fileStatus = new(); Log.Debug("fileStatus {@fileStatus}",fileStatus); AnsiConsole.Progress() .AutoRefresh(true) .Columns(new ProgressColumn[] { 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); } }); Log.Information("Conversion Status {@status}", fileStatus); var Failed = fileStatus.Where(x => x.Value == false); if ( Failed.Count() > 0) { foreach (var fail in Failed) { AnsiConsole.MarkupLineInterpolated( $"[red]File [blue]\"{fail.Key}\"[/] failed to convert[/]"); AnsiConsole.MarkupLine("[yellow]See log for more details[/]"); } } return 0; } static int Clean(DirectoryInfo? ProjectDirectory) { Log.Information("Clean command was called, Beginning cleaning"); // NOTE: Future refactors may merge ProjectSettings and RuntimeSettings ProjectSettings projectSettings = GetProjectSettings(ProjectDirectory); DirectoryInfo inputDir = new(projectSettings.Source); DirectoryInfo outputDir = new(projectSettings.Destination); 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; } } static void EnforceConsistency(ProjectSettings projectSettings) { // Grab the metadata. // Read the metadata. // Find deleted files. // Figure out what the new name for those files would be. // Remove those files. } static ProjectSettings GetProjectSettings(DirectoryInfo? ProjectDirectory) { // TODO: implement proper error handling where file access is performed. if (ProjectDirectory is null) { // use the current directory if no project directory is passed. ProjectDirectory = new DirectoryInfo("."); } Log.Information("{projectdir} => fullname {pdfn}",ProjectDirectory, ProjectDirectory.FullName); FileInfo projectFile = new (Path.Combine(ProjectDirectory.FullName,"cssitegen.json")); if (!projectFile.Exists) { Log.Fatal("Cannot locate project file {pf} in {dir}",projectFile,ProjectDirectory); Environment.Exit(1); } Log.Information("Located Project File {pf}",projectFile.FullName); ProjectSettings? projectSettings = JsonSerializer .Deserialize( projectFile .OpenText() .ReadToEnd() ); if (projectSettings is null) { Log.Fatal("Cannot deserialize projectFile"); Environment.Exit(1); } projectSettings.setProjectRoot(ProjectDirectory); Log.Information("{@ps}",projectSettings); return projectSettings; } }