From 7e423b70af52546dca124b6b38e7965f5706a8b7 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 2 Jun 2021 13:48:39 -0400 Subject: [PATCH 01/33] Initial commit --- FileVerification.sln | 25 ++++ FileVerification/FileSystemCrawlerSO.cs | 124 ++++++++++++++++++ FileVerification/FileVerification.csproj | 8 ++ FileVerification/HashInfo.cs | 125 ++++++++++++++++++ FileVerification/Logger.cs | 54 ++++++++ FileVerification/Program.cs | 29 ++++ FileVerification/VerifyFile.cs | 160 +++++++++++++++++++++++ 7 files changed, 525 insertions(+) create mode 100644 FileVerification.sln create mode 100644 FileVerification/FileSystemCrawlerSO.cs create mode 100644 FileVerification/FileVerification.csproj create mode 100644 FileVerification/HashInfo.cs create mode 100644 FileVerification/Logger.cs create mode 100644 FileVerification/Program.cs create mode 100644 FileVerification/VerifyFile.cs diff --git a/FileVerification.sln b/FileVerification.sln new file mode 100644 index 0000000..c9b2f52 --- /dev/null +++ b/FileVerification.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30717.126 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileVerification", "FileVerification\FileVerification.csproj", "{9FEB60CA-C228-48A6-B15C-BDF1858C431D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9FEB60CA-C228-48A6-B15C-BDF1858C431D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FEB60CA-C228-48A6-B15C-BDF1858C431D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FEB60CA-C228-48A6-B15C-BDF1858C431D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FEB60CA-C228-48A6-B15C-BDF1858C431D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8C753EA0-EFD1-42E4-84AF-58303236AC04} + EndGlobalSection +EndGlobal diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs new file mode 100644 index 0000000..160186a --- /dev/null +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + + +namespace FileVerification +{ + enum VerifyFileLayout + { + NAME, + HASH_ALGORITHM, + HASH + } + + public class FileSystemCrawlerSO + { + const string VERIFY_FILE_NAME = "__fv.txt"; + + public int NumFolders { get; set; } + + public int NumFiles { get; set; } + + public string FolderPath { get; set; } + + private List directories = new List(); + + private readonly ConcurrentBag tasks = new ConcurrentBag(); + private readonly ConcurrentBag fileTasks = new ConcurrentBag(); + + public void CollectFolders(string path) + { + FolderPath = path; + DirectoryInfo directoryInfo = new DirectoryInfo(path); + tasks.Add(Task.Run(() => CrawlFolder(directoryInfo))); + + Task taskToWaitFor; + while (tasks.TryTake(out taskToWaitFor)) + { + NumFolders++; + taskToWaitFor.Wait(); + } + } + + public void CollectFiles() + { + foreach (var dir in directories) + { + fileTasks.Add(Task.Run(() => GetFiles(dir))); + } + + Task taskToWaitFor; + while (fileTasks.TryTake(out taskToWaitFor)) + { + taskToWaitFor.Wait(); + } + + } + + private void CrawlFolder(DirectoryInfo dir) + { + try + { + DirectoryInfo[] directoryInfos = dir.GetDirectories(); + foreach (DirectoryInfo childInfo in directoryInfos) + { + // here may be dragons using enumeration variable as closure!! + DirectoryInfo di = childInfo; + tasks.Add(Task.Run(() => CrawlFolder(di))); + } + directories.Add(dir); + } + catch (Exception ex) + when (ex is DirectoryNotFoundException || ex is System.Security.SecurityException || ex is UnauthorizedAccessException) + { + Console.WriteLine($"ERROR: {ex.Message}"); + } + } + + private void GetFiles(DirectoryInfo dir) + { + FileInfo[] files = dir.GetFiles(); + VerifyFile verifyFile = new VerifyFile(VERIFY_FILE_NAME, dir); + + // Read the verify file, if it exists, but if the read method + // returns null, indicating an exception, and the verify file file + // exists, then assume there is an issue and don't continue with + // the hashing and verification + Dictionary filesData = verifyFile.Read(); + if (filesData == null && verifyFile.Exists()) + { + return; + } + + foreach (var file in files) + { + if (file.Name.Equals(VERIFY_FILE_NAME)) + { + continue; + } + + NumFiles++; + if (filesData.TryGetValue(file.Name, out HashInfo fileHashInfo)) + { + if (!fileHashInfo.IsEqual(file.FullName)) + { + Console.WriteLine($"Hash mismatch: {file.FullName}."); + } + } + else + { + filesData.Add(file.Name, new HashInfo(file, Algorithm.SHA256)); + } + } + + if (filesData.Count > 0) + { + verifyFile.Write(filesData, dir); + } + } + } +} diff --git a/FileVerification/FileVerification.csproj b/FileVerification/FileVerification.csproj new file mode 100644 index 0000000..c73e0d1 --- /dev/null +++ b/FileVerification/FileVerification.csproj @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp3.1 + + + diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs new file mode 100644 index 0000000..ed9665b --- /dev/null +++ b/FileVerification/HashInfo.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Security.Cryptography; +using System.IO; + +namespace FileVerification +{ + public enum Algorithm + { + MD5, + SHA, + SHA256, + SHA512 + } + + public class HashInfo + { + public Algorithm Algorithm { get; set; } + + public string Value { get; set; } + + public HashInfo(string algorithm, string hash) + { + if (algorithm == null || string.IsNullOrWhiteSpace(algorithm)) + { + throw new ArgumentNullException(nameof(algorithm)); + } + + if (hash == null || string.IsNullOrWhiteSpace(hash)) + { + throw new ArgumentNullException(nameof(hash)); + } + + Algorithm = GetHash(algorithm); + Value = hash; + } + + public HashInfo(FileInfo file, string algorithm) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + if (algorithm == null || string.IsNullOrWhiteSpace(nameof(algorithm))) + { + throw new ArgumentNullException(nameof(algorithm)); + } + + Algorithm = GetHash(algorithm); + Value = CreateFileHash(file.FullName); + } + + public HashInfo(FileInfo file, Algorithm algorithm) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + Algorithm = algorithm; + Value = CreateFileHash(file.FullName); + } + + /// + /// Gets the hash enumeration value of the hash string name. + /// + /// + /// The name of the hash. + /// + /// + /// The enum value of the hash. + /// + private Algorithm GetHash(string hash) + { + if (string.Compare(hash, "sha256", true) == 0) + { + return Algorithm.SHA256; + } + else + { + return Algorithm.SHA512; + } + } + + private string CreateFileHash(string filePath) + { + using (var hashAlgorithm = HashAlgorithm.Create(this.Algorithm.ToString())) + { + using (var stream = File.OpenRead(filePath)) + { + var hash = hashAlgorithm.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", ""); + } + } + } + + /// + /// Checks to see if the hash of a specific file is equal to this hash + /// value. + /// + /// + /// The path to the file that will be used to generate the hash to + /// compare to this hash. + /// + /// + /// True if the hashes are equal, false if the hashes are not equal. + /// + /// + /// The hash algorithm used will be the same one that is set for this + /// object. + /// + public bool IsEqual(string filePath) + { + if (filePath == null || string.IsNullOrWhiteSpace(filePath)) + { + return false; + } + + string fileHash = CreateFileHash(filePath); + return Value.Equals(fileHash); + } + } +} diff --git a/FileVerification/Logger.cs b/FileVerification/Logger.cs new file mode 100644 index 0000000..c1188da --- /dev/null +++ b/FileVerification/Logger.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace FileVerification +{ + static class Logger + { + // The default name for the log file + const string DEFAULT_NAME = "fv.log"; + + // Full path to the log file + static string fullPath; + + static Logger() + { + Initialize(Path.GetTempPath(), DEFAULT_NAME); + } + + private static void Initialize(string logFolder, string logName) + { + fullPath = Path.Combine(logFolder, logName); + Clear(); + } + + private static void Clear() + { + try + { + File.Delete(fullPath); + } + catch (Exception) + { + return; + } + } + + public static void WriteLine(string message) + { + try + { + using (StreamWriter writer = new StreamWriter(fullPath, true)) + { + writer.WriteLine(message); + } + } + catch (Exception ex) + { + Console.WriteLine($"WARNING: Couldn't write to log file. Reason: {ex.Message}"); + } + } + } +} diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs new file mode 100644 index 0000000..d8b90a8 --- /dev/null +++ b/FileVerification/Program.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace FileVerification +{ + class Program + { + public static int NumFolders { get; set; } + + static void Main(string[] args) + { + FileSystemCrawlerSO fsc = new FileSystemCrawlerSO(); + Stopwatch watch = new Stopwatch(); + + string docPath = @"F:\HashTest"; + watch.Start(); + fsc.CollectFolders(docPath); + fsc.CollectFiles(); + watch.Stop(); + Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine($"Folders: {fsc.NumFolders}"); + Logger.WriteLine($"Files: {fsc.NumFiles}"); + Logger.WriteLine($"Time (ms): { watch.ElapsedMilliseconds}"); + Logger.WriteLine("--------------------------------------------------------------------------------"); + } + } +} diff --git a/FileVerification/VerifyFile.cs b/FileVerification/VerifyFile.cs new file mode 100644 index 0000000..c7cdc35 --- /dev/null +++ b/FileVerification/VerifyFile.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace FileVerification +{ + public class VerifyFile + { + private string fileName; + + private DirectoryInfo directory; + + public string FilePath { get; private set; } + + /// + /// Initializes an instance of the class when + /// provided with the name of the verify file. + /// + /// + /// The name of the verify file. + /// + /// + /// The directory that should contain the verify file. + /// + /// + /// Thrown when an argument is not valid. + /// + /// + /// Thrown when an argument is null. + /// + public VerifyFile(string fileName, DirectoryInfo directory) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + if (directory == null) + { + throw new ArgumentNullException(nameof(directory)); + } + + this.fileName = fileName; + this.directory = directory; + + FilePath = Path.Combine(this.directory.FullName, fileName); + } + + /// + /// Returns a value indicating whether the verify file exists. + /// + /// + /// True if the verify file exists, false if the file doesn't exist. + /// + public bool Exists() + { + return File.Exists(FilePath); + } + + /// + /// Reads all the lines in the verify file, and then parses each of the + /// lines into a of + /// objects using the file name as the key. + /// + /// + /// A of + /// objects with the file name as the key. a value of null is + /// returned if there is an issue reading the file. + /// + public Dictionary Read() + { + Dictionary files = new Dictionary(); + + if (string.IsNullOrWhiteSpace(FilePath) || !File.Exists(FilePath)) + { + return files; + } + + try + { + using (var reader = new StreamReader(FilePath)) + { + while (!reader.EndOfStream) + { + string line = reader.ReadLine(); + string[] values = line.Split(','); + HashInfo info = new HashInfo(values[(int)VerifyFileLayout.HASH_ALGORITHM], values[(int)VerifyFileLayout.HASH]); + files.Add(values[(int)VerifyFileLayout.NAME], info); + } + } + + return files; + } + catch (UnauthorizedAccessException) + { + Logger.WriteLine($"ERROR: Not authorized to write to {FilePath}."); + return null; + } + catch (IOException ex) + { + Logger.WriteLine($"ERROR: Can't read the file. Reason: {ex.Message}"); + return null; + } + } + + /// + /// Writes to the verify file using the data stored in the files + /// parameter into the directory specified by the folder parameter. + /// + /// + /// A object containing the data + /// to write to the file. + /// + /// + /// The directory where the file is to be written. + /// + public void Write(Dictionary files, DirectoryInfo folder) + { + if (folder == null) + { + return; + } + + // Initialize the StringBuilder object that will contain the + // contents of the verify file + StringBuilder sb = new StringBuilder(); + + foreach (KeyValuePair file in files) + { + HashInfo hashInfo = file.Value; + sb.AppendLine($"{file.Key},{hashInfo.Algorithm.ToString().ToLower()},{hashInfo.Value}"); + } + + try + { + using (StreamWriter sw = new StreamWriter(FilePath)) + { + sw.Write(sb.ToString()); + } + } + catch (DirectoryNotFoundException) + { + Logger.WriteLine($"ERROR: The directory {folder.FullName} was not found."); + } + catch (PathTooLongException) + { + Logger.WriteLine($"ERROR: The path {FilePath} is too long."); + } + catch (UnauthorizedAccessException) + { + Logger.WriteLine($"ERROR: Not authorized to write to {FilePath}."); + } + catch (IOException ex) + { + Logger.WriteLine($"ERROR: Can't write to file. Reason: {ex.Message}"); + } + } + } +} From 762f02b3e283a8099da1e6390278deaa70bfaf90 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 2 Jun 2021 14:57:27 -0400 Subject: [PATCH 02/33] Added folder arg --- FileVerification/FileVerification.csproj | 4 ++++ FileVerification/Program.cs | 26 ++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/FileVerification/FileVerification.csproj b/FileVerification/FileVerification.csproj index c73e0d1..c6e510f 100644 --- a/FileVerification/FileVerification.csproj +++ b/FileVerification/FileVerification.csproj @@ -5,4 +5,8 @@ netcoreapp3.1 + + + + diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index d8b90a8..6f45b71 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.CommandLine; +using System.CommandLine.Invocation; namespace FileVerification { @@ -9,16 +11,32 @@ class Program { public static int NumFolders { get; set; } - static void Main(string[] args) - { + static int Main(string[] args) + { + RootCommand rootCommand = new RootCommand( + description: "Generates the hash of all files in a folder tree and stores the hashes in text files in each folder."); + + var folderOption = new Option( + aliases: new string[] { "--folder", "-f" }, + description: "The folder containing the files to verify with a hash." + ); + folderOption.IsRequired = true; + rootCommand.AddOption(folderOption); + rootCommand.Handler = CommandHandler.Create(Verify); + return rootCommand.Invoke(args); + } + + static void Verify(string folder) + { FileSystemCrawlerSO fsc = new FileSystemCrawlerSO(); Stopwatch watch = new Stopwatch(); - string docPath = @"F:\HashTest"; + //string docPath = @"F:\HashTest"; watch.Start(); - fsc.CollectFolders(docPath); + fsc.CollectFolders(folder); fsc.CollectFiles(); watch.Stop(); + Logger.WriteLine("--------------------------------------------------------------------------------"); Logger.WriteLine($"Folders: {fsc.NumFolders}"); Logger.WriteLine($"Files: {fsc.NumFiles}"); From bd52b0e3a1fc66c2675d6625a9cb2b5230d480e4 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 2 Jun 2021 15:03:52 -0400 Subject: [PATCH 03/33] Change assembly name --- FileVerification/FileVerification.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/FileVerification/FileVerification.csproj b/FileVerification/FileVerification.csproj index c6e510f..b2889be 100644 --- a/FileVerification/FileVerification.csproj +++ b/FileVerification/FileVerification.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.1 + fv From a345ed5942af90ce515ca439a519b7ae0c6264aa Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 2 Jun 2021 15:04:03 -0400 Subject: [PATCH 04/33] Change logging --- FileVerification/FileSystemCrawlerSO.cs | 2 +- FileVerification/Logger.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index 160186a..527f47a 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -106,7 +106,7 @@ private void GetFiles(DirectoryInfo dir) { if (!fileHashInfo.IsEqual(file.FullName)) { - Console.WriteLine($"Hash mismatch: {file.FullName}."); + Logger.WriteLine($"Hash mismatch: {file.FullName}."); } } else diff --git a/FileVerification/Logger.cs b/FileVerification/Logger.cs index c1188da..9d05bab 100644 --- a/FileVerification/Logger.cs +++ b/FileVerification/Logger.cs @@ -38,6 +38,8 @@ private static void Clear() public static void WriteLine(string message) { + Console.WriteLine(message); + try { using (StreamWriter writer = new StreamWriter(fullPath, true)) From faed8bf18362ef0bcd85527f6f9b3b738e5ddd10 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 4 Jun 2021 15:41:37 -0400 Subject: [PATCH 05/33] Added check to ignore system files. --- FileVerification/FileSystemCrawlerSO.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index 527f47a..be235f9 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -96,11 +96,18 @@ private void GetFiles(DirectoryInfo dir) foreach (var file in files) { + // Ignore the verification file if (file.Name.Equals(VERIFY_FILE_NAME)) { continue; } + // Ignore system files + if (file.Attributes == FileAttributes.System) + { + continue; + } + NumFiles++; if (filesData.TryGetValue(file.Name, out HashInfo fileHashInfo)) { From dd25b79a179cabfcba03f087d0a5d238a06a91cf Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 4 Jun 2021 15:41:59 -0400 Subject: [PATCH 06/33] Fixed verify file read issue where a record isn't valid --- FileVerification/VerifyFile.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FileVerification/VerifyFile.cs b/FileVerification/VerifyFile.cs index c7cdc35..591a0fc 100644 --- a/FileVerification/VerifyFile.cs +++ b/FileVerification/VerifyFile.cs @@ -85,6 +85,11 @@ public Dictionary Read() { string line = reader.ReadLine(); string[] values = line.Split(','); + if (values.Length != Enum.GetNames(typeof(VerifyFileLayout)).Length) + { + Logger.WriteLine($"WARNING: Record size incorrect (record will be created using the current file data). File: {FilePath}, Record: {line}."); + continue; + } HashInfo info = new HashInfo(values[(int)VerifyFileLayout.HASH_ALGORITHM], values[(int)VerifyFileLayout.HASH]); files.Add(values[(int)VerifyFileLayout.NAME], info); } From 2b5467c9321887e4feabbeec6af682cd80e90720 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 4 Aug 2021 14:29:29 -0400 Subject: [PATCH 07/33] Added notifications --- .../Configuration/Notifications/Data.cs | 65 +++++ .../Configuration/Notifications/Header.cs | 18 ++ .../Configuration/Notifications/Headers.cs | 15 ++ .../Notifications/Notification.cs | 252 ++++++++++++++++++ .../Notifications/Notifications.cs | 170 ++++++++++++ .../Configuration/Notifications/Request.cs | 196 ++++++++++++++ FileVerification/Configuration/Settings.cs | 17 ++ FileVerification/FileSystemCrawlerSO.cs | 74 ++--- FileVerification/FileVerification.csproj | 1 + FileVerification/HashInfo.cs | 67 +++-- FileVerification/Logger.cs | 29 +- FileVerification/Program.cs | 182 ++++++++++++- FileVerification/VerifyFile.cs | 2 +- 13 files changed, 1024 insertions(+), 64 deletions(-) create mode 100644 FileVerification/Configuration/Notifications/Data.cs create mode 100644 FileVerification/Configuration/Notifications/Header.cs create mode 100644 FileVerification/Configuration/Notifications/Headers.cs create mode 100644 FileVerification/Configuration/Notifications/Notification.cs create mode 100644 FileVerification/Configuration/Notifications/Notifications.cs create mode 100644 FileVerification/Configuration/Notifications/Request.cs create mode 100644 FileVerification/Configuration/Settings.cs diff --git a/FileVerification/Configuration/Notifications/Data.cs b/FileVerification/Configuration/Notifications/Data.cs new file mode 100644 index 0000000..39a0f09 --- /dev/null +++ b/FileVerification/Configuration/Notifications/Data.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace TE.FileVerification.Configuration.Notifications +{ + /// + /// Contains the data used to send the request. + /// + public class Data + { + // The MIME type + private string _mimeType = Request.JSON_NAME; + + /// + /// Gets or sets the headers for the request. + /// + [XmlElement("headers")] + public Headers Headers { get; set; } + + /// + /// Gets or sets the body for the request. + /// + [XmlElement("body")] + public string Body { get; set; } + + /// + /// Gets or sets the MIME type string value. + /// + [XmlElement("type")] + public string MimeTypeString + { + get + { + return _mimeType; + } + set + { + _mimeType = (value == Request.JSON_NAME || value == Request.XML_NAME) ? value : Request.JSON_NAME; + } + } + + /// + /// Gets the MIME type from the string value. + /// + [XmlIgnore] + internal Request.MimeType MimeType + { + get + { + if (_mimeType == Request.XML_NAME) + { + return Request.MimeType.Xml; + } + else + { + return Request.MimeType.Json; + } + } + } + } +} diff --git a/FileVerification/Configuration/Notifications/Header.cs b/FileVerification/Configuration/Notifications/Header.cs new file mode 100644 index 0000000..c9dae5d --- /dev/null +++ b/FileVerification/Configuration/Notifications/Header.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace TE.FileVerification.Configuration.Notifications +{ + public class Header + { + [XmlElement("name")] + public string Name { get; set; } + + [XmlElement("value")] + public string Value { get; set; } + } +} diff --git a/FileVerification/Configuration/Notifications/Headers.cs b/FileVerification/Configuration/Notifications/Headers.cs new file mode 100644 index 0000000..07db991 --- /dev/null +++ b/FileVerification/Configuration/Notifications/Headers.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace TE.FileVerification.Configuration.Notifications +{ + public class Headers + { + [XmlElement("header")] + public List
HeaderList { get; set; } + } +} diff --git a/FileVerification/Configuration/Notifications/Notification.cs b/FileVerification/Configuration/Notifications/Notification.cs new file mode 100644 index 0000000..7e6f292 --- /dev/null +++ b/FileVerification/Configuration/Notifications/Notification.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Serialization; +using System.Text.Json; + +namespace TE.FileVerification.Configuration.Notifications +{ + public class Notification + { + // The message to send with the request. + private StringBuilder _message; + + /// + /// Gets or sets the URL of the request. + /// + [XmlElement("url")] + public string Url { get; set; } + + /// + /// Gets the URI value of the string URL. + /// + [XmlIgnore] + public Uri Uri + { + get + { + try + { + if (string.IsNullOrWhiteSpace(Url)) + { + return null; + } + + Uri uri = new Uri(Url); + return uri; + } + catch (Exception ex) + when (ex is ArgumentNullException || ex is UriFormatException) + { + return null; + } + } + } + + /// + /// Gets or sets the string representation of the request method. + /// + [XmlElement("method")] + public string MethodString { get; set; } + + /// + /// Gets the request method. + /// + [XmlIgnore] + public HttpMethod Method + { + get + { + HttpMethod method = HttpMethod.Post; + if (string.IsNullOrEmpty(MethodString)) + { + return method; + } + + try + { + method = (HttpMethod)Enum.Parse(typeof(HttpMethod), MethodString.ToUpper(), true); + } + catch (Exception ex) + when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException) + { + method = HttpMethod.Post; + } + + return method; + } + } + + /// + /// Gets or sets the data to send for the request. + /// + [XmlElement("data")] + public Data Data { get; set; } + + /// + /// Returns a value indicating if there is a message waiting to be sent + /// for the notification. + /// + [XmlIgnore] + public bool HasMessage + { + get + { + if (_message == null) + { + return false; + } + + return _message.Length > 0; + } + } + + /// + /// Initializes an instance of the class. + /// + public Notification() + { + _message = new StringBuilder(); + } + + /// + /// Sends the notification. + /// + /// + /// The value that replaces the [message] placeholder. + /// + internal void QueueRequest(string message) + { + //_message.Append(CleanMessage(message) + @"\n"); + _message.Append(message); + } + + /// + /// Send the notification request. + /// + /// + /// Thrown when the URL is null or empty. + /// + internal HttpResponseMessage Send() + { + // If there isn't a message to be sent, then just return + if (_message?.Length <= 0) + { + return null; + } + + if (Uri == null) + { + throw new NullReferenceException("The URL is null or empty."); + } + + string content = Data.Body.Replace("[message]", cleanForJSON(_message.ToString())); + + HttpResponseMessage response = + Request.Send( + Method, + Uri, + Data.Headers.HeaderList, + content, + Data.MimeType); + + _message.Clear(); + return response; + } + + /// + /// Send the notification request. + /// + /// + /// Thrown when the URL is null or empty. + /// + internal async Task SendAsync() + { + // If there isn't a message to be sent, then just return + if (_message?.Length <= 0) + { + return null; + } + + if (Uri == null) + { + throw new NullReferenceException("The URL is null or empty."); + } + + string content = Data.Body.Replace("[message]", _message.ToString()); + + HttpResponseMessage response = + await Request.SendAsync( + Method, + Uri, + Data.Headers.HeaderList, + content, + Data.MimeType); + + _message.Clear(); + return response; + } + + public static string cleanForJSON(string s) + { + if (s == null || s.Length == 0) + { + return ""; + } + + char c = '\0'; + int i; + int len = s.Length; + StringBuilder sb = new StringBuilder(len + 4); + String t; + + for (i = 0; i < len; i += 1) + { + c = s[i]; + switch (c) + { + case '\\': + case '"': + sb.Append('\\'); + sb.Append(c); + break; + case '/': + sb.Append('\\'); + sb.Append(c); + break; + case '\b': + sb.Append("\\b"); + break; + case '\t': + sb.Append("\\t"); + break; + case '\n': + sb.Append("\\n"); + break; + case '\f': + sb.Append("\\f"); + break; + case '\r': + sb.Append("\\r"); + break; + default: + if (c < ' ') + { + t = "000" + String.Format("X", c); + sb.Append("\\u" + t.Substring(t.Length - 4)); + } + else + { + sb.Append(c); + } + break; + } + } + return sb.ToString(); + } + } +} diff --git a/FileVerification/Configuration/Notifications/Notifications.cs b/FileVerification/Configuration/Notifications/Notifications.cs new file mode 100644 index 0000000..bc31b0b --- /dev/null +++ b/FileVerification/Configuration/Notifications/Notifications.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Timers; +using System.Threading.Tasks; +using System.Xml.Serialization; +using TE.FileVerification; + +namespace TE.FileVerification.Configuration.Notifications +{ + /// + /// The notifications root node in the XML file. + /// + [XmlRoot("notifications")] + public class Notifications + { + // The default wait time + private const int DEFAULT_WAIT_TIME = 60000; + + // The minimum wait time + private const int MIN_WAIT_TIME = 30000; + + // The timer + private Timer _timer; + + /// + /// Gets or sets the wait time between notification requests. + /// + [XmlElement("waittime")] + public int WaitTime { get; set; } = DEFAULT_WAIT_TIME; + + /// + /// Gets or sets the notifications list. + /// + [XmlElement("notification")] + public List NotificationList { get; set; } + + /// + /// Initializes an instance of the class. + /// + public Notifications() + { + _timer = new Timer(WaitTime); + _timer.Elapsed += OnElapsed; + _timer.Start(); + } + + /// + /// Called when the timers elapsed time has been reached. + /// + /// + /// The timer object. + /// + /// + /// The information associated witht he elapsed time. + /// + private async void OnElapsed(object source, ElapsedEventArgs e) + { + // If there are no notifications, then stop the timer + if (NotificationList == null || NotificationList.Count <= 0) + { + _timer.Stop(); + return; + } + + // Ensure the wait time is not less than the minimum wait time + if (WaitTime < MIN_WAIT_TIME) + { + Logger.WriteLine($"The wait time {WaitTime} is below the minimum of {MIN_WAIT_TIME}. Setting wait time to {MIN_WAIT_TIME}."); + WaitTime = MIN_WAIT_TIME; + } + + foreach (Notification notification in NotificationList) + { + // If the notification doesn't have a message to send, then + // continue to the next notification + if (!notification.HasMessage) + { + continue; + } + + try + { + Logger.WriteLine($"Sending the request to {notification.Url}."); + using (HttpResponseMessage response = await notification.SendAsync()) + { + if (response == null) + { + continue; + } + + using (HttpContent httpContent = response.Content) + { + string resultContent = await httpContent.ReadAsStringAsync(); + Logger.WriteLine($"Response: {response.StatusCode}. Content: {resultContent}"); + } + } + } + catch (AggregateException aex) + { + foreach (Exception ex in aex.Flatten().InnerExceptions) + { + Logger.WriteLine(ex.Message); + Logger.WriteLine($"StackTrace:{Environment.NewLine}{ex.StackTrace}"); + } + } + catch (NullReferenceException ex) + { + Logger.WriteLine(ex.Message); + Logger.WriteLine($"StackTrace:{Environment.NewLine}{ex.StackTrace}"); + } + } + } + + /// + /// Sends the notification request. + /// + /// + /// The trigger associated with the request. + /// + /// + /// The message to include in the request. + /// + public void Send(string message) + { + if (NotificationList == null || NotificationList.Count <= 0) + { + return; + } + + foreach (Notification notification in NotificationList) + { + try + { + notification.QueueRequest(message); + + Logger.WriteLine($"Sending the request to {notification.Url}."); + using (HttpResponseMessage response = notification.Send()) + { + if (response == null) + { + continue; + } + + using (HttpContent httpContent = response.Content) + { + string resultContent = httpContent.ReadAsStringAsync().Result; + Logger.WriteLine($"Response: {response.StatusCode}. Content: {resultContent}"); + } + } + } + catch (AggregateException aex) + { + foreach (Exception ex in aex.Flatten().InnerExceptions) + { + Logger.WriteLine(ex.Message); + Logger.WriteLine($"StackTrace:{Environment.NewLine}{ex.StackTrace}"); + } + } + catch (NullReferenceException ex) + { + Logger.WriteLine(ex.Message); + Logger.WriteLine($"StackTrace:{Environment.NewLine}{ex.StackTrace}"); + } + } + } + } +} diff --git a/FileVerification/Configuration/Notifications/Request.cs b/FileVerification/Configuration/Notifications/Request.cs new file mode 100644 index 0000000..181e14f --- /dev/null +++ b/FileVerification/Configuration/Notifications/Request.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace TE.FileVerification.Configuration.Notifications +{ + internal static class Request + { + /// + /// The MIME type used for the request. + /// + internal enum MimeType + { + /// + /// JSON + /// + Json, + /// + /// XML + /// + Xml + } + + /// + /// The valid JSON name. + /// + internal const string JSON_NAME = "JSON"; + + /// + /// The valid XML name. + /// + internal const string XML_NAME = "XML"; + + // JSON mime type + private const string MIME_TYPE_JSON = "application/json"; + + // XML mime type + private const string MIME_TYPE_XML = "application/xml"; + + // The HTTP client + private static HttpClient _httpClient; + + /// + /// Sends a request to a remote system. + /// + /// + /// The HTTP method to use for the request. + /// + /// The URL of the request. + /// + /// A of objects associated + /// with the request. + /// + /// The content body of the request. + /// + /// + /// The MIME type associated with the request. + /// + /// + /// The response message of the request. + /// + /// + /// Thrown when an argument is null or empty. + /// + internal static HttpResponseMessage Send( + HttpMethod method, + Uri uri, + List
headers, + string body, + MimeType mimeType) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } + + HttpRequestMessage request = new HttpRequestMessage(method, uri); + foreach (Header header in headers) + { + request.Headers.Add(header.Name, header.Value); + } + request.Content = new StringContent(body, Encoding.UTF8, GetMimeTypeString(mimeType)); + + HttpResponseMessage response = null; + try + { + response = _httpClient.SendAsync(request).Result; + } + catch (Exception ex) + { + if (response == null) + { + response = new HttpResponseMessage(); + } + + response.StatusCode = System.Net.HttpStatusCode.InternalServerError; + response.ReasonPhrase = $"Request could not be sent. Reason: {ex.Message}"; + } + + return response; + } + + /// + /// Sends a request to a remote system asychronously. + /// + /// + /// The HTTP method to use for the request. + /// + /// The URL of the request. + /// + /// A of objects associated + /// with the request. + /// + /// The content body of the request. + /// + /// + /// The MIME type associated with the request. + /// + /// + /// The response message of the request. + /// + /// + /// Thrown when an argument is null or empty. + /// + internal static async Task SendAsync( + HttpMethod method, + Uri uri, + List
headers, + string body, + MimeType mimeType) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } + + HttpRequestMessage request = new HttpRequestMessage(method, uri); + foreach (Header header in headers) + { + request.Headers.Add(header.Name, header.Value); + } + request.Content = new StringContent(body, Encoding.UTF8, GetMimeTypeString(mimeType)); + + HttpResponseMessage response = null; + try + { + response = await _httpClient.SendAsync(request); + } + catch (Exception ex) + { + if (response == null) + { + response = new HttpResponseMessage(); + } + + response.StatusCode = System.Net.HttpStatusCode.InternalServerError; + response.ReasonPhrase = $"Request could not be sent. Reason: {ex.Message}"; + } + + return response; + } + + /// + /// Gets the string value of the specified MIME type. + /// + /// + /// The MIME type used for the request. + /// + /// + /// The string value of the specified MIME type. + /// + private static string GetMimeTypeString(MimeType mimeType) + { + string type = MIME_TYPE_JSON; + if (mimeType == MimeType.Xml) + { + type = MIME_TYPE_XML; + } + + return type; + } + } +} diff --git a/FileVerification/Configuration/Settings.cs b/FileVerification/Configuration/Settings.cs new file mode 100644 index 0000000..c084113 --- /dev/null +++ b/FileVerification/Configuration/Settings.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Serialization; + +namespace TE.FileVerification.Configuration +{ + [XmlRoot("settings")] + public class Settings + { + /// + /// Gets or sets the notifications for the verification. + /// + [XmlElement("notifications")] + public Notifications.Notifications Notifications { get; set; } + } +} diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index be235f9..4b047c7 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -5,8 +5,7 @@ using System.Text; using System.Threading.Tasks; - -namespace FileVerification +namespace TE.FileVerification { enum VerifyFileLayout { @@ -25,10 +24,23 @@ public class FileSystemCrawlerSO public string FolderPath { get; set; } + private int processorCount; + + private int threadCount; + private List directories = new List(); private readonly ConcurrentBag tasks = new ConcurrentBag(); - private readonly ConcurrentBag fileTasks = new ConcurrentBag(); + //private readonly ConcurrentBag fileTasks = new ConcurrentBag(); + + public FileSystemCrawlerSO() + { + processorCount = Environment.ProcessorCount; + threadCount = processorCount - 1; + + Logger.WriteLine($"Processors: {processorCount}."); + Logger.WriteLine($"Threads: {threadCount}."); + } public void CollectFolders(string path) { @@ -45,18 +57,11 @@ public void CollectFolders(string path) } public void CollectFiles() - { + { foreach (var dir in directories) - { - fileTasks.Add(Task.Run(() => GetFiles(dir))); - } - - Task taskToWaitFor; - while (fileTasks.TryTake(out taskToWaitFor)) { - taskToWaitFor.Wait(); + GetFiles(dir); } - } private void CrawlFolder(DirectoryInfo dir) @@ -88,44 +93,51 @@ private void GetFiles(DirectoryInfo dir) // returns null, indicating an exception, and the verify file file // exists, then assume there is an issue and don't continue with // the hashing and verification - Dictionary filesData = verifyFile.Read(); - if (filesData == null && verifyFile.Exists()) + Dictionary verifyFileData = verifyFile.Read(); + if (verifyFileData == null && verifyFile.Exists()) { return; } - foreach (var file in files) + ConcurrentDictionary folderFileData = new ConcurrentDictionary(); + + ParallelOptions options = new ParallelOptions(); + options.MaxDegreeOfParallelism = threadCount; + Parallel.ForEach(files, options, file => { - // Ignore the verification file - if (file.Name.Equals(VERIFY_FILE_NAME)) - { - continue; - } - - // Ignore system files - if (file.Attributes == FileAttributes.System) + // Ignore the verification file and system files + if (file.Name.Equals(VERIFY_FILE_NAME) || file.Attributes == FileAttributes.System) { - continue; + return; } + folderFileData.TryAdd(file.Name, new HashInfo(file, Algorithm.SHA256)); NumFiles++; - if (filesData.TryGetValue(file.Name, out HashInfo fileHashInfo)) + }); + + int count = 0; + foreach (var file in folderFileData) + { + if (verifyFileData.TryGetValue(file.Key, out HashInfo hashInfo)) { - if (!fileHashInfo.IsEqual(file.FullName)) + if (!hashInfo.IsHashEqual(file.Value.Value)) { - Logger.WriteLine($"Hash mismatch: {file.FullName}."); + Logger.WriteLine($"Hash mismatch: {dir.FullName}{Path.DirectorySeparatorChar}{file.Key}."); + count++; } } else { - filesData.Add(file.Name, new HashInfo(file, Algorithm.SHA256)); + verifyFileData.Add(file.Key, file.Value); } } - if (filesData.Count > 0) + if (verifyFileData.Count > 0) { - verifyFile.Write(filesData, dir); + verifyFile.Write(verifyFileData, dir); } - } + + Logger.WriteLine($"Number failed: {count}"); + } } } diff --git a/FileVerification/FileVerification.csproj b/FileVerification/FileVerification.csproj index b2889be..3dcfb20 100644 --- a/FileVerification/FileVerification.csproj +++ b/FileVerification/FileVerification.csproj @@ -4,6 +4,7 @@ Exe netcoreapp3.1 fv + TE.FileVerification diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs index ed9665b..e990e9f 100644 --- a/FileVerification/HashInfo.cs +++ b/FileVerification/HashInfo.cs @@ -4,7 +4,7 @@ using System.Security.Cryptography; using System.IO; -namespace FileVerification +namespace TE.FileVerification { public enum Algorithm { @@ -16,6 +16,9 @@ public enum Algorithm public class HashInfo { + // A megabyte + private const int Megabyte = 1024 * 1024; + public Algorithm Algorithm { get; set; } public string Value { get; set; } @@ -32,7 +35,7 @@ public HashInfo(string algorithm, string hash) throw new ArgumentNullException(nameof(hash)); } - Algorithm = GetHash(algorithm); + Algorithm = GetAlgorithm(algorithm); Value = hash; } @@ -48,8 +51,8 @@ public HashInfo(FileInfo file, string algorithm) throw new ArgumentNullException(nameof(algorithm)); } - Algorithm = GetHash(algorithm); - Value = CreateFileHash(file.FullName); + Algorithm = GetAlgorithm(algorithm); + Value = CreateFileHash(file); } public HashInfo(FileInfo file, Algorithm algorithm) @@ -60,19 +63,19 @@ public HashInfo(FileInfo file, Algorithm algorithm) } Algorithm = algorithm; - Value = CreateFileHash(file.FullName); + Value = CreateFileHash(file); } /// - /// Gets the hash enumeration value of the hash string name. + /// Gets the hash algorithm value of the algorithm string name. /// /// - /// The name of the hash. + /// The name of the algorithm. /// /// - /// The enum value of the hash. + /// The enum value of the algorithm. /// - private Algorithm GetHash(string hash) + private Algorithm GetAlgorithm(string hash) { if (string.Compare(hash, "sha256", true) == 0) { @@ -84,11 +87,15 @@ private Algorithm GetHash(string hash) } } - private string CreateFileHash(string filePath) + private string CreateFileHash(FileInfo file) { - using (var hashAlgorithm = HashAlgorithm.Create(this.Algorithm.ToString())) + //long size = file.Length; + int maxSize = 16 * Megabyte; + //int buffer = (int)((size <= maxSize) ? size : 16 * Megabyte); + + using (var hashAlgorithm = HashAlgorithm.Create(Algorithm.ToString())) { - using (var stream = File.OpenRead(filePath)) + using (var stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, maxSize)) { var hash = hashAlgorithm.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", ""); @@ -111,15 +118,35 @@ private string CreateFileHash(string filePath) /// The hash algorithm used will be the same one that is set for this /// object. /// - public bool IsEqual(string filePath) - { - if (filePath == null || string.IsNullOrWhiteSpace(filePath)) - { - return false; - } + //public bool IsEqual2(string filePath) + //{ + // if (filePath == null || string.IsNullOrWhiteSpace(filePath)) + // { + // return false; + // } + + // string fileHash = CreateFileHash(filePath); + // return Value.Equals(fileHash); + //} - string fileHash = CreateFileHash(filePath); - return Value.Equals(fileHash); + /// + /// Checks to see if the hash of a specific file is equal to this hash + /// value. + /// + /// + /// The path to the file that will be used to generate the hash to + /// compare to this hash. + /// + /// + /// True if the hashes are equal, false if the hashes are not equal. + /// + /// + /// The hash algorithm used will be the same one that is set for this + /// object. + /// + public bool IsHashEqual(string hash) + { + return Value.Equals(hash); } } } diff --git a/FileVerification/Logger.cs b/FileVerification/Logger.cs index 9d05bab..dd98087 100644 --- a/FileVerification/Logger.cs +++ b/FileVerification/Logger.cs @@ -3,7 +3,7 @@ using System.IO; using System.Text; -namespace FileVerification +namespace TE.FileVerification { static class Logger { @@ -13,6 +13,20 @@ static class Logger // Full path to the log file static string fullPath; + // The lines that have been logged + static StringBuilder _lines; + + /// + /// Gets the lines that have been logged. + /// + public static string Lines + { + get + { + return _lines != null ? _lines.ToString() : null; + } + } + static Logger() { Initialize(Path.GetTempPath(), DEFAULT_NAME); @@ -20,7 +34,7 @@ static Logger() private static void Initialize(string logFolder, string logName) { - fullPath = Path.Combine(logFolder, logName); + fullPath = Path.Combine(logFolder, logName); Clear(); } @@ -29,6 +43,14 @@ private static void Clear() try { File.Delete(fullPath); + if (_lines != null) + { + _lines.Clear(); + } + else + { + _lines = new StringBuilder(); + } } catch (Exception) { @@ -38,6 +60,7 @@ private static void Clear() public static void WriteLine(string message) { + Console.WriteLine(message); try @@ -46,6 +69,8 @@ public static void WriteLine(string message) { writer.WriteLine(message); } + + _lines.AppendLine(message); } catch (Exception ex) { diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index 6f45b71..07f4309 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -4,44 +4,206 @@ using System.IO; using System.CommandLine; using System.CommandLine.Invocation; +using TE.FileVerification.Configuration; +using System.Xml.Serialization; +using System.Reflection; -namespace FileVerification +namespace TE.FileVerification { class Program { + // Success return code + private const int SUCCESS = 0; + + // Error return code + private const int ERROR = -1; + + // The default configuration file name + const string DEFAULT_SETTINGS_FILE = "config.xml"; + public static int NumFolders { get; set; } static int Main(string[] args) { RootCommand rootCommand = new RootCommand( - description: "Generates the hash of all files in a folder tree and stores the hashes in text files in each folder."); + description: "Generates the hash of all files in a folder tree and stores the hashes in text files in each folder."); var folderOption = new Option( - aliases: new string[] { "--folder", "-f" }, + aliases: new string[] { "--folder", "-f" }, description: "The folder containing the files to verify with a hash." ); + folderOption.IsRequired = true; rootCommand.AddOption(folderOption); - rootCommand.Handler = CommandHandler.Create(Verify); + + var settingsFileOption = new Option( + aliases: new string[] { "--settingsFile", "-sfi" }, + description: "The name of the settings XML file." + ); + rootCommand.AddOption(settingsFileOption); + + var setingsFolderOption = new Option( + aliases: new string[] { "--settingsFolder", "-sfo" }, + description: "The folder containing the settings XML file." + ); + rootCommand.AddOption(setingsFolderOption); + + rootCommand.Handler = CommandHandler.Create(Verify); return rootCommand.Invoke(args); } - static void Verify(string folder) + /// + /// Gets the folder path containing the settings file. + /// + /// + /// The folder path. + /// + /// + /// The folder path of the files, otherwise null. + /// + private static string GetFolderPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + try + { + path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + } + catch (Exception ex) + { + Console.WriteLine($"The folder name is null or empty. Couldn't get the current location. Reason: {ex.Message}"); + return null; + } + } + + if (Directory.Exists(path)) + { + return path; + } + else + { + Console.WriteLine("The folder does not exist."); + return null; + } + } + + /// + /// Gets the full path to the settings file. + /// + /// + /// The path to the settings file. + /// + /// + /// The name of the settings file. + /// + /// + /// The full path to the settings file, otherwise null. + /// + private static string GetSettingsFilePath(string path, string name) + { + string folderPath = GetFolderPath(path); + if (folderPath == null) + { + return null; + } + + if (string.IsNullOrWhiteSpace(name)) + { + name = DEFAULT_SETTINGS_FILE; + } + + try + { + string fullPath = Path.Combine(folderPath, name); + if (File.Exists(fullPath)) + { + Console.WriteLine($"Settings file: {fullPath}."); + return fullPath; + } + else + { + Console.WriteLine($"The settings file '{fullPath}' was not found."); + return null; + } + } + catch (Exception ex) + { + Console.WriteLine($"Could not get the path to the settings file. Reason: {ex.Message}"); + return null; + } + } + + /// + /// Reads the settings XML file. + /// + /// + /// The path to the settings XML file. + /// + /// + /// A object if the file was read successfully, otherwise null. + /// + private static Settings ReadSettingsFile(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + Console.WriteLine("The settings file path was null or empty."); + return null; + } + + if (!File.Exists(path)) + { + Console.WriteLine($"The settings file path '{path}' does not exist."); + return null; + } + + try + { + XmlSerializer serializer = new XmlSerializer(typeof(Settings)); + using FileStream fs = new FileStream(path, FileMode.Open); + return (Settings)serializer.Deserialize(fs); + } + catch (Exception ex) + { + Console.WriteLine($"The settings file could not be read. Reason: {ex.Message}"); + return null; + } + } + + static int Verify(string folder, string settingsFile, string settingsFolder) { + string settingsFilePath = GetSettingsFilePath(settingsFolder, settingsFile); + if (string.IsNullOrWhiteSpace(settingsFilePath)) + { + return ERROR; + } + + Settings settings = ReadSettingsFile(settingsFilePath); + + Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine($"Folder: {folder}"); + FileSystemCrawlerSO fsc = new FileSystemCrawlerSO(); Stopwatch watch = new Stopwatch(); + + Logger.WriteLine("--------------------------------------------------------------------------------"); - //string docPath = @"F:\HashTest"; watch.Start(); fsc.CollectFolders(folder); fsc.CollectFiles(); - watch.Stop(); + watch.Stop(); Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folders: {fsc.NumFolders}"); - Logger.WriteLine($"Files: {fsc.NumFiles}"); - Logger.WriteLine($"Time (ms): { watch.ElapsedMilliseconds}"); + Logger.WriteLine($"Folders: {fsc.NumFolders}"); + Logger.WriteLine($"Files: {fsc.NumFiles}"); + Logger.WriteLine($"Time (ms): { watch.ElapsedMilliseconds}"); Logger.WriteLine("--------------------------------------------------------------------------------"); + + if (settings.Notifications != null) + { + settings.Notifications.Send(Logger.Lines); + } + + return SUCCESS; } } } diff --git a/FileVerification/VerifyFile.cs b/FileVerification/VerifyFile.cs index 591a0fc..6afcc90 100644 --- a/FileVerification/VerifyFile.cs +++ b/FileVerification/VerifyFile.cs @@ -3,7 +3,7 @@ using System.IO; using System.Text; -namespace FileVerification +namespace TE.FileVerification { public class VerifyFile { From 7820814afe10533dc5fef1b011d1810f14932a05 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 4 Aug 2021 14:38:19 -0400 Subject: [PATCH 08/33] Removed period in log lines --- FileVerification/FileSystemCrawlerSO.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index 4b047c7..5e31511 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -38,8 +38,8 @@ public FileSystemCrawlerSO() processorCount = Environment.ProcessorCount; threadCount = processorCount - 1; - Logger.WriteLine($"Processors: {processorCount}."); - Logger.WriteLine($"Threads: {threadCount}."); + Logger.WriteLine($"Processors: {processorCount}"); + Logger.WriteLine($"Threads: {threadCount}"); } public void CollectFolders(string path) From 200ac47fc6193f998a854e29955b46787b1e4402 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 4 Aug 2021 15:03:50 -0400 Subject: [PATCH 09/33] Added subfolder to log --- FileVerification/FileSystemCrawlerSO.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index 5e31511..56963bd 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -86,6 +86,8 @@ private void CrawlFolder(DirectoryInfo dir) private void GetFiles(DirectoryInfo dir) { + Logger.WriteLine($"Subfolder: {dir.FullName}"); + FileInfo[] files = dir.GetFiles(); VerifyFile verifyFile = new VerifyFile(VERIFY_FILE_NAME, dir); @@ -122,7 +124,7 @@ private void GetFiles(DirectoryInfo dir) { if (!hashInfo.IsHashEqual(file.Value.Value)) { - Logger.WriteLine($"Hash mismatch: {dir.FullName}{Path.DirectorySeparatorChar}{file.Key}."); + Logger.WriteLine($"Hash mismatch: {dir.FullName}{Path.DirectorySeparatorChar}{file.Key}"); count++; } } From a7ec331094349d5950577e213596deec53d61e81 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Thu, 5 Aug 2021 08:33:39 -0400 Subject: [PATCH 10/33] Reduced the logging for ease of viewing --- FileVerification/FileSystemCrawlerSO.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index 56963bd..543b284 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -85,9 +85,7 @@ private void CrawlFolder(DirectoryInfo dir) } private void GetFiles(DirectoryInfo dir) - { - Logger.WriteLine($"Subfolder: {dir.FullName}"); - + { FileInfo[] files = dir.GetFiles(); VerifyFile verifyFile = new VerifyFile(VERIFY_FILE_NAME, dir); @@ -139,7 +137,10 @@ private void GetFiles(DirectoryInfo dir) verifyFile.Write(verifyFileData, dir); } - Logger.WriteLine($"Number failed: {count}"); + if (count > 0) + { + Logger.WriteLine($"Number failed: {count}"); + } } } } From 69a19e6a7efbb27898009ddc6a31d6cb8ec05f27 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Mon, 9 Aug 2021 11:56:39 -0400 Subject: [PATCH 11/33] Added ability to generate and store hash of a single file --- FileVerification/FileSystemCrawlerSO.cs | 59 +++++++++++++++++++------ FileVerification/Program.cs | 13 +++++- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index 543b284..54ec8fd 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -42,11 +42,11 @@ public FileSystemCrawlerSO() Logger.WriteLine($"Threads: {threadCount}"); } - public void CollectFolders(string path) + public void CollectFolders(string path, bool includeSubDir) { FolderPath = path; DirectoryInfo directoryInfo = new DirectoryInfo(path); - tasks.Add(Task.Run(() => CrawlFolder(directoryInfo))); + tasks.Add(Task.Run(() => CrawlFolder(directoryInfo, includeSubDir))); Task taskToWaitFor; while (tasks.TryTake(out taskToWaitFor)) @@ -56,26 +56,43 @@ public void CollectFolders(string path) } } - public void CollectFiles() + public void CollectFiles(string filePath) { foreach (var dir in directories) { - GetFiles(dir); + GetFiles(dir, filePath); } } - private void CrawlFolder(DirectoryInfo dir) + /// + /// Returns a value indicating the path is a valid file. + /// + /// + /// The path to the file. + /// + /// + /// true if the path is a valid file, othersize false. + /// + public static bool IsFile(string path) + { + return File.Exists(path); + } + + private void CrawlFolder(DirectoryInfo dir, bool includeSubDir) { try { - DirectoryInfo[] directoryInfos = dir.GetDirectories(); - foreach (DirectoryInfo childInfo in directoryInfos) + if (includeSubDir) { - // here may be dragons using enumeration variable as closure!! - DirectoryInfo di = childInfo; - tasks.Add(Task.Run(() => CrawlFolder(di))); + DirectoryInfo[] directoryInfos = dir.GetDirectories(); + foreach (DirectoryInfo childInfo in directoryInfos) + { + // here may be dragons using enumeration variable as closure!! + DirectoryInfo di = childInfo; + tasks.Add(Task.Run(() => CrawlFolder(di))); + } } - directories.Add(dir); + directories.Add(dir); } catch (Exception ex) when (ex is DirectoryNotFoundException || ex is System.Security.SecurityException || ex is UnauthorizedAccessException) @@ -84,9 +101,23 @@ private void CrawlFolder(DirectoryInfo dir) } } - private void GetFiles(DirectoryInfo dir) - { - FileInfo[] files = dir.GetFiles(); + private void CrawlFolder(DirectoryInfo dir) + { + CrawlFolder(dir, true); + } + + private void GetFiles(DirectoryInfo dir, string filePath) + { + FileInfo[] files; + if (string.IsNullOrWhiteSpace(filePath)) + { + files = dir.GetFiles(); + } + else + { + files = new FileInfo[] { new FileInfo(filePath) }; + } + VerifyFile verifyFile = new VerifyFile(VERIFY_FILE_NAME, dir); // Read the verify file, if it exists, but if the read method diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index 07f4309..046f7d6 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -187,9 +187,18 @@ static int Verify(string folder, string settingsFile, string settingsFolder) Logger.WriteLine("--------------------------------------------------------------------------------"); + bool isFile = FileSystemCrawlerSO.IsFile(folder); + + string filePath = null; + if (isFile) + { + filePath = folder; + folder = Path.GetDirectoryName(folder); + } + watch.Start(); - fsc.CollectFolders(folder); - fsc.CollectFiles(); + fsc.CollectFolders(folder, !isFile); + fsc.CollectFiles(filePath); watch.Stop(); Logger.WriteLine("--------------------------------------------------------------------------------"); From aa150e3e1d3afb9745973a91e919907c1e4eb328 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Mon, 27 Sep 2021 09:48:22 -0400 Subject: [PATCH 12/33] Fixed issue with generating hash for open file --- FileVerification/FileSystemCrawlerSO.cs | 18 ++++++++++++++++-- FileVerification/HashInfo.cs | 13 ++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index 54ec8fd..7512a82 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -142,8 +142,22 @@ private void GetFiles(DirectoryInfo dir, string filePath) return; } - folderFileData.TryAdd(file.Name, new HashInfo(file, Algorithm.SHA256)); - NumFiles++; + try + { + HashInfo hashInfo = new HashInfo(file, Algorithm.SHA256); + if (hashInfo.Value != null) + { + folderFileData.TryAdd(file.Name, new HashInfo(file, Algorithm.SHA256)); + } + NumFiles++; + } + catch (AggregateException ae) + { + foreach (Exception ex in ae.InnerExceptions) + { + Console.WriteLine($"ERROR: {ex.Message}"); + } + } }); int count = 0; diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs index e990e9f..3c2069c 100644 --- a/FileVerification/HashInfo.cs +++ b/FileVerification/HashInfo.cs @@ -95,10 +95,17 @@ private string CreateFileHash(FileInfo file) using (var hashAlgorithm = HashAlgorithm.Create(Algorithm.ToString())) { - using (var stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, maxSize)) + try { - var hash = hashAlgorithm.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", ""); + using (var stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, maxSize)) + { + var hash = hashAlgorithm.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", ""); + } + } + catch + { + return null; } } } From cbc1420e35acc2a3dd6f767fe524d0f971272a4d Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Sat, 2 Oct 2021 12:53:32 -0400 Subject: [PATCH 13/33] Changed the file separator to avoid conflicts --- FileVerification/VerifyFile.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FileVerification/VerifyFile.cs b/FileVerification/VerifyFile.cs index 6afcc90..2bd4fac 100644 --- a/FileVerification/VerifyFile.cs +++ b/FileVerification/VerifyFile.cs @@ -7,6 +7,7 @@ namespace TE.FileVerification { public class VerifyFile { + private const char Separator = '|'; private string fileName; private DirectoryInfo directory; @@ -84,7 +85,7 @@ public Dictionary Read() while (!reader.EndOfStream) { string line = reader.ReadLine(); - string[] values = line.Split(','); + string[] values = line.Split(Separator); if (values.Length != Enum.GetNames(typeof(VerifyFileLayout)).Length) { Logger.WriteLine($"WARNING: Record size incorrect (record will be created using the current file data). File: {FilePath}, Record: {line}."); @@ -134,7 +135,7 @@ public void Write(Dictionary files, DirectoryInfo folder) foreach (KeyValuePair file in files) { HashInfo hashInfo = file.Value; - sb.AppendLine($"{file.Key},{hashInfo.Algorithm.ToString().ToLower()},{hashInfo.Value}"); + sb.AppendLine($"{file.Key}{Separator}{hashInfo.Algorithm.ToString().ToLower()}{Separator}{hashInfo.Value}"); } try From e75bd022844d9e87dbe3ae31b5f0e21825bc72d0 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Mon, 6 Jun 2022 13:40:05 -0400 Subject: [PATCH 14/33] Restructuring the code --- FileVerification/CheckSumFile.cs | 140 +++++++++++ .../Configuration/ISettingsFile.cs | 26 ++ FileVerification/Configuration/Settings.cs | 13 +- FileVerification/Configuration/XmlFile.cs | 156 ++++++++++++ FileVerification/FileSystemCrawlerSO.cs | 125 +++++----- FileVerification/FileVerification.csproj | 5 +- FileVerification/HashInfo.cs | 137 +++++------ FileVerification/PathInfo.cs | 208 ++++++++++++++++ FileVerification/Program.cs | 231 +++++++----------- FileVerification/VerifyFile.cs | 37 ++- 10 files changed, 790 insertions(+), 288 deletions(-) create mode 100644 FileVerification/CheckSumFile.cs create mode 100644 FileVerification/Configuration/ISettingsFile.cs create mode 100644 FileVerification/Configuration/XmlFile.cs create mode 100644 FileVerification/PathInfo.cs diff --git a/FileVerification/CheckSumFile.cs b/FileVerification/CheckSumFile.cs new file mode 100644 index 0000000..0e84f87 --- /dev/null +++ b/FileVerification/CheckSumFile.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TE.FileVerification +{ + enum ChecksumFileLayout + { + NAME, + HASH_ALGORITHM, + HASH + + } + public class ChecksumFile + { + private const char Separator = '|'; + + private readonly string? directory; + + /// + /// Gets the full path of the checksum file. + /// + public string FullPath { get; private set; } + + public Dictionary Checksums { get; private set; } + + public int FileCount + { + get + { + return Checksums != null ? Checksums.Count : 0; + } + } + + /// + /// Creates an instance of the class when + /// provided with the full path to the checksum file. + /// + /// + /// Full path to the checksum file. + /// + /// + /// The parameter is null or empty. + /// + /// + /// The directory name to the checksum file could not be determined. + /// + public ChecksumFile(string fullPath) + { + if (string.IsNullOrWhiteSpace(fullPath)) + { + throw new ArgumentNullException(nameof(fullPath)); + } + + FullPath = fullPath; + directory = Path.GetDirectoryName(FullPath); + + if (string.IsNullOrWhiteSpace(directory)) + { + throw new InvalidOperationException( + "The directory name could not be determined from the full path to the checksum file."); + } + + Checksums = new Dictionary(); + + Read(); + } + + /// + /// Reads the checksum file. + /// + public void Read() + { + if (!File.Exists(FullPath)) + { + Logger.WriteLine($"The checksum file '{FullPath}' was not found."); + return; + } + + if (string.IsNullOrWhiteSpace(directory)) + { + Logger.WriteLine("The directory value is null or empty."); + return; + } + + try + { + using var reader = new StreamReader(FullPath); + + while (!reader.EndOfStream) + { + string? line = reader.ReadLine(); + if (line == null) + { + continue; + } + + string[] values = line.Split(Separator); + if (values.Length != Enum.GetNames(typeof(ChecksumFileLayout)).Length) + { + Logger.WriteLine($"WARNING: Record size incorrect (record will be created using the current file data). File: {FullPath}, Record: {line}."); + continue; + } + + string fileName = values[(int)VerifyFileLayout.NAME]; + HashInfo info = + new HashInfo( + fileName, + values[(int)ChecksumFileLayout.HASH_ALGORITHM], + values[(int)ChecksumFileLayout.HASH]); + + // Get the full path to the file to use as the key to make + // it unique so it can be used for searching + string filePath = Path.Combine(directory, fileName); + Checksums.Add(filePath, info); + } + } + catch (UnauthorizedAccessException) + { + Logger.WriteLine($"ERROR: Not authorized to write to {FullPath}."); + return; + } + catch (IOException ex) + { + Logger.WriteLine($"ERROR: Can't read the file. Reason: {ex.Message}"); + return; + } + } + + public HashInfo? GetFileData(string fullPath) + { + HashInfo? hashInfo; + Checksums.TryGetValue(fullPath, out hashInfo); + return hashInfo; + } + } +} diff --git a/FileVerification/Configuration/ISettingsFile.cs b/FileVerification/Configuration/ISettingsFile.cs new file mode 100644 index 0000000..2e62cb4 --- /dev/null +++ b/FileVerification/Configuration/ISettingsFile.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TE.FileVerification.Configuration +{ + /// + /// Configuration file interface. + /// + interface ISettingsFile + { + /// + /// Reads the settings XML file. + /// + /// + /// The path to the settings XML file. + /// + /// + /// A object if the file was read successfully, + /// otherwise null. + /// + public Settings? Read(); + } +} diff --git a/FileVerification/Configuration/Settings.cs b/FileVerification/Configuration/Settings.cs index c084113..cde744f 100644 --- a/FileVerification/Configuration/Settings.cs +++ b/FileVerification/Configuration/Settings.cs @@ -12,6 +12,17 @@ public class Settings /// Gets or sets the notifications for the verification. /// [XmlElement("notifications")] - public Notifications.Notifications Notifications { get; set; } + public Notifications.Notifications? Notifications { get; set; } + + /// + /// Sends the notifications. + /// + public void Send() + { + if (Notifications != null) + { + Notifications.Send(Logger.Lines); + } + } } } diff --git a/FileVerification/Configuration/XmlFile.cs b/FileVerification/Configuration/XmlFile.cs new file mode 100644 index 0000000..c833205 --- /dev/null +++ b/FileVerification/Configuration/XmlFile.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace TE.FileVerification.Configuration +{ + /// + /// The XML settings file. + /// + public class XmlFile : ISettingsFile + { + // The default configuration file name + const string DEFAULT_SETTINGS_FILE = "config.xml"; + + // Full path to the settings XML file + private readonly string? _fullPath; + + /// + /// Initialize an instance of the class when + /// provided with the path and name of the settings file. + /// + /// + /// The folder path to the settings file. + /// + /// + /// The name of the settings file. + /// + public XmlFile(string path, string name) + { + _fullPath = GetFullPath(path, name); + } + + /// + /// Gets the folder path containing the settings file. + /// + /// + /// The folder path. + /// + /// + /// The folder path of the files, otherwise null. + /// + private string? GetFolderPath(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + try + { + path = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName); + } + catch (Exception ex) + { + Console.WriteLine($"The folder name is null or empty. Couldn't get the current location. Reason: {ex.Message}"); + return null; + } + } + + if (Directory.Exists(path)) + { + return path; + } + else + { + Console.WriteLine("The folder does not exist."); + return null; + } + } + + /// + /// Gets the full path to the settings file. + /// + /// + /// The path to the settings file. + /// + /// + /// The name of the settings file. + /// + /// + /// The full path to the settings file, otherwise null. + /// + private string? GetFullPath(string path, string name) + { + string? folderPath = GetFolderPath(path); + if (folderPath == null) + { + return null; + } + + if (string.IsNullOrWhiteSpace(name)) + { + name = DEFAULT_SETTINGS_FILE; + } + + try + { + string fullPath = Path.Combine(folderPath, name); + if (File.Exists(fullPath)) + { + Console.WriteLine($"Settings file: {fullPath}."); + return fullPath; + } + else + { + Console.WriteLine($"The settings file '{fullPath}' was not found."); + return null; + } + } + catch (Exception ex) + { + Console.WriteLine($"Could not get the path to the settings file. Reason: {ex.Message}"); + return null; + } + } + + /// + /// Reads the settings XML file. + /// + /// + /// The path to the settings XML file. + /// + /// + /// A object if the file was read successfully, + /// otherwise null. + /// + public Settings? Read() + { + if (string.IsNullOrWhiteSpace(_fullPath)) + { + Console.WriteLine("The settings file path was null or empty."); + return null; + } + + if (!File.Exists(_fullPath)) + { + Console.WriteLine($"The settings file path '{_fullPath}' does not exist."); + return null; + } + + try + { + XmlSerializer serializer = new XmlSerializer(typeof(Settings)); + using FileStream fs = new FileStream(_fullPath, FileMode.Open); + return (Settings?)serializer.Deserialize(fs); + } + catch (Exception ex) + { + Console.WriteLine($"The settings file could not be read. Reason: {ex.Message}"); + return null; + } + } + } +} diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs index 7512a82..c4b54cc 100644 --- a/FileVerification/FileSystemCrawlerSO.cs +++ b/FileVerification/FileSystemCrawlerSO.cs @@ -16,30 +16,40 @@ enum VerifyFileLayout public class FileSystemCrawlerSO { - const string VERIFY_FILE_NAME = "__fv.txt"; + const string DEFAULT_VERIFY_FILE_NAME = "__fv.txt"; public int NumFolders { get; set; } public int NumFiles { get; set; } + public HashAlgorithm Algorithm { get; set; } + public string FolderPath { get; set; } - private int processorCount; + private readonly int processorCount; - private int threadCount; + private readonly int threadCount; - private List directories = new List(); + private readonly List directories = new List(); private readonly ConcurrentBag tasks = new ConcurrentBag(); - //private readonly ConcurrentBag fileTasks = new ConcurrentBag(); - public FileSystemCrawlerSO() + public FileSystemCrawlerSO(HashAlgorithm? algorithm) { processorCount = Environment.ProcessorCount; threadCount = processorCount - 1; Logger.WriteLine($"Processors: {processorCount}"); Logger.WriteLine($"Threads: {threadCount}"); + + if (algorithm != null) + { + Algorithm = (HashAlgorithm)algorithm; + } + else + { + Algorithm = HashAlgorithm.SHA256; + } } public void CollectFolders(string path, bool includeSubDir) @@ -48,11 +58,13 @@ public void CollectFolders(string path, bool includeSubDir) DirectoryInfo directoryInfo = new DirectoryInfo(path); tasks.Add(Task.Run(() => CrawlFolder(directoryInfo, includeSubDir))); - Task taskToWaitFor; - while (tasks.TryTake(out taskToWaitFor)) + while (tasks.TryTake(out Task? taskToWaitFor)) { - NumFolders++; - taskToWaitFor.Wait(); + if (taskToWaitFor != null) + { + NumFolders++; + taskToWaitFor.Wait(); + } } } @@ -112,44 +124,45 @@ private void GetFiles(DirectoryInfo dir, string filePath) if (string.IsNullOrWhiteSpace(filePath)) { files = dir.GetFiles(); + NumFiles += files.Length; } else { files = new FileInfo[] { new FileInfo(filePath) }; } - VerifyFile verifyFile = new VerifyFile(VERIFY_FILE_NAME, dir); + //VerifyFile verifyFile = new VerifyFile(DEFAULT_VERIFY_FILE_NAME, dir); - // Read the verify file, if it exists, but if the read method - // returns null, indicating an exception, and the verify file file - // exists, then assume there is an issue and don't continue with - // the hashing and verification - Dictionary verifyFileData = verifyFile.Read(); - if (verifyFileData == null && verifyFile.Exists()) - { - return; - } + //// Read the verify file, if it exists, but if the read method + //// returns null, indicating an exception, and the verify file file + //// exists, then assume there is an issue and don't continue with + //// the hashing and verification + //Dictionary? verifyFileData = verifyFile.Read(); + //if (verifyFileData == null && verifyFile.Exists()) + //{ + // return; + //} ConcurrentDictionary folderFileData = new ConcurrentDictionary(); - + ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = threadCount; Parallel.ForEach(files, options, file => { - // Ignore the verification file and system files - if (file.Name.Equals(VERIFY_FILE_NAME) || file.Attributes == FileAttributes.System) - { - return; - } + // // Ignore the verification file and system files + // if (file.Name.Equals(DEFAULT_VERIFY_FILE_NAME) || file.Attributes == FileAttributes.System) + // { + // return; + // } try { - HashInfo hashInfo = new HashInfo(file, Algorithm.SHA256); - if (hashInfo.Value != null) + HashInfo hashInfo = new HashInfo(file.FullName, HashAlgorithm.SHA256); + if (hashInfo.Hash != null) { - folderFileData.TryAdd(file.Name, new HashInfo(file, Algorithm.SHA256)); + folderFileData.TryAdd(file.Name, new HashInfo(file.FullName, HashAlgorithm.SHA256)); } - NumFiles++; + //NumFiles++; } catch (AggregateException ae) { @@ -160,32 +173,32 @@ private void GetFiles(DirectoryInfo dir, string filePath) } }); - int count = 0; - foreach (var file in folderFileData) - { - if (verifyFileData.TryGetValue(file.Key, out HashInfo hashInfo)) - { - if (!hashInfo.IsHashEqual(file.Value.Value)) - { - Logger.WriteLine($"Hash mismatch: {dir.FullName}{Path.DirectorySeparatorChar}{file.Key}"); - count++; - } - } - else - { - verifyFileData.Add(file.Key, file.Value); - } - } - - if (verifyFileData.Count > 0) - { - verifyFile.Write(verifyFileData, dir); - } - - if (count > 0) - { - Logger.WriteLine($"Number failed: {count}"); - } + //int count = 0; + //foreach (var file in folderFileData) + //{ + // if (verifyFileData.TryGetValue(file.Key, out HashInfo? hashInfo)) + // { + // if (!hashInfo.IsHashEqual(file.Value.Hash)) + // { + // Logger.WriteLine($"Hash mismatch: {dir.FullName}{Path.DirectorySeparatorChar}{file.Key}"); + // count++; + // } + // } + // else + // { + // verifyFileData.Add(file.Key, file.Value); + // } + //} + + //if (verifyFileData.Count > 0) + //{ + // verifyFile.Write(verifyFileData, dir); + //} + + //if (count > 0) + //{ + // Logger.WriteLine($"Number failed: {count}"); + //} } } } diff --git a/FileVerification/FileVerification.csproj b/FileVerification/FileVerification.csproj index 3dcfb20..be4cf7c 100644 --- a/FileVerification/FileVerification.csproj +++ b/FileVerification/FileVerification.csproj @@ -2,13 +2,14 @@ Exe - netcoreapp3.1 + net6.0 fv TE.FileVerification + enable - + diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs index 3c2069c..c063d98 100644 --- a/FileVerification/HashInfo.cs +++ b/FileVerification/HashInfo.cs @@ -1,69 +1,76 @@ using System; using System.Collections.Generic; using System.Text; -using System.Security.Cryptography; +using Cryptography = System.Security.Cryptography; using System.IO; namespace TE.FileVerification { - public enum Algorithm + public enum HashAlgorithm { MD5, SHA, SHA256, SHA512 } - public class HashInfo { // A megabyte private const int Megabyte = 1024 * 1024; - public Algorithm Algorithm { get; set; } + /// + /// Gets the hash algorithm used to create the hash of the file. + /// + public HashAlgorithm Algorithm { get; private set;} + + /// + /// Gets the hash associated with the file. + /// + public string? Hash { get; private set; } - public string Value { get; set; } + /// + /// Gets the information about the file. + /// + public string FilePath { get; private set; } - public HashInfo(string algorithm, string hash) + private HashInfo(string filePath) { - if (algorithm == null || string.IsNullOrWhiteSpace(algorithm)) + if (filePath == null || string.IsNullOrWhiteSpace(filePath)) { - throw new ArgumentNullException(nameof(algorithm)); + throw new ArgumentNullException(nameof(filePath)); } + FilePath = filePath; + } + + public HashInfo(string filePath, string algorithm, string hash) + : this(filePath, algorithm) + { if (hash == null || string.IsNullOrWhiteSpace(hash)) { throw new ArgumentNullException(nameof(hash)); } - Algorithm = GetAlgorithm(algorithm); - Value = hash; + Hash = hash; } - public HashInfo(FileInfo file, string algorithm) - { - if (file == null) - { - throw new ArgumentNullException(nameof(file)); - } - - if (algorithm == null || string.IsNullOrWhiteSpace(nameof(algorithm))) + public HashInfo(string filePath, string algorithm) + : this(filePath) + { + if (algorithm == null || string.IsNullOrWhiteSpace(algorithm)) { throw new ArgumentNullException(nameof(algorithm)); } Algorithm = GetAlgorithm(algorithm); - Value = CreateFileHash(file); + Hash = GetFileHash(); } - public HashInfo(FileInfo file, Algorithm algorithm) + public HashInfo(string filePath, HashAlgorithm algorithm) + : this(filePath) { - if (file == null) - { - throw new ArgumentNullException(nameof(file)); - } - Algorithm = algorithm; - Value = CreateFileHash(file); + Hash = GetFileHash(); } /// @@ -75,67 +82,48 @@ public HashInfo(FileInfo file, Algorithm algorithm) /// /// The enum value of the algorithm. /// - private Algorithm GetAlgorithm(string hash) + private static HashAlgorithm GetAlgorithm(string hash) { if (string.Compare(hash, "sha256", true) == 0) { - return Algorithm.SHA256; + return HashAlgorithm.SHA256; } else { - return Algorithm.SHA512; + return HashAlgorithm.SHA512; } } - private string CreateFileHash(FileInfo file) + private string? GetFileHash() { - //long size = file.Length; int maxSize = 16 * Megabyte; - //int buffer = (int)((size <= maxSize) ? size : 16 * Megabyte); - using (var hashAlgorithm = HashAlgorithm.Create(Algorithm.ToString())) + using Cryptography.HashAlgorithm? hashAlgorithm = + Cryptography.HashAlgorithm.Create(Algorithm.ToString()); + if (hashAlgorithm == null) { - try - { - using (var stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, maxSize)) - { - var hash = hashAlgorithm.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", ""); - } - } - catch - { - return null; - } + return null; + } + + try + { + using var stream = + new FileStream( + FilePath, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + maxSize); + + var hash = hashAlgorithm.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", ""); + } + catch + { + return null; } } - /// - /// Checks to see if the hash of a specific file is equal to this hash - /// value. - /// - /// - /// The path to the file that will be used to generate the hash to - /// compare to this hash. - /// - /// - /// True if the hashes are equal, false if the hashes are not equal. - /// - /// - /// The hash algorithm used will be the same one that is set for this - /// object. - /// - //public bool IsEqual2(string filePath) - //{ - // if (filePath == null || string.IsNullOrWhiteSpace(filePath)) - // { - // return false; - // } - - // string fileHash = CreateFileHash(filePath); - // return Value.Equals(fileHash); - //} - /// /// Checks to see if the hash of a specific file is equal to this hash /// value. @@ -153,7 +141,12 @@ private string CreateFileHash(FileInfo file) /// public bool IsHashEqual(string hash) { - return Value.Equals(hash); + if (string.IsNullOrWhiteSpace(Hash)) + { + return string.IsNullOrWhiteSpace(hash); + } + + return Hash.Equals(hash); } } } diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs new file mode 100644 index 0000000..cc46780 --- /dev/null +++ b/FileVerification/PathInfo.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TE.FileVerification +{ + /// + /// Contains the properties and methods that is used to work with a + /// path. + /// + public class PathInfo + { + /// + /// Gets the path value. + /// + public string Path { get; private set; } + + public Queue? Files { get; private set; } + + public List? ChecksumFiles { get; private set; } + + /// + /// Initializes an instance of the class when + /// provided with the full path. + /// + /// + /// The full path. + /// + /// + /// Thrown if the argument is null or empty. + /// + internal PathInfo(string path) + { + if (path == null || string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException(path); + } + + Path = path; + } + + public void Crawl(bool includeSubDir, string checksumFileName) + { + + // If the path does not exist, then just return null + if (!Exists()) + { + return; + } + + // If the path is a file, then just return a string array with the + // path value as there is no directory to be crawled + if (IsFile()) + { + Files = new Queue(); + Files.Enqueue(Path); + return; + } + + Files = new Queue(CrawlDirectory(includeSubDir)); + + if (ChecksumFiles == null) + { + ChecksumFiles = new List(); + } + + foreach (string file in GetChecksumFiles(checksumFileName, includeSubDir)) + { + ChecksumFiles.Add(new ChecksumFile(file)); + } + } + + //public List? CrawlCheckSumFiles(bool includeSubDir, string checksumFileName) + //{ + // // If the path does not exist, then just return null + // if (!Exists()) + // { + // return null; + // } + + // return new List(GetChecksumFiles(checksumFileName, includeSubDir)); + //} + + public void Check() + { + if (Files == null || ChecksumFiles == null) + { + return; + } + + foreach (string file in Files) + { + ChecksumFile? checksumFile = ChecksumFiles.FirstOrDefault(c => c.Checksums.ContainsKey(file)); + + // A checksum file was found containing the file, so get the + // hash information for the file + if (checksumFile != null) + { + HashInfo? hashInfo = checksumFile.GetFileData(file); + + } + } + } + + /// + /// Returns a value indicating the path is a valid directory. + /// + /// + /// true if the path is a valid directory, otherwise false. + /// + public bool IsDirectory() + { + return Directory.Exists(Path); + } + + /// + /// Returns a value indicating the path is a valid file. + /// + /// + /// true if the path is a valid file, othersize false. + /// + public bool IsFile() + { + return File.Exists(Path); + } + + /// + /// Returns a value indicating the path exists. + /// + /// + /// true if the path exists, otherwise false. + /// + public bool Exists() + { + return IsDirectory() || IsFile(); + } + + /// + /// Crawls the path and returns the files. + /// + /// + /// Value indicating if the subdirectories are to be crawled. + /// + /// + /// Returns an enumerable collection of file paths. + /// + private IEnumerable CrawlDirectory(bool includeSubDir) + { + DirectoryInfo dirInfo = new DirectoryInfo(Path); + IEnumerable files = + dirInfo.EnumerateFiles("*", GetSearchOption(includeSubDir)); + + foreach (var file in files) + { + yield return file.FullName; + } + } + + //private void CrawlDirectory2(bool includeSubDir) + //{ + // try + // { + + // } + // catch (Exception ex) + // { + // Console.WriteLine($"{ex.GetType()} {ex.Message}\n{ex.StackTrace}"); + // } + //} + + //private void CrawlDirectory2(bool includeSubDir, DirectoryInfo dirInfo) + //{ + // try + // { + + // } + // catch (Exception ex) + // { + // Console.WriteLine($"{ex.GetType()} {ex.Message}\n{ex.StackTrace}"); + // } + //} + + private IEnumerable GetChecksumFiles(string checksumFileName, bool includeSubDir) + { + if (string.IsNullOrWhiteSpace(checksumFileName)) + { + yield break; + } + + DirectoryInfo dirInfo = new DirectoryInfo(Path); + IEnumerable checksumFiles = + dirInfo.EnumerateFiles(checksumFileName, GetSearchOption(includeSubDir)); + + foreach (var checksumFile in checksumFiles) + { + yield return checksumFile.FullName; + } + } + + private SearchOption GetSearchOption(bool includeSubDir) + { + return includeSubDir ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + } + } +} diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index 046f7d6..d9fc352 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.CommandLine; using System.CommandLine.Invocation; using TE.FileVerification.Configuration; -using System.Xml.Serialization; -using System.Reflection; +using System.Collections.Generic; +using System.Threading.Tasks; namespace TE.FileVerification { @@ -18,9 +16,6 @@ class Program // Error return code private const int ERROR = -1; - // The default configuration file name - const string DEFAULT_SETTINGS_FILE = "config.xml"; - public static int NumFolders { get; set; } static int Main(string[] args) @@ -28,188 +23,136 @@ static int Main(string[] args) RootCommand rootCommand = new RootCommand( description: "Generates the hash of all files in a folder tree and stores the hashes in text files in each folder."); - var folderOption = new Option( - aliases: new string[] { "--folder", "-f" }, - description: "The folder containing the files to verify with a hash." - ); + var fileOption = new Option( + aliases: new string[] { "--file", "-f" }, + description: "The file or folder to verify with a hash." + ); + fileOption.IsRequired = true; + rootCommand.AddOption(fileOption); - folderOption.IsRequired = true; - rootCommand.AddOption(folderOption); + var algorithmOption = new Option( + aliases: new string[] { "--algorithm", "-a" }, + description: "The hash algorithm to use." + ); + rootCommand.AddOption(algorithmOption); var settingsFileOption = new Option( aliases: new string[] { "--settingsFile", "-sfi" }, description: "The name of the settings XML file." - ); + ); rootCommand.AddOption(settingsFileOption); - var setingsFolderOption = new Option( + var settingsFolderOption = new Option( aliases: new string[] { "--settingsFolder", "-sfo" }, description: "The folder containing the settings XML file." - ); - rootCommand.AddOption(setingsFolderOption); + ); + rootCommand.AddOption(settingsFolderOption); - rootCommand.Handler = CommandHandler.Create(Verify); + rootCommand.SetHandler((fileOptionValue, algorithmOptionValue, settingsFileOptionValue, settingsFolderOptionValue) => + { + Run(fileOptionValue, algorithmOptionValue, settingsFileOptionValue, settingsFolderOptionValue); + }, + fileOption, algorithmOption, settingsFileOption, settingsFolderOption); return rootCommand.Invoke(args); } /// - /// Gets the folder path containing the settings file. + /// Runs the necessary hashing for the file or folder. /// - /// - /// The folder path. - /// - /// - /// The folder path of the files, otherwise null. - /// - private static string GetFolderPath(string path) + /// + /// + /// + /// + /// + static int Run(string? file, HashAlgorithm? algorithm, string? settingsFile, string? settingsFolder) { - if (string.IsNullOrWhiteSpace(path)) + if (string.IsNullOrWhiteSpace(file)) { - try - { - path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - } - catch (Exception ex) - { - Console.WriteLine($"The folder name is null or empty. Couldn't get the current location. Reason: {ex.Message}"); - return null; - } + Logger.WriteLine("The file or folder was not specified."); + return ERROR; } - if (Directory.Exists(path)) - { - return path; - } - else + if (algorithm == null) { - Console.WriteLine("The folder does not exist."); - return null; + algorithm = HashAlgorithm.SHA256; } - } - /// - /// Gets the full path to the settings file. - /// - /// - /// The path to the settings file. - /// - /// - /// The name of the settings file. - /// - /// - /// The full path to the settings file, otherwise null. - /// - private static string GetSettingsFilePath(string path, string name) - { - string folderPath = GetFolderPath(path); - if (folderPath == null) + // Read the settings file if one was provided as an argument + Settings? settings = null; + if (!string.IsNullOrWhiteSpace(settingsFile) && !string.IsNullOrWhiteSpace(settingsFolder)) { - return null; + ISettingsFile xmlFile = new XmlFile(settingsFolder, settingsFile); + settings = xmlFile.Read(); } - if (string.IsNullOrWhiteSpace(name)) - { - name = DEFAULT_SETTINGS_FILE; - } + Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine($"Folder: {file}"); + Logger.WriteLine("--------------------------------------------------------------------------------"); + + PathInfo path = new PathInfo(file); + Stopwatch watch = new Stopwatch(); + watch.Start(); + path.Crawl(true, "__fv.txt"); + //List? checksumFiles = path.CrawlCheckSumFiles(true, "__fv.txt"); - try + List hashInfoList = new List(); + if (path.Files != null) { - string fullPath = Path.Combine(folderPath, name); - if (File.Exists(fullPath)) + int fileCount = 0; + foreach (string fileValue in path.Files) { - Console.WriteLine($"Settings file: {fullPath}."); - return fullPath; + HashInfo hashInfo = new HashInfo(fileValue, (HashAlgorithm)algorithm); + hashInfoList.Add(hashInfo); + fileCount++; } - else + + int checksumFilesCount = 0; + if (path.ChecksumFiles != null) { - Console.WriteLine($"The settings file '{fullPath}' was not found."); - return null; + path.Check(); } + watch.Stop(); + + Logger.WriteLine("--------------------------------------------------------------------------------"); + //Logger.WriteLine($"Folders: {fsc.NumFolders}"); + Logger.WriteLine($"Files: {fileCount}"); + Logger.WriteLine($"Checksum Files:{checksumFilesCount}"); + Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); + Logger.WriteLine("--------------------------------------------------------------------------------"); } - catch (Exception ex) - { - Console.WriteLine($"Could not get the path to the settings file. Reason: {ex.Message}"); - return null; - } - } - /// - /// Reads the settings XML file. - /// - /// - /// The path to the settings XML file. - /// - /// - /// A object if the file was read successfully, otherwise null. - /// - private static Settings ReadSettingsFile(string path) - { - if (string.IsNullOrWhiteSpace(path)) - { - Console.WriteLine("The settings file path was null or empty."); - return null; - } - - if (!File.Exists(path)) - { - Console.WriteLine($"The settings file path '{path}' does not exist."); - return null; - } - - try - { - XmlSerializer serializer = new XmlSerializer(typeof(Settings)); - using FileStream fs = new FileStream(path, FileMode.Open); - return (Settings)serializer.Deserialize(fs); - } - catch (Exception ex) - { - Console.WriteLine($"The settings file could not be read. Reason: {ex.Message}"); - return null; - } - } + FileSystemCrawlerSO fsc = new FileSystemCrawlerSO(algorithm); + bool isFile = FileSystemCrawlerSO.IsFile(file); - static int Verify(string folder, string settingsFile, string settingsFolder) - { - string settingsFilePath = GetSettingsFilePath(settingsFolder, settingsFile); - if (string.IsNullOrWhiteSpace(settingsFilePath)) - { - return ERROR; - } - - Settings settings = ReadSettingsFile(settingsFilePath); - - Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folder: {folder}"); - - FileSystemCrawlerSO fsc = new FileSystemCrawlerSO(); - Stopwatch watch = new Stopwatch(); - - Logger.WriteLine("--------------------------------------------------------------------------------"); - - bool isFile = FileSystemCrawlerSO.IsFile(folder); - - string filePath = null; + string? filePath = null; if (isFile) { - filePath = folder; - folder = Path.GetDirectoryName(folder); + filePath = file; + file = Path.GetDirectoryName(file); + + if (string.IsNullOrWhiteSpace(file)) + { + Logger.WriteLine("The file or folder was not specified."); + return ERROR; + } } + watch.Reset(); watch.Start(); - fsc.CollectFolders(folder, !isFile); + fsc.CollectFolders(file, !isFile); fsc.CollectFiles(filePath); - watch.Stop(); + watch.Stop(); Logger.WriteLine("--------------------------------------------------------------------------------"); Logger.WriteLine($"Folders: {fsc.NumFolders}"); Logger.WriteLine($"Files: {fsc.NumFiles}"); - Logger.WriteLine($"Time (ms): { watch.ElapsedMilliseconds}"); + Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); Logger.WriteLine("--------------------------------------------------------------------------------"); - if (settings.Notifications != null) - { - settings.Notifications.Send(Logger.Lines); + // If settings were specified, then send the notifications + if (settings != null) + { + settings.Send(); } return SUCCESS; diff --git a/FileVerification/VerifyFile.cs b/FileVerification/VerifyFile.cs index 2bd4fac..c7e8101 100644 --- a/FileVerification/VerifyFile.cs +++ b/FileVerification/VerifyFile.cs @@ -69,9 +69,10 @@ public bool Exists() /// objects with the file name as the key. a value of null is /// returned if there is an issue reading the file. /// - public Dictionary Read() + public Dictionary? Read() { - Dictionary files = new Dictionary(); + Dictionary files = + new Dictionary(); if (string.IsNullOrWhiteSpace(FilePath) || !File.Exists(FilePath)) { @@ -81,19 +82,29 @@ public Dictionary Read() try { using (var reader = new StreamReader(FilePath)) + + while (!reader.EndOfStream) { - while (!reader.EndOfStream) + string? line = reader.ReadLine(); + if (line == null) + { + continue; + } + + string[] values = line.Split(Separator); + if (values.Length != Enum.GetNames(typeof(VerifyFileLayout)).Length) { - string line = reader.ReadLine(); - string[] values = line.Split(Separator); - if (values.Length != Enum.GetNames(typeof(VerifyFileLayout)).Length) - { - Logger.WriteLine($"WARNING: Record size incorrect (record will be created using the current file data). File: {FilePath}, Record: {line}."); - continue; - } - HashInfo info = new HashInfo(values[(int)VerifyFileLayout.HASH_ALGORITHM], values[(int)VerifyFileLayout.HASH]); - files.Add(values[(int)VerifyFileLayout.NAME], info); + Logger.WriteLine($"WARNING: Record size incorrect (record will be created using the current file data). File: {FilePath}, Record: {line}."); + continue; } + + Path.Combine(this.directory.FullName, fileName); + HashInfo info = + new HashInfo( + values[(int)VerifyFileLayout.HASH_ALGORITHM], + values[(int)VerifyFileLayout.HASH]); + + files.Add(values[(int)VerifyFileLayout.NAME], info); } return files; @@ -135,7 +146,7 @@ public void Write(Dictionary files, DirectoryInfo folder) foreach (KeyValuePair file in files) { HashInfo hashInfo = file.Value; - sb.AppendLine($"{file.Key}{Separator}{hashInfo.Algorithm.ToString().ToLower()}{Separator}{hashInfo.Value}"); + sb.AppendLine($"{file.Key}{Separator}{hashInfo.Algorithm.ToString().ToLower()}{Separator}{hashInfo.Hash}"); } try From f5fa40419947e064d36fe12cd13e5dc7d37d97c3 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Tue, 7 Jun 2022 17:02:00 -0400 Subject: [PATCH 15/33] Additional code restructuring --- FileVerification/CheckSumFile.cs | 185 ++++++++++- FileVerification/HashInfo.cs | 174 ++++++++-- FileVerification/PathInfo.cs | 305 ++++++++++++++---- FileVerification/Program.cs | 55 +--- .../Properties/launchSettings.json | 8 + 5 files changed, 583 insertions(+), 144 deletions(-) create mode 100644 FileVerification/Properties/launchSettings.json diff --git a/FileVerification/CheckSumFile.cs b/FileVerification/CheckSumFile.cs index 0e84f87..10245b0 100644 --- a/FileVerification/CheckSumFile.cs +++ b/FileVerification/CheckSumFile.cs @@ -7,26 +7,55 @@ namespace TE.FileVerification { - enum ChecksumFileLayout + /// + /// The fields used in the checksum file. + /// + public enum ChecksumFileLayout { + /// + /// The file name. + /// NAME, + /// + /// The string representation of the hash algorithm. + /// HASH_ALGORITHM, + /// + /// The hash of the file. + /// HASH } public class ChecksumFile { + /// + /// The separator used in the checksum file. + /// private const char Separator = '|'; - private readonly string? directory; + /// + /// The default checksum file name. + /// + public const string DEFAULT_CHECKSUM_FILENAME = "__fv.txt"; + + /// + /// Gets the directory where the checksum file is located. + /// + public string Directory { get; private set; } /// /// Gets the full path of the checksum file. /// public string FullPath { get; private set; } + /// + /// Gets the dictionary of checksums for the checksum file. + /// public Dictionary Checksums { get; private set; } + /// + /// Gets the number of files in the checksum file. + /// public int FileCount { get @@ -56,17 +85,21 @@ public ChecksumFile(string fullPath) } FullPath = fullPath; - directory = Path.GetDirectoryName(FullPath); + string? directory = Path.GetDirectoryName(FullPath); if (string.IsNullOrWhiteSpace(directory)) { throw new InvalidOperationException( "The directory name could not be determined from the full path to the checksum file."); } + Directory = directory; Checksums = new Dictionary(); - Read(); + if (File.Exists(FullPath)) + { + Read(); + } } /// @@ -80,7 +113,7 @@ public void Read() return; } - if (string.IsNullOrWhiteSpace(directory)) + if (string.IsNullOrWhiteSpace(Directory)) { Logger.WriteLine("The directory value is null or empty."); return; @@ -114,7 +147,7 @@ public void Read() // Get the full path to the file to use as the key to make // it unique so it can be used for searching - string filePath = Path.Combine(directory, fileName); + string filePath = Path.Combine(Directory, fileName); Checksums.Add(filePath, info); } } @@ -130,11 +163,151 @@ public void Read() } } + /// + /// Adds a checksum for a file. + /// + /// + /// The full path, including the directory, of the file to add. + /// + public void Add(string file) + { + if (string.IsNullOrWhiteSpace(file)) + { + Logger.WriteLine("Could not add file to the checksum file because the path was not specified."); + return; + } + + if (!File.Exists(file)) + { + Logger.WriteLine($"Could not add file '{file}' to the checksum file because the file does not exist."); + return; + } + + try + { + HashInfo hashInfo = new HashInfo(file, HashAlgorithm.SHA256); + Checksums.Add(file, hashInfo); + } + catch(ArgumentNullException ex) + { + Logger.WriteLine($"Could not add file '{file}' to the checksum file. Reason: {ex.Message}"); + } + } + + /// + /// Gets the checksum data for a file. + /// + /// + /// The full path to the file. + /// + /// + /// The data in a object, or null if the + /// data could not be retrieved. + /// public HashInfo? GetFileData(string fullPath) { HashInfo? hashInfo; Checksums.TryGetValue(fullPath, out hashInfo); return hashInfo; } + + /// + /// Validates the hash information of a file matches what is stored in + /// the checksum file. + /// + /// + /// The full path, including the directory, of the file. + /// + public bool IsMatch(string file) + { + if (string.IsNullOrWhiteSpace(file)) + { + Logger.WriteLine("Could not validate file to the checksum file because the path was not specified."); + return false; + } + + if (!File.Exists(file)) + { + Logger.WriteLine($"Could not validate file '{file}' to the checksum file because the file does not exist."); + return false; + } + + // Check if the file is in the checksum file + if (Checksums.ContainsKey(file)) + { + // Get the stored hash information for the file from the + // checksum data + HashInfo? hashInfo = GetFileData(file); + if (hashInfo == null) + { + Logger.WriteLine($"Validating file '{file}' failed because the hash information was not found."); + return false; + } + string? hash = HashInfo.GetFileHash(file, hashInfo.Algorithm); + if (string.IsNullOrWhiteSpace(hash)) + { + Logger.WriteLine($"Validating file '{file}' failed because the hash for the file could not be created using {hashInfo.Algorithm}."); + return false; + } + + return hashInfo.IsHashEqual(hash); + } + else + { + // Add the file if it didn't exist in the checksum file and + // then return true as it would match the hash that was just + // generated + Add(file); + return true; + } + } + + /// + /// Writes the checksum file. + /// + public void Write() + { + if (string.IsNullOrWhiteSpace(FullPath)) + { + Logger.WriteLine("Could not write checksum file as the path of the file was not provided."); + return; + } + + // Initialize the StringBuilder object that will contain the + // contents of the verify file + StringBuilder sb = new StringBuilder(); + + // Loop through the file checksum information and append the file + // information to the string builder so it can be written to the + // checksum file + foreach (KeyValuePair file in Checksums) + { + HashInfo hashInfo = file.Value; + sb.AppendLine($"{hashInfo.FileName}{Separator}{hashInfo.Algorithm.ToString().ToLower()}{Separator}{hashInfo.Hash}"); + } + + try + { + // Write the file hash information to the checksum file + using StreamWriter sw = new StreamWriter(FullPath); + sw.Write(sb.ToString()); + } + catch (DirectoryNotFoundException) + { + Logger.WriteLine($"ERROR: The directory {Directory} was not found."); + } + catch (PathTooLongException) + { + Logger.WriteLine($"ERROR: The path {FullPath} is too long."); + } + catch (UnauthorizedAccessException) + { + Logger.WriteLine($"ERROR: Not authorized to write to {FullPath}."); + } + catch (IOException ex) + { + Logger.WriteLine($"ERROR: Can't write to file. Reason: {ex.Message}"); + } + } } } diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs index c063d98..32a9ee0 100644 --- a/FileVerification/HashInfo.cs +++ b/FileVerification/HashInfo.cs @@ -9,7 +9,7 @@ namespace TE.FileVerification public enum HashAlgorithm { MD5, - SHA, + SHA1, SHA256, SHA512 } @@ -29,10 +29,31 @@ public class HashInfo public string? Hash { get; private set; } /// - /// Gets the information about the file. + /// Gets the full path to the file. /// public string FilePath { get; private set; } + /// + /// Gets the name of the file. + /// + public string FileName + { + get + { + return Path.GetFileName(FilePath); + } + } + + /// + /// Initializes an instance of the class when + /// provided with the full path to the file. + /// + /// + /// The full path, including the directory, to the file. + /// + /// + /// The parameter is null or empty. + /// private HashInfo(string filePath) { if (filePath == null || string.IsNullOrWhiteSpace(filePath)) @@ -43,6 +64,24 @@ private HashInfo(string filePath) FilePath = filePath; } + /// + /// Initializes an instance of the class when + /// provided with the full path to the file, the string representation + /// of the hash algorithm and the file hash. + /// + /// + /// The full path, including the directory, to the file. + /// + /// + /// The string representation of the hash algorithm used to create + /// the hash. + /// + /// + /// The hash value of the file. + /// + /// + /// A parameter is null or empty. + /// public HashInfo(string filePath, string algorithm, string hash) : this(filePath, algorithm) { @@ -54,6 +93,20 @@ public HashInfo(string filePath, string algorithm, string hash) Hash = hash; } + /// + /// Initializes an instance of the class when + /// provided with the full path to the file, and the string + /// representation of the hash algorithm. + /// + /// + /// The full path, including the directory, to the file. + /// + /// + /// The string representation of the hash algorithm. + /// + /// + /// A parameter is null or empty. + /// public HashInfo(string filePath, string algorithm) : this(filePath) { @@ -63,14 +116,27 @@ public HashInfo(string filePath, string algorithm) } Algorithm = GetAlgorithm(algorithm); - Hash = GetFileHash(); + Hash = GetFileHash(FilePath, Algorithm); } + /// + /// Initializes an instance of the class when + /// provided with the full path to the file, and the hash algorithm. + /// + /// + /// The full path, including the directory, to the file. + /// + /// + /// The hash algorithm. + /// + /// + /// A parameter is null or empty. + /// public HashInfo(string filePath, HashAlgorithm algorithm) : this(filePath) { Algorithm = algorithm; - Hash = GetFileHash(); + Hash = GetFileHash(FilePath, Algorithm); } /// @@ -83,44 +149,100 @@ public HashInfo(string filePath, HashAlgorithm algorithm) /// The enum value of the algorithm. /// private static HashAlgorithm GetAlgorithm(string hash) - { - if (string.Compare(hash, "sha256", true) == 0) + { + if (string.Compare(hash, "md5", true) == 0) { - return HashAlgorithm.SHA256; + return HashAlgorithm.MD5; } - else + else if (string.Compare(hash, "sha1", true) == 0) + { + return HashAlgorithm.SHA1; + } + else if (string.Compare(hash, "sha512", true) == 0) { return HashAlgorithm.SHA512; } + else + { + return HashAlgorithm.SHA256; + } } - private string? GetFileHash() + /// + /// Gets the hash of the file for the specified hash algorithm. + /// + /// + /// The full path, including directory, of the file. + /// + /// + /// The algorithm used to generate the hash. + /// + /// + /// The hash of the file, or null if the hash could not be + /// generated. + /// + public static string? GetFileHash(string file, HashAlgorithm algorithm) { - int maxSize = 16 * Megabyte; - - using Cryptography.HashAlgorithm? hashAlgorithm = - Cryptography.HashAlgorithm.Create(Algorithm.ToString()); - if (hashAlgorithm == null) + if (string.IsNullOrWhiteSpace(file)) { return null; } + int maxSize = 16 * Megabyte; + + Cryptography.HashAlgorithm? hashAlgorithm = null; + try { - using var stream = - new FileStream( - FilePath, - FileMode.Open, - FileAccess.Read, - FileShare.Read, - maxSize); - - var hash = hashAlgorithm.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", ""); + + switch (algorithm) + { + case HashAlgorithm.MD5: + hashAlgorithm = Cryptography.MD5.Create(); + break; + case HashAlgorithm.SHA1: + hashAlgorithm = Cryptography.SHA1.Create(); + break; + case HashAlgorithm.SHA256: + hashAlgorithm = Cryptography.SHA256.Create(); + break; + case HashAlgorithm.SHA512: + hashAlgorithm = Cryptography.SHA512.Create(); + break; + } + + if (hashAlgorithm == null) + { + Logger.WriteLine($"Couldn't create hash. Reason: Hash was not provided."); + return null; + } + + try + { + using var stream = + new FileStream( + file, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + maxSize); + + var hash = hashAlgorithm.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", ""); + } + catch + { + + return null; + } } - catch + finally { - return null; + if (hashAlgorithm != null) + { + hashAlgorithm.Clear(); + hashAlgorithm.Dispose(); + } } } diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index cc46780..02aa539 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -13,36 +14,107 @@ namespace TE.FileVerification /// public class PathInfo { + private readonly string? _directory; + + private readonly string? _checksumFileName; + + private readonly ConcurrentBag _tasks = new ConcurrentBag(); + + // private readonly List directories = new List(); + /// /// Gets the path value. /// - public string Path { get; private set; } + public string FullPath { get; private set; } + /// + /// Gets all the files in the path. + /// public Queue? Files { get; private set; } - public List? ChecksumFiles { get; private set; } + /// + /// Gets the number of files. + /// + public int FileCount + { + get + { + return Files != null ? Files.Count : 0; + } + } + + /// + /// Gets the number of directories. + /// + public int DirectoryCount { get; private set; } + + /// + /// Gets all the checksum files in the path. + /// + public List? ChecksumFileInfo { get; private set; } /// - /// Initializes an instance of the class when + /// Initializes an instance of the class when /// provided with the full path. /// /// - /// The full path. + /// The full path to a directory or a file. /// /// - /// Thrown if the argument is null or empty. + /// Thrown if the parameter is null or + /// empty. /// - internal PathInfo(string path) + public PathInfo(string path) { if (path == null || string.IsNullOrWhiteSpace(path)) { throw new ArgumentNullException(path); } - Path = path; + FullPath = path; + + // Check to see if the full path points to a file rather than a + // directory, and if it does, then extract and store the directory + // name + if (IsFile()) + { + _directory = Path.GetDirectoryName(FullPath); + if (string.IsNullOrWhiteSpace(_directory)) + { + throw new InvalidOperationException("The directory for the path could not be determined."); + } + } + else + { + _directory = FullPath; + } + + _checksumFileName = ChecksumFile.DEFAULT_CHECKSUM_FILENAME; + } + + /// + /// Initializes an instance of the class when + /// provided with the full path and the checksum file name. + /// + /// + /// The full path to a directory or a file. + /// + /// + /// The name of the checksum file. + /// + /// + /// Thrown if a paramter is null or empty. + /// + public PathInfo(string path, string checksumFileName) + : this(path) + { + if (!string.IsNullOrWhiteSpace(checksumFileName)) + { + _checksumFileName = checksumFileName; + } } - public void Crawl(bool includeSubDir, string checksumFileName) + public void Crawl(bool includeSubDir) { // If the path does not exist, then just return null @@ -56,52 +128,89 @@ public void Crawl(bool includeSubDir, string checksumFileName) if (IsFile()) { Files = new Queue(); - Files.Enqueue(Path); + Files.Enqueue(FullPath); return; } - Files = new Queue(CrawlDirectory(includeSubDir)); - - if (ChecksumFiles == null) + CrawlDirectory(includeSubDir); + + if (ChecksumFileInfo == null) { - ChecksumFiles = new List(); + ChecksumFileInfo = new List(); } - foreach (string file in GetChecksumFiles(checksumFileName, includeSubDir)) + foreach (string file in GetChecksumFiles(includeSubDir)) { - ChecksumFiles.Add(new ChecksumFile(file)); + ChecksumFileInfo.Add(new ChecksumFile(file)); } } - //public List? CrawlCheckSumFiles(bool includeSubDir, string checksumFileName) - //{ - // // If the path does not exist, then just return null - // if (!Exists()) - // { - // return null; - // } - - // return new List(GetChecksumFiles(checksumFileName, includeSubDir)); - //} - public void Check() { - if (Files == null || ChecksumFiles == null) + if (Files == null || ChecksumFileInfo == null) { return; } - foreach (string file in Files) - { - ChecksumFile? checksumFile = ChecksumFiles.FirstOrDefault(c => c.Checksums.ContainsKey(file)); + ParallelOptions options = new ParallelOptions(); + options.MaxDegreeOfParallelism = Environment.ProcessorCount; + Parallel.ForEach(Files, options, file => + //foreach (string file in Files) + { + if (Path.GetFileName(file).Equals(_checksumFileName) || IsSystemFile(file)) + { + return; + } + + // Find the checksum file that contains the file + ChecksumFile? checksumFile = + ChecksumFileInfo.FirstOrDefault( + c => c.Checksums.ContainsKey(file)); // A checksum file was found containing the file, so get the // hash information for the file if (checksumFile != null) { - HashInfo? hashInfo = checksumFile.GetFileData(file); + // Check if the current file matches the hash information + // stored in the checksum file + if (!checksumFile.IsMatch(file)) + { + Logger.WriteLine($"FAIL: Hash mismatch: {file}."); + } + } + else + { + // Get the file directory so it can be used to find the + // checksum file for the directory + string? fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrWhiteSpace(fileDir)) + { + Logger.WriteLine($"Could not get the directory from '{file}'."); + return; + } + + // Find the checksum file for the directory + checksumFile = + ChecksumFileInfo.FirstOrDefault( + c => c.Directory.Equals(fileDir)); + if (checksumFile == null) + { + // If no checksum file was located in the directory, + // create a new checksum file and then add it to the + // list + checksumFile = new ChecksumFile(Path.Combine(fileDir, ChecksumFile.DEFAULT_CHECKSUM_FILENAME)); + ChecksumFileInfo.Add(checksumFile); + } + + // Add the file to the checksum file + checksumFile.Add(file); } + }); + + foreach(ChecksumFile checksum in ChecksumFileInfo) + { + checksum.Write(); } } @@ -113,7 +222,7 @@ public void Check() /// public bool IsDirectory() { - return Directory.Exists(Path); + return Directory.Exists(FullPath); } /// @@ -124,7 +233,7 @@ public bool IsDirectory() /// public bool IsFile() { - return File.Exists(Path); + return File.Exists(FullPath); } /// @@ -147,9 +256,15 @@ public bool Exists() /// /// Returns an enumerable collection of file paths. /// - private IEnumerable CrawlDirectory(bool includeSubDir) + private IEnumerable CrawlDirectory2(bool includeSubDir) { - DirectoryInfo dirInfo = new DirectoryInfo(Path); + if (string.IsNullOrWhiteSpace(_directory)) + { + yield break; + } + + DirectoryInfo dirInfo = new DirectoryInfo(_directory); + IEnumerable files = dirInfo.EnumerateFiles("*", GetSearchOption(includeSubDir)); @@ -157,42 +272,91 @@ private IEnumerable CrawlDirectory(bool includeSubDir) { yield return file.FullName; } + + } + + /// + /// Crawls the path and returns the files. + /// + /// + /// Value indicating if the subdirectories are to be crawled. + /// + /// + /// Returns an enumerable collection of file paths. + /// + private void CrawlDirectory(bool includeSubDir) + { + if (string.IsNullOrWhiteSpace(_directory)) + { + return; ; + } + + DirectoryInfo directoryInfo = new DirectoryInfo(_directory); + _tasks.Add(Task.Run(() => CrawlDirectory(directoryInfo, includeSubDir))); + + while (_tasks.TryTake(out Task? taskToWaitFor)) + { + if (taskToWaitFor != null) + { + DirectoryCount++; + taskToWaitFor.Wait(); + } + } + } + + /// + /// Crawls the path and returns the files. + /// + /// + /// The object of the current directory. + /// + /// + /// Value indicating if the subdirectories are to be crawled. + /// + /// + /// Returns an enumerable collection of file paths. + /// + private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) + { + try + { + if (Files == null) + { + Files = new Queue(); + } + + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + Files.Enqueue(file.FullName); + } + + if (includeSubDir) + { + DirectoryInfo[] directoryInfo = dir.GetDirectories(); + foreach (DirectoryInfo childInfo in directoryInfo) + { + _tasks.Add(Task.Run(() => CrawlDirectory(childInfo, includeSubDir))); + } + } + } + catch (Exception ex) + when (ex is DirectoryNotFoundException || ex is System.Security.SecurityException || ex is UnauthorizedAccessException) + { + Console.WriteLine($"ERROR: {ex.Message}"); + } } - //private void CrawlDirectory2(bool includeSubDir) - //{ - // try - // { - - // } - // catch (Exception ex) - // { - // Console.WriteLine($"{ex.GetType()} {ex.Message}\n{ex.StackTrace}"); - // } - //} - - //private void CrawlDirectory2(bool includeSubDir, DirectoryInfo dirInfo) - //{ - // try - // { - - // } - // catch (Exception ex) - // { - // Console.WriteLine($"{ex.GetType()} {ex.Message}\n{ex.StackTrace}"); - // } - //} - - private IEnumerable GetChecksumFiles(string checksumFileName, bool includeSubDir) + private IEnumerable GetChecksumFiles(bool includeSubDir) { - if (string.IsNullOrWhiteSpace(checksumFileName)) + if (string.IsNullOrWhiteSpace(_checksumFileName) || string.IsNullOrWhiteSpace(_directory)) { yield break; } - DirectoryInfo dirInfo = new DirectoryInfo(Path); + DirectoryInfo dirInfo = new DirectoryInfo(_directory); IEnumerable checksumFiles = - dirInfo.EnumerateFiles(checksumFileName, GetSearchOption(includeSubDir)); + dirInfo.EnumerateFiles(_checksumFileName, GetSearchOption(includeSubDir)); foreach (var checksumFile in checksumFiles) { @@ -204,5 +368,20 @@ private SearchOption GetSearchOption(bool includeSubDir) { return includeSubDir ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; } + + /// + /// Indicates if a file is a system file. + /// + /// + /// The full path, including the directory, of the file. + /// + /// + /// true if the file is a system file, otherwise false. + /// + private bool IsSystemFile(string file) + { + FileAttributes attributes = File.GetAttributes(file); + return ((attributes & FileAttributes.System) == FileAttributes.System); + } } } diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index d9fc352..0553ca5 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -92,63 +92,20 @@ static int Run(string? file, HashAlgorithm? algorithm, string? settingsFile, str PathInfo path = new PathInfo(file); Stopwatch watch = new Stopwatch(); watch.Start(); - path.Crawl(true, "__fv.txt"); - //List? checksumFiles = path.CrawlCheckSumFiles(true, "__fv.txt"); - - List hashInfoList = new List(); + path.Crawl(true); if (path.Files != null) { - int fileCount = 0; - foreach (string fileValue in path.Files) - { - HashInfo hashInfo = new HashInfo(fileValue, (HashAlgorithm)algorithm); - hashInfoList.Add(hashInfo); - fileCount++; - } - - int checksumFilesCount = 0; - if (path.ChecksumFiles != null) - { - path.Check(); - } + path.Check(); watch.Stop(); Logger.WriteLine("--------------------------------------------------------------------------------"); - //Logger.WriteLine($"Folders: {fsc.NumFolders}"); - Logger.WriteLine($"Files: {fileCount}"); - Logger.WriteLine($"Checksum Files:{checksumFilesCount}"); - Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); + Logger.WriteLine($"Folders: {path.DirectoryCount}"); + Logger.WriteLine($"Files: {path.FileCount}"); + //Logger.WriteLine($"Checksum Files: {checksumFilesCount}"); + Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); Logger.WriteLine("--------------------------------------------------------------------------------"); } - FileSystemCrawlerSO fsc = new FileSystemCrawlerSO(algorithm); - bool isFile = FileSystemCrawlerSO.IsFile(file); - - string? filePath = null; - if (isFile) - { - filePath = file; - file = Path.GetDirectoryName(file); - - if (string.IsNullOrWhiteSpace(file)) - { - Logger.WriteLine("The file or folder was not specified."); - return ERROR; - } - } - - watch.Reset(); - watch.Start(); - fsc.CollectFolders(file, !isFile); - fsc.CollectFiles(filePath); - watch.Stop(); - - Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folders: {fsc.NumFolders}"); - Logger.WriteLine($"Files: {fsc.NumFiles}"); - Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); - Logger.WriteLine("--------------------------------------------------------------------------------"); - // If settings were specified, then send the notifications if (settings != null) { diff --git a/FileVerification/Properties/launchSettings.json b/FileVerification/Properties/launchSettings.json new file mode 100644 index 0000000..8034b2c --- /dev/null +++ b/FileVerification/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "FileVerification": { + "commandName": "Project", + "commandLineArgs": "fv -f C:\\TEMP3" + } + } +} \ No newline at end of file From 6a32f8aaa9ebe00bb9ddf0f6bf3cb935f61878a2 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 8 Jun 2022 10:25:44 -0400 Subject: [PATCH 16/33] Added file hash checking --- FileVerification/HashInfo.cs | 15 +++--- FileVerification/PathInfo.cs | 25 ++++----- FileVerification/Program.cs | 102 ++++++++++++++++++++++++----------- 3 files changed, 92 insertions(+), 50 deletions(-) diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs index 32a9ee0..dda9a66 100644 --- a/FileVerification/HashInfo.cs +++ b/FileVerification/HashInfo.cs @@ -8,9 +8,9 @@ namespace TE.FileVerification { public enum HashAlgorithm { - MD5, - SHA1, SHA256, + MD5, + SHA1, SHA512 } public class HashInfo @@ -148,17 +148,17 @@ public HashInfo(string filePath, HashAlgorithm algorithm) /// /// The enum value of the algorithm. /// - private static HashAlgorithm GetAlgorithm(string hash) + private static HashAlgorithm GetAlgorithm(string algorithm) { - if (string.Compare(hash, "md5", true) == 0) + if (string.Compare(algorithm, "md5", true) == 0) { return HashAlgorithm.MD5; } - else if (string.Compare(hash, "sha1", true) == 0) + else if (string.Compare(algorithm, "sha1", true) == 0) { return HashAlgorithm.SHA1; } - else if (string.Compare(hash, "sha512", true) == 0) + else if (string.Compare(algorithm, "sha512", true) == 0) { return HashAlgorithm.SHA512; } @@ -193,8 +193,7 @@ private static HashAlgorithm GetAlgorithm(string hash) Cryptography.HashAlgorithm? hashAlgorithm = null; try - { - + { switch (algorithm) { case HashAlgorithm.MD5: diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index 02aa539..c333c60 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -76,7 +76,7 @@ public PathInfo(string path) // Check to see if the full path points to a file rather than a // directory, and if it does, then extract and store the directory // name - if (IsFile()) + if (IsFile(FullPath)) { _directory = Path.GetDirectoryName(FullPath); if (string.IsNullOrWhiteSpace(_directory)) @@ -125,15 +125,16 @@ public void Crawl(bool includeSubDir) // If the path is a file, then just return a string array with the // path value as there is no directory to be crawled - if (IsFile()) + if (IsFile(FullPath)) { Files = new Queue(); - Files.Enqueue(FullPath); - return; + Files.Enqueue(FullPath); } - - CrawlDirectory(includeSubDir); - + else + { + CrawlDirectory(includeSubDir); + } + if (ChecksumFileInfo == null) { ChecksumFileInfo = new List(); @@ -220,9 +221,9 @@ public void Check() /// /// true if the path is a valid directory, otherwise false. /// - public bool IsDirectory() + public static bool IsDirectory(string path) { - return Directory.Exists(FullPath); + return Directory.Exists(path); } /// @@ -231,9 +232,9 @@ public bool IsDirectory() /// /// true if the path is a valid file, othersize false. /// - public bool IsFile() + public static bool IsFile(string path) { - return File.Exists(FullPath); + return File.Exists(path); } /// @@ -244,7 +245,7 @@ public bool IsFile() /// public bool Exists() { - return IsDirectory() || IsFile(); + return IsDirectory(FullPath) || IsFile(FullPath); } /// diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index 0553ca5..0baf02d 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -14,7 +14,16 @@ class Program private const int SUCCESS = 0; // Error return code - private const int ERROR = -1; + private const int ERROR = 1; + + // The path is not a file + private const int ERROR_NOT_FILE = 2; + + // The hash could not be generated + private const int ERROR_NO_HASH = 3; + + // The hash of the file does not match the provided hash + private const int ERROR_HASH_NOT_MATCH = 4; public static int NumFolders { get; set; } @@ -36,6 +45,12 @@ static int Main(string[] args) ); rootCommand.AddOption(algorithmOption); + var hashOption = new Option( + aliases: new string[] { "--hash", "-ha" }, + description: "The hash of the file to verify." + ); + rootCommand.AddOption(hashOption); + var settingsFileOption = new Option( aliases: new string[] { "--settingsFile", "-sfi" }, description: "The name of the settings XML file." @@ -48,11 +63,11 @@ static int Main(string[] args) ); rootCommand.AddOption(settingsFolderOption); - rootCommand.SetHandler((fileOptionValue, algorithmOptionValue, settingsFileOptionValue, settingsFolderOptionValue) => + rootCommand.SetHandler((fileOptionValue, algorithmOptionValue, hashOption, settingsFileOptionValue, settingsFolderOptionValue) => { - Run(fileOptionValue, algorithmOptionValue, settingsFileOptionValue, settingsFolderOptionValue); + Run(fileOptionValue, algorithmOptionValue, hashOption, settingsFileOptionValue, settingsFolderOptionValue); }, - fileOption, algorithmOption, settingsFileOption, settingsFolderOption); + fileOption, algorithmOption, hashOption, settingsFileOption, settingsFolderOption); return rootCommand.Invoke(args); } @@ -64,7 +79,7 @@ static int Main(string[] args) /// /// /// - static int Run(string? file, HashAlgorithm? algorithm, string? settingsFile, string? settingsFolder) + static int Run(string? file, HashAlgorithm? algorithm, string hashOption, string? settingsFile, string? settingsFolder) { if (string.IsNullOrWhiteSpace(file)) { @@ -72,10 +87,7 @@ static int Run(string? file, HashAlgorithm? algorithm, string? settingsFile, str return ERROR; } - if (algorithm == null) - { - algorithm = HashAlgorithm.SHA256; - } + // Read the settings file if one was provided as an argument Settings? settings = null; @@ -86,33 +98,63 @@ static int Run(string? file, HashAlgorithm? algorithm, string? settingsFile, str } Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folder: {file}"); + Logger.WriteLine($"Folder/File: {file}"); + Logger.WriteLine($"Hash Algorithm: {algorithm}"); Logger.WriteLine("--------------------------------------------------------------------------------"); - PathInfo path = new PathInfo(file); - Stopwatch watch = new Stopwatch(); - watch.Start(); - path.Crawl(true); - if (path.Files != null) + if (string.IsNullOrWhiteSpace(hashOption)) { - path.Check(); - watch.Stop(); - - Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folders: {path.DirectoryCount}"); - Logger.WriteLine($"Files: {path.FileCount}"); - //Logger.WriteLine($"Checksum Files: {checksumFilesCount}"); - Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); - Logger.WriteLine("--------------------------------------------------------------------------------"); + PathInfo path = new PathInfo(file); + Stopwatch watch = new Stopwatch(); + watch.Start(); + path.Crawl(true); + if (path.Files != null) + { + path.Check(); + watch.Stop(); + + Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine($"Folders: {path.DirectoryCount}"); + Logger.WriteLine($"Files: {path.FileCount}"); + Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); + Logger.WriteLine("--------------------------------------------------------------------------------"); + } + + // If settings gdwere specified, then send the notifications + if (settings != null) + { + settings.Send(); + } + + return SUCCESS; } - - // If settings were specified, then send the notifications - if (settings != null) + else { - settings.Send(); + if (!PathInfo.IsFile(file)) + { + Logger.WriteLine($"The file '{file}' is not a valid file."); + return ERROR_NOT_FILE; + } + + string? fileHash = HashInfo.GetFileHash(file, (HashAlgorithm)algorithm); + if (string.IsNullOrWhiteSpace(fileHash)) + { + Logger.WriteLine($"The hash for file '{file}' could not be generated."); + return ERROR_NO_HASH; + } + + int returnValue = string.Compare(fileHash, hashOption, true) == 0 ? SUCCESS : ERROR_HASH_NOT_MATCH; + + if (returnValue == SUCCESS) + { + Logger.WriteLine($"The file hash matches the hash '{hashOption}'"); + } + else + { + Logger.WriteLine($"The file hash '{fileHash}' does not match the hash '{hashOption}'"); + } + return returnValue; } - - return SUCCESS; } } } From d2b93fe0ce813f8a49c42ece3e106589929b2003 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 8 Jun 2022 10:29:53 -0400 Subject: [PATCH 17/33] Added more comments --- FileVerification/PathInfo.cs | 52 ++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index c333c60..a13114f 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -146,6 +146,11 @@ public void Crawl(bool includeSubDir) } } + /// + /// Check the hash values of the file against the stored hashes in the + /// checksum files. If the file hashes aren't in the checksum files, + /// then add the file and its hash to the checksum files. + /// public void Check() { if (Files == null || ChecksumFileInfo == null) @@ -156,7 +161,6 @@ public void Check() ParallelOptions options = new ParallelOptions(); options.MaxDegreeOfParallelism = Environment.ProcessorCount; Parallel.ForEach(Files, options, file => - //foreach (string file in Files) { if (Path.GetFileName(file).Equals(_checksumFileName) || IsSystemFile(file)) { @@ -248,34 +252,6 @@ public bool Exists() return IsDirectory(FullPath) || IsFile(FullPath); } - /// - /// Crawls the path and returns the files. - /// - /// - /// Value indicating if the subdirectories are to be crawled. - /// - /// - /// Returns an enumerable collection of file paths. - /// - private IEnumerable CrawlDirectory2(bool includeSubDir) - { - if (string.IsNullOrWhiteSpace(_directory)) - { - yield break; - } - - DirectoryInfo dirInfo = new DirectoryInfo(_directory); - - IEnumerable files = - dirInfo.EnumerateFiles("*", GetSearchOption(includeSubDir)); - - foreach (var file in files) - { - yield return file.FullName; - } - - } - /// /// Crawls the path and returns the files. /// @@ -348,6 +324,15 @@ private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) } } + /// + /// Gets the checksum files from each directory. + /// + /// + /// Include subdirectories when searching for checksum files. + /// + /// + /// A object of all the checksum files. + /// private IEnumerable GetChecksumFiles(bool includeSubDir) { if (string.IsNullOrWhiteSpace(_checksumFileName) || string.IsNullOrWhiteSpace(_directory)) @@ -365,6 +350,15 @@ private IEnumerable GetChecksumFiles(bool includeSubDir) } } + /// + /// Gets the search option used to search the directories. + /// + /// + /// Indicates if the subdirectories are to be included in the search. + /// + /// + /// A value. + /// private SearchOption GetSearchOption(bool includeSubDir) { return includeSubDir ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; From bb3f2ca009396dfceedafd614bb0982aff1ebcbc Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Wed, 8 Jun 2022 16:53:38 -0400 Subject: [PATCH 18/33] Changed the get checksum method --- FileVerification/PathInfo.cs | 43 +++++++++++++----------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index a13114f..c0cddf0 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -20,8 +20,6 @@ public class PathInfo private readonly ConcurrentBag _tasks = new ConcurrentBag(); - // private readonly List directories = new List(); - /// /// Gets the path value. /// @@ -30,7 +28,7 @@ public class PathInfo /// /// Gets all the files in the path. /// - public Queue? Files { get; private set; } + public ConcurrentQueue? Files { get; private set; } /// /// Gets the number of files. @@ -127,23 +125,15 @@ public void Crawl(bool includeSubDir) // path value as there is no directory to be crawled if (IsFile(FullPath)) { - Files = new Queue(); + Files = new ConcurrentQueue(); Files.Enqueue(FullPath); } else { CrawlDirectory(includeSubDir); } - - if (ChecksumFileInfo == null) - { - ChecksumFileInfo = new List(); - } - foreach (string file in GetChecksumFiles(includeSubDir)) - { - ChecksumFileInfo.Add(new ChecksumFile(file)); - } + GetChecksumFiles(includeSubDir); } /// @@ -290,16 +280,13 @@ private void CrawlDirectory(bool includeSubDir) /// /// Value indicating if the subdirectories are to be crawled. /// - /// - /// Returns an enumerable collection of file paths. - /// private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) { try - { + { if (Files == null) { - Files = new Queue(); + Files = new ConcurrentQueue(); } FileInfo[] files = dir.GetFiles(); @@ -330,23 +317,23 @@ private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) /// /// Include subdirectories when searching for checksum files. /// - /// - /// A object of all the checksum files. - /// - private IEnumerable GetChecksumFiles(bool includeSubDir) + private void GetChecksumFiles(bool includeSubDir) { if (string.IsNullOrWhiteSpace(_checksumFileName) || string.IsNullOrWhiteSpace(_directory)) { - yield break; + return; } - DirectoryInfo dirInfo = new DirectoryInfo(_directory); - IEnumerable checksumFiles = - dirInfo.EnumerateFiles(_checksumFileName, GetSearchOption(includeSubDir)); + // Get all the checksums in the directory and sub-directories, + // if specified + string[] checksumFiles = Directory.GetFiles(_directory, _checksumFileName, GetSearchOption(includeSubDir)); + ChecksumFileInfo = new List(checksumFiles.Length); - foreach (var checksumFile in checksumFiles) + // Loop through each of the checksum files and add the information + // in the checksum list + foreach (string file in checksumFiles) { - yield return checksumFile.FullName; + ChecksumFileInfo.Add(new ChecksumFile(file)); } } From 9a6923f86b53b93139aad32c45cde88334e3067d Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Thu, 9 Jun 2022 08:10:53 -0400 Subject: [PATCH 19/33] Changed method to static --- FileVerification/PathInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index c0cddf0..4d22862 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -360,7 +360,7 @@ private SearchOption GetSearchOption(bool includeSubDir) /// /// true if the file is a system file, otherwise false. /// - private bool IsSystemFile(string file) + private static bool IsSystemFile(string file) { FileAttributes attributes = File.GetAttributes(file); return ((attributes & FileAttributes.System) == FileAttributes.System); From 5b1ec0b83bbeef218fef92f0c8df48e884d86e00 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 10 Jun 2022 13:25:27 -0400 Subject: [PATCH 20/33] Restructured code and added exception logging --- FileVerification/CheckSumFile.cs | 31 +++--- FileVerification/HashInfo.cs | 36 +++++-- FileVerification/PathInfo.cs | 175 ++++++++++++++++++++++--------- FileVerification/Program.cs | 126 ++++++++++++---------- 4 files changed, 236 insertions(+), 132 deletions(-) diff --git a/FileVerification/CheckSumFile.cs b/FileVerification/CheckSumFile.cs index 10245b0..80f5bac 100644 --- a/FileVerification/CheckSumFile.cs +++ b/FileVerification/CheckSumFile.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Text; @@ -28,11 +29,6 @@ public enum ChecksumFileLayout } public class ChecksumFile { - /// - /// The separator used in the checksum file. - /// - private const char Separator = '|'; - /// /// The default checksum file name. /// @@ -131,7 +127,7 @@ public void Read() continue; } - string[] values = line.Split(Separator); + string[] values = line.Split(HashInfo.Separator); if (values.Length != Enum.GetNames(typeof(ChecksumFileLayout)).Length) { Logger.WriteLine($"WARNING: Record size incorrect (record will be created using the current file data). File: {FullPath}, Record: {line}."); @@ -275,38 +271,39 @@ public void Write() // Initialize the StringBuilder object that will contain the // contents of the verify file - StringBuilder sb = new StringBuilder(); + ConcurrentBag info = new ConcurrentBag(); // Loop through the file checksum information and append the file // information to the string builder so it can be written to the // checksum file - foreach (KeyValuePair file in Checksums) + Parallel.ForEach(Checksums, checksumInfo => { - HashInfo hashInfo = file.Value; - sb.AppendLine($"{hashInfo.FileName}{Separator}{hashInfo.Algorithm.ToString().ToLower()}{Separator}{hashInfo.Hash}"); - } + HashInfo hashInfo = checksumInfo.Value; + info.Add(hashInfo.ToString() + Environment.NewLine); + }); try { // Write the file hash information to the checksum file using StreamWriter sw = new StreamWriter(FullPath); - sw.Write(sb.ToString()); + sw.Write(string.Join("", info)); } catch (DirectoryNotFoundException) { - Logger.WriteLine($"ERROR: The directory {Directory} was not found."); + Logger.WriteLine($"Could not write the checksum file because the directory {Directory} was not found."); } catch (PathTooLongException) { - Logger.WriteLine($"ERROR: The path {FullPath} is too long."); + Logger.WriteLine($"Could not write the checksum file because the path {FullPath} is too long."); } catch (UnauthorizedAccessException) { - Logger.WriteLine($"ERROR: Not authorized to write to {FullPath}."); + Logger.WriteLine($"Could not write the checksum file because the user is not authorized to write to {FullPath}."); } - catch (IOException ex) + catch (Exception ex) + when (ex is ArgumentException || ex is ArgumentNullException || ex is IOException || ex is System.Security.SecurityException) { - Logger.WriteLine($"ERROR: Can't write to file. Reason: {ex.Message}"); + Logger.WriteLine($"Could not write the checksum file. Reason: {ex.Message}"); } } } diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs index dda9a66..7955860 100644 --- a/FileVerification/HashInfo.cs +++ b/FileVerification/HashInfo.cs @@ -15,6 +15,11 @@ public enum HashAlgorithm } public class HashInfo { + /// + /// The separator used in the checksum file. + /// + public const char Separator = '|'; + // A megabyte private const int Megabyte = 1024 * 1024; @@ -187,8 +192,7 @@ private static HashAlgorithm GetAlgorithm(string algorithm) { return null; } - - int maxSize = 16 * Megabyte; + int maxSize = 64 * 1024; // Megabyte; Cryptography.HashAlgorithm? hashAlgorithm = null; @@ -219,12 +223,17 @@ private static HashAlgorithm GetAlgorithm(string algorithm) try { using var stream = - new FileStream( - file, - FileMode.Open, - FileAccess.Read, - FileShare.Read, - maxSize); + //new FileStream( + // file, + // FileMode.Open, + // FileAccess.Read, + // FileShare.None); + new FileStream( + file, + FileMode.Open, + FileAccess.Read, + FileShare.None, + maxSize); var hash = hashAlgorithm.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", ""); @@ -269,5 +278,16 @@ public bool IsHashEqual(string hash) return Hash.Equals(hash); } + + /// + /// Returns a string representing the hash information. + /// + /// + /// A string representation of the hash information. + /// + public override string ToString() + { + return $"{FileName}{Separator}{Algorithm.ToString().ToLower()}{Separator}{Hash}"; + } } } diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index 4d22862..62fc899 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -14,11 +14,15 @@ namespace TE.FileVerification /// public class PathInfo { + // The directory associated with the path private readonly string? _directory; + // The name of the checksum files private readonly string? _checksumFileName; - private readonly ConcurrentBag _tasks = new ConcurrentBag(); + // A queue of tasks used to crawl the directory tree + private readonly ConcurrentQueue _tasks = + new ConcurrentQueue(); /// /// Gets the path value. @@ -49,7 +53,7 @@ public int FileCount /// /// Gets all the checksum files in the path. /// - public List? ChecksumFileInfo { get; private set; } + public ConcurrentDictionary? ChecksumFileInfo { get; private set; } /// /// Initializes an instance of the class when @@ -62,6 +66,10 @@ public int FileCount /// Thrown if the parameter is null or /// empty. /// + /// + /// Thrown if the directory of the path could not be determined. + /// + /// + /// Crawl the directory associated with the path. + /// + /// + /// Indicates if subdirectories are to be crawled. + /// public void Crawl(bool includeSubDir) { @@ -149,7 +163,7 @@ public void Check() } ParallelOptions options = new ParallelOptions(); - options.MaxDegreeOfParallelism = Environment.ProcessorCount; + options.MaxDegreeOfParallelism = Environment.ProcessorCount; Parallel.ForEach(Files, options, file => { if (Path.GetFileName(file).Equals(_checksumFileName) || IsSystemFile(file)) @@ -157,10 +171,19 @@ public void Check() return; } - // Find the checksum file that contains the file - ChecksumFile? checksumFile = + // Get the file directory so it can be used to find the + // checksum file for the directory + string? fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrWhiteSpace(fileDir)) + { + Logger.WriteLine($"Could not get the directory from '{file}'."); + return; + } + + // Find the checksum file for the directory containing the file + ChecksumFile? checksumFile = ChecksumFileInfo.FirstOrDefault( - c => c.Checksums.ContainsKey(file)); + c => c.Key.Equals(fileDir)).Value; // A checksum file was found containing the file, so get the // hash information for the file @@ -175,37 +198,26 @@ public void Check() } else { - // Get the file directory so it can be used to find the - // checksum file for the directory - string? fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrWhiteSpace(fileDir)) - { - Logger.WriteLine($"Could not get the directory from '{file}'."); - return; - } - - // Find the checksum file for the directory - checksumFile = - ChecksumFileInfo.FirstOrDefault( - c => c.Directory.Equals(fileDir)); - if (checksumFile == null) - { - // If no checksum file was located in the directory, - // create a new checksum file and then add it to the - // list - checksumFile = new ChecksumFile(Path.Combine(fileDir, ChecksumFile.DEFAULT_CHECKSUM_FILENAME)); - ChecksumFileInfo.Add(checksumFile); - } + // If no checksum file was located in the directory, create + // a new checksum file and then add it to the list + checksumFile = + new ChecksumFile + (Path.Combine( + fileDir, + ChecksumFile.DEFAULT_CHECKSUM_FILENAME)); // Add the file to the checksum file checksumFile.Add(file); + ChecksumFileInfo.TryAdd(fileDir, checksumFile); + } }); - foreach(ChecksumFile checksum in ChecksumFileInfo) + // Write out each checksum file with the updated information + foreach (var keyPair in ChecksumFileInfo) { - checksum.Write(); + keyPair.Value.Write(); } } @@ -258,15 +270,29 @@ private void CrawlDirectory(bool includeSubDir) return; ; } + // Get the directory information, and then enqueue the task + // to crawl the directory DirectoryInfo directoryInfo = new DirectoryInfo(_directory); - _tasks.Add(Task.Run(() => CrawlDirectory(directoryInfo, includeSubDir))); + _tasks.Enqueue(Task.Run(() => CrawlDirectory(directoryInfo, includeSubDir))); - while (_tasks.TryTake(out Task? taskToWaitFor)) + // Preform each directory crawl task while there are still crawl + // tasks - waiting for each task to be completed + while (_tasks.TryDequeue(out Task? taskToWaitFor)) { if (taskToWaitFor != null) { DirectoryCount++; - taskToWaitFor.Wait(); + try + { + taskToWaitFor.Wait(); + } + catch (AggregateException ae) + { + foreach (var ex in ae.Flatten().InnerExceptions ) + { + Logger.WriteLine($"A directory could not be crawled. Reason: {ex.Message}"); + } + } } } } @@ -282,32 +308,48 @@ private void CrawlDirectory(bool includeSubDir) /// private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) { - try - { - if (Files == null) - { - Files = new ConcurrentQueue(); - } + // If no files have been stored, create a new queue for storing + // the files + if (Files == null) + { + Files = new ConcurrentQueue(); + } - FileInfo[] files = dir.GetFiles(); + try + { + // Get the files and then add them to the queue for verifying + IEnumerable files = + dir.EnumerateFiles( + "*", + SearchOption.TopDirectoryOnly); foreach (FileInfo file in files) { Files.Enqueue(file.FullName); } + } + catch (Exception ex) + when (ex is ArgumentNullException || ex is ArgumentOutOfRangeException || ex is DirectoryNotFoundException || ex is System.Security.SecurityException) + { + Console.WriteLine($"Could not get files from '{dir.FullName}'. Reason: {ex.Message}"); + } + try + { if (includeSubDir) { - DirectoryInfo[] directoryInfo = dir.GetDirectories(); + // Enumerate all directories within the current directory + // and add them to the task array so they can be crawled + IEnumerable directoryInfo = dir.EnumerateDirectories(); foreach (DirectoryInfo childInfo in directoryInfo) { - _tasks.Add(Task.Run(() => CrawlDirectory(childInfo, includeSubDir))); + _tasks.Enqueue(Task.Run(() => CrawlDirectory(childInfo, includeSubDir))); } } } catch (Exception ex) - when (ex is DirectoryNotFoundException || ex is System.Security.SecurityException || ex is UnauthorizedAccessException) + when (ex is DirectoryNotFoundException || ex is System.Security.SecurityException) { - Console.WriteLine($"ERROR: {ex.Message}"); + Console.WriteLine($"Could not get subdirectories for '{dir.FullName}'. Reason: {ex.Message}"); } } @@ -324,16 +366,49 @@ private void GetChecksumFiles(bool includeSubDir) return; } - // Get all the checksums in the directory and sub-directories, - // if specified - string[] checksumFiles = Directory.GetFiles(_directory, _checksumFileName, GetSearchOption(includeSubDir)); - ChecksumFileInfo = new List(checksumFiles.Length); + IEnumerable? checksumFiles; + + try + { + + // Get all the checksums in the directory and sub-directories, + // if specified + checksumFiles = + Directory.EnumerateFiles( + _directory, + _checksumFileName, + GetSearchOption(includeSubDir)); + ChecksumFileInfo = new ConcurrentDictionary(); + } + catch (Exception ex) + { + Logger.WriteLine($"Could not get checksum files for directory '{_directory}'. Reason: {ex.Message}"); + return; + } // Loop through each of the checksum files and add the information // in the checksum list foreach (string file in checksumFiles) { - ChecksumFileInfo.Add(new ChecksumFile(file)); + try + { + // Get the directory of the file as it will be used as the + // key for the checksum file dictionary + string? fileDir = Path.GetDirectoryName(file); + if (!string.IsNullOrWhiteSpace(fileDir)) + { + ChecksumFileInfo.TryAdd(fileDir, new ChecksumFile(file)); + } + else + { + Logger.WriteLine($"Could not get directory of checksum file '{file}'."); + } + } + catch (Exception ex) + when (ex is ArgumentException || ex is PathTooLongException) + { + Logger.WriteLine($"Could not get directory of checksum file '{file}'. Reason: {ex.Message}"); + } } } @@ -346,7 +421,7 @@ private void GetChecksumFiles(bool includeSubDir) /// /// A value. /// - private SearchOption GetSearchOption(bool includeSubDir) + private static SearchOption GetSearchOption(bool includeSubDir) { return includeSubDir ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; } diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index 0baf02d..71678fd 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -5,6 +5,7 @@ using TE.FileVerification.Configuration; using System.Collections.Generic; using System.Threading.Tasks; +using System; namespace TE.FileVerification { @@ -81,79 +82,90 @@ static int Main(string[] args) /// static int Run(string? file, HashAlgorithm? algorithm, string hashOption, string? settingsFile, string? settingsFolder) { - if (string.IsNullOrWhiteSpace(file)) + try { - Logger.WriteLine("The file or folder was not specified."); - return ERROR; - } - - - - // Read the settings file if one was provided as an argument - Settings? settings = null; - if (!string.IsNullOrWhiteSpace(settingsFile) && !string.IsNullOrWhiteSpace(settingsFolder)) - { - ISettingsFile xmlFile = new XmlFile(settingsFolder, settingsFile); - settings = xmlFile.Read(); - } - - Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folder/File: {file}"); - Logger.WriteLine($"Hash Algorithm: {algorithm}"); - Logger.WriteLine("--------------------------------------------------------------------------------"); - - if (string.IsNullOrWhiteSpace(hashOption)) - { - PathInfo path = new PathInfo(file); - Stopwatch watch = new Stopwatch(); - watch.Start(); - path.Crawl(true); - if (path.Files != null) + if (string.IsNullOrWhiteSpace(file)) { - path.Check(); - watch.Stop(); - - Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folders: {path.DirectoryCount}"); - Logger.WriteLine($"Files: {path.FileCount}"); - Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); - Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine("The file or folder was not specified."); + return ERROR; } - // If settings gdwere specified, then send the notifications - if (settings != null) + if (algorithm == null) { - settings.Send(); + algorithm = HashAlgorithm.SHA256; } - return SUCCESS; - } - else - { - if (!PathInfo.IsFile(file)) - { - Logger.WriteLine($"The file '{file}' is not a valid file."); - return ERROR_NOT_FILE; - } - - string? fileHash = HashInfo.GetFileHash(file, (HashAlgorithm)algorithm); - if (string.IsNullOrWhiteSpace(fileHash)) + // Read the settings file if one was provided as an argument + Settings? settings = null; + if (!string.IsNullOrWhiteSpace(settingsFile) && !string.IsNullOrWhiteSpace(settingsFolder)) { - Logger.WriteLine($"The hash for file '{file}' could not be generated."); - return ERROR_NO_HASH; + ISettingsFile xmlFile = new XmlFile(settingsFolder, settingsFile); + settings = xmlFile.Read(); } - int returnValue = string.Compare(fileHash, hashOption, true) == 0 ? SUCCESS : ERROR_HASH_NOT_MATCH; + Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine($"Folder/File: {file}"); + Logger.WriteLine($"Hash Algorithm: {algorithm}"); + Logger.WriteLine("--------------------------------------------------------------------------------"); - if (returnValue == SUCCESS) + if (string.IsNullOrWhiteSpace(hashOption)) { - Logger.WriteLine($"The file hash matches the hash '{hashOption}'"); + PathInfo path = new PathInfo(file); + Stopwatch watch = new Stopwatch(); + watch.Start(); + path.Crawl(true); + if (path.Files != null) + { + path.Check(); + watch.Stop(); + + Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine($"Folders: {path.DirectoryCount}"); + Logger.WriteLine($"Files: {path.FileCount}"); + Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); + Logger.WriteLine("--------------------------------------------------------------------------------"); + } + + // If settings gdwere specified, then send the notifications + if (settings != null) + { + settings.Send(); + } + + return SUCCESS; } else { - Logger.WriteLine($"The file hash '{fileHash}' does not match the hash '{hashOption}'"); + if (!PathInfo.IsFile(file)) + { + Logger.WriteLine($"The file '{file}' is not a valid file."); + return ERROR_NOT_FILE; + } + + string? fileHash = HashInfo.GetFileHash(file, (HashAlgorithm)algorithm); + if (string.IsNullOrWhiteSpace(fileHash)) + { + Logger.WriteLine($"The hash for file '{file}' could not be generated."); + return ERROR_NO_HASH; + } + + int returnValue = string.Compare(fileHash, hashOption, true) == 0 ? SUCCESS : ERROR_HASH_NOT_MATCH; + + if (returnValue == SUCCESS) + { + Logger.WriteLine($"The file hash matches the hash '{hashOption}'"); + } + else + { + Logger.WriteLine($"The file hash '{fileHash}' does not match the hash '{hashOption}'"); + } + return returnValue; } - return returnValue; + } + catch (Exception ex) + { + Logger.WriteLine($"An error occurred. Error: {ex.Message}"); + return ERROR; } } } From bcb87504b06f70b7b9c601b286d143b3bb0d48d4 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 10 Jun 2022 13:26:31 -0400 Subject: [PATCH 21/33] Removed obsolete classes --- FileVerification/CheckSumFile.cs | 2 +- FileVerification/FileSystemCrawlerSO.cs | 204 ------------------------ FileVerification/VerifyFile.cs | 177 -------------------- 3 files changed, 1 insertion(+), 382 deletions(-) delete mode 100644 FileVerification/FileSystemCrawlerSO.cs delete mode 100644 FileVerification/VerifyFile.cs diff --git a/FileVerification/CheckSumFile.cs b/FileVerification/CheckSumFile.cs index 80f5bac..d2d6f28 100644 --- a/FileVerification/CheckSumFile.cs +++ b/FileVerification/CheckSumFile.cs @@ -134,7 +134,7 @@ public void Read() continue; } - string fileName = values[(int)VerifyFileLayout.NAME]; + string fileName = values[(int)ChecksumFileLayout.NAME]; HashInfo info = new HashInfo( fileName, diff --git a/FileVerification/FileSystemCrawlerSO.cs b/FileVerification/FileSystemCrawlerSO.cs deleted file mode 100644 index c4b54cc..0000000 --- a/FileVerification/FileSystemCrawlerSO.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; - -namespace TE.FileVerification -{ - enum VerifyFileLayout - { - NAME, - HASH_ALGORITHM, - HASH - } - - public class FileSystemCrawlerSO - { - const string DEFAULT_VERIFY_FILE_NAME = "__fv.txt"; - - public int NumFolders { get; set; } - - public int NumFiles { get; set; } - - public HashAlgorithm Algorithm { get; set; } - - public string FolderPath { get; set; } - - private readonly int processorCount; - - private readonly int threadCount; - - private readonly List directories = new List(); - - private readonly ConcurrentBag tasks = new ConcurrentBag(); - - public FileSystemCrawlerSO(HashAlgorithm? algorithm) - { - processorCount = Environment.ProcessorCount; - threadCount = processorCount - 1; - - Logger.WriteLine($"Processors: {processorCount}"); - Logger.WriteLine($"Threads: {threadCount}"); - - if (algorithm != null) - { - Algorithm = (HashAlgorithm)algorithm; - } - else - { - Algorithm = HashAlgorithm.SHA256; - } - } - - public void CollectFolders(string path, bool includeSubDir) - { - FolderPath = path; - DirectoryInfo directoryInfo = new DirectoryInfo(path); - tasks.Add(Task.Run(() => CrawlFolder(directoryInfo, includeSubDir))); - - while (tasks.TryTake(out Task? taskToWaitFor)) - { - if (taskToWaitFor != null) - { - NumFolders++; - taskToWaitFor.Wait(); - } - } - } - - public void CollectFiles(string filePath) - { - foreach (var dir in directories) - { - GetFiles(dir, filePath); - } - } - - /// - /// Returns a value indicating the path is a valid file. - /// - /// - /// The path to the file. - /// - /// - /// true if the path is a valid file, othersize false. - /// - public static bool IsFile(string path) - { - return File.Exists(path); - } - - private void CrawlFolder(DirectoryInfo dir, bool includeSubDir) - { - try - { - if (includeSubDir) - { - DirectoryInfo[] directoryInfos = dir.GetDirectories(); - foreach (DirectoryInfo childInfo in directoryInfos) - { - // here may be dragons using enumeration variable as closure!! - DirectoryInfo di = childInfo; - tasks.Add(Task.Run(() => CrawlFolder(di))); - } - } - directories.Add(dir); - } - catch (Exception ex) - when (ex is DirectoryNotFoundException || ex is System.Security.SecurityException || ex is UnauthorizedAccessException) - { - Console.WriteLine($"ERROR: {ex.Message}"); - } - } - - private void CrawlFolder(DirectoryInfo dir) - { - CrawlFolder(dir, true); - } - - private void GetFiles(DirectoryInfo dir, string filePath) - { - FileInfo[] files; - if (string.IsNullOrWhiteSpace(filePath)) - { - files = dir.GetFiles(); - NumFiles += files.Length; - } - else - { - files = new FileInfo[] { new FileInfo(filePath) }; - } - - //VerifyFile verifyFile = new VerifyFile(DEFAULT_VERIFY_FILE_NAME, dir); - - //// Read the verify file, if it exists, but if the read method - //// returns null, indicating an exception, and the verify file file - //// exists, then assume there is an issue and don't continue with - //// the hashing and verification - //Dictionary? verifyFileData = verifyFile.Read(); - //if (verifyFileData == null && verifyFile.Exists()) - //{ - // return; - //} - - ConcurrentDictionary folderFileData = new ConcurrentDictionary(); - - ParallelOptions options = new ParallelOptions(); - options.MaxDegreeOfParallelism = threadCount; - Parallel.ForEach(files, options, file => - { - // // Ignore the verification file and system files - // if (file.Name.Equals(DEFAULT_VERIFY_FILE_NAME) || file.Attributes == FileAttributes.System) - // { - // return; - // } - - try - { - HashInfo hashInfo = new HashInfo(file.FullName, HashAlgorithm.SHA256); - if (hashInfo.Hash != null) - { - folderFileData.TryAdd(file.Name, new HashInfo(file.FullName, HashAlgorithm.SHA256)); - } - //NumFiles++; - } - catch (AggregateException ae) - { - foreach (Exception ex in ae.InnerExceptions) - { - Console.WriteLine($"ERROR: {ex.Message}"); - } - } - }); - - //int count = 0; - //foreach (var file in folderFileData) - //{ - // if (verifyFileData.TryGetValue(file.Key, out HashInfo? hashInfo)) - // { - // if (!hashInfo.IsHashEqual(file.Value.Hash)) - // { - // Logger.WriteLine($"Hash mismatch: {dir.FullName}{Path.DirectorySeparatorChar}{file.Key}"); - // count++; - // } - // } - // else - // { - // verifyFileData.Add(file.Key, file.Value); - // } - //} - - //if (verifyFileData.Count > 0) - //{ - // verifyFile.Write(verifyFileData, dir); - //} - - //if (count > 0) - //{ - // Logger.WriteLine($"Number failed: {count}"); - //} - } - } -} diff --git a/FileVerification/VerifyFile.cs b/FileVerification/VerifyFile.cs deleted file mode 100644 index c7e8101..0000000 --- a/FileVerification/VerifyFile.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace TE.FileVerification -{ - public class VerifyFile - { - private const char Separator = '|'; - private string fileName; - - private DirectoryInfo directory; - - public string FilePath { get; private set; } - - /// - /// Initializes an instance of the class when - /// provided with the name of the verify file. - /// - /// - /// The name of the verify file. - /// - /// - /// The directory that should contain the verify file. - /// - /// - /// Thrown when an argument is not valid. - /// - /// - /// Thrown when an argument is null. - /// - public VerifyFile(string fileName, DirectoryInfo directory) - { - if (string.IsNullOrWhiteSpace(fileName)) - { - throw new ArgumentNullException(nameof(fileName)); - } - - if (directory == null) - { - throw new ArgumentNullException(nameof(directory)); - } - - this.fileName = fileName; - this.directory = directory; - - FilePath = Path.Combine(this.directory.FullName, fileName); - } - - /// - /// Returns a value indicating whether the verify file exists. - /// - /// - /// True if the verify file exists, false if the file doesn't exist. - /// - public bool Exists() - { - return File.Exists(FilePath); - } - - /// - /// Reads all the lines in the verify file, and then parses each of the - /// lines into a of - /// objects using the file name as the key. - /// - /// - /// A of - /// objects with the file name as the key. a value of null is - /// returned if there is an issue reading the file. - /// - public Dictionary? Read() - { - Dictionary files = - new Dictionary(); - - if (string.IsNullOrWhiteSpace(FilePath) || !File.Exists(FilePath)) - { - return files; - } - - try - { - using (var reader = new StreamReader(FilePath)) - - while (!reader.EndOfStream) - { - string? line = reader.ReadLine(); - if (line == null) - { - continue; - } - - string[] values = line.Split(Separator); - if (values.Length != Enum.GetNames(typeof(VerifyFileLayout)).Length) - { - Logger.WriteLine($"WARNING: Record size incorrect (record will be created using the current file data). File: {FilePath}, Record: {line}."); - continue; - } - - Path.Combine(this.directory.FullName, fileName); - HashInfo info = - new HashInfo( - values[(int)VerifyFileLayout.HASH_ALGORITHM], - values[(int)VerifyFileLayout.HASH]); - - files.Add(values[(int)VerifyFileLayout.NAME], info); - } - - return files; - } - catch (UnauthorizedAccessException) - { - Logger.WriteLine($"ERROR: Not authorized to write to {FilePath}."); - return null; - } - catch (IOException ex) - { - Logger.WriteLine($"ERROR: Can't read the file. Reason: {ex.Message}"); - return null; - } - } - - /// - /// Writes to the verify file using the data stored in the files - /// parameter into the directory specified by the folder parameter. - /// - /// - /// A object containing the data - /// to write to the file. - /// - /// - /// The directory where the file is to be written. - /// - public void Write(Dictionary files, DirectoryInfo folder) - { - if (folder == null) - { - return; - } - - // Initialize the StringBuilder object that will contain the - // contents of the verify file - StringBuilder sb = new StringBuilder(); - - foreach (KeyValuePair file in files) - { - HashInfo hashInfo = file.Value; - sb.AppendLine($"{file.Key}{Separator}{hashInfo.Algorithm.ToString().ToLower()}{Separator}{hashInfo.Hash}"); - } - - try - { - using (StreamWriter sw = new StreamWriter(FilePath)) - { - sw.Write(sb.ToString()); - } - } - catch (DirectoryNotFoundException) - { - Logger.WriteLine($"ERROR: The directory {folder.FullName} was not found."); - } - catch (PathTooLongException) - { - Logger.WriteLine($"ERROR: The path {FilePath} is too long."); - } - catch (UnauthorizedAccessException) - { - Logger.WriteLine($"ERROR: Not authorized to write to {FilePath}."); - } - catch (IOException ex) - { - Logger.WriteLine($"ERROR: Can't write to file. Reason: {ex.Message}"); - } - } - } -} From c1d9f84910d03db16a037d8290eac95d343e7109 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 10 Jun 2022 15:31:18 -0400 Subject: [PATCH 22/33] Removed unneeded variable declarations --- FileVerification/CheckSumFile.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/FileVerification/CheckSumFile.cs b/FileVerification/CheckSumFile.cs index d2d6f28..da521d7 100644 --- a/FileVerification/CheckSumFile.cs +++ b/FileVerification/CheckSumFile.cs @@ -143,8 +143,7 @@ public void Read() // Get the full path to the file to use as the key to make // it unique so it can be used for searching - string filePath = Path.Combine(Directory, fileName); - Checksums.Add(filePath, info); + Checksums.Add(Path.Combine(Directory, fileName), info); } } catch (UnauthorizedAccessException) @@ -181,8 +180,7 @@ public void Add(string file) try { - HashInfo hashInfo = new HashInfo(file, HashAlgorithm.SHA256); - Checksums.Add(file, hashInfo); + Checksums.Add(file, new HashInfo(file, HashAlgorithm.SHA256)); } catch(ArgumentNullException ex) { @@ -202,8 +200,7 @@ public void Add(string file) /// public HashInfo? GetFileData(string fullPath) { - HashInfo? hashInfo; - Checksums.TryGetValue(fullPath, out hashInfo); + Checksums.TryGetValue(fullPath, out HashInfo? hashInfo); return hashInfo; } @@ -278,8 +275,7 @@ public void Write() // checksum file Parallel.ForEach(Checksums, checksumInfo => { - HashInfo hashInfo = checksumInfo.Value; - info.Add(hashInfo.ToString() + Environment.NewLine); + info.Add(checksumInfo.Value.ToString() + Environment.NewLine); }); try From 0f8bc8ed922cc3ab407c07ab765eeb2ed6566ce9 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 10 Jun 2022 15:31:33 -0400 Subject: [PATCH 23/33] Fixed checksum file creation --- FileVerification/PathInfo.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index 62fc899..ff075cb 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -206,11 +206,25 @@ public void Check() fileDir, ChecksumFile.DEFAULT_CHECKSUM_FILENAME)); - // Add the file to the checksum file - checksumFile.Add(file); + // If the new checksum fle could not be added, then another + // thread had it created at the same time, so try and grab + // the other checksum file + if (!ChecksumFileInfo.TryAdd(fileDir, checksumFile)) + { + // Find the checksum file for the directory containing the file + checksumFile = + ChecksumFileInfo.FirstOrDefault( + c => c.Key.Equals(fileDir)).Value; - ChecksumFileInfo.TryAdd(fileDir, checksumFile); + if (checksumFile == null) + { + Logger.WriteLine("The checksum file could not be determined. The file was not added."); + return; + } + } + // Add the file to the checksum file + checksumFile.Add(file); } }); From f2a61f7d4d63883120e27cbcfc043204c5c01322 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 10 Jun 2022 16:54:21 -0400 Subject: [PATCH 24/33] Restructured code --- FileVerification/CheckSumFile.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/FileVerification/CheckSumFile.cs b/FileVerification/CheckSumFile.cs index da521d7..9a6a3ae 100644 --- a/FileVerification/CheckSumFile.cs +++ b/FileVerification/CheckSumFile.cs @@ -225,17 +225,13 @@ public bool IsMatch(string file) return false; } + // Get the stored hash information for the file from the + // checksum data + HashInfo? hashInfo = GetFileData(file); + // Check if the file is in the checksum file - if (Checksums.ContainsKey(file)) + if (hashInfo != null) { - // Get the stored hash information for the file from the - // checksum data - HashInfo? hashInfo = GetFileData(file); - if (hashInfo == null) - { - Logger.WriteLine($"Validating file '{file}' failed because the hash information was not found."); - return false; - } string? hash = HashInfo.GetFileHash(file, hashInfo.Algorithm); if (string.IsNullOrWhiteSpace(hash)) { From dcabfc6af9077a0f174dfaa2cc133974ca831fea Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Sun, 12 Jun 2022 11:16:28 -0400 Subject: [PATCH 25/33] Changed buffer size --- FileVerification/HashInfo.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs index 7955860..da98953 100644 --- a/FileVerification/HashInfo.cs +++ b/FileVerification/HashInfo.cs @@ -9,7 +9,7 @@ namespace TE.FileVerification public enum HashAlgorithm { SHA256, - MD5, + MD5, SHA1, SHA512 } @@ -20,8 +20,11 @@ public class HashInfo /// public const char Separator = '|'; + // A kilobyte + private const int Kilobyte = 1024; + // A megabyte - private const int Megabyte = 1024 * 1024; + private const int Megabyte = Kilobyte * 1024; /// /// Gets the hash algorithm used to create the hash of the file. @@ -192,7 +195,9 @@ private static HashAlgorithm GetAlgorithm(string algorithm) { return null; } - int maxSize = 64 * 1024; // Megabyte; + + //int maxSize = 64 * Kilobyte; + int maxSize = Megabyte; Cryptography.HashAlgorithm? hashAlgorithm = null; From 220ac7c14bccd90ef50bd3a6d0b9c0545f066514 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Sun, 12 Jun 2022 11:17:10 -0400 Subject: [PATCH 26/33] Added ability to specify algorithm --- FileVerification/CheckSumFile.cs | 16 ++++++++++++---- FileVerification/PathInfo.cs | 19 ++++++++++++++----- FileVerification/Program.cs | 2 +- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/FileVerification/CheckSumFile.cs b/FileVerification/CheckSumFile.cs index 9a6a3ae..f63ec76 100644 --- a/FileVerification/CheckSumFile.cs +++ b/FileVerification/CheckSumFile.cs @@ -164,7 +164,10 @@ public void Read() /// /// The full path, including the directory, of the file to add. /// - public void Add(string file) + /// + /// The hash algorithm to use for files added to the checksum file. + /// + public void Add(string file, HashAlgorithm hashAlgorithm) { if (string.IsNullOrWhiteSpace(file)) { @@ -180,7 +183,7 @@ public void Add(string file) try { - Checksums.Add(file, new HashInfo(file, HashAlgorithm.SHA256)); + Checksums.Add(file, new HashInfo(file, hashAlgorithm)); } catch(ArgumentNullException ex) { @@ -211,7 +214,12 @@ public void Add(string file) /// /// The full path, including the directory, of the file. /// - public bool IsMatch(string file) + /// + /// The hash algorithm to use for files added to the checksum file. + /// Existing files will use the hash algorithm stored in the checksum + /// file. + /// + public bool IsMatch(string file, HashAlgorithm hashAlgorithm) { if (string.IsNullOrWhiteSpace(file)) { @@ -246,7 +254,7 @@ public bool IsMatch(string file) // Add the file if it didn't exist in the checksum file and // then return true as it would match the hash that was just // generated - Add(file); + Add(file, hashAlgorithm); return true; } } diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index ff075cb..d20a641 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -155,15 +155,20 @@ public void Crawl(bool includeSubDir) /// checksum files. If the file hashes aren't in the checksum files, /// then add the file and its hash to the checksum files. /// - public void Check() + /// + /// The hash algorithm to use for files added to the checksum file. + /// Existing files will use the hash algorithm stored in the checksum + /// file. + /// + public void Check(HashAlgorithm hashAlgorithm) { if (Files == null || ChecksumFileInfo == null) { return; } - + ParallelOptions options = new ParallelOptions(); - options.MaxDegreeOfParallelism = Environment.ProcessorCount; + options.MaxDegreeOfParallelism = Environment.ProcessorCount; Parallel.ForEach(Files, options, file => { if (Path.GetFileName(file).Equals(_checksumFileName) || IsSystemFile(file)) @@ -191,7 +196,7 @@ public void Check() { // Check if the current file matches the hash information // stored in the checksum file - if (!checksumFile.IsMatch(file)) + if (!checksumFile.IsMatch(file, hashAlgorithm)) { Logger.WriteLine($"FAIL: Hash mismatch: {file}."); } @@ -224,7 +229,7 @@ public void Check() } // Add the file to the checksum file - checksumFile.Add(file); + checksumFile.Add(file, hashAlgorithm); } }); @@ -338,6 +343,10 @@ private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) SearchOption.TopDirectoryOnly); foreach (FileInfo file in files) { + if (file.Name.Equals(_checksumFileName) || IsSystemFile(file.FullName)) + { + continue; + } Files.Enqueue(file.FullName); } } diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index 71678fd..b2877b4 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -116,7 +116,7 @@ static int Run(string? file, HashAlgorithm? algorithm, string hashOption, string path.Crawl(true); if (path.Files != null) { - path.Check(); + path.Check((HashAlgorithm)algorithm); watch.Stop(); Logger.WriteLine("--------------------------------------------------------------------------------"); From ef0d208cb3e3dbc308a983ffb1906f44e4899653 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Mon, 13 Jun 2022 10:01:09 -0400 Subject: [PATCH 27/33] Added threads option --- FileVerification/Program.cs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index b2877b4..a6d53ab 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -52,6 +52,12 @@ static int Main(string[] args) ); rootCommand.AddOption(hashOption); + var threadsOption = new Option( + aliases: new string[] { "--threads", "-t" }, + description: "The number of threads to use to verify the files." + ); + rootCommand.AddOption(threadsOption); + var settingsFileOption = new Option( aliases: new string[] { "--settingsFile", "-sfi" }, description: "The name of the settings XML file." @@ -64,11 +70,11 @@ static int Main(string[] args) ); rootCommand.AddOption(settingsFolderOption); - rootCommand.SetHandler((fileOptionValue, algorithmOptionValue, hashOption, settingsFileOptionValue, settingsFolderOptionValue) => + rootCommand.SetHandler((fileOptionValue, algorithmOptionValue, hashOptionValue, threadsOptionValue, settingsFileOptionValue, settingsFolderOptionValue) => { - Run(fileOptionValue, algorithmOptionValue, hashOption, settingsFileOptionValue, settingsFolderOptionValue); + Run(fileOptionValue, algorithmOptionValue, hashOptionValue, threadsOptionValue, settingsFileOptionValue, settingsFolderOptionValue); }, - fileOption, algorithmOption, hashOption, settingsFileOption, settingsFolderOption); + fileOption, algorithmOption, hashOption, threadsOption, settingsFileOption, settingsFolderOption); return rootCommand.Invoke(args); } @@ -80,7 +86,7 @@ static int Main(string[] args) /// /// /// - static int Run(string? file, HashAlgorithm? algorithm, string hashOption, string? settingsFile, string? settingsFolder) + static int Run(string? file, HashAlgorithm? algorithm, string hashOption, int? threads, string? settingsFile, string? settingsFolder) { try { @@ -95,6 +101,15 @@ static int Run(string? file, HashAlgorithm? algorithm, string hashOption, string algorithm = HashAlgorithm.SHA256; } + if (threads == null || threads == default(int)) + { + threads = Environment.ProcessorCount; + } + else if (threads <= 0) + { + threads = 1; + } + // Read the settings file if one was provided as an argument Settings? settings = null; if (!string.IsNullOrWhiteSpace(settingsFile) && !string.IsNullOrWhiteSpace(settingsFolder)) @@ -106,6 +121,7 @@ static int Run(string? file, HashAlgorithm? algorithm, string hashOption, string Logger.WriteLine("--------------------------------------------------------------------------------"); Logger.WriteLine($"Folder/File: {file}"); Logger.WriteLine($"Hash Algorithm: {algorithm}"); + Logger.WriteLine($"Threads: {threads}"); Logger.WriteLine("--------------------------------------------------------------------------------"); if (string.IsNullOrWhiteSpace(hashOption)) @@ -116,7 +132,7 @@ static int Run(string? file, HashAlgorithm? algorithm, string hashOption, string path.Crawl(true); if (path.Files != null) { - path.Check((HashAlgorithm)algorithm); + path.Check((HashAlgorithm)algorithm, (int)threads); watch.Stop(); Logger.WriteLine("--------------------------------------------------------------------------------"); From c9cc3aa1c077843a87dd06836cf0c8f04d9787c8 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Mon, 13 Jun 2022 10:01:47 -0400 Subject: [PATCH 28/33] Moved checksum retrieval to reduce directory crawling --- FileVerification/PathInfo.cs | 40 +++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index d20a641..d3d6a9f 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -147,7 +147,7 @@ public void Crawl(bool includeSubDir) CrawlDirectory(includeSubDir); } - GetChecksumFiles(includeSubDir); + //GetChecksumFiles(includeSubDir); } /// @@ -160,15 +160,23 @@ public void Crawl(bool includeSubDir) /// Existing files will use the hash algorithm stored in the checksum /// file. /// - public void Check(HashAlgorithm hashAlgorithm) + /// + /// The number of threads to use to verify the files. + /// + public void Check(HashAlgorithm hashAlgorithm, int threads) { if (Files == null || ChecksumFileInfo == null) { return; } - + + if (threads <= 0) + { + threads = 1; + } + ParallelOptions options = new ParallelOptions(); - options.MaxDegreeOfParallelism = Environment.ProcessorCount; + options.MaxDegreeOfParallelism = threads; Parallel.ForEach(Files, options, file => { if (Path.GetFileName(file).Equals(_checksumFileName) || IsSystemFile(file)) @@ -334,6 +342,13 @@ private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) Files = new ConcurrentQueue(); } + // Initialize the checksum dictionary, if needed, so the checksum + // files can be added if they are found in the directory + if (ChecksumFileInfo == null) + { + ChecksumFileInfo = new ConcurrentDictionary(); + } + try { // Get the files and then add them to the queue for verifying @@ -343,11 +358,22 @@ private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) SearchOption.TopDirectoryOnly); foreach (FileInfo file in files) { - if (file.Name.Equals(_checksumFileName) || IsSystemFile(file.FullName)) + // Check if the file is the checksum file, and if it is, + // add it to the dictionary + if (file.Name.Equals(_checksumFileName)) + { + string? fileDir = file.DirectoryName; + if (!string.IsNullOrWhiteSpace(fileDir)) + { + ChecksumFileInfo.TryAdd(fileDir, new ChecksumFile(file.FullName)); + } + } + + // Only add the file to the queue if it isn't a system file + if (!IsSystemFile(file.FullName)) { - continue; + Files.Enqueue(file.FullName); } - Files.Enqueue(file.FullName); } } catch (Exception ex) From 8729127dd5c0313e2968f55b4e289fa43b761d00 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Mon, 13 Jun 2022 10:03:14 -0400 Subject: [PATCH 29/33] Adjusted tabbing --- FileVerification/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index a6d53ab..6f3aa9d 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -136,9 +136,9 @@ static int Run(string? file, HashAlgorithm? algorithm, string hashOption, int? t watch.Stop(); Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folders: {path.DirectoryCount}"); - Logger.WriteLine($"Files: {path.FileCount}"); - Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); + Logger.WriteLine($"Folders: {path.DirectoryCount}"); + Logger.WriteLine($"Files: {path.FileCount}"); + Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); Logger.WriteLine("--------------------------------------------------------------------------------"); } From 8272e72c92b5f1d2f2aa84a1f5c2b57391a811d7 Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 17 Jun 2022 08:06:12 -0400 Subject: [PATCH 30/33] Added content to readme.md --- README.md | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b0d03ee..6e999c4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,113 @@ # FileVerification -Generates a hash of all files in a folder tree and stores the hashes in a text file in each folder. + +The basic function of File Verification is to generate the checksum values of all files in a directory and save those checksums to a text file that is located in the same directory. Once the checksums are saved to the file, running File Verification against the same directory will validate that the files in the directory still match the checksums saved in the checksum file. + +In addition, File Verification can also validate the checksum of a single file by accepting both the file and checksum as arguments. + +For a quick checksum validation, File Verification can simply display the checksum of a file to the console. + +# Arguments + +The following arguments can be passed into File Verification: + +| Argument | Description | +| --------- | ----------- | +| -f, --file <_file_> | (Required) The file or folder to generate the checksum. | +| -a, --algorithm <_MD5,SHA1,SHA256,SHA512_> | The hash algorithm used to generate the checksum. Default: SHA256. | +| -ha, --hash <_hash_> | The hash used to validate against the file specified with the -f argument. | +| -t, --threads <_threads_> | The number of threads to use to verify the file(s). Default: number of processors. | +| -ho, --hashonly | Generate and display the file hash - doesn't save the hash to the checksum file. | +| - sfi, --settingsFile <_settingsFile_> | The name of the settings XML file. | +| - sfo, --settingsFolder <_settingsFolder_> | The folder containing the settings file. | +| --version | Version information. | +| -?, -h, --help | Show the help and usage information. | + +When either the `hash` or `hashonly` arguments are specified, the checksum of the file is not saved to the checksum file. The `file` attribute must contain the location of a file and not a folder. + +# Settings File + +The settings file is used to specify additional settings that aren't passed in from the command line. Currently, the XML structure of the settings file is as follows: + +```xml + + + + + + + + + +
+ + +
+
+ +
+
+
+
+``` + +## Notification Elements + +To send a notification request to an endpoint, the following information can be specified: + +| Element | Description | +| --------- | ----------- | +| url | The URL to connect to for the notification. | +| method | The HTTP method to use for the request. Default: POST | +| data | Data to send for the request. | + +### URL + +This is the valid URL to the endpoint and is specified using the `` element. + +### Method + +The `` element specifies the HTTP method used in the request to the endpoint. The valid values are: +| Method | +| --------- | +| POST | +| GET | +| PUT | +| DELETE | +>**Note:** The method names are case-sensitive, so they must be added to the configuration file exactly as shown in the table above. + +The default value for the `` element is `POST`. + +### Data + +The `` element contains information that is sent to the endpoint. This element contains the ``, ``, and `` child elements to provide details about the data sent with the request. + +#### Headers + +The `` element allows you to specify various headers to include in the request. Each header is specified within a `
` child element, and contains a `` and `` pair of elements. For example: + +```xml + +
+ HeaderName + HeaderValue +
+
+``` + +### Body + +The `` element provides information to send in the request. You can specify any message in the `` element, or you can use the `[message]` placeholder to have File Watcher write the change message into the body. + +## Examples + +Generate the checksums for all files located in `C:\Temp`: + +`fv.exe -f C:\Temp` + +Validate that the checksum of a file called `notes.txt` in `C:\Temp` matches `E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855`: + +`fv.exe -f C:\Temp\notes.txt -ha E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855` + +Use 32 threads when generating or validating the hashes of files in a directory: + +`fv.exe -f C:\Temp -t 32` \ No newline at end of file From 528694d92a9a5e119f7ce6668f8c44f73904c24c Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 17 Jun 2022 08:06:41 -0400 Subject: [PATCH 31/33] Changed the checksum file discovery --- FileVerification/PathInfo.cs | 168 ++++++++++++++++------------------- 1 file changed, 77 insertions(+), 91 deletions(-) diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs index d3d6a9f..5490aeb 100644 --- a/FileVerification/PathInfo.cs +++ b/FileVerification/PathInfo.cs @@ -139,15 +139,12 @@ public void Crawl(bool includeSubDir) // path value as there is no directory to be crawled if (IsFile(FullPath)) { - Files = new ConcurrentQueue(); - Files.Enqueue(FullPath); + CrawlDirectory(); } else { CrawlDirectory(includeSubDir); } - - //GetChecksumFiles(includeSubDir); } /// @@ -249,36 +246,65 @@ public void Check(HashAlgorithm hashAlgorithm, int threads) } /// - /// Returns a value indicating the path is a valid directory. + /// Crawls the directory for a single file by getting the checksum file + /// for the directory. /// - /// - /// true if the path is a valid directory, otherwise false. - /// - public static bool IsDirectory(string path) + /// + /// This method is used for getting the hash and verifying a single + /// file and is used to get the checksum file from the directory. The + /// method then adds the full path to the file to the Files queue. + /// + private void CrawlDirectory() { - return Directory.Exists(path); - } + // If no files have been stored, create a new queue for storing + // the files + if (Files == null) + { + Files = new ConcurrentQueue(); + } - /// - /// Returns a value indicating the path is a valid file. - /// - /// - /// true if the path is a valid file, othersize false. - /// - public static bool IsFile(string path) - { - return File.Exists(path); - } + // Initialize the checksum dictionary, if needed, so the checksum + // files can be added if they are found in the directory + if (ChecksumFileInfo == null) + { + ChecksumFileInfo = new ConcurrentDictionary(); + } - /// - /// Returns a value indicating the path exists. - /// - /// - /// true if the path exists, otherwise false. - /// - public bool Exists() - { - return IsDirectory(FullPath) || IsFile(FullPath); + if (string.IsNullOrWhiteSpace(_directory) || string.IsNullOrWhiteSpace(_checksumFileName)) + { + return; + } + + DirectoryInfo? dir = null; + try + { + dir = new DirectoryInfo(_directory); + + // Get the files and then add them to the queue for verifying + IEnumerable files = + dir.EnumerateFiles( + _checksumFileName, + SearchOption.TopDirectoryOnly); + + foreach (FileInfo checksumFile in files) + { + ChecksumFileInfo.TryAdd(_directory, new ChecksumFile(checksumFile.FullName)); + } + + Files.Enqueue(FullPath); + } + catch (Exception ex) + when (ex is ArgumentNullException || ex is ArgumentOutOfRangeException || ex is DirectoryNotFoundException || ex is System.Security.SecurityException) + { + if (dir != null) + { + Console.WriteLine($"Could not get files from '{dir.FullName}'. Reason: {ex.Message}"); + } + else + { + Console.WriteLine($"Could not get files from directory. Reason: {ex.Message}"); + } + } } /// @@ -315,7 +341,7 @@ private void CrawlDirectory(bool includeSubDir) } catch (AggregateException ae) { - foreach (var ex in ae.Flatten().InnerExceptions ) + foreach (var ex in ae.Flatten().InnerExceptions) { Logger.WriteLine($"A directory could not be crawled. Reason: {ex.Message}"); } @@ -403,76 +429,36 @@ private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) } /// - /// Gets the checksum files from each directory. + /// Returns a value indicating the path exists. /// - /// - /// Include subdirectories when searching for checksum files. - /// - private void GetChecksumFiles(bool includeSubDir) + /// + /// true if the path exists, otherwise false. + /// + public bool Exists() { - if (string.IsNullOrWhiteSpace(_checksumFileName) || string.IsNullOrWhiteSpace(_directory)) - { - return; - } - - IEnumerable? checksumFiles; - - try - { - - // Get all the checksums in the directory and sub-directories, - // if specified - checksumFiles = - Directory.EnumerateFiles( - _directory, - _checksumFileName, - GetSearchOption(includeSubDir)); - ChecksumFileInfo = new ConcurrentDictionary(); - } - catch (Exception ex) - { - Logger.WriteLine($"Could not get checksum files for directory '{_directory}'. Reason: {ex.Message}"); - return; - } + return IsDirectory(FullPath) || IsFile(FullPath); + } - // Loop through each of the checksum files and add the information - // in the checksum list - foreach (string file in checksumFiles) - { - try - { - // Get the directory of the file as it will be used as the - // key for the checksum file dictionary - string? fileDir = Path.GetDirectoryName(file); - if (!string.IsNullOrWhiteSpace(fileDir)) - { - ChecksumFileInfo.TryAdd(fileDir, new ChecksumFile(file)); - } - else - { - Logger.WriteLine($"Could not get directory of checksum file '{file}'."); - } - } - catch (Exception ex) - when (ex is ArgumentException || ex is PathTooLongException) - { - Logger.WriteLine($"Could not get directory of checksum file '{file}'. Reason: {ex.Message}"); - } - } + /// + /// Returns a value indicating the path is a valid directory. + /// + /// + /// true if the path is a valid directory, otherwise false. + /// + public static bool IsDirectory(string path) + { + return Directory.Exists(path); } /// - /// Gets the search option used to search the directories. + /// Returns a value indicating the path is a valid file. /// - /// - /// Indicates if the subdirectories are to be included in the search. - /// /// - /// A value. + /// true if the path is a valid file, othersize false. /// - private static SearchOption GetSearchOption(bool includeSubDir) + public static bool IsFile(string path) { - return includeSubDir ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + return File.Exists(path); } /// From 3a2b7d7353dbfa02f63c06282cacf02cda22cefe Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 17 Jun 2022 08:07:09 -0400 Subject: [PATCH 32/33] Added the hash only option --- FileVerification/Program.cs | 117 +++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 28 deletions(-) diff --git a/FileVerification/Program.cs b/FileVerification/Program.cs index 6f3aa9d..35095d1 100644 --- a/FileVerification/Program.cs +++ b/FileVerification/Program.cs @@ -58,6 +58,12 @@ static int Main(string[] args) ); rootCommand.AddOption(threadsOption); + var getHashOnlyOption = new Option( + aliases: new string[] { "--hashonly", "-ho" }, + description: "Generate and display the file hash." + ); + rootCommand.AddOption(getHashOnlyOption); + var settingsFileOption = new Option( aliases: new string[] { "--settingsFile", "-sfi" }, description: "The name of the settings XML file." @@ -70,11 +76,34 @@ static int Main(string[] args) ); rootCommand.AddOption(settingsFolderOption); - rootCommand.SetHandler((fileOptionValue, algorithmOptionValue, hashOptionValue, threadsOptionValue, settingsFileOptionValue, settingsFolderOptionValue) => - { - Run(fileOptionValue, algorithmOptionValue, hashOptionValue, threadsOptionValue, settingsFileOptionValue, settingsFolderOptionValue); - }, - fileOption, algorithmOption, hashOption, threadsOption, settingsFileOption, settingsFolderOption); + rootCommand.SetHandler( + ( + fileOptionValue, + algorithmOptionValue, + hashOptionValue, + getHashOnlyOptionValue, + threadsOptionValue, + settingsFileOptionValue, + settingsFolderOptionValue + ) => + { + Run( + fileOptionValue, + algorithmOptionValue, + hashOptionValue, + getHashOnlyOptionValue, + threadsOptionValue, + settingsFileOptionValue, + settingsFolderOptionValue); + }, + fileOption, + algorithmOption, + hashOption, + getHashOnlyOption, + threadsOption, + settingsFileOption, + settingsFolderOption + ); return rootCommand.Invoke(args); } @@ -86,7 +115,14 @@ static int Main(string[] args) /// /// /// - static int Run(string? file, HashAlgorithm? algorithm, string hashOption, int? threads, string? settingsFile, string? settingsFolder) + static int Run( + string? file, + HashAlgorithm? algorithm, + string hashOption, + bool hashOnlyOption, + int? threads, + string? settingsFile, + string? settingsFolder) { try { @@ -110,22 +146,31 @@ static int Run(string? file, HashAlgorithm? algorithm, string hashOption, int? t threads = 1; } - // Read the settings file if one was provided as an argument - Settings? settings = null; - if (!string.IsNullOrWhiteSpace(settingsFile) && !string.IsNullOrWhiteSpace(settingsFolder)) + // Trim the double-quote from the path, since it can cause an + // issue if the path ends with a slash ('\'), because the code + // will interpret the slash and double-quote as an escape + // character for the double quote ('\"' to '"') + file = file.Trim('"'); + + // If the hash option has not been specified, or the hash only + // option is false then continue with cralwing the directory to + // generate and verify the hashes of the files + if (string.IsNullOrWhiteSpace(hashOption) && !hashOnlyOption) { - ISettingsFile xmlFile = new XmlFile(settingsFolder, settingsFile); - settings = xmlFile.Read(); - } + // Read the settings file if one was provided as an argument + Settings? settings = null; + if (!string.IsNullOrWhiteSpace(settingsFile) && !string.IsNullOrWhiteSpace(settingsFolder)) + { + ISettingsFile xmlFile = new XmlFile(settingsFolder, settingsFile); + settings = xmlFile.Read(); + } - Logger.WriteLine("--------------------------------------------------------------------------------"); - Logger.WriteLine($"Folder/File: {file}"); - Logger.WriteLine($"Hash Algorithm: {algorithm}"); - Logger.WriteLine($"Threads: {threads}"); - Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine($"Folder/File: {file}"); + Logger.WriteLine($"Hash Algorithm: {algorithm}"); + Logger.WriteLine($"Threads: {threads}"); + Logger.WriteLine("--------------------------------------------------------------------------------"); - if (string.IsNullOrWhiteSpace(hashOption)) - { PathInfo path = new PathInfo(file); Stopwatch watch = new Stopwatch(); watch.Start(); @@ -142,7 +187,7 @@ static int Run(string? file, HashAlgorithm? algorithm, string hashOption, int? t Logger.WriteLine("--------------------------------------------------------------------------------"); } - // If settings gdwere specified, then send the notifications + // If settings were specified, then send the notifications if (settings != null) { settings.Send(); @@ -165,18 +210,34 @@ static int Run(string? file, HashAlgorithm? algorithm, string hashOption, int? t return ERROR_NO_HASH; } - int returnValue = string.Compare(fileHash, hashOption, true) == 0 ? SUCCESS : ERROR_HASH_NOT_MATCH; - - if (returnValue == SUCCESS) + // If the hash only option was specified, then just display + // the hash of the file + if (hashOnlyOption) { - Logger.WriteLine($"The file hash matches the hash '{hashOption}'"); + Logger.WriteLine(fileHash); + return SUCCESS; } - else + + // The the hash option was specified, compare the file hash + // with the hash passed through the argument + if (!string.IsNullOrWhiteSpace(hashOption)) { - Logger.WriteLine($"The file hash '{fileHash}' does not match the hash '{hashOption}'"); - } - return returnValue; + int returnValue = string.Compare(fileHash, hashOption, true) == 0 ? SUCCESS : ERROR_HASH_NOT_MATCH; + + if (returnValue == SUCCESS) + { + Logger.WriteLine($"The file hash matches the hash '{hashOption}'"); + } + else + { + Logger.WriteLine($"The file hash '{fileHash}' does not match the hash '{hashOption}'"); + } + + return returnValue; + } } + + return SUCCESS; } catch (Exception ex) { From d9e2e759ea24ac69d42943d6b01c82a0c384dd9b Mon Sep 17 00:00:00 2001 From: TechieGuy12 Date: Fri, 17 Jun 2022 08:07:21 -0400 Subject: [PATCH 33/33] Changed the debug arguments for testing --- FileVerification/Properties/launchSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FileVerification/Properties/launchSettings.json b/FileVerification/Properties/launchSettings.json index 8034b2c..bb9f278 100644 --- a/FileVerification/Properties/launchSettings.json +++ b/FileVerification/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "FileVerification": { "commandName": "Project", - "commandLineArgs": "fv -f C:\\TEMP3" + "commandLineArgs": "-f \"C:\\TEMP3\" -t 128" } } } \ No newline at end of file