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; /// /// An operation that writes out entities to a file system. /// [WithProperties] public partial class WriteFiles : FileSystemOperationBase, IOperation { private readonly IValidator validator; private Dictionary> factories; public WriteFiles( IValidator validator, ILogger logger, IFileSystem fileSystem) : base(fileSystem) { this.Logger = logger; this.validator = validator; this.TextEncoding = Encoding.UTF8; this.factories = new Dictionary> { [typeof(IBinaryContent)] = GetBinaryStream, [typeof(ITextContent)] = this.GetTextStream, }; } public ILogger Logger { get; set; } = null!; public Dictionary> StreamFactories { get => this.factories; set => this.factories = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Gets or sets the encoding to force any text output. /// public Encoding TextEncoding { get; set; } /// /// 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. /// /// The entities to parse. /// The same list of entities without changes. public override IEnumerable Run(IEnumerable entities) { this.validator.ValidateAndThrow(this); IEnumerable results = entities.SelectEntity(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; } /// /// Internal method for writing out the entity. This handles the /// registered writers to allow for multiple `IContent` types being /// written out automatically. /// /// The entity to write out. /// The path of the entity. /// The entity passed in. 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? 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? binaryContent)) { Stream stream = binaryContent(binaryConvertable.ToBinaryContent()); return this.Process(entity, path, stream); } if (content is ITextContentConvertable textConvertable && this.factories.TryGetValue( typeof(ITextContent), out Func? 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 types = new() { content.GetType() }; while (types.Count > 0) { // Check to see if we have any of these types. Func? found = types .Select( x => this.factories.TryGetValue( x, out Func? 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."); } /// /// Writes out a stream to the given path in the file system. /// /// The entity being written out. /// The path to write out, directories will be created. /// The stream to write out. /// The entity passed in. 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; } }