This repository has been archived on 2023-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
mfgames-nitride-cil/src/MfGames.Nitride.IO/Contents/WriteFiles.cs

228 lines
7.0 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using Serilog;
using Zio;
namespace MfGames.Nitride.IO.Contents;
/// <summary>
/// An operation that writes out entities to a file system.
/// </summary>
[WithProperties]
public partial class WriteFiles : FileSystemOperationBase, IOperation
{
private readonly IValidator<WriteFiles> validator;
private Dictionary<Type, Func<IContent, Stream>> factories;
public WriteFiles(
IValidator<WriteFiles> validator,
ILogger logger,
IFileSystem fileSystem)
: base(fileSystem)
{
this.Logger = logger;
this.validator = validator;
this.TextEncoding = Encoding.UTF8;
this.factories = new Dictionary<Type, Func<IContent, Stream>>
{
[typeof(IBinaryContent)] = GetBinaryStream,
[typeof(ITextContent)] = this.GetTextStream,
};
}
public ILogger Logger { get; set; } = null!;
public Dictionary<Type, Func<IContent, Stream>> StreamFactories
{
get => this.factories;
set => this.factories = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// Gets or sets the encoding to force any text output.
/// </summary>
public Encoding TextEncoding { get; set; }
/// <summary>
/// Writes out all the files to the given file system using the paths
/// currently stored in the `UPath` component. Only files that have
/// a path and a registered writer will be written.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <returns>The same list of entities without changes.</returns>
public override IEnumerable<Entity> Run(IEnumerable<Entity> entities)
{
this.validator.ValidateAndThrow(this);
IEnumerable<Entity> results = entities.SelectEntity<UPath>(this.Process)
.ToList();
return results;
}
public WriteFiles WithFileSystem(IFileSystem fileSystem)
{
this.FileSystem = fileSystem;
return this;
}
private static Stream GetBinaryStream(IContent content)
{
return ((IBinaryContent)content).GetStream();
}
private Stream GetTextStream(IContent content)
{
// See if we can convert the stream first. If that is the case, then
// we don't have to load it entirely in memory.
if (content is IBinaryContentConvertable convertable)
{
return convertable.ToBinaryContent()
.GetStream();
}
// We have the load the text into memory and convert it.
var textContent = (ITextContent)content;
string text = textContent.GetReader()
.ReadToEnd();
var stream = new MemoryStream();
var writer = new StreamWriter(stream, this.TextEncoding ?? Encoding.UTF8);
writer.Write(text);
writer.Flush();
stream.Position = 0;
return stream;
}
/// <summary>
/// Internal method for writing out the entity. This handles the
/// registered writers to allow for multiple `IContent` types being
/// written out automatically.
/// </summary>
/// <param name="entity">The entity to write out.</param>
/// <param name="path">The path of the entity.</param>
/// <returns>The entity passed in.</returns>
private Entity Process(
Entity entity,
UPath path)
{
// See if we have any content. If we don't, then there is nothing
// to do.
if (!entity.HasContent())
{
return entity;
}
// First see if we have a factory for the exact type of content.
IContent content = entity.GetContent();
if (this.factories.TryGetValue(content.GetType(), out Func<IContent, Stream>? getStream))
{
Stream stream = getStream(content);
return this.Process(entity, path, stream);
}
// If we have an easy conversion, then use that so we don't have to
// walk up the tree looking for one we do have.
if (content is IBinaryContentConvertable binaryConvertable
&& this.factories.TryGetValue(typeof(IBinaryContent), out Func<IContent, Stream>? binaryContent))
{
Stream stream = binaryContent(binaryConvertable.ToBinaryContent());
return this.Process(entity, path, stream);
}
if (content is ITextContentConvertable textConvertable
&& this.factories.TryGetValue(typeof(ITextContent), out Func<IContent, Stream>? textContent))
{
Stream stream = textContent(textConvertable.ToTextContent());
return this.Process(entity, path, stream);
}
// For everything else, we have to find a content that we have a
// registered type for by walking up the inheritance tree and
// finding the right type.
List<Type> types = new() { content.GetType() };
while (types.Count > 0)
{
// Check to see if we have any of these types.
Func<IContent, Stream>? found = types
.Select(x => this.factories.TryGetValue(x, out Func<IContent, Stream>? factory) ? factory : null)
.FirstOrDefault(x => x != null);
if (found != null)
{
Stream stream = found(content);
return this.Process(entity, path, stream);
}
// We didn't find one, so add all the parent types and try
// again with the new list.
types = types.SelectMany(x => new[] { x.BaseType }.Union(x.GetInterfaces()))
.Where(x => x != null)
.Select(x => x!)
.ToList();
}
// If we got this far, we never found a content to handle.
throw new InvalidOperationException(
"Cannot write out entity "
+ path
+ " because cannot determine how to get a stream out content type "
+ content.GetType()
.FullName
+ ". To resolve, register a function to this.StreamFactories.");
}
/// <summary>
/// Writes out a stream to the given path in the file system.
/// </summary>
/// <param name="entity">The entity being written out.</param>
/// <param name="path">The path to write out, directories will be created.</param>
/// <param name="stream">The stream to write out.</param>
/// <returns>The entity passed in.</returns>
private Entity Process(
Entity entity,
UPath path,
Stream stream)
{
// Make sure we have the directory structure.
UPath directory = path.GetDirectory();
if (directory != "/")
{
this.FileSystem.CreateDirectory(directory);
}
// Write out the file.
using Stream fileStream = this.FileSystem.CreateFile(path);
stream.CopyTo(fileStream);
stream.Close();
// Return the entity because we've written out the files.
return entity;
}
}