Files
SabreTools/SabreTools.DatTools/DatFromDir.cs

546 lines
21 KiB
C#
Raw Normal View History

using System.Collections.Generic;
using System.IO;
using System.Threading;
2024-03-05 03:04:47 -05:00
#if NET40_OR_GREATER || NETCOREAPP
using System.Threading.Tasks;
2024-03-05 03:04:47 -05:00
#endif
2024-03-11 16:26:28 -04:00
using SabreTools.Core.Tools;
2020-12-10 23:24:09 -08:00
using SabreTools.DatFiles;
using SabreTools.DatItems;
2021-02-02 10:23:43 -08:00
using SabreTools.DatItems.Formats;
using SabreTools.FileTypes;
2020-12-10 22:31:23 -08:00
using SabreTools.FileTypes.Archives;
2024-03-04 23:56:05 -05:00
using SabreTools.Hashing;
2024-04-24 13:45:38 -04:00
using SabreTools.IO.Extensions;
2024-10-24 00:36:44 -04:00
using SabreTools.IO.Logging;
2020-12-10 23:24:09 -08:00
namespace SabreTools.DatTools
{
2020-12-10 14:11:35 -08:00
/// <summary>
/// This file represents all methods related to populating a DatFile
/// from a set of files and directories
/// </summary>
2020-12-10 15:42:39 -08:00
public class DatFromDir
{
2024-10-24 05:52:48 -04:00
#region Fields
/// <summary>
/// Hashes to include in the information
/// </summary>
private readonly HashType[] _hashes;
/// <summary>
/// Type of files that should be skipped
/// </summary>
private readonly SkipFileType _skipFileType;
/// <summary>
/// Indicates if blank items should be created for empty folders
/// </summary>
private readonly bool _addBlanks;
#endregion
2020-12-10 13:30:08 -08:00
#region Logging
/// <summary>
/// Logging object
/// </summary>
private static readonly Logger logger = new();
2020-12-10 13:30:08 -08:00
#endregion
2024-10-24 05:52:48 -04:00
#region Constructors
public DatFromDir(HashType[]? hashes, SkipFileType skipFileType, bool addBlanks)
{
_hashes = hashes ?? [HashType.CRC32, HashType.MD5, HashType.SHA1];
_skipFileType = skipFileType;
_addBlanks = addBlanks;
}
#endregion
/// <summary>
/// Create a new Dat from a directory
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="basePath">Base folder to be used in creating the DAT</param>
2025-01-05 21:51:35 -05:00
/// <param name="asFile">TreatAsFile representing CHD and Archive scanning</param>
public bool PopulateFromDir(DatFile datFile, string basePath, TreatAsFile asFile = 0x00)
{
// Set the progress variables
long totalSize = 0;
long currentSize = 0;
InternalStopwatch watch = new($"Populating DAT from {basePath}");
2021-02-02 14:09:49 -08:00
// Process the input
if (Directory.Exists(basePath))
{
logger.Verbose($"Folder found: {basePath}");
// Get a list of all files to process
2024-02-29 00:14:16 -05:00
#if NET20 || NET35
List<string> files = [.. Directory.GetFiles(basePath, "*")];
2024-02-29 00:14:16 -05:00
#else
List<string> files = [.. Directory.EnumerateFiles(basePath, "*", SearchOption.AllDirectories)];
2024-02-29 00:14:16 -05:00
#endif
// Loop through and add the file sizes
2024-02-28 22:54:56 -05:00
#if NET452_OR_GREATER || NETCOREAPP
2024-10-24 05:52:48 -04:00
Parallel.ForEach(files, Core.Globals.ParallelOptions, item =>
2024-02-28 22:54:56 -05:00
#elif NET40_OR_GREATER
Parallel.ForEach(files, item =>
#else
foreach (var item in files)
#endif
{
Interlocked.Add(ref totalSize, new FileInfo(item).Length);
2024-02-28 21:59:13 -05:00
#if NET40_OR_GREATER || NETCOREAPP
});
2024-02-28 21:59:13 -05:00
#else
}
#endif
// Process the files in the main folder or any subfolder
logger.User(totalSize, currentSize);
foreach (string item in files)
{
currentSize += new FileInfo(item).Length;
2024-07-19 15:14:30 -04:00
2025-01-05 21:51:35 -05:00
CheckFileForHashes(datFile, item, basePath, asFile);
logger.User(totalSize, currentSize, item);
}
// Now find all folders that are empty, if we are supposed to
2024-10-24 05:52:48 -04:00
if (_addBlanks)
2020-12-10 10:39:39 -08:00
ProcessDirectoryBlanks(datFile, basePath);
}
2023-04-17 13:22:35 -04:00
else if (System.IO.File.Exists(basePath))
{
logger.Verbose($"File found: {basePath}");
totalSize = new FileInfo(basePath).Length;
logger.User(totalSize, currentSize);
2024-02-28 19:19:50 -05:00
string? parentPath = Path.GetDirectoryName(Path.GetDirectoryName(basePath));
2025-01-05 21:51:35 -05:00
CheckFileForHashes(datFile, basePath, parentPath, asFile);
logger.User(totalSize, totalSize, basePath);
}
2021-02-02 14:09:49 -08:00
watch.Stop();
return true;
}
/// <summary>
/// Check a given file for hashes, based on current settings
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="item">Filename of the item to be checked</param>
/// <param name="basePath">Base folder to be used in creating the DAT</param>
2025-01-05 21:51:35 -05:00
/// <param name="asFile">TreatAsFile representing CHD and Archive scanning</param>
private void CheckFileForHashes(DatFile datFile, string item, string? basePath, TreatAsFile asFile)
{
// If we're in depot mode, process it separately
2020-12-10 10:39:39 -08:00
if (CheckDepotFile(datFile, item))
return;
// Initialize possible archive variables
BaseArchive? archive = FileTypeTool.CreateArchiveType(item);
// Process archives according to flags
if (archive != null)
{
// Set the archive flags
archive.SetHashTypes(_hashes);
// Skip if we're treating archives as files and skipping files
2024-12-28 20:15:32 -05:00
#if NET20 || NET35
2025-01-05 21:51:35 -05:00
if ((asFile & TreatAsFile.Archive) != 0 && _skipFileType == SkipFileType.File)
2024-02-28 22:54:56 -05:00
#else
2025-01-05 21:51:35 -05:00
if (asFile.HasFlag(TreatAsFile.Archive) && _skipFileType == SkipFileType.File)
2024-02-28 22:54:56 -05:00
#endif
{
return;
}
// Skip if we're skipping archives
2024-10-24 05:52:48 -04:00
else if (_skipFileType == SkipFileType.Archive)
{
return;
}
// Process as archive if we're not treating archives as files
2024-12-28 20:15:32 -05:00
#if NET20 || NET35
2025-01-05 21:51:35 -05:00
else if ((asFile & TreatAsFile.Archive) == 0)
2024-02-28 22:54:56 -05:00
#else
2025-01-05 21:51:35 -05:00
else if (!asFile.HasFlag(TreatAsFile.Archive))
2024-02-28 22:54:56 -05:00
#endif
{
var extracted = archive.GetChildren();
// If we have internal items to process, do so
if (extracted != null)
2020-12-10 10:39:39 -08:00
ProcessArchive(datFile, item, basePath, extracted);
// Now find all folders that are empty, if we are supposed to
2024-10-24 05:52:48 -04:00
if (_addBlanks)
2020-12-10 10:39:39 -08:00
ProcessArchiveBlanks(datFile, item, basePath, archive);
}
// Process as file if we're treating archives as files
else
{
2025-01-05 21:51:35 -05:00
ProcessFile(datFile, item, basePath, asFile);
}
}
// Process non-archives according to flags
else
{
// Skip if we're skipping files
2024-10-24 05:52:48 -04:00
if (_skipFileType == SkipFileType.File)
return;
// Process as file
else
2025-01-05 21:51:35 -05:00
ProcessFile(datFile, item, basePath, asFile);
}
}
/// <summary>
/// Check an item as if it's supposed to be in a depot
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="item">Filename of the item to be checked</param>
/// <returns>True if we checked a depot file, false otherwise</returns>
2020-12-10 11:58:46 -08:00
private static bool CheckDepotFile(DatFile datFile, string item)
{
// If we're not in Depot mode, return false
2024-03-10 22:08:08 -04:00
if (datFile.Header.GetFieldValue<DepotInformation?>(DatHeader.OutputDepotKey)?.IsActive != true)
return false;
// Check the file as if it were in a depot
GZipArchive gzarc = new(item);
2024-02-28 19:19:50 -05:00
BaseFile? baseFile = gzarc.GetTorrentGZFileInfo();
// If the rom is valid, add it
if (baseFile != null && baseFile.Filename != null)
{
// Add the list if it doesn't exist already
Rom rom = new(baseFile);
2020-12-14 15:43:01 -08:00
datFile.Items.Add(rom.GetKey(ItemKey.CRC), rom);
logger.Verbose($"File added: {Path.GetFileNameWithoutExtension(item)}");
}
else
{
logger.Verbose($"File not added: {Path.GetFileNameWithoutExtension(item)}");
return true;
}
return true;
}
/// <summary>
/// Process a single file as an archive
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="item">File to be added</param>
/// <param name="basePath">Path the represents the parent directory</param>
/// <param name="extracted">List of BaseFiles representing the internal files</param>
2024-02-28 19:19:50 -05:00
private static void ProcessArchive(DatFile datFile, string item, string? basePath, List<BaseFile> extracted)
{
// Get the parent path for all items
2024-02-28 19:19:50 -05:00
string parent = (Path.GetDirectoryName(Path.GetFullPath(item)) + Path.DirectorySeparatorChar).Remove(0, basePath?.Length ?? 0) + Path.GetFileNameWithoutExtension(item);
// First take care of the found items
2024-02-28 22:54:56 -05:00
#if NET452_OR_GREATER || NETCOREAPP
2024-10-24 05:52:48 -04:00
Parallel.ForEach(extracted, Core.Globals.ParallelOptions, baseFile =>
2024-02-28 22:54:56 -05:00
#elif NET40_OR_GREATER
Parallel.ForEach(extracted, baseFile =>
#else
foreach (var baseFile in extracted)
#endif
{
2024-02-28 19:19:50 -05:00
DatItem? datItem = DatItem.Create(baseFile);
if (datItem == null)
2024-03-05 02:52:53 -05:00
#if NET40_OR_GREATER || NETCOREAPP
2024-02-28 19:19:50 -05:00
return;
2024-03-05 02:52:53 -05:00
#else
continue;
#endif
2024-02-28 19:19:50 -05:00
2020-12-10 10:39:39 -08:00
ProcessFileHelper(datFile, item, datItem, basePath, parent);
2024-02-28 21:59:13 -05:00
#if NET40_OR_GREATER || NETCOREAPP
});
2024-02-28 21:59:13 -05:00
#else
}
#endif
}
/// <summary>
/// Process blank folders in an archive
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="item">File containing the blanks</param>
/// <param name="basePath">Path the represents the parent directory</param>
/// <param name="archive">BaseArchive to get blanks from</param>
2024-02-28 19:19:50 -05:00
private static void ProcessArchiveBlanks(DatFile datFile, string item, string? basePath, BaseArchive archive)
{
2024-02-28 19:19:50 -05:00
List<string> empties = [];
// Get the parent path for all items
2024-02-28 19:19:50 -05:00
string parent = (Path.GetDirectoryName(Path.GetFullPath(item)) + Path.DirectorySeparatorChar).Remove(0, basePath?.Length ?? 0) + Path.GetFileNameWithoutExtension(item);
// Now get all blank folders from the archive
if (archive != null)
empties = archive.GetEmptyFolders();
// Add add all of the found empties to the DAT
2024-02-28 22:54:56 -05:00
#if NET452_OR_GREATER || NETCOREAPP
2024-10-24 05:52:48 -04:00
Parallel.ForEach(empties, Core.Globals.ParallelOptions, empty =>
2024-02-28 22:54:56 -05:00
#elif NET40_OR_GREATER
Parallel.ForEach(empties, empty =>
#else
foreach (var empty in empties)
#endif
{
var emptyMachine = new Machine();
emptyMachine.SetFieldValue<string?>(Models.Metadata.Machine.NameKey, item);
var emptyRom = new Rom();
emptyRom.SetName(Path.Combine(empty, "_"));
emptyRom.SetFieldValue<Machine?>(DatItem.MachineKey, emptyMachine);
2020-12-10 10:39:39 -08:00
ProcessFileHelper(datFile, item, emptyRom, basePath, parent);
2024-02-28 21:59:13 -05:00
#if NET40_OR_GREATER || NETCOREAPP
});
2024-02-28 21:59:13 -05:00
#else
}
#endif
}
/// <summary>
/// Process blank folders in a directory
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="basePath">Path the represents the parent directory</param>
2024-02-28 19:19:50 -05:00
private static void ProcessDirectoryBlanks(DatFile datFile, string? basePath)
{
// If we're in depot mode, we don't process blanks
2024-03-10 22:08:08 -04:00
if (datFile.Header.GetFieldValue<DepotInformation?>(DatHeader.OutputDepotKey)?.IsActive == true)
return;
2024-02-28 19:19:50 -05:00
List<string> empties = basePath.ListEmpty() ?? [];
2024-02-28 22:54:56 -05:00
#if NET452_OR_GREATER || NETCOREAPP
2024-10-24 05:52:48 -04:00
Parallel.ForEach(empties, Core.Globals.ParallelOptions, dir =>
2024-02-28 22:54:56 -05:00
#elif NET40_OR_GREATER
Parallel.ForEach(empties, dir =>
#else
foreach (var dir in empties)
#endif
{
// Get the full path for the directory
string fulldir = Path.GetFullPath(dir);
// Set the temporary variables
string gamename = string.Empty;
string romname = string.Empty;
// If we have a SuperDAT, we want anything that's not the base path as the game, and the file as the rom
if (datFile.Header.GetStringFieldValue(Models.Metadata.Header.TypeKey) == "SuperDAT")
{
2024-02-28 19:19:50 -05:00
if (basePath != null)
gamename = fulldir.Remove(0, basePath.Length + 1);
else
gamename = fulldir;
romname = "_";
}
// Otherwise, we want just the top level folder as the game, and the file as everything else
else
{
2024-02-28 19:19:50 -05:00
if (basePath != null)
{
gamename = fulldir.Remove(0, basePath.Length + 1).Split(Path.DirectorySeparatorChar)[0];
romname = Path.Combine(fulldir.Remove(0, basePath.Length + 1 + gamename.Length), "_");
}
else
{
gamename = fulldir;
romname = Path.Combine(fulldir, "_");
}
}
// Sanitize the names
gamename = gamename.Trim(Path.DirectorySeparatorChar);
romname = romname.Trim(Path.DirectorySeparatorChar);
logger.Verbose($"Adding blank empty folder: {gamename}");
var blankMachine = new Machine();
blankMachine.SetFieldValue<string?>(Models.Metadata.Machine.NameKey, gamename);
var blankRom = new Blank();
blankRom.SetName(romname);
blankRom.SetFieldValue<Machine?>(DatItem.MachineKey, blankMachine);
datFile.Items["null"]?.Add(blankRom);
2024-02-28 21:59:13 -05:00
#if NET40_OR_GREATER || NETCOREAPP
});
2024-02-28 21:59:13 -05:00
#else
}
#endif
}
/// <summary>
/// Process a single file as a file
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="item">File to be added</param>
/// <param name="basePath">Path the represents the parent directory</param>
2025-01-05 21:51:35 -05:00
/// <param name="asFile">TreatAsFile representing CHD and Archive scanning</param>
private void ProcessFile(DatFile datFile, string item, string? basePath, TreatAsFile asFile)
{
logger.Verbose($"'{Path.GetFileName(item)}' treated like a file");
2024-10-24 05:52:48 -04:00
var header = datFile.Header.GetStringFieldValue(Models.Metadata.Header.HeaderKey);
2025-01-05 21:35:06 -05:00
BaseFile? baseFile = FileTypeTool.GetInfo(item, header, _hashes);
2025-01-05 21:51:35 -05:00
DatItem? datItem = DatItem.Create(baseFile, asFile);
2024-02-28 19:19:50 -05:00
if (datItem != null)
ProcessFileHelper(datFile, item, datItem, basePath, string.Empty);
}
/// <summary>
/// Process a single file as a file (with found Rom data)
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="item">File to be added</param>
/// <param name="item">Rom data to be used to write to file</param>
/// <param name="basepath">Path the represents the parent directory</param>
/// <param name="parent">Parent game to be used</param>
2024-02-28 19:19:50 -05:00
private static void ProcessFileHelper(DatFile datFile, string item, DatItem datItem, string? basepath, string parent)
{
// If we didn't get an accepted parsed type somehow, cancel out
2024-02-28 19:19:50 -05:00
List<ItemType> parsed = [ItemType.Disk, ItemType.File, ItemType.Media, ItemType.Rom];
2024-03-11 16:26:28 -04:00
if (!parsed.Contains(datItem.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue<ItemType>()))
return;
try
{
// If the basepath doesn't end with a directory separator, add it
2024-02-28 19:19:50 -05:00
if (basepath != null && !basepath.EndsWith(Path.DirectorySeparatorChar.ToString()))
basepath += Path.DirectorySeparatorChar.ToString();
// Make sure we have the full item path
item = Path.GetFullPath(item);
// Process the item to sanitize names based on input
2020-12-10 10:39:39 -08:00
SetDatItemInfo(datFile, datItem, item, parent, basepath);
// Add the file information to the DAT
2020-12-14 15:31:28 -08:00
string key = datItem.GetKey(ItemKey.CRC);
2020-12-10 10:39:39 -08:00
datFile.Items.Add(key, datItem);
logger.Verbose($"File added: {datItem.GetName() ?? string.Empty}");
}
catch (IOException ex)
{
logger.Error(ex);
return;
}
}
/// <summary>
/// Set proper Game and Rom names from user inputs
/// </summary>
2020-12-10 10:39:39 -08:00
/// <param name="datFile">Current DatFile object to add to</param>
/// <param name="datItem">DatItem representing the input file</param>
/// <param name="item">Item name to use</param>
/// <param name="parent">Parent name to use</param>
/// <param name="basepath">Base path to use</param>
2024-02-28 19:19:50 -05:00
private static void SetDatItemInfo(DatFile datFile, DatItem datItem, string item, string parent, string? basepath)
{
// Get the data to be added as game and item names
2024-02-28 19:19:50 -05:00
string? machineName, itemName;
// If the parent is blank, then we have a non-archive file
2024-02-29 00:14:16 -05:00
if (string.IsNullOrEmpty(parent))
{
// If we have a SuperDAT, we want anything that's not the base path as the game, and the file as the rom
if (datFile.Header.GetStringFieldValue(Models.Metadata.Header.TypeKey) == "SuperDAT")
{
2024-02-28 19:19:50 -05:00
machineName = Path.GetDirectoryName(item.Remove(0, basepath?.Length ?? 0));
itemName = Path.GetFileName(item);
}
// Otherwise, we want just the top level folder as the game, and the file as everything else
else
{
2024-02-28 19:19:50 -05:00
machineName = item.Remove(0, basepath?.Length ?? 0).Split(Path.DirectorySeparatorChar)[0];
if (basepath != null)
itemName = item.Remove(0, (Path.Combine(basepath, machineName).Length));
else
itemName = item.Remove(0, (machineName.Length));
}
}
// Otherwise, we assume that we have an archive
else
{
// If we have a SuperDAT, we want the archive name as the game, and the file as everything else (?)
if (datFile.Header.GetStringFieldValue(Models.Metadata.Header.TypeKey) == "SuperDAT")
{
machineName = parent;
itemName = datItem.GetName();
}
// Otherwise, we want the archive name as the game, and the file as everything else
else
{
machineName = parent;
itemName = datItem.GetName();
}
}
// Sanitize the names
2024-02-28 19:19:50 -05:00
machineName = machineName?.Trim(Path.DirectorySeparatorChar);
itemName = itemName?.Trim(Path.DirectorySeparatorChar) ?? string.Empty;
2024-02-29 00:14:16 -05:00
if (!string.IsNullOrEmpty(machineName) && string.IsNullOrEmpty(itemName))
{
itemName = machineName;
machineName = "Default";
}
// Update machine information
2024-03-10 16:49:07 -04:00
datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.SetFieldValue<string?>(Models.Metadata.Machine.DescriptionKey, machineName);
datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.SetFieldValue<string?>(Models.Metadata.Machine.NameKey, machineName);
// If we have a Disk, then the ".chd" extension needs to be removed
2024-03-10 16:49:07 -04:00
if (datItem is Disk && itemName!.EndsWith(".chd"))
{
2024-02-29 00:14:16 -05:00
itemName = itemName.Substring(0, itemName.Length - 4);
}
// If we have a Media, then the extension needs to be removed
2024-03-10 16:49:07 -04:00
else if (datItem is Media)
{
2024-02-29 00:14:16 -05:00
if (itemName!.EndsWith(".dicf"))
itemName = itemName.Substring(0, itemName.Length - 5);
else if (itemName.EndsWith(".aaru"))
2024-02-29 00:14:16 -05:00
itemName = itemName.Substring(0, itemName.Length - 5);
else if (itemName.EndsWith(".aaruformat"))
2024-02-29 00:14:16 -05:00
itemName = itemName.Substring(0, itemName.Length - 11);
else if (itemName.EndsWith(".aaruf"))
2024-02-29 00:14:16 -05:00
itemName = itemName.Substring(0, itemName.Length - 6);
else if (itemName.EndsWith(".aif"))
2024-02-29 00:14:16 -05:00
itemName = itemName.Substring(0, itemName.Length - 4);
}
// Set the item name back
datItem.SetName(itemName);
}
}
}