cssitegen/SiteFile/SiteFile.cs
Robert Morrison da24073f14
feat: Add support for removing deleted source files
Detects files removed from the source directory, deletes the file from
destination, removes it from the metadata cache.
2025-03-12 02:31:54 +00:00

176 lines
5.5 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
using Serilog;
namespace csSiteGen;
/// <summary>
/// A <c>SiteFile</c> represents an individual file to be converted for the static site.
/// </summary>
public partial class SiteFile
{
FileInfo info;
Conversions.ConvertFunc ConverterFunction;
static Dictionary<string, DateTime>? Metadata = null;
/// <summary>
/// Get the metadata for the current project, loading it if not already loaded.
/// </summary>
/// <param name="settings">
/// The current <c>ProjectSettings</c>
/// </param>
/// <returns>
/// A <c>Dictionary<string,DateTime></c> that represents the metadata stored.
/// </returns>
public static Dictionary<string,DateTime> getMetadata(ProjectSettings settings) {
if (Metadata is null)
{
LoadMetadata(settings);
}
return Metadata?? new(); // Metadata is unlikely(if not impossible) to be null here but the compiler isn't convinced
}
public static void invalidateMetadata(List<string> files, ProjectSettings settings)
{
if (Metadata is null)
{
return;
}
files.ForEach( file => {
Metadata.Remove(file);
});
SaveMetadata(settings);
}
/// <summary>
/// The name of the file, Not Guaranteed to be unique.
/// Use only for output and logging, never file operations,
/// nor as a Dictionary key.
/// </summary>
public string Name => info.Name;
/// <summary>
/// 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.
/// </summary>
public string FullName => info.FullName;
public SiteFile(FileInfo fileInfo)
{
info = fileInfo;
Log.Debug("{file} extension is {ext}",fileInfo.FullName, fileInfo.Extension);
// Using this Ensures that the ConverterFunction is Always set.
// ConverterFunctions ALWAYS accept just the FileInfo, and ProjectSettings passed at convert time.
ConverterFunction = Conversions.Mappings.GetValueOrDefault(info.Extension, (Conversions.RawCpy,info.Extension)).function;
}
/// <summary>
/// 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.
/// </summary>
public bool Convert(ProjectSettings 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 has {Metadata} items", Metadata.Count);
SaveMetadata(settings);
}
return res;
}
private bool NeedsUpdating(ProjectSettings 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 blank MetaData
// The compiler however is unaware of this as it uses side effects, so we tell it there is no possible null value here.
Log.Debug("Attempting to check metadata for file {f}",info.FullName);
Log.Debug("METADATA={MD}",Metadata!.GetValueOrDefault(info.FullName,DateTime.MinValue));
Log.Debug("FILETIME={FT}",info.LastWriteTimeUtc);
Log.Debug("RES={res}",(Metadata!.GetValueOrDefault(info.FullName, DateTime.MinValue) != info.LastWriteTimeUtc));
return (Metadata!.GetValueOrDefault(info.FullName, DateTime.MinValue) != info.LastWriteTimeUtc);
}
/*
* 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 static void LoadMetadata(ProjectSettings 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<Dictionary<string, DateTime>>(metaJson,SerializeMetadataContext.Default.DictionaryStringDateTime);
Log.Debug("Deserialized Metadata with {c} items",Metadata!.Count);
}
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 static void SaveMetadata(ProjectSettings settings)
{
if (Metadata is null)
{
Log.Warning("Attempted to save null Metadata");
return;
}
string metaFile = $"{settings.OutputDirectory}/.files";
string metaJson;
try
{
metaJson = JsonSerializer.Serialize<Dictionary<string, DateTime>>(Metadata,SerializeMetadataContext.Default.DictionaryStringDateTime);
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);
}
}
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(Dictionary<string,DateTime>))]
internal partial class SerializeMetadataContext : JsonSerializerContext
{
}