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/CheckSumFile.cs b/FileVerification/CheckSumFile.cs new file mode 100644 index 0000000..f63ec76 --- /dev/null +++ b/FileVerification/CheckSumFile.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TE.FileVerification +{ + /// + /// 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 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 + { + 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; + + 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(); + + if (File.Exists(FullPath)) + { + 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(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}."); + continue; + } + + string fileName = values[(int)ChecksumFileLayout.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 + Checksums.Add(Path.Combine(Directory, fileName), 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; + } + } + + /// + /// Adds a checksum for a file. + /// + /// + /// The full path, including the directory, of the file to add. + /// + /// + /// The hash algorithm to use for files added to the checksum file. + /// + public void Add(string file, HashAlgorithm hashAlgorithm) + { + 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 + { + Checksums.Add(file, new HashInfo(file, hashAlgorithm)); + } + 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) + { + Checksums.TryGetValue(fullPath, out HashInfo? 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. + /// + /// + /// 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)) + { + 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; + } + + // 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 (hashInfo != null) + { + 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, hashAlgorithm); + 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 + 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 + Parallel.ForEach(Checksums, checksumInfo => + { + info.Add(checksumInfo.Value.ToString() + Environment.NewLine); + }); + + try + { + // Write the file hash information to the checksum file + using StreamWriter sw = new StreamWriter(FullPath); + sw.Write(string.Join("", info)); + } + catch (DirectoryNotFoundException) + { + Logger.WriteLine($"Could not write the checksum file because the directory {Directory} was not found."); + } + catch (PathTooLongException) + { + Logger.WriteLine($"Could not write the checksum file because the path {FullPath} is too long."); + } + catch (UnauthorizedAccessException) + { + Logger.WriteLine($"Could not write the checksum file because the user is not authorized to write to {FullPath}."); + } + catch (Exception ex) + when (ex is ArgumentException || ex is ArgumentNullException || ex is IOException || ex is System.Security.SecurityException) + { + Logger.WriteLine($"Could not write the checksum file. Reason: {ex.Message}"); + } + } + } +} 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/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..cde744f --- /dev/null +++ b/FileVerification/Configuration/Settings.cs @@ -0,0 +1,28 @@ +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; } + + /// + /// 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/FileVerification.csproj b/FileVerification/FileVerification.csproj new file mode 100644 index 0000000..be4cf7c --- /dev/null +++ b/FileVerification/FileVerification.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + fv + TE.FileVerification + enable + + + + + + + diff --git a/FileVerification/HashInfo.cs b/FileVerification/HashInfo.cs new file mode 100644 index 0000000..da98953 --- /dev/null +++ b/FileVerification/HashInfo.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Cryptography = System.Security.Cryptography; +using System.IO; + +namespace TE.FileVerification +{ + public enum HashAlgorithm + { + SHA256, + MD5, + SHA1, + SHA512 + } + public class HashInfo + { + /// + /// The separator used in the checksum file. + /// + public const char Separator = '|'; + + // A kilobyte + private const int Kilobyte = 1024; + + // A megabyte + private const int Megabyte = Kilobyte * 1024; + + /// + /// 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; } + + /// + /// 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)) + { + throw new ArgumentNullException(nameof(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) + { + if (hash == null || string.IsNullOrWhiteSpace(hash)) + { + throw new ArgumentNullException(nameof(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) + { + if (algorithm == null || string.IsNullOrWhiteSpace(algorithm)) + { + throw new ArgumentNullException(nameof(algorithm)); + } + + Algorithm = GetAlgorithm(algorithm); + 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(FilePath, Algorithm); + } + + /// + /// Gets the hash algorithm value of the algorithm string name. + /// + /// + /// The name of the algorithm. + /// + /// + /// The enum value of the algorithm. + /// + private static HashAlgorithm GetAlgorithm(string algorithm) + { + if (string.Compare(algorithm, "md5", true) == 0) + { + return HashAlgorithm.MD5; + } + else if (string.Compare(algorithm, "sha1", true) == 0) + { + return HashAlgorithm.SHA1; + } + else if (string.Compare(algorithm, "sha512", true) == 0) + { + return HashAlgorithm.SHA512; + } + else + { + return HashAlgorithm.SHA256; + } + } + + /// + /// 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) + { + if (string.IsNullOrWhiteSpace(file)) + { + return null; + } + + //int maxSize = 64 * Kilobyte; + int maxSize = Megabyte; + + Cryptography.HashAlgorithm? hashAlgorithm = null; + + try + { + 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.None); + new FileStream( + file, + FileMode.Open, + FileAccess.Read, + FileShare.None, + maxSize); + + var hash = hashAlgorithm.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", ""); + } + catch + { + + return null; + } + } + finally + { + if (hashAlgorithm != null) + { + hashAlgorithm.Clear(); + hashAlgorithm.Dispose(); + } + } + } + + /// + /// 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) + { + if (string.IsNullOrWhiteSpace(Hash)) + { + return string.IsNullOrWhiteSpace(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/Logger.cs b/FileVerification/Logger.cs new file mode 100644 index 0000000..dd98087 --- /dev/null +++ b/FileVerification/Logger.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace TE.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; + + // 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); + } + + private static void Initialize(string logFolder, string logName) + { + fullPath = Path.Combine(logFolder, logName); + Clear(); + } + + private static void Clear() + { + try + { + File.Delete(fullPath); + if (_lines != null) + { + _lines.Clear(); + } + else + { + _lines = new StringBuilder(); + } + } + catch (Exception) + { + return; + } + } + + public static void WriteLine(string message) + { + + Console.WriteLine(message); + + try + { + using (StreamWriter writer = new StreamWriter(fullPath, true)) + { + writer.WriteLine(message); + } + + _lines.AppendLine(message); + } + catch (Exception ex) + { + Console.WriteLine($"WARNING: Couldn't write to log file. Reason: {ex.Message}"); + } + } + } +} diff --git a/FileVerification/PathInfo.cs b/FileVerification/PathInfo.cs new file mode 100644 index 0000000..5490aeb --- /dev/null +++ b/FileVerification/PathInfo.cs @@ -0,0 +1,479 @@ +using System; +using System.Collections.Concurrent; +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 + { + // The directory associated with the path + private readonly string? _directory; + + // The name of the checksum files + private readonly string? _checksumFileName; + + // A queue of tasks used to crawl the directory tree + private readonly ConcurrentQueue _tasks = + new ConcurrentQueue(); + + /// + /// Gets the path value. + /// + public string FullPath { get; private set; } + + /// + /// Gets all the files in the path. + /// + public ConcurrentQueue? Files { 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 ConcurrentDictionary? ChecksumFileInfo { get; private set; } + + /// + /// Initializes an instance of the class when + /// provided with the full path. + /// + /// + /// The full path to a directory or a file. + /// + /// + /// Thrown if the parameter is null or + /// empty. + /// + /// + /// Thrown if the directory of the path could not be determined. + /// + /// + /// 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; + } + } + + /// + /// Crawl the directory associated with the path. + /// + /// + /// Indicates if subdirectories are to be crawled. + /// + public void Crawl(bool includeSubDir) + { + + // 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(FullPath)) + { + CrawlDirectory(); + } + else + { + CrawlDirectory(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. + /// + /// + /// The hash algorithm to use for files added to the checksum file. + /// Existing files will use the hash algorithm stored in the checksum + /// file. + /// + /// + /// 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 = threads; + Parallel.ForEach(Files, options, file => + { + if (Path.GetFileName(file).Equals(_checksumFileName) || IsSystemFile(file)) + { + return; + } + + // 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.Key.Equals(fileDir)).Value; + + // A checksum file was found containing the file, so get the + // hash information for the file + if (checksumFile != null) + { + // Check if the current file matches the hash information + // stored in the checksum file + if (!checksumFile.IsMatch(file, hashAlgorithm)) + { + Logger.WriteLine($"FAIL: Hash mismatch: {file}."); + } + } + else + { + // 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)); + + // 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; + + 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, hashAlgorithm); + } + }); + + // Write out each checksum file with the updated information + foreach (var keyPair in ChecksumFileInfo) + { + keyPair.Value.Write(); + } + } + + /// + /// Crawls the directory for a single file by getting the checksum file + /// for the directory. + /// + /// + /// 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() + { + // If no files have been stored, create a new queue for storing + // the files + if (Files == null) + { + 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(); + } + + 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}"); + } + } + } + + /// + /// 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; ; + } + + // Get the directory information, and then enqueue the task + // to crawl the directory + DirectoryInfo directoryInfo = new DirectoryInfo(_directory); + _tasks.Enqueue(Task.Run(() => CrawlDirectory(directoryInfo, includeSubDir))); + + // 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++; + try + { + taskToWaitFor.Wait(); + } + catch (AggregateException ae) + { + foreach (var ex in ae.Flatten().InnerExceptions) + { + Logger.WriteLine($"A directory could not be crawled. Reason: {ex.Message}"); + } + } + } + } + } + + /// + /// Crawls the path and returns the files. + /// + /// + /// The object of the current directory. + /// + /// + /// Value indicating if the subdirectories are to be crawled. + /// + private void CrawlDirectory(DirectoryInfo dir, bool includeSubDir) + { + // If no files have been stored, create a new queue for storing + // the files + if (Files == null) + { + 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 + IEnumerable files = + dir.EnumerateFiles( + "*", + SearchOption.TopDirectoryOnly); + foreach (FileInfo file in files) + { + // 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)) + { + 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) + { + // 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.Enqueue(Task.Run(() => CrawlDirectory(childInfo, includeSubDir))); + } + } + } + catch (Exception ex) + when (ex is DirectoryNotFoundException || ex is System.Security.SecurityException) + { + Console.WriteLine($"Could not get subdirectories for '{dir.FullName}'. Reason: {ex.Message}"); + } + } + + /// + /// Returns a value indicating the path exists. + /// + /// + /// true if the path exists, otherwise false. + /// + public bool Exists() + { + return IsDirectory(FullPath) || IsFile(FullPath); + } + + /// + /// 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); + } + + /// + /// 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); + } + + /// + /// 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 static 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 new file mode 100644 index 0000000..35095d1 --- /dev/null +++ b/FileVerification/Program.cs @@ -0,0 +1,249 @@ +using System.Diagnostics; +using System.IO; +using System.CommandLine; +using System.CommandLine.Invocation; +using TE.FileVerification.Configuration; +using System.Collections.Generic; +using System.Threading.Tasks; +using System; + +namespace TE.FileVerification +{ + class Program + { + // Success return code + private const int SUCCESS = 0; + + // Error return code + 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; } + + 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 fileOption = new Option( + aliases: new string[] { "--file", "-f" }, + description: "The file or folder to verify with a hash." + ); + fileOption.IsRequired = true; + rootCommand.AddOption(fileOption); + + var algorithmOption = new Option( + aliases: new string[] { "--algorithm", "-a" }, + description: "The hash algorithm to use." + ); + rootCommand.AddOption(algorithmOption); + + var hashOption = new Option( + aliases: new string[] { "--hash", "-ha" }, + description: "The hash of the file to verify." + ); + 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 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." + ); + rootCommand.AddOption(settingsFileOption); + + var settingsFolderOption = new Option( + aliases: new string[] { "--settingsFolder", "-sfo" }, + description: "The folder containing the settings XML file." + ); + rootCommand.AddOption(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); + } + + /// + /// Runs the necessary hashing for the file or folder. + /// + /// + /// + /// + /// + /// + static int Run( + string? file, + HashAlgorithm? algorithm, + string hashOption, + bool hashOnlyOption, + int? threads, + string? settingsFile, + string? settingsFolder) + { + try + { + if (string.IsNullOrWhiteSpace(file)) + { + Logger.WriteLine("The file or folder was not specified."); + return ERROR; + } + + if (algorithm == null) + { + algorithm = HashAlgorithm.SHA256; + } + + if (threads == null || threads == default(int)) + { + threads = Environment.ProcessorCount; + } + else if (threads <= 0) + { + threads = 1; + } + + // 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) + { + // 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("--------------------------------------------------------------------------------"); + + PathInfo path = new PathInfo(file); + Stopwatch watch = new Stopwatch(); + watch.Start(); + path.Crawl(true); + if (path.Files != null) + { + path.Check((HashAlgorithm)algorithm, (int)threads); + watch.Stop(); + + Logger.WriteLine("--------------------------------------------------------------------------------"); + Logger.WriteLine($"Folders: {path.DirectoryCount}"); + Logger.WriteLine($"Files: {path.FileCount}"); + Logger.WriteLine($"Time (ms): {watch.ElapsedMilliseconds}"); + Logger.WriteLine("--------------------------------------------------------------------------------"); + } + + // If settings were specified, then send the notifications + if (settings != null) + { + settings.Send(); + } + + 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)) + { + Logger.WriteLine($"The hash for file '{file}' could not be generated."); + return ERROR_NO_HASH; + } + + // If the hash only option was specified, then just display + // the hash of the file + if (hashOnlyOption) + { + Logger.WriteLine(fileHash); + return SUCCESS; + } + + // The the hash option was specified, compare the file hash + // with the hash passed through the argument + if (!string.IsNullOrWhiteSpace(hashOption)) + { + 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) + { + Logger.WriteLine($"An error occurred. Error: {ex.Message}"); + return ERROR; + } + } + } +} diff --git a/FileVerification/Properties/launchSettings.json b/FileVerification/Properties/launchSettings.json new file mode 100644 index 0000000..bb9f278 --- /dev/null +++ b/FileVerification/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "FileVerification": { + "commandName": "Project", + "commandLineArgs": "-f \"C:\\TEMP3\" -t 128" + } + } +} \ No newline at end of file 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