cssitegen/Program.cs
Robert Morrison 700dc715fb
chore(dependencies): Upgrade dependencies
This commit brings the dependencies up to date.

This more importantly brings the stable version of system.commandline
and all the changes needed to make it work properly with the program.

Importantly this changes how validation is done, and how defaults are
passed to commandline. This also allows me to remove the original null
protection as when the argument is not specified commandline defaults to
the current directory.

This has also meant I can remove unnecessary async calls that appear to
have no performance benefits, and remove some null checking that isn't
needed any more.
2026-01-03 04:00:54 +00:00

314 lines
11 KiB
C#

/*
* csSiteGen - A static site generator written in c#
* Copyright © 2022 Robert Morrison<sherlock5512>
*
* 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 <http://www.gnu.org/licenses/>.
*/
using Serilog;
using System.CommandLine;
using System.Diagnostics;
using System.Reflection;
using Spectre.Console;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace csSiteGen;
class Program
{
static int Main(string[] args)
{
// Get the current version number
string? version = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.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();
/* WARN:
This code uses system.commandline
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
Option<DirectoryInfo> ProjectDirectoryOption = new("--project","-p")
{
Description = "The directory of the project",
Required = false,
DefaultValueFactory = _ =>
{
return new DirectoryInfo(Directory.GetCurrentDirectory());
}
};
// Add a validator as we want the directory to exist.
ProjectDirectoryOption.Validators.Add(result =>
{
if (! result.GetValue(ProjectDirectoryOption)!.Exists)
{
result.AddError($"Directory {result.GetValue(ProjectDirectoryOption)} does not exist");
Log.Fatal($"Passed directory '{result.GetValue(ProjectDirectoryOption)}' does not exist");
}
});
// The root command is the entry point for commandline but otherwise does nothing.
RootCommand rootCommand = new("csSiteGen");
// This creates the command for cleaning a projects output directory.
var cleanCommand = new Command("clean", "Clean the projects output directory");
cleanCommand.Options.Add(ProjectDirectoryOption); // This command can use the project directory option we created earlier
cleanCommand.SetAction(parseResult =>
{
Clean(parseResult.GetValue<DirectoryInfo>("--project")!);
});
// 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.Options.Add(ProjectDirectoryOption); // This command can use the project directory option
convertCommand.SetAction(parseResult =>
{
Convert(parseResult.GetValue<DirectoryInfo>("--project")!);
});
// Adding the commands to the root command makes them actually callable on the commandline
rootCommand.Subcommands.Add(cleanCommand);
rootCommand.Subcommands.Add(convertCommand);
// Parse the commandline and immediately execute.
rootCommand.Parse(args).Invoke();
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
// TODO: A better solution that checks the terminal value and sets this option should be added in the future.
AnsiConsole.Console.Profile.Capabilities.Ansi = true;
ProjectSettings settings = GetProjectSettings(ProjectDirectory);
List<SiteFile> siteFiles = new();
Utils.GetFiles(settings.InputDirectory).ForEach(x => {
siteFiles.Add(new SiteFile(x));
Log.Debug("Found file {file}",x.FullName);
});
Log.Information("SiteFiles Found {count}", siteFiles.Count);
Console.WriteLine($"Converting {siteFiles.Count} files from {settings.InputDirectory.FullName} to {settings.OutputDirectory.FullName}");
Dictionary<string,bool> fileStatus = new();
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].FullName);
fileStatus.Add(siteFiles[i].FullName,res);
overallTask.Increment(1);
}
});
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[/]");
}
}
EnforceConsistency(settings);
return 0;
}
static int Clean(DirectoryInfo ProjectDirectory)
{
Log.Information("Clean command was called, Beginning cleaning");
ProjectSettings settings = GetProjectSettings(ProjectDirectory);
if (!settings.OutputDirectory.Exists)
{
Log.Warning("Not deleting {dir} as it doesn't exist",settings.OutputDirectory.FullName);
AnsiConsole.MarkupLineInterpolated($"[bold][[[yellow]Warning[/]]][/] Not cleaning [blue]\"{settings.OutputDirectory.FullName}\"[/] as it does not exist.");
return 0; // success because it doesn't exist.
}
try
{
Log.Information("Cleaning {dir}",settings.OutputDirectory.FullName);
AnsiConsole.MarkupInterpolated($"Cleaning [blue]\"{settings.OutputDirectory.FullName}\"[/]");
settings.OutputDirectory.Delete(recursive: true);
settings.OutputDirectory.Create();
AnsiConsole.MarkupLine(" [bold][[[green]OK[/]]][/]");
AnsiConsole.MarkupLineInterpolated($"\t[grey]>>[/] [green]All files in {settings.OutputDirectory.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.", settings.OutputDirectory.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}", settings.OutputDirectory.FullName);
return 1;
}
}
static void EnforceConsistency(ProjectSettings projectSettings)
{
AnsiConsole.MarkupLine("[blue]Checking for stale files[/]");
// Grab the metadata.
List<string> staleMetadataFiles = new();
var metadata = SiteFile.getMetadata(projectSettings);
// Read the metadata.
foreach (var entry in metadata)
{
var file = entry.Key;
Log.Information("Checking if {f} has been deleted", file);
if (!File.Exists(file))
{
Log.Information("{f} HAS been deleted", file);
// what should that files new name be.
var newFileName = Conversions.GetNewName(new FileInfo(file),projectSettings);
try{
// Delete that file and mark the metadata for removal.
File.Delete(newFileName);
AnsiConsole.MarkupLineInterpolated($"[red]Deleted [blue]{newFileName}[/][/]");
Log.Information("Deleted {f}",newFileName);
staleMetadataFiles.Add(file);
}
catch (Exception e)
{
Log.Error(e,"Could not delete {f}",newFileName);
}
}
SiteFile.invalidateMetadata(staleMetadataFiles,projectSettings);
}
}
static ProjectSettings GetProjectSettings(DirectoryInfo ProjectDirectory)
{
// TODO: implement proper error handling where file access is performed.
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);
AnsiConsole.MarkupLineInterpolated($"[red]No project file found in [yellow]{ProjectDirectory}[/][/]");
Environment.Exit(1);
}
Log.Information("Located Project File {pf}",projectFile.FullName);
ProjectSettings? projectSettings = JsonSerializer
.Deserialize<ProjectSettings>(
projectFile
.OpenText()
.ReadToEnd(),
SourceGenerationContext.Default.ProjectSettings
);
if (projectSettings is null)
{
Log.Fatal("Cannot deserialize projectFile");
Environment.Exit(1);
}
projectSettings.setProjectRoot(ProjectDirectory);
return projectSettings;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(ProjectSettings))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}