diff --git a/RombaSharp/Features/Archive.cs b/RombaSharp/Features/Archive.cs new file mode 100644 index 00000000..22e6a7e2 --- /dev/null +++ b/RombaSharp/Features/Archive.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.DatItems; +using SabreTools.Library.Help; +using Microsoft.Data.Sqlite; + +namespace RombaSharp.Features +{ + internal class Archive : BaseFeature + { + public const string Value = "Archive"; + + public Archive() + { + Name = Value; + Flags = new List() { "archive" }; + Description = "Adds ROM files from the specified directories to the ROM archive."; + _featureType = FeatureType.Flag; + LongDescription = @"Adds ROM files from the specified directories to the ROM archive. +Traverses the specified directory trees looking for zip files and normal files. +Unpacked files will be stored as individual entries. Prior to unpacking a zip +file, the external SHA1 is checked against the DAT index. +If -only-needed is set, only those files are put in the ROM archive that +have a current entry in the DAT index."; + Features = new Dictionary(); + + AddFeature(OnlyNeededFlag); + AddFeature(ResumeStringInput); + AddFeature(IncludeZipsInt32Input); // Defaults to 0 + AddFeature(WorkersInt32Input); + AddFeature(IncludeGZipsInt32Input); // Defaults to 0 + AddFeature(Include7ZipsInt32Input); // Defaults to 0 + AddFeature(SkipInitialScanFlag); + AddFeature(UseGolangZipFlag); + AddFeature(NoDbFlag); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get the archive scanning level + // TODO: Remove usage + int sevenzip = GetInt32(features, Include7ZipsInt32Value); + int gz = GetInt32(features, IncludeGZipsInt32Value); + int zip = GetInt32(features, IncludeZipsInt32Value); + + // Get feature flags + bool noDb = GetBoolean(features, NoDbValue); + bool onlyNeeded = GetBoolean(features, OnlyNeededValue); + + // First we want to get just all directories from the inputs + List onlyDirs = new List(); + foreach (string input in Inputs) + { + if (Directory.Exists(input)) + onlyDirs.Add(Path.GetFullPath(input)); + } + + // Then process all of the input directories into an internal DAT + DatFile df = DatFile.Create(); + foreach (string dir in onlyDirs) + { + // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + df.PopulateFromDir(dir, Hash.DeepHashes, false, false, SkipFileType.None, false, false, _tmpdir, false, null, true, null); + df.PopulateFromDir(dir, Hash.DeepHashes, false, true, SkipFileType.None, false, false, _tmpdir, false, null, true, null); + } + + // Create an empty Dat for files that need to be rebuilt + DatFile need = DatFile.Create(); + + // Open the database connection + SqliteConnection dbc = new SqliteConnection(_connectionString); + dbc.Open(); + + // Now that we have the Dats, add the files to the database + string crcquery = "INSERT OR IGNORE INTO crc (crc) VALUES"; + string md5query = "INSERT OR IGNORE INTO md5 (md5) VALUES"; + string sha1query = "INSERT OR IGNORE INTO sha1 (sha1, depot) VALUES"; + string crcsha1query = "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES"; + string md5sha1query = "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES"; + + foreach (string key in df.Items.Keys) + { + List datItems = df.Items[key]; + foreach (Rom rom in datItems) + { + // If we care about if the file exists, check the databse first + if (onlyNeeded && !noDb) + { + string query = "SELECT * FROM crcsha1 JOIN md5sha1 ON crcsha1.sha1=md5sha1.sha1" + + $" WHERE crcsha1.crc=\"{rom.CRC}\"" + + $" OR md5sha1.md5=\"{rom.MD5}\"" + + $" OR md5sha1.sha1=\"{rom.SHA1}\""; + SqliteCommand slc = new SqliteCommand(query, dbc); + SqliteDataReader sldr = slc.ExecuteReader(); + + if (sldr.HasRows) + { + // Add to the queries + if (!string.IsNullOrWhiteSpace(rom.CRC)) + crcquery += $" (\"{rom.CRC}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.MD5)) + md5query += $" (\"{rom.MD5}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.SHA1)) + { + sha1query += $" (\"{rom.SHA1}\", \"{_depots.Keys.ToList()[0]}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.CRC)) + crcsha1query += $" (\"{rom.CRC}\", \"{rom.SHA1}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.MD5)) + md5sha1query += $" (\"{rom.MD5}\", \"{rom.SHA1}\"),"; + } + + // Add to the Dat + need.Items.Add(key, rom); + } + } + // Otherwise, just add the file to the list + else + { + // Add to the queries + if (!noDb) + { + if (!string.IsNullOrWhiteSpace(rom.CRC)) + crcquery += $" (\"{rom.CRC}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.MD5)) + md5query += $" (\"{rom.MD5}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.SHA1)) + { + sha1query += $" (\"{rom.SHA1}\", \"{_depots.Keys.ToList()[0]}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.CRC)) + crcsha1query += $" (\"{rom.CRC}\", \"{rom.SHA1}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.MD5)) + md5sha1query += $" (\"{rom.MD5}\", \"{rom.SHA1}\"),"; + } + } + + // Add to the Dat + need.Items.Add(key, rom); + } + } + } + + // Now run the queries, if they're populated + if (crcquery != "INSERT OR IGNORE INTO crc (crc) VALUES") + { + SqliteCommand slc = new SqliteCommand(crcquery.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + slc.Dispose(); + } + + if (md5query != "INSERT OR IGNORE INTO md5 (md5) VALUES") + { + SqliteCommand slc = new SqliteCommand(md5query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + slc.Dispose(); + } + + if (sha1query != "INSERT OR IGNORE INTO sha1 (sha1, depot) VALUES") + { + SqliteCommand slc = new SqliteCommand(sha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + slc.Dispose(); + } + + if (crcsha1query != "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES") + { + SqliteCommand slc = new SqliteCommand(crcsha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + slc.Dispose(); + } + + if (md5sha1query != "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES") + { + SqliteCommand slc = new SqliteCommand(md5sha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + slc.Dispose(); + } + + // Create the sorting object to use and rebuild the needed files + need.RebuildGeneric(onlyDirs, _depots.Keys.ToList()[0], false /*quickScan*/, false /*date*/, + false /*delete*/, false /*inverse*/, OutputFormat.TorrentGzipRomba, false /*updateDat*/, + null /*headerToCheckAgainst*/, true /* chdsAsFiles */); + } + } +} diff --git a/RombaSharp/RombaSharp.Helpers.cs b/RombaSharp/Features/BaseFeature.cs similarity index 53% rename from RombaSharp/RombaSharp.Helpers.cs rename to RombaSharp/Features/BaseFeature.cs index 82902132..198fb295 100644 --- a/RombaSharp/RombaSharp.Helpers.cs +++ b/RombaSharp/Features/BaseFeature.cs @@ -7,13 +7,375 @@ using System.Xml; using SabreTools.Library.Data; using SabreTools.Library.DatFiles; using SabreTools.Library.DatItems; +using SabreTools.Library.Help; using SabreTools.Library.Tools; using Microsoft.Data.Sqlite; -namespace RombaSharp +namespace RombaSharp.Features { - public partial class RombaSharp + internal class BaseFeature : TopLevel { + #region Private Flag features + + internal const string CopyValue = "copy"; + internal static Feature CopyFlag + { + get + { + return new Feature( + CopyValue, + "-copy", + "Copy files to output instead of rebuilding", + FeatureType.Flag); + } + } // Unique to RombaSharp + + internal const string FixdatOnlyValue = "fixdat-only"; + internal static Feature FixdatOnlyFlag + { + get + { + return new Feature( + FixdatOnlyValue, + "-fixdatOnly", + "only fix dats and don't generate torrentzips", + FeatureType.Flag); + } + } + + internal const string LogOnlyValue = "log-only"; + internal static Feature LogOnlyFlag + { + get + { + return new Feature( + LogOnlyValue, + "-log-only", + "Only write out actions to log", + FeatureType.Flag); + } + } + + internal const string NoDbValue = "no-db"; + internal static Feature NoDbFlag + { + get + { + return new Feature( + NoDbValue, + "-no-db", + "archive into depot but do not touch DB index and ignore only-needed flag", + FeatureType.Flag); + } + } + + internal const string OnlyNeededValue = "only-needed"; + internal static Feature OnlyNeededFlag + { + get + { + return new Feature( + OnlyNeededValue, + "-only-needed", + "only archive ROM files actually referenced by DAT files from the DAT index", + FeatureType.Flag); + } + } + + internal const string SkipInitialScanValue = "skip-initial-scan"; + internal static Feature SkipInitialScanFlag + { + get + { + return new Feature( + SkipInitialScanValue, + "-skip-initial-scan", + "skip the initial scan of the files to determine amount of work", + FeatureType.Flag); + } + } + + internal const string UseGolangZipValue = "use-golang-zip"; + internal static Feature UseGolangZipFlag + { + get + { + return new Feature( + UseGolangZipValue, + "-use-golang-zip", + "use go zip implementation instead of zlib", + FeatureType.Flag); + } + } + + #endregion + + #region Private Int32 features + + internal const string Include7ZipsInt32Value = "include-7zips"; + internal static Feature Include7ZipsInt32Input + { + get + { + return new Feature( + Include7ZipsInt32Value, + "-include-7zips", + "flag value == 0 means: add 7zip files themselves into the depot in addition to their contents, flag value == 2 means add 7zip files themselves but don't add content", + FeatureType.Int32); + } + } + + internal const string IncludeGZipsInt32Value = "include-gzips"; + internal static Feature IncludeGZipsInt32Input + { + get + { + return new Feature( + IncludeGZipsInt32Value, + "-include-gzips", + "flag value == 0 means: add gzip files themselves into the depot in addition to their contents, flag value == 2 means add gzip files themselves but don't add content", + FeatureType.Int32); + } + } + + internal const string IncludeZipsInt32Value = "include-zips"; + internal static Feature IncludeZipsInt32Input + { + get + { + return new Feature( + IncludeZipsInt32Value, + "-include-zips", + "flag value == 0 means: add zip files themselves into the depot in addition to their contents, flag value == 2 means add zip files themselves but don't add content", + FeatureType.Int32); + } + } + + internal const string SubworkersInt32Value = "subworkers"; + internal static Feature SubworkersInt32Input + { + get + { + return new Feature( + SubworkersInt32Value, + "-subworkers", + "how many subworkers to launch for each worker", + FeatureType.Int32); + } + } // Defaults to Workers count in config + + internal const string WorkersInt32Value = "workers"; + internal static Feature WorkersInt32Input + { + get + { + return new Feature( + WorkersInt32Value, + "-workers", + "how many workers to launch for the job", + FeatureType.Int32); + } + } // Defaults to Workers count in config + + #endregion + + #region Private Int64 features + + internal const string SizeInt64Value = "size"; + internal static Feature SizeInt64Input + { + get + { + return new Feature( + SizeInt64Value, + "-size", + "size of the rom to lookup", + FeatureType.Int64); + } + } + + #endregion + + #region Private List features + + internal const string DatsListStringValue = "dats"; + internal static Feature DatsListStringInput + { + get + { + return new Feature( + DatsListStringValue, + "-dats", + "purge only roms declared in these dats", + FeatureType.List); + } + } + + internal const string DepotListStringValue = "depot"; + internal static Feature DepotListStringInput + { + get + { + return new Feature( + DepotListStringValue, + "-depot", + "work only on specified depot path", + FeatureType.List); + } + } + + #endregion + + #region Private String features + + internal const string BackupStringValue = "backup"; + internal static Feature BackupStringInput + { + get + { + return new Feature( + BackupStringValue, + "-backup", + "backup directory where backup files are moved to", + FeatureType.String); + } + } + + internal const string DescriptionStringValue = "description"; + internal static Feature DescriptionStringInput + { + get + { + return new Feature( + DescriptionStringValue, + "-description", + "description value in DAT header", + FeatureType.String); + } + } + + internal const string MissingSha1sStringValue = "missing-sha1s"; + internal static Feature MissingSha1sStringInput + { + get + { + return new Feature( + MissingSha1sStringValue, + "-missingSha1s", + "write paths of dats with missing sha1s into this file", + FeatureType.String); + } + } + + internal const string NameStringValue = "name"; + internal static Feature NameStringInput + { + get + { + return new Feature( + NameStringValue, + "-name", + "name value in DAT header", + FeatureType.String); + } + } + + internal const string NewStringValue = "new"; + internal static Feature NewStringInput + { + get + { + return new Feature( + NewStringValue, + "-new", + "new DAT file", + FeatureType.String); + } + } + + internal const string OldStringValue = "old"; + internal static Feature OldStringInput + { + get + { + return new Feature( + OldStringValue, + "-old", + "old DAT file", + FeatureType.String); + } + } + + internal const string OutStringValue = "out"; + internal static Feature OutStringInput + { + get + { + return new Feature( + OutStringValue, + "-out", + "output file", + FeatureType.String); + } + } + + internal const string ResumeStringValue = "resume"; + internal static Feature ResumeStringInput + { + get + { + return new Feature( + ResumeStringValue, + "-resume", + "resume a previously interrupted operation from the specified path", + FeatureType.String); + } + } + + internal const string SourceStringValue = "source"; + internal static Feature SourceStringInput + { + get + { + return new Feature( + SourceStringValue, + "-source", + "source directory", + FeatureType.String); + } + } + + #endregion + + // General settings + internal static string _logdir; // Log folder location + internal static string _tmpdir; // Temp folder location + internal static string _webdir; // Web frontend location + internal static string _baddir; // Fail-to-unpack file folder location + internal static int _verbosity; // Verbosity of the output + internal static int _cores; // Forced CPU cores + + // DatRoot settings + internal static string _dats; // DatRoot folder location + internal static string _db; // Database name + + // Depot settings + internal static Dictionary> _depots; // Folder location, Max size + + // Server settings + internal static int _port; // Web server port + + // Other internal variables + internal const string _config = "config.xml"; + internal const string _dbSchema = "rombasharp"; + internal static string _connectionString; + + public override void ProcessFeatures(Dictionary features) + { + InitializeConfiguration(); + DatabaseTools.EnsureDatabase(_dbSchema, _db, _connectionString); + } + #region Helper methods /// @@ -21,7 +383,7 @@ namespace RombaSharp /// /// List of input strings to check for, presumably file names /// Dictionary of hash/full path for each of the valid DATs - private static Dictionary GetValidDats(List inputs) + internal static Dictionary GetValidDats(List inputs) { // Get a dictionary of filenames that actually exist in the DATRoot, logging which ones are not List datRootDats = Directory.EnumerateFiles(_dats, "*", SearchOption.AllDirectories).ToList(); @@ -195,7 +557,7 @@ namespace RombaSharp if (!Directory.Exists(dats)) Directory.CreateDirectory(dats); - + db = $"{Path.GetFileNameWithoutExtension(db)}.sqlite"; string connectionString = $"Data Source={db};Version = 3;"; foreach (string key in depots.Keys) @@ -242,7 +604,7 @@ namespace RombaSharp /// /// DatFile hash information to add /// Database connection to use - private static void AddDatToDatabase(Rom dat, SqliteConnection dbc) + internal static void AddDatToDatabase(Rom dat, SqliteConnection dbc) { // Get the dat full path string fullpath = Path.Combine(_dats, (dat.MachineName == "dats" ? string.Empty : dat.MachineName), dat.Name); diff --git a/RombaSharp/Features/Build.cs b/RombaSharp/Features/Build.cs new file mode 100644 index 00000000..78ea5028 --- /dev/null +++ b/RombaSharp/Features/Build.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; + +namespace RombaSharp.Features +{ + internal class Build : BaseFeature + { + public const string Value = "Build"; + + public Build() + { + Name = Value; + Flags = new List() { "build" }; + Description = "For each specified DAT file it creates the torrentzip files."; + _featureType = FeatureType.Flag; + LongDescription = @"For each specified DAT file it creates the torrentzip files in the specified +output dir. The files will be placed in the specified location using a folder +structure according to the original DAT master directory tree structure."; + Features = new Dictionary(); + + AddFeature(OutStringInput); + AddFeature(FixdatOnlyFlag); + AddFeature(CopyFlag); + AddFeature(WorkersInt32Input); + AddFeature(SubworkersInt32Input); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + bool copy = GetBoolean(features, CopyValue); + string outdat = GetString(features, OutStringValue); + + // Verify the filenames + Dictionary foundDats = GetValidDats(Inputs); + + // Ensure the output directory is set + if (string.IsNullOrWhiteSpace(outdat)) + outdat = "out"; + + // Now that we have the dictionary, we can loop through and output to a new folder for each + foreach (string key in foundDats.Keys) + { + // Get the DAT file associated with the key + DatFile datFile = DatFile.CreateAndParse(Path.Combine(_dats, foundDats[key])); + + // Create the new output directory if it doesn't exist + string outputFolder = Path.Combine(outdat, Path.GetFileNameWithoutExtension(foundDats[key])); + DirectoryExtensions.Ensure(outputFolder, create: true); + + // Get all online depots + List onlineDepots = _depots.Where(d => d.Value.Item2).Select(d => d.Key).ToList(); + + // Now scan all of those depots and rebuild + datFile.RebuildDepot(onlineDepots, outputFolder, false /*date*/, + false /*delete*/, false /*inverse*/, (copy ? OutputFormat.TorrentGzipRomba : OutputFormat.TorrentZip), + false /*updateDat*/, null /*headerToCheckAgainst*/); + } + } + } +} diff --git a/RombaSharp/Features/Cancel.cs b/RombaSharp/Features/Cancel.cs new file mode 100644 index 00000000..75ff8048 --- /dev/null +++ b/RombaSharp/Features/Cancel.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class Cancel : BaseFeature + { + public const string Value = "Cancel"; + + public Cancel() + { + Name = Value; + Flags = new List() { "cancel" }; + Description = "Cancels current long-running job"; + _featureType = FeatureType.Flag; + LongDescription = "Cancels current long-running job."; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + Globals.Logger.User("This feature is not yet implemented: cancel"); + } + } +} diff --git a/RombaSharp/Features/DatStats.cs b/RombaSharp/Features/DatStats.cs new file mode 100644 index 00000000..afca3fa6 --- /dev/null +++ b/RombaSharp/Features/DatStats.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.IO; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class DatStats : BaseFeature + { + public const string Value = "DatStats"; + + public DatStats() + { + Name = Value; + Flags = new List() { "datstats" }; + Description = "Prints dat stats."; + _featureType = FeatureType.Flag; + LongDescription = "Print dat stats."; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // If we have no inputs listed, we want to use datroot + if (Inputs == null || Inputs.Count == 0) + Inputs = new List { Path.GetFullPath(_dats) }; + + // Now output the stats for all inputs + ItemDictionary.OutputStats(Inputs, "rombasharp-datstats", null /* outDir */, true /* single */, true /* baddumpCol */, true /* nodumpCol */, StatReportFormat.Textfile); + } + } +} diff --git a/RombaSharp/Features/DbStats.cs b/RombaSharp/Features/DbStats.cs new file mode 100644 index 00000000..addd3517 --- /dev/null +++ b/RombaSharp/Features/DbStats.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; +using Microsoft.Data.Sqlite; + +namespace RombaSharp.Features +{ + internal class DbStats : BaseFeature + { + public const string Value = "DbStats"; + + public DbStats() + { + Name = Value; + Flags = new List() { "dbstats" }; + Description = "Prints db stats."; + _featureType = FeatureType.Flag; + LongDescription = "Print db stats."; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + SqliteConnection dbc = new SqliteConnection(_connectionString); + dbc.Open(); + + // Total number of CRCs + string query = "SELECT COUNT(*) FROM crc"; + SqliteCommand slc = new SqliteCommand(query, dbc); + Globals.Logger.User($"Total CRCs: {(long)slc.ExecuteScalar()}"); + + // Total number of MD5s + query = "SELECT COUNT(*) FROM md5"; + slc = new SqliteCommand(query, dbc); + Globals.Logger.User($"Total MD5s: {(long)slc.ExecuteScalar()}"); + + // Total number of SHA1s + query = "SELECT COUNT(*) FROM sha1"; + slc = new SqliteCommand(query, dbc); + Globals.Logger.User($"Total SHA1s: {(long)slc.ExecuteScalar()}"); + + // Total number of DATs + query = "SELECT COUNT(*) FROM dat"; + slc = new SqliteCommand(query, dbc); + Globals.Logger.User($"Total DATs: {(long)slc.ExecuteScalar()}"); + + slc.Dispose(); + dbc.Dispose(); + } + } +} diff --git a/RombaSharp/Features/Diffdat.cs b/RombaSharp/Features/Diffdat.cs new file mode 100644 index 00000000..40a8611e --- /dev/null +++ b/RombaSharp/Features/Diffdat.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.IO; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.Filtering; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; + +namespace RombaSharp.Features +{ + internal class Diffdat : BaseFeature + { + public const string Value = "Diffdat"; + + public Diffdat() + { + Name = Value; + Flags = new List() { "diffdat" }; + Description = "Creates a DAT file with those entries that are in -new DAT."; + _featureType = FeatureType.Flag; + LongDescription = @"Creates a DAT file with those entries that are in -new DAT file and not +in -old DAT file. Ignores those entries in -old that are not in -new."; + this.Features = new Dictionary(); + + AddFeature(OutStringInput); + AddFeature(OldStringInput); + AddFeature(NewStringInput); + AddFeature(NameStringInput); + AddFeature(DescriptionStringInput); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + string name = GetString(features, NameStringValue); + string description = GetString(features, DescriptionStringValue); + string newdat = GetString(features, NewStringValue); + string olddat = GetString(features, OldStringValue); + string outdat = GetString(features, OutStringValue); + + // Ensure the output directory + DirectoryExtensions.Ensure(outdat, create: true); + + // Check that all required files exist + if (!File.Exists(olddat)) + { + Globals.Logger.Error($"File '{olddat}' does not exist!"); + return; + } + + if (!File.Exists(newdat)) + { + Globals.Logger.Error($"File '{newdat}' does not exist!"); + return; + } + + // Create the encapsulating datfile + DatFile datfile = DatFile.Create(); + datfile.Header.Name = name; + datfile.Header.Description = description; + + // Create the inputs + List dats = new List { newdat }; + List basedats = new List { olddat }; + + // Now run the diff on the inputs + datfile.DetermineUpdateType(dats, basedats, outdat, UpdateMode.DiffAgainst, false /* inplace */, false /* skip */, + new Filter(), new List(), false /* onlySame */); + } + } +} diff --git a/RombaSharp/Features/Dir2Dat.cs b/RombaSharp/Features/Dir2Dat.cs new file mode 100644 index 00000000..f251131c --- /dev/null +++ b/RombaSharp/Features/Dir2Dat.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.IO; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; + +namespace RombaSharp.Features +{ + internal class Dir2Dat : BaseFeature + { + public const string Value = "Dir2Dat"; + + public Dir2Dat() + { + Name = Value; + Flags = new List() { "dir2dat" }; + Description = "Creates a DAT file for the specified input directory and saves it to the -out filename."; + _featureType = FeatureType.Flag; + LongDescription = "Creates a DAT file for the specified input directory and saves it to the -out filename."; + Features = new Dictionary(); + + AddFeature(OutStringInput); + AddFeature(SourceStringInput); + AddFeature(NameStringInput); // Defaults to "untitled" + AddFeature(DescriptionStringInput); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + string name = GetString(features, NameStringValue); + string description = GetString(features, DescriptionStringValue); + string source = GetString(features, SourceStringValue); + string outdat = GetString(features, OutStringValue); + + // Ensure the output directory + DirectoryExtensions.Ensure(outdat, create: true); + + // Check that all required directories exist + if (!Directory.Exists(source)) + { + Globals.Logger.Error($"File '{source}' does not exist!"); + return; + } + + // Create the encapsulating datfile + DatFile datfile = DatFile.Create(); + datfile.Header.Name = string.IsNullOrWhiteSpace(name) ? "untitled" : name; + datfile.Header.Description = description; + + // Now run the D2D on the input and write out + // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + datfile.PopulateFromDir(source, Hash.DeepHashes, true /* bare */, false /* archivesAsFiles */, SkipFileType.None, false /* addBlanks */, + false /* addDate */, _tmpdir, false /* copyFiles */, null /* headerToCheckAgainst */, true /* chdsAsFiles */, null /* filter */); + datfile.Write(outDir: outdat); + } + } +} diff --git a/RombaSharp/Features/DisplayHelp.cs b/RombaSharp/Features/DisplayHelp.cs new file mode 100644 index 00000000..444bd75d --- /dev/null +++ b/RombaSharp/Features/DisplayHelp.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class DisplayHelp : BaseFeature + { + public const string Value = "Help"; + + public DisplayHelp() + { + Name = Value; + Flags = new List() { "-?", "-h", "--help" }; + Description = "Show this help"; + _featureType = FeatureType.Flag; + LongDescription = "Built-in to most of the programs is a basic help text."; + Features = new Dictionary(); + } + + public override bool ProcessArgs(string[] args, Help help) + { + // If we had something else after help + if (args.Length > 1) + { + help.OutputIndividualFeature(args[1]); + return true; + } + + // Otherwise, show generic help + else + { + help.OutputGenericHelp(); + return true; + } + } + } +} diff --git a/RombaSharp/Features/DisplayHelpDetailed.cs b/RombaSharp/Features/DisplayHelpDetailed.cs new file mode 100644 index 00000000..487356f5 --- /dev/null +++ b/RombaSharp/Features/DisplayHelpDetailed.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class DisplayHelpDetailed : BaseFeature + { + public const string Value = "Help (Detailed)"; + + public DisplayHelpDetailed() + { + Name = Value; + Flags = new List() { "-??", "-hd", "--help-detailed" }; + Description = "Show this detailed help"; + _featureType = FeatureType.Flag; + LongDescription = "Display a detailed help text to the screen."; + Features = new Dictionary(); + } + + public override bool ProcessArgs(string[] args, Help help) + { + // If we had something else after help + if (args.Length > 1) + { + help.OutputIndividualFeature(args[1], includeLongDescription: true); + return true; + } + + // Otherwise, show generic help + else + { + help.OutputAllHelp(); + return true; + } + } + } +} diff --git a/RombaSharp/Features/EDiffdat.cs b/RombaSharp/Features/EDiffdat.cs new file mode 100644 index 00000000..dcf3221b --- /dev/null +++ b/RombaSharp/Features/EDiffdat.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.IO; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.Filtering; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; + +namespace RombaSharp.Features +{ + internal class EDiffdat : BaseFeature + { + public const string Value = "EDiffdat"; + + public EDiffdat() + { + Name = Value; + Flags = new List() { "ediffdat" }; + Description = "Creates a DAT file with those entries that are in -new DAT."; + _featureType = FeatureType.Flag; + LongDescription = @"Creates a DAT file with those entries that are in -new DAT files and not in -old DAT files. Ignores those entries in -old that are not in -new."; + Features = new Dictionary(); + + AddFeature(OutStringInput); + AddFeature(OldStringInput); + AddFeature(NewStringInput); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + string olddat = GetString(features, OldStringValue); + string outdat = GetString(features, OutStringValue); + string newdat = GetString(features, NewStringValue); + + // Ensure the output directory + DirectoryExtensions.Ensure(outdat, create: true); + + // Check that all required files exist + if (!File.Exists(olddat)) + { + Globals.Logger.Error($"File '{olddat}' does not exist!"); + return; + } + + if (!File.Exists(newdat)) + { + Globals.Logger.Error($"File '{newdat}' does not exist!"); + return; + } + + // Create the encapsulating datfile + DatFile datfile = DatFile.Create(); + + // Create the inputs + List dats = new List { newdat }; + List basedats = new List { olddat }; + + // Now run the diff on the inputs + datfile.DetermineUpdateType(dats, basedats, outdat, UpdateMode.DiffAgainst, false /* inplace */, false /* skip */, + new Filter(), new List(), false /* onlySame */); + } + } +} diff --git a/RombaSharp/Features/Export.cs b/RombaSharp/Features/Export.cs new file mode 100644 index 00000000..000275e0 --- /dev/null +++ b/RombaSharp/Features/Export.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.IO; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; +using Microsoft.Data.Sqlite; + +namespace RombaSharp.Features +{ + internal class Export : BaseFeature + { + public const string Value = "Export"; + + // Unique to RombaSharp + public Export() + { + Name = Value; + Flags = new List() { "export" }; + Description = "Exports db to export.csv"; + _featureType = FeatureType.Flag; + LongDescription = "Exports db to standardized export.csv"; + Features = new Dictionary(); + } + + // TODO: Add ability to say which depot the files are found in + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + SqliteConnection dbc = new SqliteConnection(_connectionString); + dbc.Open(); + StreamWriter sw = new StreamWriter(FileExtensions.TryCreate("export.csv")); + + // First take care of all file hashes + sw.WriteLine("CRC,MD5,SHA-1"); // ,Depot + + string query = "SELECT crcsha1.crc, md5sha1.md5, md5sha1.sha1 FROM crcsha1 JOIN md5sha1 ON crcsha1.sha1=md5sha1.sha1"; // md5sha1.sha1=sha1depot.sha1 + SqliteCommand slc = new SqliteCommand(query, dbc); + SqliteDataReader sldr = slc.ExecuteReader(); + + if (sldr.HasRows) + { + while (sldr.Read()) + { + string line = $"{sldr.GetString(0)},{sldr.GetString(1)},{sldr.GetString(2)}"; // + ",{sldr.GetString(3)}"; + sw.WriteLine(line); + } + } + + // Then take care of all DAT hashes + sw.WriteLine(); + sw.WriteLine("DAT Hash"); + + query = "SELECT hash FROM dat"; + slc = new SqliteCommand(query, dbc); + sldr = slc.ExecuteReader(); + + if (sldr.HasRows) + { + while (sldr.Read()) + { + sw.WriteLine(sldr.GetString(0)); + } + } + + sldr.Dispose(); + slc.Dispose(); + sw.Dispose(); + dbc.Dispose(); + } + } +} diff --git a/RombaSharp/Features/Fixdat.cs b/RombaSharp/Features/Fixdat.cs new file mode 100644 index 00000000..548e8361 --- /dev/null +++ b/RombaSharp/Features/Fixdat.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class Fixdat : BaseFeature + { + public const string Value = "Fixdat"; + + public Fixdat() + { + Name = Value; + Flags = new List() { "fixdat" }; + Description = "For each specified DAT file it creates a fix DAT."; + _featureType = FeatureType.Flag; + LongDescription = @"For each specified DAT file it creates a fix DAT with the missing entries for that DAT. If nothing is missing it doesn't create a fix DAT for that particular DAT."; + Features = new Dictionary(); + + AddFeature(OutStringInput); + AddFeature(FixdatOnlyFlag); // Enabled by default + AddFeature(WorkersInt32Input); + AddFeature(SubworkersInt32Input); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + // Inputs + bool fixdatOnly = GetBoolean(features, FixdatOnlyValue); + int subworkers = GetInt32(features, SubworkersInt32Value); + int workers = GetInt32(features, WorkersInt32Value); + string outdat = GetString(features, OutStringValue); + + Globals.Logger.Error("This feature is not yet implemented: fixdat"); + } + } +} diff --git a/RombaSharp/Features/Import.cs b/RombaSharp/Features/Import.cs new file mode 100644 index 00000000..c47077de --- /dev/null +++ b/RombaSharp/Features/Import.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; +using Microsoft.Data.Sqlite; + +namespace RombaSharp.Features +{ + internal class Import : BaseFeature + { + public const string Value = "Import"; + + // Unique to RombaSharp + public Import() + { + Name = Value; + Flags = new List() { "import" }; + Description = "Import a database from a formatted CSV file"; + _featureType = FeatureType.Flag; + LongDescription = "Import a database from a formatted CSV file"; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + Globals.Logger.Error("This feature is not yet implemented: import"); + + // First ensure the inputs and database connection + Inputs = DirectoryExtensions.GetFilesOnly(Inputs).Select(p => p.CurrentPath).ToList(); + SqliteConnection dbc = new SqliteConnection(_connectionString); + SqliteCommand slc = new SqliteCommand(); + dbc.Open(); + + // Now, for each of these files, attempt to add the data found inside + foreach (string input in Inputs) + { + StreamReader sr = new StreamReader(FileExtensions.TryOpenRead(input)); + + // The first line should be the hash header + string line = sr.ReadLine(); + if (line != "CRC,MD5,SHA-1") // ,Depot + { + Globals.Logger.Error("{0} is not a valid export file"); + continue; + } + + // Define the insert queries + string crcquery = "INSERT OR IGNORE INTO crc (crc) VALUES"; + string md5query = "INSERT OR IGNORE INTO md5 (md5) VALUES"; + string sha1query = "INSERT OR IGNORE INTO sha1 (sha1) VALUES"; + string crcsha1query = "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES"; + string md5sha1query = "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES"; + + // For each line until we hit a blank line... + while (!sr.EndOfStream && line != string.Empty) + { + line = sr.ReadLine(); + string[] hashes = line.Split(','); + + // Loop through the parsed entries + if (!string.IsNullOrWhiteSpace(hashes[0])) + crcquery += $" (\"{hashes[0]}\"),"; + + if (!string.IsNullOrWhiteSpace(hashes[1])) + md5query += $" (\"{hashes[1]}\"),"; + + if (!string.IsNullOrWhiteSpace(hashes[2])) + { + sha1query += $" (\"{hashes[2]}\"),"; + + if (!string.IsNullOrWhiteSpace(hashes[0])) + crcsha1query += $" (\"{hashes[0]}\", \"{hashes[2]}\"),"; + + if (!string.IsNullOrWhiteSpace(hashes[1])) + md5sha1query += $" (\"{hashes[1]}\", \"{hashes[2]}\"),"; + } + } + + // Now run the queries after fixing them + if (crcquery != "INSERT OR IGNORE INTO crc (crc) VALUES") + { + slc = new SqliteCommand(crcquery.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + if (md5query != "INSERT OR IGNORE INTO md5 (md5) VALUES") + { + slc = new SqliteCommand(md5query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + if (sha1query != "INSERT OR IGNORE INTO sha1 (sha1) VALUES") + { + slc = new SqliteCommand(sha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + if (crcsha1query != "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES") + { + slc = new SqliteCommand(crcsha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + if (md5sha1query != "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES") + { + slc = new SqliteCommand(md5sha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + // Now add all of the DAT hashes + // TODO: Do we really need to save the DAT hashes? + + sr.Dispose(); + } + + slc.Dispose(); + dbc.Dispose(); + } + } +} diff --git a/RombaSharp/Features/Lookup.cs b/RombaSharp/Features/Lookup.cs new file mode 100644 index 00000000..d80bb64c --- /dev/null +++ b/RombaSharp/Features/Lookup.cs @@ -0,0 +1,145 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; +using Microsoft.Data.Sqlite; + +namespace RombaSharp.Features +{ + internal class Lookup : BaseFeature + { + public const string Value = "Lookup"; + + public Lookup() + { + Name = Value; + Flags = new List() { "lookup" }; + Description = "For each specified hash it looks up any available information."; + _featureType = FeatureType.Flag; + LongDescription = "For each specified hash it looks up any available information (dat or rom)."; + Features = new Dictionary(); + + AddFeature(SizeInt64Input); // Defaults to -1 + AddFeature(OutStringInput); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + long size = GetInt64(features, SizeInt64Value); + string outdat = GetString(features, OutStringValue); + + // First, try to figure out what type of hash each is by length and clean it + List crc = new List(); + List md5 = new List(); + List sha1 = new List(); + foreach (string input in Inputs) + { + string temp = string.Empty; + if (input.Length == Constants.CRCLength) + { + temp = Sanitizer.CleanCRC32(input); + if (!string.IsNullOrWhiteSpace(temp)) + { + crc.Add(temp); + } + } + else if (input.Length == Constants.MD5Length) + { + temp = Sanitizer.CleanMD5(input); + if (!string.IsNullOrWhiteSpace(temp)) + { + md5.Add(temp); + } + } + else if (input.Length == Constants.SHA1Length) + { + temp = Sanitizer.CleanSHA1(input); + if (!string.IsNullOrWhiteSpace(temp)) + { + sha1.Add(temp); + } + } + } + + SqliteConnection dbc = new SqliteConnection(_connectionString); + dbc.Open(); + + // Now, search for each of them and return true or false for each + foreach (string input in crc) + { + string query = $"SELECT * FROM crc WHERE crc=\"{input}\""; + SqliteCommand slc = new SqliteCommand(query, dbc); + SqliteDataReader sldr = slc.ExecuteReader(); + if (sldr.HasRows) + { + int count = 0; + while (sldr.Read()) + { + count++; + } + + Globals.Logger.User($"For hash '{input}' there were {count} matches in the database"); + } + else + { + Globals.Logger.User($"Hash '{input}' had no matches in the database"); + } + + sldr.Dispose(); + slc.Dispose(); + } + foreach (string input in md5) + { + string query = $"SELECT * FROM md5 WHERE md5=\"{input}\""; + SqliteCommand slc = new SqliteCommand(query, dbc); + SqliteDataReader sldr = slc.ExecuteReader(); + if (sldr.HasRows) + { + int count = 0; + while (sldr.Read()) + { + count++; + } + + Globals.Logger.User($"For hash '{input}' there were {count} matches in the database"); + } + else + { + Globals.Logger.User($"Hash '{input}' had no matches in the database"); + } + + sldr.Dispose(); + slc.Dispose(); + } + foreach (string input in sha1) + { + string query = $"SELECT * FROM sha1 WHERE sha1=\"{input}\""; + SqliteCommand slc = new SqliteCommand(query, dbc); + SqliteDataReader sldr = slc.ExecuteReader(); + if (sldr.HasRows) + { + int count = 0; + while (sldr.Read()) + { + count++; + } + + Globals.Logger.User($"For hash '{input}' there were {count} matches in the database"); + } + else + { + Globals.Logger.User($"Hash '{input}' had no matches in the database"); + } + + sldr.Dispose(); + slc.Dispose(); + } + + dbc.Dispose(); + } + } +} diff --git a/RombaSharp/Features/Memstats.cs b/RombaSharp/Features/Memstats.cs new file mode 100644 index 00000000..672eb23a --- /dev/null +++ b/RombaSharp/Features/Memstats.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class Memstats : BaseFeature + { + public const string Value = "Memstats"; + + public Memstats() + { + Name = Value; + Flags = new List() { "memstats" }; + Description = "Prints memory stats."; + _featureType = FeatureType.Flag; + LongDescription = "Print memory stats."; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + Globals.Logger.User("This feature is not yet implemented: memstats"); + } + } +} diff --git a/RombaSharp/Features/Merge.cs b/RombaSharp/Features/Merge.cs new file mode 100644 index 00000000..1d23f7c2 --- /dev/null +++ b/RombaSharp/Features/Merge.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; + +namespace RombaSharp.Features +{ + internal class Merge : BaseFeature + { + public const string Value = "Merge"; + + public Merge() + { + Name = Value; + Flags = new List() { "merge" }; + Description = "Merges depot"; + _featureType = FeatureType.Flag; + LongDescription = "Merges specified depot into current depot."; + Features = new Dictionary(); + + AddFeature(OnlyNeededFlag); + AddFeature(ResumeStringInput); + AddFeature(WorkersInt32Input); + AddFeature(SkipInitialScanFlag); + } + + // TODO: Add way of specifying "current depot" since that's what Romba relies on + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + bool onlyNeeded = GetBoolean(features, OnlyNeededValue); + bool skipInitialscan = GetBoolean(features, SkipInitialScanValue); + int workers = GetInt32(features, WorkersInt32Value); + string resume = GetString(features, ResumeStringValue); + + Globals.Logger.Error("This feature is not yet implemented: merge"); + + // Verify that the inputs are valid directories + Inputs = DirectoryExtensions.GetDirectoriesOnly(Inputs).Select(p => p.CurrentPath).ToList(); + + // Loop over all input directories + foreach (string input in Inputs) + { + List depotFiles = Directory.EnumerateFiles(input, "*.gz", SearchOption.AllDirectories).ToList(); + + // If we are copying all that is possible but we want to scan first + if (!onlyNeeded && !skipInitialscan) + { + + } + + // If we are copying all that is possible but we don't care to scan first + else if (!onlyNeeded && skipInitialscan) + { + + } + + // If we are copying only what is needed but we want to scan first + else if (onlyNeeded && !skipInitialscan) + { + + } + + // If we are copying only what is needed but we don't care to scan first + else if (onlyNeeded && skipInitialscan) + { + + } + } + } + } +} diff --git a/RombaSharp/Features/Miss.cs b/RombaSharp/Features/Miss.cs new file mode 100644 index 00000000..1247ac41 --- /dev/null +++ b/RombaSharp/Features/Miss.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.IO; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; + +namespace RombaSharp.Features +{ + internal class Miss : BaseFeature + { + public const string Value = "Miss"; + + // Unique to RombaSharp + public Miss() + { + Name = Value; + Flags = new List() { "miss" }; + Description = "Create miss and have file"; + _featureType = FeatureType.Flag; + LongDescription = "For each specified DAT file, create miss and have file"; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Verify the filenames + Dictionary foundDats = GetValidDats(Inputs); + + // Create the new output directory if it doesn't exist + DirectoryExtensions.Ensure(Path.Combine(Globals.ExeDir, "out"), create: true); + + // Now that we have the dictionary, we can loop through and output to a new folder for each + foreach (string key in foundDats.Keys) + { + // Get the DAT file associated with the key + DatFile datFile = DatFile.CreateAndParse(Path.Combine(_dats, foundDats[key])); + + // Now loop through and see if all of the hash combinations exist in the database + /* ended here */ + } + + Globals.Logger.Error("This feature is not yet implemented: miss"); + } + } +} diff --git a/RombaSharp/Features/Progress.cs b/RombaSharp/Features/Progress.cs new file mode 100644 index 00000000..a970c042 --- /dev/null +++ b/RombaSharp/Features/Progress.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class Progress : BaseFeature + { + public const string Value = "Progress"; + + public Progress() + { + Name = Value; + Flags = new List() { "progress" }; + Description = "Shows progress of the currently running command."; + _featureType = FeatureType.Flag; + LongDescription = "Shows progress of the currently running command."; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + Globals.Logger.User("This feature is not yet implemented: progress"); + } + } +} diff --git a/RombaSharp/Features/PurgeBackup.cs b/RombaSharp/Features/PurgeBackup.cs new file mode 100644 index 00000000..6b205667 --- /dev/null +++ b/RombaSharp/Features/PurgeBackup.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class PurgeBackup : BaseFeature + { + public const string Value = "Purge Backup"; + + public PurgeBackup() + { + Name = Value; + Flags = new List() { "purge-backup" }; + Description = "Moves DAT index entries for orphaned DATs."; + _featureType = FeatureType.Flag; + LongDescription = @"Deletes DAT index entries for orphaned DATs and moves ROM files that are no +longer associated with any current DATs to the specified backup folder. +The files will be placed in the backup location using +a folder structure according to the original DAT master directory tree +structure. It also deletes the specified DATs from the DAT index."; + Features = new Dictionary(); + + AddFeature(BackupStringInput); + AddFeature(WorkersInt32Input); + AddFeature(DepotListStringInput); + AddFeature(DatsListStringInput); + AddFeature(LogOnlyFlag); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + bool logOnly = GetBoolean(features, LogOnlyValue); + int workers = GetInt32(features, WorkersInt32Value); + string backup = GetString(features, BackupStringValue); + List dats = GetList(features, DatsListStringValue); + List depot = GetList(features, DepotListStringValue); + + Globals.Logger.Error("This feature is not yet implemented: purge-backup"); + } + } +} diff --git a/RombaSharp/Features/PurgeDelete.cs b/RombaSharp/Features/PurgeDelete.cs new file mode 100644 index 00000000..151a6429 --- /dev/null +++ b/RombaSharp/Features/PurgeDelete.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class PurgeDelete : BaseFeature + { + public const string Value = "Purge Delete"; + + // Unique to RombaSharp + public PurgeDelete() + { + Name = Value; + Flags = new List() { "purge-delete" }; + Description = "Deletes DAT index entries for orphaned DATs"; + _featureType = FeatureType.Flag; + LongDescription = @"Deletes DAT index entries for orphaned DATs and moves ROM files that are no +longer associated with any current DATs to the specified backup folder. +The files will be placed in the backup location using +a folder structure according to the original DAT master directory tree +structure. It also deletes the specified DATs from the DAT index."; + Features = new Dictionary(); + + AddFeature(WorkersInt32Input); + AddFeature(DepotListStringInput); + AddFeature(DatsListStringInput); + AddFeature(LogOnlyFlag); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + bool logOnly = GetBoolean(features, LogOnlyValue); + int workers = GetInt32(features, WorkersInt32Value); + List dats = GetList(features, DatsListStringValue); + List depot = GetList(features, DepotListStringValue); + + Globals.Logger.Error("This feature is not yet implemented: purge-delete"); + } + } +} diff --git a/RombaSharp/Features/RefreshDats.cs b/RombaSharp/Features/RefreshDats.cs new file mode 100644 index 00000000..3dd7348b --- /dev/null +++ b/RombaSharp/Features/RefreshDats.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using System.IO; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.DatItems; +using SabreTools.Library.Help; +using SabreTools.Library.Tools; +using Microsoft.Data.Sqlite; + +namespace RombaSharp.Features +{ + internal class RefreshDats : BaseFeature + { + public const string Value = "Refresh DATs"; + + public RefreshDats() + { + Name = Value; + Flags = new List() { "refresh-dats" }; + Description = "Refreshes the DAT index from the files in the DAT master directory tree."; + _featureType = FeatureType.Flag; + LongDescription = @"Refreshes the DAT index from the files in the DAT master directory tree. +Detects any changes in the DAT master directory tree and updates the DAT index +accordingly, marking deleted or overwritten dats as orphaned and updating +contents of any changed dats."; + Features = new Dictionary(); + + AddFeature(WorkersInt32Input); + AddFeature(MissingSha1sStringInput); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + + // Get feature flags + int workers = GetInt32(features, WorkersInt32Value); + string missingSha1s = GetString(features, MissingSha1sStringValue); + + // Make sure the db is set + if (string.IsNullOrWhiteSpace(_db)) + { + _db = "db.sqlite"; + _connectionString = $"Data Source={_db};Version = 3;"; + } + + // Make sure the file exists + if (!File.Exists(_db)) + DatabaseTools.EnsureDatabase(_dbSchema, _db, _connectionString); + + // Make sure the dats dir is set + if (string.IsNullOrWhiteSpace(_dats)) + _dats = "dats"; + + _dats = Path.Combine(Globals.ExeDir, _dats); + + // Make sure the folder exists + if (!Directory.Exists(_dats)) + Directory.CreateDirectory(_dats); + + // First get a list of SHA-1's from the input DATs + DatFile datroot = DatFile.Create(); + datroot.Header.Type = "SuperDAT"; + + // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + datroot.PopulateFromDir(_dats, Hash.DeepHashes, false, false, SkipFileType.None, false, false, _tmpdir, false, null, true, null); + datroot.Items.BucketBy(BucketedBy.SHA1, DedupeType.None); + + // Create a List of dat hashes in the database (SHA-1) + List databaseDats = new List(); + List unneeded = new List(); + + SqliteConnection dbc = new SqliteConnection(_connectionString); + dbc.Open(); + + // Populate the List from the database + InternalStopwatch watch = new InternalStopwatch("Populating the list of existing DATs"); + + string query = "SELECT DISTINCT hash FROM dat"; + SqliteCommand slc = new SqliteCommand(query, dbc); + SqliteDataReader sldr = slc.ExecuteReader(); + if (sldr.HasRows) + { + sldr.Read(); + string hash = sldr.GetString(0); + if (datroot.Items.ContainsKey(hash)) + { + datroot.Items.Remove(hash); + databaseDats.Add(hash); + } + else if (!databaseDats.Contains(hash)) + { + unneeded.Add(hash); + } + } + + datroot.Items.BucketBy(BucketedBy.Game, DedupeType.None, norename: true); + + watch.Stop(); + + slc.Dispose(); + sldr.Dispose(); + + // Loop through the Dictionary and add all data + watch.Start("Adding new DAT information"); + foreach (string key in datroot.Items.Keys) + { + foreach (Rom value in datroot.Items[key]) + { + AddDatToDatabase(value, dbc); + } + } + + watch.Stop(); + + // Now loop through and remove all references to old Dats + if (unneeded.Count > 0) + { + watch.Start("Removing unmatched DAT information"); + + query = "DELETE FROM dat WHERE"; + foreach (string dathash in unneeded) + { + query += $" OR hash=\"{dathash}\""; + } + + query = query.Replace("WHERE OR", "WHERE"); + slc = new SqliteCommand(query, dbc); + slc.ExecuteNonQuery(); + slc.Dispose(); + + watch.Stop(); + } + + dbc.Dispose(); + } + } +} diff --git a/RombaSharp/Features/RescanDepots.cs b/RombaSharp/Features/RescanDepots.cs new file mode 100644 index 00000000..256ed41f --- /dev/null +++ b/RombaSharp/Features/RescanDepots.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using System.IO; + +using SabreTools.Library.Data; +using SabreTools.Library.DatFiles; +using SabreTools.Library.DatItems; +using SabreTools.Library.Help; +using Microsoft.Data.Sqlite; + +namespace RombaSharp.Features +{ + internal class RescanDepots : BaseFeature + { + public const string Value = "Rescan Depots"; + + // Unique to RombaSharp + public RescanDepots() + { + Name = Value; + Flags = new List() { "depot-rescan" }; + Description = "Rescan a specific depot to get new information"; + _featureType = FeatureType.Flag; + LongDescription = "Rescan a specific depot to get new information"; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + Globals.Logger.Error("This feature is not yet implemented: rescan-depots"); + + foreach (string depotname in Inputs) + { + // Check that it's a valid depot first + if (!_depots.ContainsKey(depotname)) + { + Globals.Logger.User($"'{depotname}' is not a recognized depot. Please add it to your configuration file and try again"); + return; + } + + // Then check that the depot is online + if (!Directory.Exists(depotname)) + { + Globals.Logger.User($"'{depotname}' does not appear to be online. Please check its status and try again"); + return; + } + + // Open the database connection + SqliteConnection dbc = new SqliteConnection(_connectionString); + dbc.Open(); + + // If we have it, then check for all hashes that are in that depot + List hashes = new List(); + string query = $"SELECT sha1 FROM sha1 WHERE depot=\"{depotname}\""; + SqliteCommand slc = new SqliteCommand(query, dbc); + SqliteDataReader sldr = slc.ExecuteReader(); + if (sldr.HasRows) + { + while (sldr.Read()) + { + hashes.Add(sldr.GetString(0)); + } + } + + // Now rescan the depot itself + DatFile depot = DatFile.Create(); + + // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + depot.PopulateFromDir(depotname, Hash.DeepHashes, false, false, SkipFileType.None, false, false, _tmpdir, false, null, true, null); + depot.Items.BucketBy(BucketedBy.SHA1, DedupeType.None); + + // Set the base queries to use + string crcquery = "INSERT OR IGNORE INTO crc (crc) VALUES"; + string md5query = "INSERT OR IGNORE INTO md5 (md5) VALUES"; + string sha1query = "INSERT OR IGNORE INTO sha1 (sha1, depot) VALUES"; + string crcsha1query = "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES"; + string md5sha1query = "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES"; + + // Once we have both, check for any new files + List dupehashes = new List(); + IEnumerable keys = depot.Items.Keys; + foreach (string key in keys) + { + List roms = depot.Items[key]; + foreach (Rom rom in roms) + { + if (hashes.Contains(rom.SHA1)) + { + dupehashes.Add(rom.SHA1); + hashes.Remove(rom.SHA1); + } + else if (!dupehashes.Contains(rom.SHA1)) + { + if (!string.IsNullOrWhiteSpace(rom.CRC)) + crcquery += $" (\"{rom.CRC}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.MD5)) + md5query += $" (\"{rom.MD5}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.SHA1)) + { + sha1query += $" (\"{rom.SHA1}\", \"{depotname}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.CRC)) + crcsha1query += $" (\"{rom.CRC}\", \"{rom.SHA1}\"),"; + + if (!string.IsNullOrWhiteSpace(rom.MD5)) + md5sha1query += $" (\"{rom.MD5}\", \"{rom.SHA1}\"),"; + } + } + } + } + + // Now run the queries after fixing them + if (crcquery != "INSERT OR IGNORE INTO crc (crc) VALUES") + { + slc = new SqliteCommand(crcquery.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + if (md5query != "INSERT OR IGNORE INTO md5 (md5) VALUES") + { + slc = new SqliteCommand(md5query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + if (sha1query != "INSERT OR IGNORE INTO sha1 (sha1, depot) VALUES") + { + slc = new SqliteCommand(sha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + if (crcsha1query != "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES") + { + slc = new SqliteCommand(crcsha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + if (md5sha1query != "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES") + { + slc = new SqliteCommand(md5sha1query.TrimEnd(','), dbc); + slc.ExecuteNonQuery(); + } + + // Now that we've added the information, we get to remove all of the hashes that we want to + query = @"DELETE FROM sha1 +JOIN crcsha1 + ON sha1.sha1=crcsha1.sha1 +JOIN md5sha1 + ON sha1.sha1=md5sha1.sha1 +JOIN crc + ON crcsha1.crc=crc.crc +JOIN md5 + ON md5sha1.md5=md5.md5 +WHERE sha1.sha1 IN "; + query += $"({string.Join("\",\"", hashes)}\")"; + slc = new SqliteCommand(query, dbc); + slc.ExecuteNonQuery(); + + // Dispose of the database connection + slc.Dispose(); + dbc.Dispose(); + } + } + } +} diff --git a/RombaSharp/Features/Script.cs b/RombaSharp/Features/Script.cs new file mode 100644 index 00000000..e388a23f --- /dev/null +++ b/RombaSharp/Features/Script.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class Script : BaseFeature + { + public const string Value = "Script"; + + public Script() + { + Name = Value; + Flags = new List() { "--script" }; + Description = "Enable script mode (no clear screen)"; + _featureType = FeatureType.Flag; + LongDescription = "For times when RombaSharp is being used in a scripted environment, the user may not want the screen to be cleared every time that it is called. This flag allows the user to skip clearing the screen on run just like if the console was being redirected."; + Features = new Dictionary(); + } + } +} diff --git a/RombaSharp/Features/Shutdown.cs b/RombaSharp/Features/Shutdown.cs new file mode 100644 index 00000000..4e31479d --- /dev/null +++ b/RombaSharp/Features/Shutdown.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class Shutdown : BaseFeature + { + public const string Value = "Shutdown"; + + public Shutdown() + { + Name = Value; + Flags = new List() { "shutdown" }; + Description = "Gracefully shuts down server."; + _featureType = FeatureType.Flag; + LongDescription = "Gracefully shuts down server saving all the cached data."; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + Globals.Logger.User("This feature is not yet implemented: shutdown"); + } + } +} diff --git a/RombaSharp/Features/Version.cs b/RombaSharp/Features/Version.cs new file mode 100644 index 00000000..15e6eafb --- /dev/null +++ b/RombaSharp/Features/Version.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +using SabreTools.Library.Data; +using SabreTools.Library.Help; + +namespace RombaSharp.Features +{ + internal class Version : BaseFeature + { + public const string Value = "Version"; + + public Version() + { + Name = Value; + Flags = new List() { "version" }; + Description = "Prints version"; + _featureType = FeatureType.Flag; + LongDescription = "Prints version."; + Features = new Dictionary(); + } + + public override void ProcessFeatures(Dictionary features) + { + base.ProcessFeatures(features); + Globals.Logger.User($"RombaSharp version: {Constants.Version}"); + } + } +} diff --git a/RombaSharp/RombaSharp.cs b/RombaSharp/Program.cs similarity index 56% rename from RombaSharp/RombaSharp.cs rename to RombaSharp/Program.cs index 19485926..64e5d06d 100644 --- a/RombaSharp/RombaSharp.cs +++ b/RombaSharp/Program.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; +using RombaSharp.Features; using SabreTools.Library.Data; using SabreTools.Library.Help; using SabreTools.Library.Tools; @@ -15,30 +16,8 @@ namespace RombaSharp /// that needs to read from the depot themselves, if the depot folder cannot be found, the /// user is prompted to reconnect the depot OR skip that depot entirely. /// - public partial class RombaSharp + public class Program { - // General settings - private static string _logdir; // Log folder location - private static string _tmpdir; // Temp folder location - private static string _webdir; // Web frontend location - private static string _baddir; // Fail-to-unpack file folder location - private static int _verbosity; // Verbosity of the output - private static int _cores; // Forced CPU cores - - // DatRoot settings - private static string _dats; // DatRoot folder location - private static string _db; // Database name - - // Depot settings - private static Dictionary> _depots; // Folder location, Max size - - // Server settings - private static int _port; // Web server port - - // Other private variables - private const string _config = "config.xml"; - private const string _dbSchema = "rombasharp"; - private static string _connectionString; private static Help _help; /// @@ -49,11 +28,8 @@ namespace RombaSharp // Perform initial setup and verification Globals.Logger = new Logger(true, "romba.log"); - InitializeConfiguration(); - DatabaseTools.EnsureDatabase(_dbSchema, _db, _connectionString); - // Create a new Help object for this program - _help = RombaSharp.RetrieveHelp(); + _help = RetrieveHelp(); // Get the location of the script tag, if it exists int scriptLocation = (new List(args)).IndexOf("--script"); @@ -62,7 +38,7 @@ namespace RombaSharp if (!Console.IsOutputRedirected && scriptLocation == -1) { Console.Clear(); - Build.PrepareConsole("RombaSharp"); + SabreTools.Library.Data.Build.PrepareConsole("RombaSharp"); } // Now we remove the script tag because it messes things up @@ -105,10 +81,10 @@ namespace RombaSharp featureName = _help.GetFeatureName(featureName); // Get the associated feature - RombaSharpFeature feature = _help[featureName] as RombaSharpFeature; + BaseFeature feature = _help[featureName] as BaseFeature; // If we had the help feature first - if (featureName == HelpFeature.Value || featureName == DetailedHelpFeature.Value) + if (featureName == DisplayHelp.Value || featureName == DisplayHelpDetailed.Value) { feature.ProcessArgs(args, _help); Globals.Logger.Close(); @@ -126,40 +102,40 @@ namespace RombaSharp Dictionary features = _help.GetEnabledFeatures(); switch (featureName) { - case DetailedHelpFeature.Value: - case HelpFeature.Value: - case ScriptFeature.Value: + case DisplayHelpDetailed.Value: + case DisplayHelp.Value: + case Script.Value: // No-op as this should be caught break; // Require input verification - case ArchiveFeature.Value: - case BuildFeature.Value: - case DatStatsFeature.Value: - case FixdatFeature.Value: - case ImportFeature.Value: - case LookupFeature.Value: - case MergeFeature.Value: - case MissFeature.Value: - case RescanDepotsFeature.Value: + case Archive.Value: + case Features.Build.Value: + case DatStats.Value: + case Fixdat.Value: + case Import.Value: + case Lookup.Value: + case Merge.Value: + case Miss.Value: + case RescanDepots.Value: VerifyInputs(feature.Inputs, featureName); feature.ProcessFeatures(features); break; // Requires no input verification - case CancelFeature.Value: - case DbStatsFeature.Value: - case DiffdatFeature.Value: - case Dir2DatFeature.Value: - case EDiffdatFeature.Value: - case ExportFeature.Value: - case MemstatsFeature.Value: - case ProgressFeature.Value: - case PurgeBackupFeature.Value: - case PurgeDeleteFeature.Value: - case RefreshDatsFeature.Value: - case ShutdownFeature.Value: - case VersionFeature.Value: + case Cancel.Value: + case DbStats.Value: + case Diffdat.Value: + case Dir2Dat.Value: + case EDiffdat.Value: + case Export.Value: + case Memstats.Value: + case Progress.Value: + case PurgeBackup.Value: + case PurgeDelete.Value: + case RefreshDats.Value: + case Shutdown.Value: + case Features.Version.Value: feature.ProcessFeatures(features); break; @@ -173,6 +149,60 @@ namespace RombaSharp return; } + /// + /// Generate a Help object for this program + /// + /// Populated Help object + private static Help RetrieveHelp() + { + // Create and add the header to the Help object + string barrier = "-----------------------------------------"; + List helpHeader = new List() + { + "RombaSharp - C# port of the Romba rom management tool", + barrier, + "Usage: RombaSharp [option] [filename|dirname] ...", + string.Empty + }; + + // Create the base help object with header + Help help = new Help(helpHeader); + + // Add all of the features + help.Add(new DisplayHelp()); + help.Add(new DisplayHelpDetailed()); + help.Add(new Script()); + help.Add(new Archive()); + help.Add(new Features.Build()); + help.Add(new Cancel()); + help.Add(new DatStats()); + help.Add(new DbStats()); + help.Add(new Diffdat()); + help.Add(new Dir2Dat()); + help.Add(new EDiffdat()); + help.Add(new Export()); + help.Add(new Fixdat()); + help.Add(new Import()); + help.Add(new Lookup()); + help.Add(new Memstats()); + help.Add(new Merge()); + help.Add(new Miss()); + help.Add(new PurgeBackup()); + help.Add(new PurgeDelete()); + help.Add(new RefreshDats()); + help.Add(new RescanDepots()); + help.Add(new Progress()); + help.Add(new Shutdown()); + help.Add(new Features.Version()); + + return help; + } + + /// + /// Verify that there are inputs, show help otherwise + /// + /// List of inputs + /// Name of the current feature private static void VerifyInputs(List inputs, string feature) { if (inputs.Count == 0) diff --git a/RombaSharp/RombaSharp.Help.cs b/RombaSharp/RombaSharp.Help.cs deleted file mode 100644 index e7a85627..00000000 --- a/RombaSharp/RombaSharp.Help.cs +++ /dev/null @@ -1,1854 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; - -using SabreTools.Library.Data; -using SabreTools.Library.DatFiles; -using SabreTools.Library.DatItems; -using SabreTools.Library.Filtering; -using SabreTools.Library.Help; -using SabreTools.Library.Tools; -using Microsoft.Data.Sqlite; - -namespace RombaSharp -{ - // TODO: Split this like SabreTools - public partial class RombaSharp - { - #region Private Flag features - - public const string CopyValue = "copy"; - private static Feature CopyFlag - { - get - { - return new Feature( - CopyValue, - "-copy", - "Copy files to output instead of rebuilding", - FeatureType.Flag); - } - } // Unique to RombaSharp - - public const string FixdatOnlyValue = "fixdat-only"; - private static Feature FixdatOnlyFlag - { - get - { - return new Feature( - FixdatOnlyValue, - "-fixdatOnly", - "only fix dats and don't generate torrentzips", - FeatureType.Flag); - } - } - - public const string LogOnlyValue = "log-only"; - private static Feature LogOnlyFlag - { - get - { - return new Feature( - LogOnlyValue, - "-log-only", - "Only write out actions to log", - FeatureType.Flag); - } - } - - public const string NoDbValue = "no-db"; - private static Feature NoDbFlag - { - get - { - return new Feature( - NoDbValue, - "-no-db", - "archive into depot but do not touch DB index and ignore only-needed flag", - FeatureType.Flag); - } - } - - public const string OnlyNeededValue = "only-needed"; - private static Feature OnlyNeededFlag - { - get - { - return new Feature( - OnlyNeededValue, - "-only-needed", - "only archive ROM files actually referenced by DAT files from the DAT index", - FeatureType.Flag); - } - } - - public const string SkipInitialScanValue = "skip-initial-scan"; - private static Feature SkipInitialScanFlag - { - get - { - return new Feature( - SkipInitialScanValue, - "-skip-initial-scan", - "skip the initial scan of the files to determine amount of work", - FeatureType.Flag); - } - } - - public const string UseGolangZipValue = "use-golang-zip"; - private static Feature UseGolangZipFlag - { - get - { - return new Feature( - UseGolangZipValue, - "-use-golang-zip", - "use go zip implementation instead of zlib", - FeatureType.Flag); - } - } - - #endregion - - #region Private Int32 features - - public const string Include7ZipsInt32Value = "include-7zips"; - private static Feature Include7ZipsInt32Input - { - get - { - return new Feature( - Include7ZipsInt32Value, - "-include-7zips", - "flag value == 0 means: add 7zip files themselves into the depot in addition to their contents, flag value == 2 means add 7zip files themselves but don't add content", - FeatureType.Int32); - } - } - - public const string IncludeGZipsInt32Value = "include-gzips"; - private static Feature IncludeGZipsInt32Input - { - get - { - return new Feature( - IncludeGZipsInt32Value, - "-include-gzips", - "flag value == 0 means: add gzip files themselves into the depot in addition to their contents, flag value == 2 means add gzip files themselves but don't add content", - FeatureType.Int32); - } - } - - public const string IncludeZipsInt32Value = "include-zips"; - private static Feature IncludeZipsInt32Input - { - get - { - return new Feature( - IncludeZipsInt32Value, - "-include-zips", - "flag value == 0 means: add zip files themselves into the depot in addition to their contents, flag value == 2 means add zip files themselves but don't add content", - FeatureType.Int32); - } - } - - public const string SubworkersInt32Value = "subworkers"; - private static Feature SubworkersInt32Input - { - get - { - return new Feature( - SubworkersInt32Value, - "-subworkers", - "how many subworkers to launch for each worker", - FeatureType.Int32); - } - } // Defaults to Workers count in config - - public const string WorkersInt32Value = "workers"; - private static Feature WorkersInt32Input - { - get - { - return new Feature( - WorkersInt32Value, - "-workers", - "how many workers to launch for the job", - FeatureType.Int32); - } - } // Defaults to Workers count in config - - #endregion - - #region Private Int64 features - - public const string SizeInt64Value = "size"; - private static Feature SizeInt64Input - { - get - { - return new Feature( - SizeInt64Value, - "-size", - "size of the rom to lookup", - FeatureType.Int64); - } - } - - #endregion - - #region Private List features - - public const string DatsListStringValue = "dats"; - private static Feature DatsListStringInput - { - get - { - return new Feature( - DatsListStringValue, - "-dats", - "purge only roms declared in these dats", - FeatureType.List); - } - } - - public const string DepotListStringValue = "depot"; - private static Feature DepotListStringInput - { - get - { - return new Feature( - DepotListStringValue, - "-depot", - "work only on specified depot path", - FeatureType.List); - } - } - - #endregion - - #region Private String features - - public const string BackupStringValue = "backup"; - private static Feature BackupStringInput - { - get - { - return new Feature( - BackupStringValue, - "-backup", - "backup directory where backup files are moved to", - FeatureType.String); - } - } - - public const string DescriptionStringValue = "description"; - private static Feature DescriptionStringInput - { - get - { - return new Feature( - DescriptionStringValue, - "-description", - "description value in DAT header", - FeatureType.String); - } - } - - public const string MissingSha1sStringValue = "missing-sha1s"; - private static Feature MissingSha1sStringInput - { - get - { - return new Feature( - MissingSha1sStringValue, - "-missingSha1s", - "write paths of dats with missing sha1s into this file", - FeatureType.String); - } - } - - public const string NameStringValue = "name"; - private static Feature NameStringInput - { - get - { - return new Feature( - NameStringValue, - "-name", - "name value in DAT header", - FeatureType.String); - } - } - - public const string NewStringValue = "new"; - private static Feature NewStringInput - { - get - { - return new Feature( - NewStringValue, - "-new", - "new DAT file", - FeatureType.String); - } - } - - public const string OldStringValue = "old"; - private static Feature OldStringInput - { - get - { - return new Feature( - OldStringValue, - "-old", - "old DAT file", - FeatureType.String); - } - } - - public const string OutStringValue = "out"; - private static Feature OutStringInput - { - get - { - return new Feature( - OutStringValue, - "-out", - "output file", - FeatureType.String); - } - } - - public const string ResumeStringValue = "resume"; - private static Feature ResumeStringInput - { - get - { - return new Feature( - ResumeStringValue, - "-resume", - "resume a previously interrupted operation from the specified path", - FeatureType.String); - } - } - - public const string SourceStringValue = "source"; - private static Feature SourceStringInput - { - get - { - return new Feature( - SourceStringValue, - "-source", - "source directory", - FeatureType.String); - } - } - - #endregion - - public static Help RetrieveHelp() - { - // Create and add the header to the Help object - string barrier = "-----------------------------------------"; - List helpHeader = new List() - { - "RombaSharp - C# port of the Romba rom management tool", - barrier, - "Usage: RombaSharp [option] [filename|dirname] ...", - string.Empty - }; - - // Create the base help object with header - Help help = new Help(helpHeader); - - // Add all of the features - help.Add(new HelpFeature()); - help.Add(new DetailedHelpFeature()); - help.Add(new ScriptFeature()); - help.Add(new ArchiveFeature()); - help.Add(new BuildFeature()); - help.Add(new CancelFeature()); - help.Add(new DatStatsFeature()); - help.Add(new DbStatsFeature()); - help.Add(new DiffdatFeature()); - help.Add(new Dir2DatFeature()); - help.Add(new EDiffdatFeature()); - help.Add(new ExportFeature()); - help.Add(new FixdatFeature()); - help.Add(new ImportFeature()); - help.Add(new LookupFeature()); - help.Add(new MemstatsFeature()); - help.Add(new MergeFeature()); - help.Add(new MissFeature()); - help.Add(new PurgeBackupFeature()); - help.Add(new PurgeDeleteFeature()); - help.Add(new RefreshDatsFeature()); - help.Add(new RescanDepotsFeature()); - help.Add(new ProgressFeature()); - help.Add(new ShutdownFeature()); - help.Add(new VersionFeature()); - - return help; - } - - #region Top-level Features - - private class RombaSharpFeature : TopLevel - { - } - - private class ArchiveFeature : RombaSharpFeature - { - public const string Value = "Archive"; - - public ArchiveFeature() - { - this.Name = Value; - this.Flags = new List() { "archive" }; - this.Description = "Adds ROM files from the specified directories to the ROM archive."; - this._featureType = FeatureType.Flag; - this.LongDescription = @"Adds ROM files from the specified directories to the ROM archive. -Traverses the specified directory trees looking for zip files and normal files. -Unpacked files will be stored as individual entries. Prior to unpacking a zip -file, the external SHA1 is checked against the DAT index. -If -only-needed is set, only those files are put in the ROM archive that -have a current entry in the DAT index."; - this.Features = new Dictionary(); - - AddFeature(OnlyNeededFlag); - AddFeature(ResumeStringInput); - AddFeature(IncludeZipsInt32Input); // Defaults to 0 - AddFeature(WorkersInt32Input); - AddFeature(IncludeGZipsInt32Input); // Defaults to 0 - AddFeature(Include7ZipsInt32Input); // Defaults to 0 - AddFeature(SkipInitialScanFlag); - AddFeature(UseGolangZipFlag); - AddFeature(NoDbFlag); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get the archive scanning level - // TODO: Remove usage - int sevenzip = GetInt32(features, Include7ZipsInt32Value); - int gz = GetInt32(features, IncludeGZipsInt32Value); - int zip = GetInt32(features, IncludeZipsInt32Value); - - // Get feature flags - bool noDb = GetBoolean(features, NoDbValue); - bool onlyNeeded = GetBoolean(features, OnlyNeededValue); - - // First we want to get just all directories from the inputs - List onlyDirs = new List(); - foreach (string input in Inputs) - { - if (Directory.Exists(input)) - onlyDirs.Add(Path.GetFullPath(input)); - } - - // Then process all of the input directories into an internal DAT - DatFile df = DatFile.Create(); - foreach (string dir in onlyDirs) - { - // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - df.PopulateFromDir(dir, Hash.DeepHashes, false, false, SkipFileType.None, false, false, _tmpdir, false, null, true, null); - df.PopulateFromDir(dir, Hash.DeepHashes, false, true, SkipFileType.None, false, false, _tmpdir, false, null, true, null); - } - - // Create an empty Dat for files that need to be rebuilt - DatFile need = DatFile.Create(); - - // Open the database connection - SqliteConnection dbc = new SqliteConnection(_connectionString); - dbc.Open(); - - // Now that we have the Dats, add the files to the database - string crcquery = "INSERT OR IGNORE INTO crc (crc) VALUES"; - string md5query = "INSERT OR IGNORE INTO md5 (md5) VALUES"; - string sha1query = "INSERT OR IGNORE INTO sha1 (sha1, depot) VALUES"; - string crcsha1query = "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES"; - string md5sha1query = "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES"; - - foreach (string key in df.Items.Keys) - { - List datItems = df.Items[key]; - foreach (Rom rom in datItems) - { - // If we care about if the file exists, check the databse first - if (onlyNeeded && !noDb) - { - string query = "SELECT * FROM crcsha1 JOIN md5sha1 ON crcsha1.sha1=md5sha1.sha1" - + $" WHERE crcsha1.crc=\"{rom.CRC}\"" - + $" OR md5sha1.md5=\"{rom.MD5}\"" - + $" OR md5sha1.sha1=\"{rom.SHA1}\""; - SqliteCommand slc = new SqliteCommand(query, dbc); - SqliteDataReader sldr = slc.ExecuteReader(); - - if (sldr.HasRows) - { - // Add to the queries - if (!string.IsNullOrWhiteSpace(rom.CRC)) - crcquery += $" (\"{rom.CRC}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.MD5)) - md5query += $" (\"{rom.MD5}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.SHA1)) - { - sha1query += $" (\"{rom.SHA1}\", \"{_depots.Keys.ToList()[0]}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.CRC)) - crcsha1query += $" (\"{rom.CRC}\", \"{rom.SHA1}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.MD5)) - md5sha1query += $" (\"{rom.MD5}\", \"{rom.SHA1}\"),"; - } - - // Add to the Dat - need.Items.Add(key, rom); - } - } - // Otherwise, just add the file to the list - else - { - // Add to the queries - if (!noDb) - { - if (!string.IsNullOrWhiteSpace(rom.CRC)) - crcquery += $" (\"{rom.CRC}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.MD5)) - md5query += $" (\"{rom.MD5}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.SHA1)) - { - sha1query += $" (\"{rom.SHA1}\", \"{_depots.Keys.ToList()[0]}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.CRC)) - crcsha1query += $" (\"{rom.CRC}\", \"{rom.SHA1}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.MD5)) - md5sha1query += $" (\"{rom.MD5}\", \"{rom.SHA1}\"),"; - } - } - - // Add to the Dat - need.Items.Add(key, rom); - } - } - } - - // Now run the queries, if they're populated - if (crcquery != "INSERT OR IGNORE INTO crc (crc) VALUES") - { - SqliteCommand slc = new SqliteCommand(crcquery.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - slc.Dispose(); - } - - if (md5query != "INSERT OR IGNORE INTO md5 (md5) VALUES") - { - SqliteCommand slc = new SqliteCommand(md5query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - slc.Dispose(); - } - - if (sha1query != "INSERT OR IGNORE INTO sha1 (sha1, depot) VALUES") - { - SqliteCommand slc = new SqliteCommand(sha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - slc.Dispose(); - } - - if (crcsha1query != "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES") - { - SqliteCommand slc = new SqliteCommand(crcsha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - slc.Dispose(); - } - - if (md5sha1query != "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES") - { - SqliteCommand slc = new SqliteCommand(md5sha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - slc.Dispose(); - } - - // Create the sorting object to use and rebuild the needed files - need.RebuildGeneric(onlyDirs, _depots.Keys.ToList()[0], false /*quickScan*/, false /*date*/, - false /*delete*/, false /*inverse*/, OutputFormat.TorrentGzipRomba, false /*updateDat*/, - null /*headerToCheckAgainst*/, true /* chdsAsFiles */); - } - } - - private class BuildFeature : RombaSharpFeature - { - public const string Value = "Build"; - - public BuildFeature() - { - this.Name = Value; - this.Flags = new List() { "build" }; - this.Description = "For each specified DAT file it creates the torrentzip files."; - this._featureType = FeatureType.Flag; - this.LongDescription = @"For each specified DAT file it creates the torrentzip files in the specified -output dir. The files will be placed in the specified location using a folder -structure according to the original DAT master directory tree structure."; - this.Features = new Dictionary(); - - AddFeature(OutStringInput); - AddFeature(FixdatOnlyFlag); - AddFeature(CopyFlag); - AddFeature(WorkersInt32Input); - AddFeature(SubworkersInt32Input); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - bool copy = GetBoolean(features, CopyValue); - string outdat = GetString(features, OutStringValue); - - // Verify the filenames - Dictionary foundDats = GetValidDats(Inputs); - - // Ensure the output directory is set - if (string.IsNullOrWhiteSpace(outdat)) - outdat = "out"; - - // Now that we have the dictionary, we can loop through and output to a new folder for each - foreach (string key in foundDats.Keys) - { - // Get the DAT file associated with the key - DatFile datFile = DatFile.CreateAndParse(Path.Combine(_dats, foundDats[key])); - - // Create the new output directory if it doesn't exist - string outputFolder = Path.Combine(outdat, Path.GetFileNameWithoutExtension(foundDats[key])); - DirectoryExtensions.Ensure(outputFolder, create: true); - - // Get all online depots - List onlineDepots = _depots.Where(d => d.Value.Item2).Select(d => d.Key).ToList(); - - // Now scan all of those depots and rebuild - datFile.RebuildDepot(onlineDepots, outputFolder, false /*date*/, - false /*delete*/, false /*inverse*/, (copy ? OutputFormat.TorrentGzipRomba : OutputFormat.TorrentZip), - false /*updateDat*/, null /*headerToCheckAgainst*/); - } - } - } - - private class CancelFeature : RombaSharpFeature - { - public const string Value = "Cancel"; - - public CancelFeature() - { - this.Name = Value; - this.Flags = new List() { "cancel" }; - this.Description = "Cancels current long-running job"; - this._featureType = FeatureType.Flag; - this.LongDescription = "Cancels current long-running job."; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - Globals.Logger.User("This feature is not yet implemented: cancel"); - } - } - - private class DatStatsFeature : RombaSharpFeature - { - public const string Value = "DatStats"; - - public DatStatsFeature() - { - this.Name = Value; - this.Flags = new List() { "datstats" }; - this.Description = "Prints dat stats."; - this._featureType = FeatureType.Flag; - this.LongDescription = "Print dat stats."; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - // If we have no inputs listed, we want to use datroot - if (Inputs == null || Inputs.Count == 0) - { - Inputs = new List { Path.GetFullPath(_dats) }; - } - - // Now output the stats for all inputs - ItemDictionary.OutputStats(Inputs, "rombasharp-datstats", null /* outDir */, true /* single */, true /* baddumpCol */, true /* nodumpCol */, StatReportFormat.Textfile); - } - } - - private class DbStatsFeature : RombaSharpFeature - { - public const string Value = "DbStats"; - - public DbStatsFeature() - { - this.Name = Value; - this.Flags = new List() { "dbstats" }; - this.Description = "Prints db stats."; - this._featureType = FeatureType.Flag; - this.LongDescription = "Print db stats."; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - SqliteConnection dbc = new SqliteConnection(_connectionString); - dbc.Open(); - - // Total number of CRCs - string query = "SELECT COUNT(*) FROM crc"; - SqliteCommand slc = new SqliteCommand(query, dbc); - Globals.Logger.User($"Total CRCs: {(long)slc.ExecuteScalar()}"); - - // Total number of MD5s - query = "SELECT COUNT(*) FROM md5"; - slc = new SqliteCommand(query, dbc); - Globals.Logger.User($"Total MD5s: {(long)slc.ExecuteScalar()}"); - - // Total number of SHA1s - query = "SELECT COUNT(*) FROM sha1"; - slc = new SqliteCommand(query, dbc); - Globals.Logger.User($"Total SHA1s: {(long)slc.ExecuteScalar()}"); - - // Total number of DATs - query = "SELECT COUNT(*) FROM dat"; - slc = new SqliteCommand(query, dbc); - Globals.Logger.User($"Total DATs: {(long)slc.ExecuteScalar()}"); - - slc.Dispose(); - dbc.Dispose(); - } - } - - private class DetailedHelpFeature : RombaSharpFeature - { - public const string Value = "Help (Detailed)"; - - public DetailedHelpFeature() - { - this.Name = Value; - this.Flags = new List() { "-??", "-hd", "--help-detailed" }; - this.Description = "Show this detailed help"; - this._featureType = FeatureType.Flag; - this.LongDescription = "Display a detailed help text to the screen."; - this.Features = new Dictionary(); - } - - public override bool ProcessArgs(string[] args, Help help) - { - // If we had something else after help - if (args.Length > 1) - { - help.OutputIndividualFeature(args[1], includeLongDescription: true); - return true; - } - - // Otherwise, show generic help - else - { - help.OutputAllHelp(); - return true; - } - } - } - - private class DiffdatFeature : RombaSharpFeature - { - public const string Value = "Diffdat"; - - public DiffdatFeature() - { - this.Name = Value; - this.Flags = new List() { "diffdat" }; - this.Description = "Creates a DAT file with those entries that are in -new DAT."; - this._featureType = FeatureType.Flag; - this.LongDescription = @"Creates a DAT file with those entries that are in -new DAT file and not -in -old DAT file. Ignores those entries in -old that are not in -new."; - this.Features = new Dictionary(); - - AddFeature(OutStringInput); - AddFeature(OldStringInput); - AddFeature(NewStringInput); - AddFeature(NameStringInput); - AddFeature(DescriptionStringInput); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - string name = GetString(features, NameStringValue); - string description = GetString(features, DescriptionStringValue); - string newdat = GetString(features, NewStringValue); - string olddat = GetString(features, OldStringValue); - string outdat = GetString(features, OutStringValue); - - // Ensure the output directory - DirectoryExtensions.Ensure(outdat, create: true); - - // Check that all required files exist - if (!File.Exists(olddat)) - { - Globals.Logger.Error($"File '{olddat}' does not exist!"); - return; - } - - if (!File.Exists(newdat)) - { - Globals.Logger.Error($"File '{newdat}' does not exist!"); - return; - } - - // Create the encapsulating datfile - DatFile datfile = DatFile.Create(); - datfile.Header.Name = name; - datfile.Header.Description = description; - - // Create the inputs - List dats = new List { newdat }; - List basedats = new List { olddat }; - - // Now run the diff on the inputs - datfile.DetermineUpdateType(dats, basedats, outdat, UpdateMode.DiffAgainst, false /* inplace */, false /* skip */, - new Filter(), new List(), false /* onlySame */); - } - } - - private class Dir2DatFeature : RombaSharpFeature - { - public const string Value = "Dir2Dat"; - - public Dir2DatFeature() - { - this.Name = Value; - this.Flags = new List() { "dir2dat" }; - this.Description = "Creates a DAT file for the specified input directory and saves it to the -out filename."; - this._featureType = FeatureType.Flag; - this.LongDescription = "Creates a DAT file for the specified input directory and saves it to the -out filename."; - this.Features = new Dictionary(); - - AddFeature(OutStringInput); - AddFeature(SourceStringInput); - AddFeature(NameStringInput); // Defaults to "untitled" - AddFeature(DescriptionStringInput); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - string name = GetString(features, NameStringValue); - string description = GetString(features, DescriptionStringValue); - string source = GetString(features, SourceStringValue); - string outdat = GetString(features, OutStringValue); - - // Ensure the output directory - DirectoryExtensions.Ensure(outdat, create: true); - - // Check that all required directories exist - if (!Directory.Exists(source)) - { - Globals.Logger.Error($"File '{source}' does not exist!"); - return; - } - - // Create the encapsulating datfile - DatFile datfile = DatFile.Create(); - datfile.Header.Name = string.IsNullOrWhiteSpace(name) ? "untitled" : name; - datfile.Header.Description = description; - - // Now run the D2D on the input and write out - // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - datfile.PopulateFromDir(source, Hash.DeepHashes, true /* bare */, false /* archivesAsFiles */, SkipFileType.None, false /* addBlanks */, - false /* addDate */, _tmpdir, false /* copyFiles */, null /* headerToCheckAgainst */, true /* chdsAsFiles */, null /* filter */); - datfile.Write(outDir: outdat); - } - } - - private class EDiffdatFeature : RombaSharpFeature - { - public const string Value = "EDiffdat"; - - public EDiffdatFeature() - { - this.Name = Value; - this.Flags = new List() { "ediffdat" }; - this.Description = "Creates a DAT file with those entries that are in -new DAT."; - this._featureType = FeatureType.Flag; - this.LongDescription = @"Creates a DAT file with those entries that are in -new DAT files and not -in -old DAT files. Ignores those entries in -old that are not in -new."; - this.Features = new Dictionary(); - - AddFeature(OutStringInput); - AddFeature(OldStringInput); - AddFeature(NewStringInput); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - string olddat = GetString(features, OldStringValue); - string outdat = GetString(features, OutStringValue); - string newdat = GetString(features, NewStringValue); - - // Ensure the output directory - DirectoryExtensions.Ensure(outdat, create: true); - - // Check that all required files exist - if (!File.Exists(olddat)) - { - Globals.Logger.Error($"File '{olddat}' does not exist!"); - return; - } - - if (!File.Exists(newdat)) - { - Globals.Logger.Error($"File '{newdat}' does not exist!"); - return; - } - - // Create the encapsulating datfile - DatFile datfile = DatFile.Create(); - - // Create the inputs - List dats = new List { newdat }; - List basedats = new List { olddat }; - - // Now run the diff on the inputs - datfile.DetermineUpdateType(dats, basedats, outdat, UpdateMode.DiffAgainst, false /* inplace */, false /* skip */, - new Filter(), new List(), false /* onlySame */); - } - } - - private class ExportFeature : RombaSharpFeature - { - public const string Value = "Export"; - - // Unique to RombaSharp - public ExportFeature() - { - this.Name = Value; - this.Flags = new List() { "export" }; - this.Description = "Exports db to export.csv"; - this._featureType = FeatureType.Flag; - this.LongDescription = "Exports db to standardized export.csv"; - this.Features = new Dictionary(); - } - - // TODO: Add ability to say which depot the files are found in - public override void ProcessFeatures(Dictionary features) - { - SqliteConnection dbc = new SqliteConnection(_connectionString); - dbc.Open(); - StreamWriter sw = new StreamWriter(FileExtensions.TryCreate("export.csv")); - - // First take care of all file hashes - sw.WriteLine("CRC,MD5,SHA-1"); // ,Depot - - string query = "SELECT crcsha1.crc, md5sha1.md5, md5sha1.sha1 FROM crcsha1 JOIN md5sha1 ON crcsha1.sha1=md5sha1.sha1"; // md5sha1.sha1=sha1depot.sha1 - SqliteCommand slc = new SqliteCommand(query, dbc); - SqliteDataReader sldr = slc.ExecuteReader(); - - if (sldr.HasRows) - { - while (sldr.Read()) - { - string line = $"{sldr.GetString(0)},{sldr.GetString(1)},{sldr.GetString(2)}"; // + ",{sldr.GetString(3)}"; - sw.WriteLine(line); - } - } - - // Then take care of all DAT hashes - sw.WriteLine(); - sw.WriteLine("DAT Hash"); - - query = "SELECT hash FROM dat"; - slc = new SqliteCommand(query, dbc); - sldr = slc.ExecuteReader(); - - if (sldr.HasRows) - { - while (sldr.Read()) - { - sw.WriteLine(sldr.GetString(0)); - } - } - - sldr.Dispose(); - slc.Dispose(); - sw.Dispose(); - dbc.Dispose(); - } - } - - private class FixdatFeature : RombaSharpFeature - { - public const string Value = "Fixdat"; - - public FixdatFeature() - { - this.Name = Value; - this.Flags = new List() { "fixdat" }; - this.Description = "For each specified DAT file it creates a fix DAT."; - this._featureType = FeatureType.Flag; - this.LongDescription = @"For each specified DAT file it creates a fix DAT with the missing entries for -that DAT. If nothing is missing it doesn't create a fix DAT for that -particular DAT."; - this.Features = new Dictionary(); - - AddFeature(OutStringInput); - AddFeature(FixdatOnlyFlag); // Enabled by default - AddFeature(WorkersInt32Input); - AddFeature(SubworkersInt32Input); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - // Inputs - bool fixdatOnly = GetBoolean(features, FixdatOnlyValue); - int subworkers = GetInt32(features, SubworkersInt32Value); - int workers = GetInt32(features, WorkersInt32Value); - string outdat = GetString(features, OutStringValue); - - Globals.Logger.Error("This feature is not yet implemented: fixdat"); - } - } - - private class HelpFeature : RombaSharpFeature - { - public const string Value = "Help"; - - public HelpFeature() - { - this.Name = Value; - this.Flags = new List() { "-?", "-h", "--help" }; - this.Description = "Show this help"; - this._featureType = FeatureType.Flag; - this.LongDescription = "Built-in to most of the programs is a basic help text."; - this.Features = new Dictionary(); - } - - public override bool ProcessArgs(string[] args, Help help) - { - // If we had something else after help - if (args.Length > 1) - { - help.OutputIndividualFeature(args[1]); - return true; - } - - // Otherwise, show generic help - else - { - help.OutputGenericHelp(); - return true; - } - } - } - - private class ImportFeature : RombaSharpFeature - { - public const string Value = "Import"; - - // Unique to RombaSharp - public ImportFeature() - { - this.Name = Value; - this.Flags = new List() { "import" }; - this.Description = "Import a database from a formatted CSV file"; - this._featureType = FeatureType.Flag; - this.LongDescription = "Import a database from a formatted CSV file"; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - Globals.Logger.Error("This feature is not yet implemented: import"); - - // First ensure the inputs and database connection - Inputs = DirectoryExtensions.GetFilesOnly(Inputs).Select(p => p.CurrentPath).ToList(); - SqliteConnection dbc = new SqliteConnection(_connectionString); - SqliteCommand slc = new SqliteCommand(); - dbc.Open(); - - // Now, for each of these files, attempt to add the data found inside - foreach (string input in Inputs) - { - StreamReader sr = new StreamReader(FileExtensions.TryOpenRead(input)); - - // The first line should be the hash header - string line = sr.ReadLine(); - if (line != "CRC,MD5,SHA-1") // ,Depot - { - Globals.Logger.Error("{0} is not a valid export file"); - continue; - } - - // Define the insert queries - string crcquery = "INSERT OR IGNORE INTO crc (crc) VALUES"; - string md5query = "INSERT OR IGNORE INTO md5 (md5) VALUES"; - string sha1query = "INSERT OR IGNORE INTO sha1 (sha1) VALUES"; - string crcsha1query = "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES"; - string md5sha1query = "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES"; - - // For each line until we hit a blank line... - while (!sr.EndOfStream && line != string.Empty) - { - line = sr.ReadLine(); - string[] hashes = line.Split(','); - - // Loop through the parsed entries - if (!string.IsNullOrWhiteSpace(hashes[0])) - crcquery += $" (\"{hashes[0]}\"),"; - - if (!string.IsNullOrWhiteSpace(hashes[1])) - md5query += $" (\"{hashes[1]}\"),"; - - if (!string.IsNullOrWhiteSpace(hashes[2])) - { - sha1query += $" (\"{hashes[2]}\"),"; - - if (!string.IsNullOrWhiteSpace(hashes[0])) - crcsha1query += $" (\"{hashes[0]}\", \"{hashes[2]}\"),"; - - if (!string.IsNullOrWhiteSpace(hashes[1])) - md5sha1query += $" (\"{hashes[1]}\", \"{hashes[2]}\"),"; - } - } - - // Now run the queries after fixing them - if (crcquery != "INSERT OR IGNORE INTO crc (crc) VALUES") - { - slc = new SqliteCommand(crcquery.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - if (md5query != "INSERT OR IGNORE INTO md5 (md5) VALUES") - { - slc = new SqliteCommand(md5query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - if (sha1query != "INSERT OR IGNORE INTO sha1 (sha1) VALUES") - { - slc = new SqliteCommand(sha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - if (crcsha1query != "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES") - { - slc = new SqliteCommand(crcsha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - if (md5sha1query != "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES") - { - slc = new SqliteCommand(md5sha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - // Now add all of the DAT hashes - // TODO: Do we really need to save the DAT hashes? - - sr.Dispose(); - } - - slc.Dispose(); - dbc.Dispose(); - } - } - - private class LookupFeature : RombaSharpFeature - { - public const string Value = "Lookup"; - - public LookupFeature() - { - this.Name = Value; - this.Flags = new List() { "lookup" }; - this.Description = "For each specified hash it looks up any available information."; - this._featureType = FeatureType.Flag; - this.LongDescription = "For each specified hash it looks up any available information (dat or rom)."; - this.Features = new Dictionary(); - - AddFeature(SizeInt64Input); // Defaults to -1 - AddFeature(OutStringInput); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - long size = GetInt64(features, SizeInt64Value); - string outdat = GetString(features, OutStringValue); - - // First, try to figure out what type of hash each is by length and clean it - List crc = new List(); - List md5 = new List(); - List sha1 = new List(); - foreach (string input in Inputs) - { - string temp = string.Empty; - if (input.Length == Constants.CRCLength) - { - temp = Sanitizer.CleanCRC32(input); - if (!string.IsNullOrWhiteSpace(temp)) - { - crc.Add(temp); - } - } - else if (input.Length == Constants.MD5Length) - { - temp = Sanitizer.CleanMD5(input); - if (!string.IsNullOrWhiteSpace(temp)) - { - md5.Add(temp); - } - } - else if (input.Length == Constants.SHA1Length) - { - temp = Sanitizer.CleanSHA1(input); - if (!string.IsNullOrWhiteSpace(temp)) - { - sha1.Add(temp); - } - } - } - - SqliteConnection dbc = new SqliteConnection(_connectionString); - dbc.Open(); - - // Now, search for each of them and return true or false for each - foreach (string input in crc) - { - string query = $"SELECT * FROM crc WHERE crc=\"{input}\""; - SqliteCommand slc = new SqliteCommand(query, dbc); - SqliteDataReader sldr = slc.ExecuteReader(); - if (sldr.HasRows) - { - int count = 0; - while (sldr.Read()) - { - count++; - } - - Globals.Logger.User($"For hash '{input}' there were {count} matches in the database"); - } - else - { - Globals.Logger.User($"Hash '{input}' had no matches in the database"); - } - - sldr.Dispose(); - slc.Dispose(); - } - foreach (string input in md5) - { - string query = $"SELECT * FROM md5 WHERE md5=\"{input}\""; - SqliteCommand slc = new SqliteCommand(query, dbc); - SqliteDataReader sldr = slc.ExecuteReader(); - if (sldr.HasRows) - { - int count = 0; - while (sldr.Read()) - { - count++; - } - - Globals.Logger.User($"For hash '{input}' there were {count} matches in the database"); - } - else - { - Globals.Logger.User($"Hash '{input}' had no matches in the database"); - } - - sldr.Dispose(); - slc.Dispose(); - } - foreach (string input in sha1) - { - string query = $"SELECT * FROM sha1 WHERE sha1=\"{input}\""; - SqliteCommand slc = new SqliteCommand(query, dbc); - SqliteDataReader sldr = slc.ExecuteReader(); - if (sldr.HasRows) - { - int count = 0; - while (sldr.Read()) - { - count++; - } - - Globals.Logger.User($"For hash '{input}' there were {count} matches in the database"); - } - else - { - Globals.Logger.User($"Hash '{input}' had no matches in the database"); - } - - sldr.Dispose(); - slc.Dispose(); - } - - dbc.Dispose(); - } - } - - private class MemstatsFeature : RombaSharpFeature - { - public const string Value = "Memstats"; - - public MemstatsFeature() - { - this.Name = Value; - this.Flags = new List() { "memstats" }; - this.Description = "Prints memory stats."; - this._featureType = FeatureType.Flag; - this.LongDescription = "Print memory stats."; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - Globals.Logger.User("This feature is not yet implemented: memstats"); - } - } - - private class MergeFeature : RombaSharpFeature - { - public const string Value = "Merge"; - - public MergeFeature() - { - this.Name = Value; - this.Flags = new List() { "merge" }; - this.Description = "Merges depot"; - this._featureType = FeatureType.Flag; - this.LongDescription = "Merges specified depot into current depot."; - this.Features = new Dictionary(); - - AddFeature(OnlyNeededFlag); - AddFeature(ResumeStringInput); - AddFeature(WorkersInt32Input); - AddFeature(SkipInitialScanFlag); - } - - // TODO: Add way of specifying "current depot" since that's what Romba relies on - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - bool onlyNeeded = GetBoolean(features, OnlyNeededValue); - bool skipInitialscan = GetBoolean(features, SkipInitialScanValue); - int workers = GetInt32(features, WorkersInt32Value); - string resume = GetString(features, ResumeStringValue); - - Globals.Logger.Error("This feature is not yet implemented: merge"); - - // Verify that the inputs are valid directories - Inputs = DirectoryExtensions.GetDirectoriesOnly(Inputs).Select(p => p.CurrentPath).ToList(); - - // Loop over all input directories - foreach (string input in Inputs) - { - List depotFiles = Directory.EnumerateFiles(input, "*.gz", SearchOption.AllDirectories).ToList(); - - // If we are copying all that is possible but we want to scan first - if (!onlyNeeded && !skipInitialscan) - { - - } - - // If we are copying all that is possible but we don't care to scan first - else if (!onlyNeeded && skipInitialscan) - { - - } - - // If we are copying only what is needed but we want to scan first - else if (onlyNeeded && !skipInitialscan) - { - - } - - // If we are copying only what is needed but we don't care to scan first - else if (onlyNeeded && skipInitialscan) - { - - } - } - } - } - - private class MissFeature : RombaSharpFeature - { - public const string Value = "Miss"; - - // Unique to RombaSharp - public MissFeature() - { - this.Name = Value; - this.Flags = new List() { "miss" }; - this.Description = "Create miss and have file"; - this._featureType = FeatureType.Flag; - this.LongDescription = "For each specified DAT file, create miss and have file"; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - // Verify the filenames - Dictionary foundDats = GetValidDats(Inputs); - - // Create the new output directory if it doesn't exist - DirectoryExtensions.Ensure(Path.Combine(Globals.ExeDir, "out"), create: true); - - // Now that we have the dictionary, we can loop through and output to a new folder for each - foreach (string key in foundDats.Keys) - { - // Get the DAT file associated with the key - DatFile datFile = DatFile.CreateAndParse(Path.Combine(_dats, foundDats[key])); - - // Now loop through and see if all of the hash combinations exist in the database - /* ended here */ - } - - Globals.Logger.Error("This feature is not yet implemented: miss"); - } - } - - private class ProgressFeature : RombaSharpFeature - { - public const string Value = "Progress"; - - public ProgressFeature() - { - this.Name = Value; - this.Flags = new List() { "progress" }; - this.Description = "Shows progress of the currently running command."; - this._featureType = FeatureType.Flag; - this.LongDescription = "Shows progress of the currently running command."; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - Globals.Logger.User("This feature is not yet implemented: progress"); - } - } - - private class PurgeBackupFeature : RombaSharpFeature - { - public const string Value = "Purge Backup"; - - public PurgeBackupFeature() - { - this.Name = Value; - this.Flags = new List() { "purge-backup" }; - this.Description = "Moves DAT index entries for orphaned DATs."; - this._featureType = FeatureType.Flag; - this.LongDescription = @"Deletes DAT index entries for orphaned DATs and moves ROM files that are no -longer associated with any current DATs to the specified backup folder. -The files will be placed in the backup location using -a folder structure according to the original DAT master directory tree -structure. It also deletes the specified DATs from the DAT index."; - this.Features = new Dictionary(); - - AddFeature(BackupStringInput); - AddFeature(WorkersInt32Input); - AddFeature(DepotListStringInput); - AddFeature(DatsListStringInput); - AddFeature(LogOnlyFlag); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - bool logOnly = GetBoolean(features, LogOnlyValue); - int workers = GetInt32(features, WorkersInt32Value); - string backup = GetString(features, BackupStringValue); - List dats = GetList(features, DatsListStringValue); - List depot = GetList(features, DepotListStringValue); - - Globals.Logger.Error("This feature is not yet implemented: purge-backup"); - } - } - - private class PurgeDeleteFeature : RombaSharpFeature - { - public const string Value = "Purge Delete"; - - // Unique to RombaSharp - public PurgeDeleteFeature() - { - this.Name = Value; - this.Flags = new List() { "purge-delete" }; - this.Description = "Deletes DAT index entries for orphaned DATs"; - this._featureType = FeatureType.Flag; - this.LongDescription = @"Deletes DAT index entries for orphaned DATs and moves ROM files that are no -longer associated with any current DATs to the specified backup folder. -The files will be placed in the backup location using -a folder structure according to the original DAT master directory tree -structure. It also deletes the specified DATs from the DAT index."; - this.Features = new Dictionary(); - - AddFeature(WorkersInt32Input); - AddFeature(DepotListStringInput); - AddFeature(DatsListStringInput); - AddFeature(LogOnlyFlag); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - bool logOnly = GetBoolean(features, LogOnlyValue); - int workers = GetInt32(features, WorkersInt32Value); - List dats = GetList(features, DatsListStringValue); - List depot = GetList(features, DepotListStringValue); - - Globals.Logger.Error("This feature is not yet implemented: purge-delete"); - } - } - - private class RefreshDatsFeature : RombaSharpFeature - { - public const string Value = "Refresh DATs"; - - public RefreshDatsFeature() - { - this.Name = Value; - this.Flags = new List() { "refresh-dats" }; - this.Description = "Refreshes the DAT index from the files in the DAT master directory tree."; - this._featureType = FeatureType.Flag; - this.LongDescription = @"Refreshes the DAT index from the files in the DAT master directory tree. -Detects any changes in the DAT master directory tree and updates the DAT index -accordingly, marking deleted or overwritten dats as orphaned and updating -contents of any changed dats."; - this.Features = new Dictionary(); - - AddFeature(WorkersInt32Input); - AddFeature(MissingSha1sStringInput); - } - - public override void ProcessFeatures(Dictionary features) - { - // Get feature flags - int workers = GetInt32(features, WorkersInt32Value); - string missingSha1s = GetString(features, MissingSha1sStringValue); - - // Make sure the db is set - if (string.IsNullOrWhiteSpace(_db)) - { - _db = "db.sqlite"; - _connectionString = $"Data Source={_db};Version = 3;"; - } - - // Make sure the file exists - if (!File.Exists(_db)) - DatabaseTools.EnsureDatabase(_dbSchema, _db, _connectionString); - - // Make sure the dats dir is set - if (string.IsNullOrWhiteSpace(_dats)) - _dats = "dats"; - - _dats = Path.Combine(Globals.ExeDir, _dats); - - // Make sure the folder exists - if (!Directory.Exists(_dats)) - Directory.CreateDirectory(_dats); - - // First get a list of SHA-1's from the input DATs - DatFile datroot = DatFile.Create(); - datroot.Header.Type = "SuperDAT"; - - // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - datroot.PopulateFromDir(_dats, Hash.DeepHashes, false, false, SkipFileType.None, false, false, _tmpdir, false, null, true, null); - datroot.Items.BucketBy(BucketedBy.SHA1, DedupeType.None); - - // Create a List of dat hashes in the database (SHA-1) - List databaseDats = new List(); - List unneeded = new List(); - - SqliteConnection dbc = new SqliteConnection(_connectionString); - dbc.Open(); - - // Populate the List from the database - InternalStopwatch watch = new InternalStopwatch("Populating the list of existing DATs"); - - string query = "SELECT DISTINCT hash FROM dat"; - SqliteCommand slc = new SqliteCommand(query, dbc); - SqliteDataReader sldr = slc.ExecuteReader(); - if (sldr.HasRows) - { - sldr.Read(); - string hash = sldr.GetString(0); - if (datroot.Items.ContainsKey(hash)) - { - datroot.Items.Remove(hash); - databaseDats.Add(hash); - } - else if (!databaseDats.Contains(hash)) - { - unneeded.Add(hash); - } - } - - datroot.Items.BucketBy(BucketedBy.Game, DedupeType.None, norename: true); - - watch.Stop(); - - slc.Dispose(); - sldr.Dispose(); - - // Loop through the Dictionary and add all data - watch.Start("Adding new DAT information"); - foreach (string key in datroot.Items.Keys) - { - foreach (Rom value in datroot.Items[key]) - { - AddDatToDatabase(value, dbc); - } - } - - watch.Stop(); - - // Now loop through and remove all references to old Dats - if (unneeded.Count > 0) - { - watch.Start("Removing unmatched DAT information"); - - query = "DELETE FROM dat WHERE"; - foreach (string dathash in unneeded) - { - query += $" OR hash=\"{dathash}\""; - } - - query = query.Replace("WHERE OR", "WHERE"); - slc = new SqliteCommand(query, dbc); - slc.ExecuteNonQuery(); - slc.Dispose(); - - watch.Stop(); - } - - dbc.Dispose(); - } - } - - private class RescanDepotsFeature : RombaSharpFeature - { - public const string Value = "Rescan Depots"; - - // Unique to RombaSharp - public RescanDepotsFeature() - { - this.Name = Value; - this.Flags = new List() { "depot-rescan" }; - this.Description = "Rescan a specific depot to get new information"; - this._featureType = FeatureType.Flag; - this.LongDescription = "Rescan a specific depot to get new information"; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - Globals.Logger.Error("This feature is not yet implemented: rescan-depots"); - - foreach (string depotname in Inputs) - { - // Check that it's a valid depot first - if (!_depots.ContainsKey(depotname)) - { - Globals.Logger.User($"'{depotname}' is not a recognized depot. Please add it to your configuration file and try again"); - return; - } - - // Then check that the depot is online - if (!Directory.Exists(depotname)) - { - Globals.Logger.User($"'{depotname}' does not appear to be online. Please check its status and try again"); - return; - } - - // Open the database connection - SqliteConnection dbc = new SqliteConnection(_connectionString); - dbc.Open(); - - // If we have it, then check for all hashes that are in that depot - List hashes = new List(); - string query = $"SELECT sha1 FROM sha1 WHERE depot=\"{depotname}\""; - SqliteCommand slc = new SqliteCommand(query, dbc); - SqliteDataReader sldr = slc.ExecuteReader(); - if (sldr.HasRows) - { - while (sldr.Read()) - { - hashes.Add(sldr.GetString(0)); - } - } - - // Now rescan the depot itself - DatFile depot = DatFile.Create(); - - // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - depot.PopulateFromDir(depotname, Hash.DeepHashes, false, false, SkipFileType.None, false, false, _tmpdir, false, null, true, null); - depot.Items.BucketBy(BucketedBy.SHA1, DedupeType.None); - - // Set the base queries to use - string crcquery = "INSERT OR IGNORE INTO crc (crc) VALUES"; - string md5query = "INSERT OR IGNORE INTO md5 (md5) VALUES"; - string sha1query = "INSERT OR IGNORE INTO sha1 (sha1, depot) VALUES"; - string crcsha1query = "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES"; - string md5sha1query = "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES"; - - // Once we have both, check for any new files - List dupehashes = new List(); - IEnumerable keys = depot.Items.Keys; - foreach (string key in keys) - { - List roms = depot.Items[key]; - foreach (Rom rom in roms) - { - if (hashes.Contains(rom.SHA1)) - { - dupehashes.Add(rom.SHA1); - hashes.Remove(rom.SHA1); - } - else if (!dupehashes.Contains(rom.SHA1)) - { - if (!string.IsNullOrWhiteSpace(rom.CRC)) - crcquery += $" (\"{rom.CRC}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.MD5)) - md5query += $" (\"{rom.MD5}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.SHA1)) - { - sha1query += $" (\"{rom.SHA1}\", \"{depotname}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.CRC)) - crcsha1query += $" (\"{rom.CRC}\", \"{rom.SHA1}\"),"; - - if (!string.IsNullOrWhiteSpace(rom.MD5)) - md5sha1query += $" (\"{rom.MD5}\", \"{rom.SHA1}\"),"; - } - } - } - } - - // Now run the queries after fixing them - if (crcquery != "INSERT OR IGNORE INTO crc (crc) VALUES") - { - slc = new SqliteCommand(crcquery.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - if (md5query != "INSERT OR IGNORE INTO md5 (md5) VALUES") - { - slc = new SqliteCommand(md5query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - if (sha1query != "INSERT OR IGNORE INTO sha1 (sha1, depot) VALUES") - { - slc = new SqliteCommand(sha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - if (crcsha1query != "INSERT OR IGNORE INTO crcsha1 (crc, sha1) VALUES") - { - slc = new SqliteCommand(crcsha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - if (md5sha1query != "INSERT OR IGNORE INTO md5sha1 (md5, sha1) VALUES") - { - slc = new SqliteCommand(md5sha1query.TrimEnd(','), dbc); - slc.ExecuteNonQuery(); - } - - // Now that we've added the information, we get to remove all of the hashes that we want to - query = @"DELETE FROM sha1 -JOIN crcsha1 - ON sha1.sha1=crcsha1.sha1 -JOIN md5sha1 - ON sha1.sha1=md5sha1.sha1 -JOIN crc - ON crcsha1.crc=crc.crc -JOIN md5 - ON md5sha1.md5=md5.md5 -WHERE sha1.sha1 IN "; - query += $"({string.Join("\",\"", hashes)}\")"; - slc = new SqliteCommand(query, dbc); - slc.ExecuteNonQuery(); - - // Dispose of the database connection - slc.Dispose(); - dbc.Dispose(); - } - } - } - - private class ScriptFeature : RombaSharpFeature - { - public const string Value = "Script"; - - public ScriptFeature() - { - this.Name = Value; - this.Flags = new List() { "--script" }; - this.Description = "Enable script mode (no clear screen)"; - this._featureType = FeatureType.Flag; - this.LongDescription = "For times when RombaSharp is being used in a scripted environment, the user may not want the screen to be cleared every time that it is called. This flag allows the user to skip clearing the screen on run just like if the console was being redirected."; - this.Features = new Dictionary(); - } - } - - private class ShutdownFeature : RombaSharpFeature - { - public const string Value = "Shutdown"; - - public ShutdownFeature() - { - this.Name = Value; - this.Flags = new List() { "shutdown" }; - this.Description = "Gracefully shuts down server."; - this._featureType = FeatureType.Flag; - this.LongDescription = "Gracefully shuts down server saving all the cached data."; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - Globals.Logger.User("This feature is not yet implemented: shutdown"); - } - } - - private class VersionFeature : RombaSharpFeature - { - public const string Value = "Version"; - - public VersionFeature() - { - this.Name = Value; - this.Flags = new List() { "version" }; - this.Description = "Prints version"; - this._featureType = FeatureType.Flag; - this.LongDescription = "Prints version."; - this.Features = new Dictionary(); - } - - public override void ProcessFeatures(Dictionary features) - { - Globals.Logger.User($"RombaSharp version: {Constants.Version}"); - } - } - - #endregion - } -} diff --git a/SabreTools/Program.cs b/SabreTools/Program.cs index 91cd17ed..b1ee3a08 100644 --- a/SabreTools/Program.cs +++ b/SabreTools/Program.cs @@ -132,7 +132,7 @@ namespace SabreTools /// /// Generate a Help object for this program /// - /// + /// Populated Help object private static Help RetrieveHelp() { // Create and add the header to the Help object