using System; using System.Collections.Generic; using System.IO; using System.Linq; using SabreTools.Core; using SabreTools.IO; using SabreTools.Logging; namespace SabreTools.FileTypes { /// /// Represents a folder for reading and writing /// public class Folder : BaseFile { #region Protected instance variables protected List _children; /// /// Logging object /// protected Logger logger; /// /// Static logger for static methods /// protected static Logger staticLogger = new Logger(); /// /// Flag specific to Folder to omit Machine name from output path /// private bool writeToParent = false; #endregion #region Constructors /// /// Create a new folder with no base file /// /// True to write directly to parent, false otherwise public Folder(bool writeToParent = false) : base() { this.Type = FileType.Folder; this.writeToParent = writeToParent; logger = new Logger(this); } /// /// Create a new folder from the given file /// /// Name of the file to use as an archive /// True for opening file as read, false for opening file as write /// True if hashes for this file should be calculated, false otherwise (default) public Folder(string filename, bool getHashes = false) : base(filename, getHashes) { this.Type = FileType.Folder; logger = new Logger(this); } /// /// Create an folder object of the specified type, if possible /// /// SabreTools.Library.Data.OutputFormat representing the archive to create /// Archive object representing the inputs public static Folder Create(OutputFormat outputFormat) { switch (outputFormat) { case OutputFormat.Folder: return new Folder(false); case OutputFormat.ParentFolder: return new Folder(true); case OutputFormat.TapeArchive: return new TapeArchive(); case OutputFormat.Torrent7Zip: return new SevenZipArchive(); case OutputFormat.TorrentGzip: case OutputFormat.TorrentGzipRomba: return new GZipArchive(); case OutputFormat.TorrentLRZip: return new LRZipArchive(); case OutputFormat.TorrentLZ4: return new LZ4Archive(); case OutputFormat.TorrentRar: return new RarArchive(); case OutputFormat.TorrentXZ: case OutputFormat.TorrentXZRomba: return new XZArchive(); case OutputFormat.TorrentZip: return new ZipArchive(); case OutputFormat.TorrentZPAQ: return new ZPAQArchive(); case OutputFormat.TorrentZstd: return new ZstdArchive(); default: return null; } } #endregion #region Extraction /// /// Attempt to extract a file as an archive /// /// Output directory for archive extraction /// True if the extraction was a success, false otherwise public virtual bool CopyAll(string outDir) { // Copy all files from the current folder to the output directory recursively try { // Make sure the folders exist Directory.CreateDirectory(this.Filename); Directory.CreateDirectory(outDir); DirectoryCopy(this.Filename, outDir, true); } catch (Exception ex) { logger.Error(ex); return false; } return true; } // https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) { // Get the subdirectories for the specified directory. DirectoryInfo dir = new DirectoryInfo(sourceDirName); if (!dir.Exists) { throw new DirectoryNotFoundException( "Source directory does not exist or could not be found: " + sourceDirName); } DirectoryInfo[] dirs = dir.GetDirectories(); // If the destination directory doesn't exist, create it. if (!Directory.Exists(destDirName)) { Directory.CreateDirectory(destDirName); } // Get the files in the directory and copy them to the new location. FileInfo[] files = dir.GetFiles(); foreach (FileInfo file in files) { string temppath = Path.Combine(destDirName, file.Name); file.CopyTo(temppath, false); } // If copying subdirectories, copy them and their contents to new location. if (copySubDirs) { foreach (DirectoryInfo subdir in dirs) { string temppath = Path.Combine(destDirName, subdir.Name); DirectoryCopy(subdir.FullName, temppath, copySubDirs); } } } /// /// Attempt to extract a file from an archive /// /// Name of the entry to be extracted /// Output directory for archive extraction /// Name of the extracted file, null on error public virtual string CopyToFile(string entryName, string outDir) { string realentry = null; // Copy single file from the current folder to the output directory, if exists try { // Make sure the folders exist Directory.CreateDirectory(this.Filename); Directory.CreateDirectory(outDir); // Get all files from the input directory List files = DirectoryExtensions.GetFilesOrdered(this.Filename); // Now sort through to find the first file that matches string match = files.Where(s => s.EndsWith(entryName)).FirstOrDefault(); // If we had a file, copy that over to the new name if (!string.IsNullOrWhiteSpace(match)) { realentry = match; File.Copy(match, Path.Combine(outDir, entryName)); } } catch (Exception ex) { logger.Error(ex); return realentry; } return realentry; } /// /// Attempt to extract a stream from an archive /// /// Name of the entry to be extracted /// MemoryStream representing the entry, null on error public virtual (MemoryStream, string) CopyToStream(string entryName) { MemoryStream ms = new MemoryStream(); string realentry = null; // Copy single file from the current folder to the output directory, if exists try { // Make sure the folders exist Directory.CreateDirectory(this.Filename); // Get all files from the input directory List files = DirectoryExtensions.GetFilesOrdered(this.Filename); // Now sort through to find the first file that matches string match = files.Where(s => s.EndsWith(entryName)).FirstOrDefault(); // If we had a file, copy that over to the new name if (!string.IsNullOrWhiteSpace(match)) { File.OpenRead(match).CopyTo(ms); realentry = match; } } catch (Exception ex) { logger.Error(ex); return (ms, realentry); } return (ms, realentry); } #endregion #region Information /// /// Generate a list of immediate children from the current folder /// /// List of BaseFile objects representing the found data public virtual List GetChildren() { if (_children == null || _children.Count == 0) { _children = new List(); foreach (string file in Directory.EnumerateFiles(this.Filename, "*", SearchOption.TopDirectoryOnly)) { BaseFile nf = GetInfo(file, hashes: this.AvailableHashes); _children.Add(nf); } foreach (string dir in Directory.EnumerateDirectories(this.Filename, "*", SearchOption.TopDirectoryOnly)) { Folder fl = new Folder(dir); _children.Add(fl); } } return _children; } /// /// Generate a list of empty folders in an archive /// /// Input file to get data from /// List of empty folders in the folder public virtual List GetEmptyFolders() { return DirectoryExtensions.ListEmpty(this.Filename); } #endregion #region Writing /// /// Write an input file to an output folder /// /// Input filename to be moved /// Output directory to build to /// BaseFile representing the new information /// True if the write was a success, false otherwise /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. public virtual bool Write(string inputFile, string outDir, BaseFile baseFile) { FileStream fs = File.OpenRead(inputFile); return Write(fs, outDir, baseFile); } /// /// Write an input stream to an output folder /// /// Input stream to be moved /// Output directory to build to /// BaseFile representing the new information /// True if the write was a success, false otherwise /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. public virtual bool Write(Stream inputStream, string outDir, BaseFile baseFile) { bool success = false; // If either input is null or empty, return if (inputStream == null || baseFile == null || baseFile.Filename == null) return success; // If the stream is not readable, return if (!inputStream.CanRead) return success; // Set internal variables FileStream outputStream = null; // Get the output folder name from the first rebuild rom string fileName; if (writeToParent) fileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(baseFile.Filename)); else fileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(baseFile.Parent), Sanitizer.RemovePathUnsafeCharacters(baseFile.Filename)); try { // If the full output path doesn't exist, create it if (!Directory.Exists(Path.GetDirectoryName(fileName))) Directory.CreateDirectory(Path.GetDirectoryName(fileName)); // Overwrite output files by default outputStream = File.Create(fileName); // If the output stream isn't null if (outputStream != null) { // Copy the input stream to the output inputStream.Seek(0, SeekOrigin.Begin); int bufferSize = 4096 * 128; byte[] ibuffer = new byte[bufferSize]; int ilen; while ((ilen = inputStream.Read(ibuffer, 0, bufferSize)) > 0) { outputStream.Write(ibuffer, 0, ilen); outputStream.Flush(); } outputStream.Dispose(); if (!string.IsNullOrWhiteSpace(baseFile.Date)) File.SetCreationTime(fileName, DateTime.Parse(baseFile.Date)); success = true; } } catch (Exception ex) { logger.Error(ex); success = false; } finally { outputStream?.Dispose(); } return success; } /// /// Write a set of input files to an output folder (assuming the same output archive name) /// /// Input files to be moved /// Output directory to build to /// BaseFiles representing the new information /// True if the inputs were written properly, false otherwise public virtual bool Write(List inputFiles, string outDir, List baseFiles) { throw new NotImplementedException(); } #endregion } }