2021-09-07 05:15:45 +00:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.IO;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Text;
|
2022-06-05 18:44:51 +00:00
|
|
|
|
|
|
|
using FluentValidation;
|
|
|
|
|
2022-09-06 05:53:22 +00:00
|
|
|
using MfGames.Gallium;
|
2022-06-05 18:44:51 +00:00
|
|
|
|
2022-09-06 05:53:22 +00:00
|
|
|
using MfGames.Nitride.Contents;
|
2022-06-05 18:44:51 +00:00
|
|
|
|
2021-09-07 05:15:45 +00:00
|
|
|
using Serilog;
|
2022-06-05 18:44:51 +00:00
|
|
|
|
2021-09-07 05:15:45 +00:00
|
|
|
using Zio;
|
|
|
|
|
2022-09-06 05:53:22 +00:00
|
|
|
namespace MfGames.Nitride.IO.Contents;
|
2022-06-05 18:44:51 +00:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// An operation that writes out entities to a file system.
|
|
|
|
/// </summary>
|
|
|
|
[WithProperties]
|
|
|
|
public partial class WriteFiles : FileSystemOperationBase, IOperation
|
2021-09-07 05:15:45 +00:00
|
|
|
{
|
2022-06-05 18:44:51 +00:00
|
|
|
private readonly IValidator<WriteFiles> validator;
|
|
|
|
|
|
|
|
private Dictionary<Type, Func<IContent, Stream>> factories;
|
|
|
|
|
2022-07-09 04:52:10 +00:00
|
|
|
public WriteFiles(
|
|
|
|
IValidator<WriteFiles> validator,
|
|
|
|
ILogger logger,
|
|
|
|
IFileSystem fileSystem)
|
2022-06-05 18:44:51 +00:00
|
|
|
: base(fileSystem)
|
|
|
|
{
|
|
|
|
this.Logger = logger;
|
|
|
|
this.validator = validator;
|
|
|
|
this.TextEncoding = Encoding.UTF8;
|
2022-07-09 04:52:10 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2021-09-07 05:15:45 +00:00
|
|
|
/// <summary>
|
2022-06-05 18:44:51 +00:00
|
|
|
/// Gets or sets the encoding to force any text output.
|
2021-09-07 05:15:45 +00:00
|
|
|
/// </summary>
|
2022-06-05 18:44:51 +00:00
|
|
|
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)
|
2021-09-07 05:15:45 +00:00
|
|
|
{
|
2022-06-05 18:44:51 +00:00
|
|
|
this.validator.ValidateAndThrow(this);
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-07-09 04:52:10 +00:00
|
|
|
IEnumerable<Entity> results = entities.SelectEntity<UPath>(this.Process)
|
|
|
|
.ToList();
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
return results;
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
public WriteFiles WithFileSystem(IFileSystem fileSystem)
|
|
|
|
{
|
|
|
|
this.FileSystem = fileSystem;
|
2022-07-09 04:52:10 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
return this;
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
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)
|
2021-09-07 05:15:45 +00:00
|
|
|
{
|
2022-07-09 04:52:10 +00:00
|
|
|
return convertable.ToBinaryContent()
|
|
|
|
.GetStream();
|
2021-09-07 05:15:45 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
// We have the load the text into memory and convert it.
|
|
|
|
var textContent = (ITextContent)content;
|
2022-07-09 04:52:10 +00:00
|
|
|
|
|
|
|
string text = textContent.GetReader()
|
|
|
|
.ReadToEnd();
|
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
var stream = new MemoryStream();
|
|
|
|
var writer = new StreamWriter(stream, this.TextEncoding ?? Encoding.UTF8);
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
writer.Write(text);
|
|
|
|
writer.Flush();
|
|
|
|
stream.Position = 0;
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
return stream;
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
/// <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>
|
2022-07-09 04:52:10 +00:00
|
|
|
private Entity Process(
|
|
|
|
Entity entity,
|
|
|
|
UPath path)
|
2022-06-05 18:44:51 +00:00
|
|
|
{
|
|
|
|
// See if we have any content. If we don't, then there is nothing
|
|
|
|
// to do.
|
|
|
|
if (!entity.HasContent())
|
2021-09-07 05:15:45 +00:00
|
|
|
{
|
2022-06-05 18:44:51 +00:00
|
|
|
return entity;
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
// First see if we have a factory for the exact type of content.
|
|
|
|
IContent content = entity.GetContent();
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
if (this.factories.TryGetValue(content.GetType(), out Func<IContent, Stream>? getStream))
|
|
|
|
{
|
|
|
|
Stream stream = getStream(content);
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
return this.Process(entity, path, stream);
|
2021-09-07 05:15:45 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
// 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))
|
2021-09-07 05:15:45 +00:00
|
|
|
{
|
2022-06-05 18:44:51 +00:00
|
|
|
Stream stream = binaryContent(binaryConvertable.ToBinaryContent());
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
return this.Process(entity, path, stream);
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
if (content is ITextContentConvertable textConvertable
|
|
|
|
&& this.factories.TryGetValue(typeof(ITextContent), out Func<IContent, Stream>? textContent))
|
|
|
|
{
|
|
|
|
Stream stream = textContent(textConvertable.ToTextContent());
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
return this.Process(entity, path, stream);
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
// 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() };
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
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);
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
if (found != null)
|
2021-09-07 05:15:45 +00:00
|
|
|
{
|
2022-06-05 18:44:51 +00:00
|
|
|
Stream stream = found(content);
|
2021-09-07 05:15:45 +00:00
|
|
|
|
|
|
|
return this.Process(entity, path, stream);
|
|
|
|
}
|
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
// 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();
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
// 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 "
|
2022-07-09 04:52:10 +00:00
|
|
|
+ content.GetType()
|
|
|
|
.FullName
|
2022-06-05 18:44:51 +00:00
|
|
|
+ ". To resolve, register a function to this.StreamFactories.");
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
/// <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>
|
2022-07-09 04:52:10 +00:00
|
|
|
private Entity Process(
|
|
|
|
Entity entity,
|
|
|
|
UPath path,
|
|
|
|
Stream stream)
|
2022-06-05 18:44:51 +00:00
|
|
|
{
|
|
|
|
// Make sure we have the directory structure.
|
|
|
|
UPath directory = path.GetDirectory();
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
if (directory != "/")
|
2021-09-07 05:15:45 +00:00
|
|
|
{
|
2022-06-05 18:44:51 +00:00
|
|
|
this.FileSystem.CreateDirectory(directory);
|
|
|
|
}
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
// Write out the file.
|
|
|
|
using Stream fileStream = this.FileSystem.CreateFile(path);
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
stream.CopyTo(fileStream);
|
|
|
|
stream.Close();
|
2021-09-07 05:15:45 +00:00
|
|
|
|
2022-06-05 18:44:51 +00:00
|
|
|
// Return the entity because we've written out the files.
|
|
|
|
return entity;
|
2021-09-07 05:15:45 +00:00
|
|
|
}
|
|
|
|
}
|