using System; using System.Collections.Generic; using System.IO; using System.Linq; using SabreTools.Core.Tools; using SabreTools.FileTypes.Archives; using SabreTools.IO; using SabreTools.IO.Extensions; 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(); /// /// Flag specific to Folder to omit Machine name from output path /// private readonly 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 /// /// OutputFormat representing the archive to create /// Archive object representing the inputs public static Folder? Create(OutputFormat outputFormat) { return outputFormat switch { OutputFormat.Folder => new Folder(false), OutputFormat.ParentFolder => new Folder(true), OutputFormat.TapeArchive => new TapeArchive(), OutputFormat.Torrent7Zip => new SevenZipArchive(), OutputFormat.TorrentGzip => new GZipArchive(), OutputFormat.TorrentGzipRomba => new GZipArchive(), OutputFormat.TorrentLRZip => new LRZipArchive(), OutputFormat.TorrentLZ4 => new LZ4Archive(), OutputFormat.TorrentRar => new RarArchive(), OutputFormat.TorrentXZ => new XZArchive(), OutputFormat.TorrentXZRomba => new XZArchive(), OutputFormat.TorrentZip => new ZipArchive(), OutputFormat.TorrentZPAQ => new ZPAQArchive(), OutputFormat.TorrentZstd => new ZstdArchive(), _ => 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) { // If we have an invalid filename if (this.Filename == null) return false; // 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(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; // If we have an invalid filename if (this.Filename == null) return 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 = PathTool.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.IsNullOrEmpty(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 /// Stream representing the entry, null on error public virtual (Stream?, string?) GetEntryStream(string entryName) { var ms = new MemoryStream(); string? realentry = null; // If we have an invalid filename if (this.Filename == null) return (null, 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 = PathTool.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.IsNullOrEmpty(match)) { #if NET20 || NET35 var tempStream = File.OpenRead(match); byte[] buffer = new byte[32768]; int read; while ((read = tempStream.Read(buffer, 0, buffer.Length)) > 0) { ms.Write(buffer, 0, read); } #else File.OpenRead(match).CopyTo(ms); #endif 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 we have an invalid filename if (this.Filename == null) return null; if (_children == null || _children.Count == 0) { _children = []; #if NET20 || NET35 foreach (string file in Directory.GetFiles(this.Filename, "*")) #else foreach (string file in Directory.EnumerateFiles(this.Filename, "*", SearchOption.TopDirectoryOnly)) #endif { BaseFile? nf = GetInfo(file, hashes: this.AvailableHashTypes); if (nf != null) _children.Add(nf); } #if NET20 || NET35 foreach (string dir in Directory.GetDirectories(this.Filename, "*")) #else foreach (string dir in Directory.EnumerateDirectories(this.Filename, "*", SearchOption.TopDirectoryOnly)) #endif { Folder fl = new(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 this.Filename.ListEmpty(); } #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, TextHelper.RemovePathUnsafeCharacters(baseFile.Filename) ?? string.Empty); else #if NET20 || NET35 fileName = Path.Combine(Path.Combine(outDir, TextHelper.RemovePathUnsafeCharacters(baseFile.Parent) ?? string.Empty), TextHelper.RemovePathUnsafeCharacters(baseFile.Filename) ?? string.Empty); #else fileName = Path.Combine(outDir, TextHelper.RemovePathUnsafeCharacters(baseFile.Parent) ?? string.Empty, TextHelper.RemovePathUnsafeCharacters(baseFile.Filename) ?? string.Empty); #endif // Replace any incorrect directory characters if (Path.DirectorySeparatorChar == '\\') fileName = fileName.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') fileName = fileName.Replace('\\', '/'); try { // If the full output path doesn't exist, create it string? dir = Path.GetDirectoryName(fileName); if (dir != null && !Directory.Exists(dir)) Directory.CreateDirectory(dir); // 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.IsNullOrEmpty(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 } }