diff --git a/FSI.Lib/FSI.Lib.csproj b/FSI.Lib/FSI.Lib.csproj
index 5551135..99f14e6 100644
--- a/FSI.Lib/FSI.Lib.csproj
+++ b/FSI.Lib/FSI.Lib.csproj
@@ -9,6 +9,10 @@
3.0
+
+
+
+
@@ -28,6 +32,7 @@
+
diff --git a/FSI.Lib/Guis/SieTiaWinCCMsgMgt/WinCC.cs b/FSI.Lib/Guis/SieTiaWinCCMsgMgt/WinCC.cs
index 4492bed..9ae9cac 100644
--- a/FSI.Lib/Guis/SieTiaWinCCMsgMgt/WinCC.cs
+++ b/FSI.Lib/Guis/SieTiaWinCCMsgMgt/WinCC.cs
@@ -59,6 +59,25 @@ namespace FSI.Lib.Guis.SieTiaWinCCMsgMgt
backgroundWorker.DoWork += BackgroundWorker_DoWork;
backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
+
+ }
+
+ public WinCC(bool autoStart, int updateIntervall, string windowsName, string windowsClassName, string buttonName)
+ {
+ AutoStart = autoStart;
+ UpdateIntervall = updateIntervall;
+ WindowsName = windowsName;
+ WindowsClassName = windowsClassName;
+ ButtonName = buttonName;
+
+ backgroundWorker = new BackgroundWorker
+ {
+ WorkerReportsProgress = true,
+ WorkerSupportsCancellation = true
+ };
+ backgroundWorker.DoWork += BackgroundWorker_DoWork;
+ backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
+
if (AutoStart)
{
Start();
diff --git a/FSI.Lib/Tools/RoboSharp/ApplicationConstants.cs b/FSI.Lib/Tools/RoboSharp/ApplicationConstants.cs
new file mode 100644
index 0000000..54f3303
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/ApplicationConstants.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+ internal class ApplicationConstants
+ {
+ internal static Dictionary ErrorCodes = new Dictionary()
+ {
+ { "ERROR 33 (0x00000021)", "The process cannot access the file because another process has locked a portion of the file." },
+ { "ERROR 32 (0x00000020)", "The process cannot access the file because it is being used by another process." },
+ { "ERROR 5 (0x00000005)", "Access is denied." }
+ };
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/CopyOptions.cs b/FSI.Lib/Tools/RoboSharp/CopyOptions.cs
new file mode 100644
index 0000000..5ba33e3
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/CopyOptions.cs
@@ -0,0 +1,607 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Runtime.CompilerServices;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ /// Source, Destination, and options for how to move or copy files.
+ ///
+ ///
+ ///
+ ///
+ public class CopyOptions : ICloneable
+ {
+ #region Constructors
+
+ ///
+ /// Create new CopyOptions with Default Settings
+ ///
+ public CopyOptions() { }
+
+ ///
+ /// Clone a CopyOptions Object
+ ///
+ /// CopyOptions object to clone
+ /// Specify a new source if desired. If left as null, will use Source from
+ /// Specify a new source if desired. If left as null, will use Destination from
+ public CopyOptions(CopyOptions copyOptions, string NewSource = null, string NewDestination = null)
+ {
+ Source = NewSource ?? copyOptions.Source;
+ Destination = NewDestination ?? copyOptions.Destination;
+
+ AddAttributes = copyOptions.AddAttributes;
+ CheckPerFile = copyOptions.CheckPerFile;
+ CopyAll = copyOptions.CopyAll;
+ CopyFilesWithSecurity = copyOptions.CopyFilesWithSecurity;
+ CopyFlags = copyOptions.CopyFlags;
+ CopySubdirectories = copyOptions.CopySubdirectories;
+ CopySubdirectoriesIncludingEmpty = copyOptions.CopySubdirectoriesIncludingEmpty;
+ CopySymbolicLink = copyOptions.CopySymbolicLink;
+ CreateDirectoryAndFileTree = copyOptions.CreateDirectoryAndFileTree;
+ Depth = copyOptions.Depth;
+ DirectoryCopyFlags = copyOptions.DirectoryCopyFlags;
+ DoNotCopyDirectoryInfo = copyOptions.DoNotCopyDirectoryInfo;
+ DoNotUseWindowsCopyOffload = copyOptions.DoNotUseWindowsCopyOffload;
+ EnableBackupMode = copyOptions.EnableBackupMode;
+ EnableEfsRawMode = copyOptions.EnableEfsRawMode;
+ EnableRestartMode = copyOptions.EnableRestartMode;
+ EnableRestartModeWithBackupFallback = copyOptions.EnableRestartModeWithBackupFallback;
+ FatFiles = copyOptions.FatFiles;
+ FileFilter = copyOptions.FileFilter;
+ FixFileSecurityOnAllFiles = copyOptions.FixFileSecurityOnAllFiles;
+ FixFileTimesOnAllFiles = copyOptions.FixFileTimesOnAllFiles;
+ InterPacketGap = copyOptions.InterPacketGap;
+ Mirror = copyOptions.Mirror;
+ MonitorSourceChangesLimit = copyOptions.MonitorSourceChangesLimit;
+ MonitorSourceTimeLimit = copyOptions.MonitorSourceTimeLimit;
+ MoveFiles = copyOptions.MoveFiles;
+ MoveFilesAndDirectories = copyOptions.MoveFilesAndDirectories;
+ MultiThreadedCopiesCount = copyOptions.MultiThreadedCopiesCount;
+ Purge = copyOptions.Purge;
+ RemoveAttributes = copyOptions.RemoveAttributes;
+ RemoveFileInformation = copyOptions.RemoveFileInformation;
+ RunHours = copyOptions.RunHours;
+ TurnLongPathSupportOff = copyOptions.TurnLongPathSupportOff;
+ UseUnbufferedIo = copyOptions.UseUnbufferedIo;
+ }
+
+ ///
+ public CopyOptions Clone(string NewSource = null, string NewDestination = null) => new CopyOptions(this, NewSource, NewDestination);
+ object ICloneable.Clone() => Clone();
+
+ #endregion
+
+ #region Option Constants
+
+ internal const string COPY_SUBDIRECTORIES = "/S ";
+ internal const string COPY_SUBDIRECTORIES_INCLUDING_EMPTY = "/E ";
+ internal const string DEPTH = "/LEV:{0} ";
+ internal const string ENABLE_RESTART_MODE = "/Z ";
+ internal const string ENABLE_BACKUP_MODE = "/B ";
+ internal const string ENABLE_RESTART_MODE_WITH_BACKUP_FALLBACK = "/ZB ";
+ internal const string USE_UNBUFFERED_IO = "/J ";
+ internal const string ENABLE_EFSRAW_MODE = "/EFSRAW ";
+ internal const string COPY_FLAGS = "/COPY:{0} ";
+ internal const string COPY_FILES_WITH_SECURITY = "/SEC ";
+ internal const string COPY_ALL = "/COPYALL ";
+ internal const string REMOVE_FILE_INFORMATION = "/NOCOPY ";
+ internal const string FIX_FILE_SECURITY_ON_ALL_FILES = "/SECFIX ";
+ internal const string FIX_FILE_TIMES_ON_ALL_FILES = "/TIMFIX ";
+ internal const string PURGE = "/PURGE ";
+ internal const string MIRROR = "/MIR ";
+ internal const string MOVE_FILES = "/MOV ";
+ internal const string MOVE_FILES_AND_DIRECTORIES = "/MOVE ";
+ internal const string ADD_ATTRIBUTES = "/A+:{0} ";
+ internal const string REMOVE_ATTRIBUTES = "/A-:{0} ";
+ internal const string CREATE_DIRECTORY_AND_FILE_TREE = "/CREATE ";
+ internal const string FAT_FILES = "/FAT ";
+ internal const string TURN_LONG_PATH_SUPPORT_OFF = "/256 ";
+ internal const string MONITOR_SOURCE_CHANGES_LIMIT = "/MON:{0} ";
+ internal const string MONITOR_SOURCE_TIME_LIMIT = "/MOT:{0} ";
+ internal const string RUN_HOURS = "/RH:{0} ";
+ internal const string CHECK_PER_FILE = "/PF ";
+ internal const string INTER_PACKET_GAP = "/IPG:{0} ";
+ internal const string COPY_SYMBOLIC_LINK = "/SL ";
+ internal const string MULTITHREADED_COPIES_COUNT = "/MT:{0} ";
+ internal const string DIRECTORY_COPY_FLAGS = "/DCOPY:{0} ";
+ internal const string DO_NOT_COPY_DIRECTORY_INFO = "/NODCOPY ";
+ internal const string DO_NOT_USE_WINDOWS_COPY_OFFLOAD = "/NOOFFLOAD ";
+
+ #endregion Option Constants
+
+ #region Option Defaults
+
+ private IEnumerable fileFilter = new[] { "*.*" };
+ private string copyFlags = "DAT";
+ private string directoryCopyFlags = VersionManager.Version >= 6.2 ? "DA" : "T";
+
+ #endregion Option Defaults
+
+ #region Public Properties
+
+ ///
+ /// The source file path where the RoboCommand is copying files from.
+ ///
+ public virtual string Source { get { return _source; } set { _source = value.CleanDirectoryPath(); } }
+ private string _source;
+
+ ///
+ /// The destination file path where the RoboCommand is copying files to.
+ ///
+ public virtual string Destination { get { return _destination; } set { _destination = value.CleanDirectoryPath(); } }
+ private string _destination;
+
+ ///
+ /// Allows you to supply a set of files to copy or use wildcard characters (* or ?).
+ /// JobOptions file saves these into the /IF (Include Files) section
+ ///
+ public IEnumerable FileFilter
+ {
+ get
+ {
+ return fileFilter;
+ }
+ set
+ {
+ fileFilter = value;
+ }
+ }
+ ///
+ /// Copies subdirectories. Note that this option excludes empty directories.
+ /// [/S]
+ ///
+ public virtual bool CopySubdirectories { get; set; }
+ ///
+ /// Copies subdirectories. Note that this option includes empty directories.
+ /// [/E]
+ ///
+ public virtual bool CopySubdirectoriesIncludingEmpty { get; set; }
+ ///
+ /// Copies only the top N levels of the source directory tree. The default is
+ /// zero which does not limit the depth.
+ /// [/LEV:N]
+ ///
+ public virtual int Depth { get; set; }
+ ///
+ /// Copies files in Restart mode.
+ /// [/Z]
+ ///
+ public virtual bool EnableRestartMode { get; set; }
+ ///
+ /// Copies files in Backup mode.
+ /// [/B]
+ ///
+ public virtual bool EnableBackupMode { get; set; }
+ ///
+ /// Uses Restart mode. If access is denied, this option uses Backup mode.
+ /// [/ZB]
+ ///
+ public virtual bool EnableRestartModeWithBackupFallback { get; set; }
+ ///
+ /// Copy using unbuffered I/O (recommended for large files).
+ /// [/J]
+ ///
+ public virtual bool UseUnbufferedIo { get; set; }
+ ///
+ /// Copies all encrypted files in EFS RAW mode.
+ /// [/EFSRAW]
+ ///
+ public virtual bool EnableEfsRawMode { get; set; }
+ ///
+ /// This property should be set to a string consisting of all the flags to include (eg. DAT; DATSOU)
+ /// Specifies the file properties to be copied. The following are the valid values for this option:
+ ///D Data
+ ///A Attributes
+ ///T Time stamps
+ ///S NTFS access control list (ACL)
+ ///O Owner information
+ ///U Auditing information
+ ///The default value for copyflags is DAT (data, attributes, and time stamps).
+ ///[/COPY:copyflags]
+ ///
+ public string CopyFlags
+ {
+ get
+ {
+ return copyFlags;
+ }
+ set => copyFlags = value;
+ }
+ ///
+ /// Copies files with security (equivalent to /copy:DAT).
+ /// [/SEC]
+ ///
+ public virtual bool CopyFilesWithSecurity { get; set; }
+ ///
+ /// Copies all file information (equivalent to /copy:DATSOU).
+ /// [/COPYALL]
+ ///
+ public virtual bool CopyAll { get; set; }
+ ///
+ /// Copies no file information (useful with Purge option).
+ /// [/NOCOPY]
+ ///
+ public virtual bool RemoveFileInformation { get; set; }
+ ///
+ /// Fixes file security on all files, even skipped ones.
+ /// [/SECFIX]
+ ///
+ public virtual bool FixFileSecurityOnAllFiles { get; set; }
+ ///
+ /// Fixes file times on all files, even skipped ones.
+ /// [/TIMFIX]
+ ///
+ public virtual bool FixFileTimesOnAllFiles { get; set; }
+ ///
+ /// Deletes destination files and directories that no longer exist in the source.
+ /// [/PURGE]
+ ///
+ public virtual bool Purge { get; set; }
+ ///
+ /// Mirrors a directory tree (equivalent to CopySubdirectoriesIncludingEmpty plus Purge).
+ /// [/MIR]
+ ///
+ public virtual bool Mirror { get; set; }
+ ///
+ /// Moves files, and deletes them from the source after they are copied.
+ /// [/MOV]
+ ///
+ public virtual bool MoveFiles { get; set; }
+ ///
+ /// Moves files and directories, and deletes them from the source after they are copied.
+ /// [/MOVE]
+ ///
+ public virtual bool MoveFilesAndDirectories { get; set; }
+ ///
+ /// This property should be set to a string consisting of all the attributes to add (eg. AH; RASHCNET).
+ /// Adds the specified attributes to copied files.
+ /// [/A+:attributes]
+ ///
+ public string AddAttributes { get; set; }
+ ///
+ /// This property should be set to a string consisting of all the attributes to remove (eg. AH; RASHCNET).
+ /// Removes the specified attributes from copied files.
+ /// [/A-:attributes]
+ ///
+ public string RemoveAttributes { get; set; }
+ ///
+ /// Creates a directory tree and zero-length files only.
+ /// [/CREATE]
+ ///
+ public virtual bool CreateDirectoryAndFileTree { get; set; }
+ ///
+ /// Creates destination files by using 8.3 character-length FAT file names only.
+ /// [/FAT]
+ ///
+ public virtual bool FatFiles { get; set; }
+ ///
+ /// Turns off support for very long paths (longer than 256 characters).
+ /// [/256]
+ ///
+ public virtual bool TurnLongPathSupportOff { get; set; }
+ ///
+ /// The default value of zero indicates that you do not wish to monitor for changes.
+ /// Monitors the source, and runs again when more than N changes are detected.
+ /// [/MON:N]
+ ///
+ public virtual int MonitorSourceChangesLimit { get; set; }
+ ///
+ /// The default value of zero indicates that you do not wish to monitor for changes.
+ /// Monitors source, and runs again in M minutes if changes are detected.
+ /// [/MOT:M]
+ ///
+ public virtual int MonitorSourceTimeLimit { get; set; }
+ ///
+ /// Specifies run times when new copies may be started. ( Copy Operation is scheduled to only operate within specified timeframe )
+ /// [/rh:hhmm-hhmm]
+ /// If copy operation is unfinished, robocopy will remain active in idle state until the specified time, at which it will resume copying.
+ /// Must be in correct format. Incorrectly formatted strings will be ignored.
+ /// Examples:
+ /// 1500-1800 -> Robocopy will only copy between 3 PM and 5 PM
+ /// 0015-0530 -> Robocopy will only copy between 12:15 AM and 5:30 AM
+ ///
+ ///
+ /// If this is set up, then the robocopy process will remain active after the program exits if the calling asemmbly does not call prior to exiting the application.
+ ///
+ public string RunHours
+ {
+ get => runHours;
+ set
+ {
+ if (String.IsNullOrWhiteSpace(value))
+ runHours = value?.Trim() ?? string.Empty;
+ else if (CheckRunHoursString(value))
+ runHours = value.Trim();
+ }
+ }
+ private string runHours;
+
+ ///
+ /// Checks the scheduled /RH (run hours) per file instead of per pass.
+ /// [/PF]
+ ///
+ public virtual bool CheckPerFile { get; set; }
+
+ ///
+ /// The default value of zero indicates that this feature is turned off.
+ /// Specifies the inter-packet gap to free bandwidth on slow lines.
+ /// [/IPG:N]
+ ///
+ public virtual int InterPacketGap { get; set; }
+ ///
+ /// Copies the symbolic link instead of the target.
+ /// [/SL]
+ ///
+ public virtual bool CopySymbolicLink { get; set; }
+
+ ///
+ /// The default value of zero indicates that this feature is turned off.
+ /// Creates multi-threaded copies with N threads. Must be an integer between 1 and 128.
+ /// The MultiThreadedCopiesCount parameter cannot be used with the /IPG and EnableEfsRawMode parameters.
+ /// [/MT:N]
+ ///
+ public virtual int MultiThreadedCopiesCount { get; set; }
+ ///
+ /// What to copy for directories (default is DA).
+ /// (copyflags: D=Data, A=Attributes, T=Timestamps).
+ /// [/DCOPY:copyflags]
+ ///
+ public string DirectoryCopyFlags
+ {
+ get { return directoryCopyFlags; }
+ set { directoryCopyFlags = value; }
+ }
+ ///
+ /// Do not copy any directory info.
+ /// [/NODCOPY]
+ ///
+ public virtual bool DoNotCopyDirectoryInfo { get; set; }
+ ///
+ /// Copy files without using the Windows Copy Offload mechanism.
+ /// [/NOOFFLOAD]
+ ///
+ public virtual bool DoNotUseWindowsCopyOffload { get; set; }
+
+ #endregion Public Properties
+
+ #region < Parse (INTERNAL) >
+
+ ///
+ /// Used by the Parse method to sanitize path for the command options.
+ /// Evaluate the path. If needed, wrap it in quotes.
+ /// If the path ends in a DirectorySeperatorChar, santize it to work as expected.
+ ///
+ ///
+ /// Each return string includes a space at the end of the string to seperate it from the next option variable.
+ private string WrapPath(string path)
+ {
+ if (!path.Contains(" ")) return $"{path} "; //No spaces, just return the path
+ //Below this line, the path contains a space, so it must be wrapped in quotes.
+ if (path.EndsWithDirectorySeperator()) return $"\"{path}.\" "; // Ends with a directory seperator - Requires a '.' to denote using that directory. ex: "F:\."
+ return $"\"{path}\" ";
+ }
+
+ ///
+ /// Parse the class properties and generate the command arguments
+ ///
+ ///
+ internal string Parse()
+ {
+ Debugger.Instance.DebugMessage("Parsing CopyOptions...");
+ var version = VersionManager.Version;
+ var options = new StringBuilder();
+
+ // Set Source and Destination
+ options.Append(WrapPath(Source));
+ options.Append(WrapPath(Destination));
+
+ // Set FileFilter
+ // Quote each FileFilter item. The quotes are trimmed first to ensure that they are applied only once.
+ var fileFilterQuotedItems = FileFilter.Select(word => "\"" + word.Trim('"') + "\"");
+ string fileFilter = String.Join(" ", fileFilterQuotedItems);
+ options.Append($"{fileFilter} ");
+
+ Debugger.Instance.DebugMessage(string.Format("Parsing CopyOptions progress ({0}).", options.ToString()));
+
+ #region Set Options
+ var cleanedCopyFlags = CopyFlags.CleanOptionInput();
+ var cleanedDirectoryCopyFlags = DirectoryCopyFlags.CleanOptionInput();
+
+ if (!cleanedCopyFlags.IsNullOrWhiteSpace())
+ {
+ options.Append(string.Format(COPY_FLAGS, cleanedCopyFlags));
+ Debugger.Instance.DebugMessage(string.Format("Parsing CopyOptions progress ({0}).", options.ToString()));
+ }
+ if (!cleanedDirectoryCopyFlags.IsNullOrWhiteSpace() && version >= 5.1260026)
+ {
+ options.Append(string.Format(DIRECTORY_COPY_FLAGS, cleanedDirectoryCopyFlags));
+ Debugger.Instance.DebugMessage(string.Format("Parsing CopyOptions progress ({0}).", options.ToString()));
+ }
+ if (CopySubdirectories)
+ {
+ options.Append(COPY_SUBDIRECTORIES);
+ Debugger.Instance.DebugMessage(string.Format("Parsing CopyOptions progress ({0}).", options.ToString()));
+ }
+ if (CopySubdirectoriesIncludingEmpty)
+ options.Append(COPY_SUBDIRECTORIES_INCLUDING_EMPTY);
+ if (Depth > 0)
+ options.Append(string.Format(DEPTH, Depth));
+ if (EnableRestartMode)
+ options.Append(ENABLE_RESTART_MODE);
+ if (EnableBackupMode)
+ options.Append(ENABLE_BACKUP_MODE);
+ if (EnableRestartModeWithBackupFallback)
+ options.Append(ENABLE_RESTART_MODE_WITH_BACKUP_FALLBACK);
+ if (UseUnbufferedIo && version >= 6.2)
+ options.Append(USE_UNBUFFERED_IO);
+ if (EnableEfsRawMode)
+ options.Append(ENABLE_EFSRAW_MODE);
+ if (CopyFilesWithSecurity)
+ options.Append(COPY_FILES_WITH_SECURITY);
+ if (CopyAll)
+ options.Append(COPY_ALL);
+ if (RemoveFileInformation)
+ options.Append(REMOVE_FILE_INFORMATION);
+ if (FixFileSecurityOnAllFiles)
+ options.Append(FIX_FILE_SECURITY_ON_ALL_FILES);
+ if (FixFileTimesOnAllFiles)
+ options.Append(FIX_FILE_TIMES_ON_ALL_FILES);
+ if (Purge)
+ options.Append(PURGE);
+ if (Mirror)
+ options.Append(MIRROR);
+ if (MoveFiles)
+ options.Append(MOVE_FILES);
+ if (MoveFilesAndDirectories)
+ options.Append(MOVE_FILES_AND_DIRECTORIES);
+ if (!AddAttributes.IsNullOrWhiteSpace())
+ options.Append(string.Format(ADD_ATTRIBUTES, AddAttributes.CleanOptionInput()));
+ if (!RemoveAttributes.IsNullOrWhiteSpace())
+ options.Append(string.Format(REMOVE_ATTRIBUTES, RemoveAttributes.CleanOptionInput()));
+ if (CreateDirectoryAndFileTree)
+ options.Append(CREATE_DIRECTORY_AND_FILE_TREE);
+ if (FatFiles)
+ options.Append(FAT_FILES);
+ if (TurnLongPathSupportOff)
+ options.Append(TURN_LONG_PATH_SUPPORT_OFF);
+ if (MonitorSourceChangesLimit > 0)
+ options.Append(string.Format(MONITOR_SOURCE_CHANGES_LIMIT, MonitorSourceChangesLimit));
+ if (MonitorSourceTimeLimit > 0)
+ options.Append(string.Format(MONITOR_SOURCE_TIME_LIMIT, MonitorSourceTimeLimit));
+ if (!RunHours.IsNullOrWhiteSpace())
+ options.Append(string.Format(RUN_HOURS, RunHours.CleanOptionInput()));
+ if (CheckPerFile)
+ options.Append(CHECK_PER_FILE);
+ if (InterPacketGap > 0)
+ options.Append(string.Format(INTER_PACKET_GAP, InterPacketGap));
+ if (CopySymbolicLink)
+ options.Append(COPY_SYMBOLIC_LINK);
+ if (MultiThreadedCopiesCount > 0)
+ options.Append(string.Format(MULTITHREADED_COPIES_COUNT, MultiThreadedCopiesCount));
+ if (DoNotCopyDirectoryInfo && version >= 6.2)
+ options.Append(DO_NOT_COPY_DIRECTORY_INFO);
+ if (DoNotUseWindowsCopyOffload && version >= 6.2)
+ options.Append(DO_NOT_USE_WINDOWS_COPY_OFFLOAD);
+ #endregion Set Options
+
+ var parsedOptions = options.ToString();
+ Debugger.Instance.DebugMessage(string.Format("CopyOptions parsed ({0}).", parsedOptions));
+ return parsedOptions;
+ }
+
+ #endregion
+
+ #region < RunHours (Public) >
+
+ private static Regex RunHours_OverallRegex = new Regex("^(?[0-2][0-9][0-5][0-9])-(?[0-2][0-9][0-5][0-9])$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ private static Regex RunHours_Check1 = new Regex("^[0-1][0-9][0-5][0-9]$", RegexOptions.Compiled); // Checks 0000 - 1959
+ private static Regex RunHours_Check2 = new Regex("^[2][0-3][0-5][0-9]$", RegexOptions.Compiled); // Checks 2000 - 2359
+ private GroupCollection RunHoursGroups => RunHours_OverallRegex.Match(RunHours).Groups;
+
+ ///
+ /// Get the StartTime portion of
+ ///
+ /// hhmm or String.Empty
+ public string GetRunHours_StartTime()
+ {
+ if (RunHours.IsNullOrWhiteSpace()) return string.Empty;
+ return RunHoursGroups["StartTime"]?.Value ?? String.Empty;
+ }
+
+ ///
+ /// Get the EndTime portion of
+ ///
+ /// hhmm or String.Empty
+ public string GetRunHours_EndTime()
+ {
+ if (RunHours.IsNullOrWhiteSpace()) return string.Empty;
+ return RunHoursGroups["EndTime"]?.Value ?? String.Empty;
+ }
+
+ ///
+ /// Method to check if some string is valid for use as with the property.
+ ///
+ ///
+ /// True if correct format, otherwise false
+ public bool CheckRunHoursString(string runHours)
+ {
+ if (string.IsNullOrWhiteSpace(runHours)) return true;
+ if (!RunHours_OverallRegex.IsMatch(runHours.Trim())) return false;
+ var times = RunHours_OverallRegex.Match(runHours.Trim());
+ bool StartMatch = RunHours_Check1.IsMatch(times.Groups["StartTime"].Value) || RunHours_Check2.IsMatch(times.Groups["StartTime"].Value);
+ bool EndMatch = RunHours_Check1.IsMatch(times.Groups["EndTime"].Value) || RunHours_Check2.IsMatch(times.Groups["EndTime"].Value);
+ return StartMatch && EndMatch;
+ }
+
+ #endregion
+
+ #region < Other Public Methods >
+
+ ///
+ /// Combine this object with another CopyOptions object.
+ /// Any properties marked as true take priority. IEnumerable items are combined.
+ ///
+ ///
+ /// Source and Destination are only taken from the merged item if this object's Source/Destination values are null/empty.
+ /// RunHours follows the same rules.
+ ///
+ ///
+ ///
+ public void Merge(CopyOptions copyOptions)
+ {
+ Source = Source.ReplaceIfEmpty(copyOptions.Source);
+ Destination = Destination.ReplaceIfEmpty(copyOptions.Destination);
+ RunHours = RunHours.ReplaceIfEmpty(copyOptions.RunHours);
+
+ //int -> Take Greater Value
+ Depth = Depth.GetGreaterVal(copyOptions.Depth);
+ InterPacketGap = InterPacketGap.GetGreaterVal(copyOptions.InterPacketGap);
+ MonitorSourceChangesLimit = MonitorSourceChangesLimit.GetGreaterVal(copyOptions.MonitorSourceChangesLimit);
+ MonitorSourceTimeLimit = MonitorSourceTimeLimit.GetGreaterVal(copyOptions.MonitorSourceTimeLimit);
+ MultiThreadedCopiesCount = MultiThreadedCopiesCount.GetGreaterVal(copyOptions.MultiThreadedCopiesCount);
+
+ //Flags
+ AddAttributes = AddAttributes.CombineCharArr(copyOptions.AddAttributes);
+ CopyFlags = CopyFlags.CombineCharArr(copyOptions.CopyFlags);
+ DirectoryCopyFlags = DirectoryCopyFlags.CombineCharArr(copyOptions.DirectoryCopyFlags);
+ RemoveAttributes = RemoveAttributes.CombineCharArr(copyOptions.RemoveAttributes);
+
+ //IEnumerable
+ var list = new List(FileFilter);
+ list.AddRange(copyOptions.FileFilter);
+ FileFilter = list;
+
+ //Bool
+ CheckPerFile |= copyOptions.CheckPerFile;
+ CopyAll |= copyOptions.CopyAll;
+ CopyFilesWithSecurity |= copyOptions.CopyFilesWithSecurity;
+ CopySubdirectories |= copyOptions.CopySubdirectories;
+ CopySubdirectoriesIncludingEmpty |= copyOptions.CopySubdirectoriesIncludingEmpty;
+ CopySymbolicLink |= copyOptions.CopySymbolicLink;
+ CreateDirectoryAndFileTree |= copyOptions.CreateDirectoryAndFileTree;
+ DoNotCopyDirectoryInfo |= copyOptions.DoNotCopyDirectoryInfo;
+ DoNotUseWindowsCopyOffload |= copyOptions.DoNotUseWindowsCopyOffload;
+ EnableBackupMode |= copyOptions.EnableBackupMode;
+ EnableEfsRawMode |= copyOptions.EnableEfsRawMode;
+ EnableRestartMode |= copyOptions.EnableRestartMode;
+ EnableRestartModeWithBackupFallback |= copyOptions.EnableRestartModeWithBackupFallback;
+ FatFiles |= copyOptions.FatFiles;
+ FixFileSecurityOnAllFiles |= copyOptions.FixFileSecurityOnAllFiles;
+ FixFileTimesOnAllFiles |= copyOptions.FixFileTimesOnAllFiles;
+ Mirror |= copyOptions.Mirror;
+ MoveFiles |= copyOptions.MoveFiles;
+ MoveFilesAndDirectories |= copyOptions.MoveFilesAndDirectories;
+ Purge |= copyOptions.Purge;
+ RemoveFileInformation |= copyOptions.RemoveFileInformation;
+ TurnLongPathSupportOff |= copyOptions.TurnLongPathSupportOff;
+ UseUnbufferedIo |= copyOptions.UseUnbufferedIo;
+ }
+
+ #endregion
+
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Debugger.cs b/FSI.Lib/Tools/RoboSharp/Debugger.cs
new file mode 100644
index 0000000..e2dfdda
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Debugger.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Diagnostics;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ public sealed class Debugger
+ {
+ private static readonly Lazy instance = new Lazy(() => new Debugger());
+
+ [DebuggerHidden()]
+ private Debugger()
+ {
+
+ }
+
+ public static Debugger Instance
+ {
+ get { return instance.Value; }
+ }
+
+ public EventHandler DebugMessageEvent;
+
+ public class DebugMessageArgs : EventArgs
+ {
+ public object Message { get; set; }
+ }
+
+ [DebuggerHidden()]
+ private void RaiseDebugMessageEvent(object message)
+ {
+ DebugMessageEvent?.Invoke(this, new DebugMessageArgs
+ {
+ Message = message
+ });
+ }
+
+ [DebuggerHidden()]
+ internal void DebugMessage(object data)
+ {
+ RaiseDebugMessageEvent(data);
+ }
+ }
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+}
diff --git a/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_DE.cs b/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_DE.cs
new file mode 100644
index 0000000..dca8989
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_DE.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace FSI.Lib.Tools.RoboSharp.DefaultConfigurations
+{
+ internal class RoboSharpConfig_DE : RoboSharpConfiguration
+ {
+ public RoboSharpConfig_DE() : base()
+ {
+ errorToken = "FEHLER";
+ errorTokenRegex = RoboSharpConfiguration.ErrorTokenRegexGenerator(errorToken);
+ //errorTokenRegex = new Regex($" FEHLER " + @"(\d{1,3}) \(0x\d{8}\) ", RegexOptions.Compiled);
+
+ // < File Tokens >
+
+ //LogParsing_NewFile = "New File";
+ //LogParsing_OlderFile = "Older";
+ //LogParsing_NewerFile = "Newer";
+ //LogParsing_SameFile = "same";
+ //LogParsing_ExtraFile = "*EXTRA File";
+ //LogParsing_MismatchFile = "*Mismatch";
+ //LogParsing_FailedFile = "*Failed";
+ //LogParsing_FileExclusion = "named";
+
+ // < Directory Tokens >
+
+ //LogParsing_NewDir = "New Dir";
+ //LogParsing_ExtraDir = "*EXTRA Dir";
+ //LogParsing_ExistingDir = "Existing Dir";
+ //LogParsing_DirectoryExclusion = "named";
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs b/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs
new file mode 100644
index 0000000..cb23a40
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace FSI.Lib.Tools.RoboSharp.DefaultConfigurations
+{
+ ///
+ /// This is the Default Configuration class to use
+ ///
+ internal class RoboSharpConfig_EN : RoboSharpConfiguration
+ {
+ public RoboSharpConfig_EN() : base()
+ {
+ errorToken = "ERROR";
+ errorTokenRegex = RoboSharpConfiguration.ErrorTokenRegexGenerator(errorToken);
+ //errorTokenRegex = new Regex($" ERROR " + @"(\d{1,3}) \(0x\d{8}\) ", RegexOptions.Compiled);
+
+ // < File Tokens >
+
+ LogParsing_NewFile = "New File";
+ LogParsing_OlderFile = "Older";
+ LogParsing_NewerFile = "Newer";
+ LogParsing_SameFile = "same";
+ LogParsing_ExtraFile = "*EXTRA File";
+ LogParsing_MismatchFile = "*Mismatch";
+ LogParsing_FailedFile = "*Failed";
+ LogParsing_FileExclusion = "named";
+
+ // < Directory Tokens >
+
+ LogParsing_NewDir = "New Dir";
+ LogParsing_ExtraDir = "*EXTRA Dir";
+ LogParsing_ExistingDir = "Existing Dir";
+ LogParsing_DirectoryExclusion = "named";
+ }
+ }
+}
\ No newline at end of file
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/CommandErrorEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/CommandErrorEventArgs.cs
new file mode 100644
index 0000000..bf91ef5
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/CommandErrorEventArgs.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+// Do Not change NameSpace here! -> Must be RoboSharp due to prior releases
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ /// Describes an error that occured when generating the command
+ ///
+ public class CommandErrorEventArgs : EventArgs
+ {
+ ///
+ /// Error Description
+ ///
+ public string Error { get; }
+
+ ///
+ /// If this CommandErrorEventArgs object was created in response to an exception, that exception is captured here.
+ /// If no exception was thrown, this property will be null.
+ ///
+ public Exception Exception { get; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public CommandErrorEventArgs(string error, Exception ex)
+ {
+ Error = error;
+ this.Exception = ex;
+ }
+
+ ///
+ ///
+ ///
+ /// Exception to data to pass to the event handler
+ public CommandErrorEventArgs(Exception ex)
+ {
+ Error = ex.Message;
+ this.Exception = ex;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/CopyProgressEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/CopyProgressEventArgs.cs
new file mode 100644
index 0000000..d99c2ec
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/CopyProgressEventArgs.cs
@@ -0,0 +1,40 @@
+using System;
+
+// Do Not change NameSpace here! -> Must be RoboSharp due to prior releases
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ /// Current File Progress reported as
+ ///
+ public class CopyProgressEventArgs : EventArgs
+ {
+ ///
+ ///
+ public CopyProgressEventArgs(double progress)
+ {
+ CurrentFileProgress = progress;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public CopyProgressEventArgs(double progress, ProcessedFileInfo currentFile, ProcessedFileInfo SourceDir)
+ {
+ CurrentFileProgress = progress;
+ CurrentFile = currentFile;
+ CurrentDirectory = SourceDir;
+ }
+
+ ///
+ /// Current File Progress Percentage
+ ///
+ public double CurrentFileProgress { get; internal set; }
+
+ ///
+ public ProcessedFileInfo CurrentFile { get; internal set; }
+
+ /// Contains information about the Last Directory RoboCopy reported into the log.
+ public ProcessedFileInfo CurrentDirectory{ get; internal set; }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/ErrorEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/ErrorEventArgs.cs
new file mode 100644
index 0000000..29a0829
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/ErrorEventArgs.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+// Do Not change NameSpace here! -> Must be RoboSharp due to prior releases
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ /// Information about an Error reported by the RoboCopy process
+ ///
+ public class ErrorEventArgs : EventArgs
+ {
+ ///
+ /// Error Code
+ ///
+ public string Error { get; }
+
+ ///
+ /// Error Description
+ ///
+ public string ErrorDescription { get; }
+
+ ///
+ /// Error Code
+ ///
+ public int ErrorCode { get; }
+
+ ///
+ /// Signed Error Code
+ ///
+ public string SignedErrorCode { get; }
+
+ ///
+ /// The File or Directory Path the Error refers to
+ ///
+ public string ErrorPath { get; }
+
+ ///
+ /// DateTime the error occurred
+ ///
+ public DateTime DateTime { get; }
+
+ ///
+ /// Concatenate the and into a string seperated by an
+ ///
+ ///
+ public override string ToString()
+ {
+ if (ErrorDescription.IsNullOrWhiteSpace())
+ return Error;
+ else
+ return String.Format("{0}{1}{2}", Error, Environment.NewLine, ErrorDescription);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Regex used to split the Error Code into its various parts.
+ /// Must have the following groups: Date, ErrCode, SignedErrCode, Descrip, Path
+ ///
+ internal ErrorEventArgs(string errorData, string descripData, Regex errTokenRegex)
+ {
+ var match = errTokenRegex.Match(errorData);
+ var groups = match.Groups;
+
+ //Date
+ string dateStr = groups["Date"].Value;
+ if (DateTime.TryParse(dateStr, out var DT))
+ this.DateTime = DT;
+ else
+ this.DateTime = DateTime.Now;
+
+ //Path
+ ErrorPath = groups["Path"].Value;
+
+ //Error Code
+ ErrorCode = Convert.ToInt32(groups["ErrCode"].Value);
+ SignedErrorCode = groups["SignedErrCode"].Value;
+
+ //Error String
+ Error = errorData;
+ ErrorDescription = descripData;
+
+ }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/FileProcessedEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/FileProcessedEventArgs.cs
new file mode 100644
index 0000000..7ba605d
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/FileProcessedEventArgs.cs
@@ -0,0 +1,20 @@
+using System;
+
+// Do Not change NameSpace here! -> Must be RoboSharp due to prior releases
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ ///
+ ///
+ public class FileProcessedEventArgs : EventArgs
+ {
+ ///
+ public ProcessedFileInfo ProcessedFile { get; set; }
+
+ ///
+ public FileProcessedEventArgs(ProcessedFileInfo file)
+ {
+ ProcessedFile = file;
+ }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/IProgressEstimatorUpdateEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/IProgressEstimatorUpdateEventArgs.cs
new file mode 100644
index 0000000..2ac7083
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/IProgressEstimatorUpdateEventArgs.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using FSI.Lib.Tools.RoboSharp.Interfaces;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+// Do Not change NameSpace here! -> Must be RoboSharp due to prior releases
+namespace FSI.Lib.Tools.RoboSharp.EventArgObjects
+{
+ ///
+ /// Event Args provided by IProgressEstimator objects to notify the UI it should refresh the stat values
+ ///
+ public class IProgressEstimatorUpdateEventArgs : EventArgs
+ {
+ /// Dummy Args with Values of 0 to perform final updates through ProgressEstimator without creating new args every time
+ internal static IProgressEstimatorUpdateEventArgs DummyArgs { get; } = new IProgressEstimatorUpdateEventArgs(null, null, null, null);
+
+ private IProgressEstimatorUpdateEventArgs() : base() { }
+
+ internal IProgressEstimatorUpdateEventArgs(IProgressEstimator estimator, IStatistic ByteChange, IStatistic FileChange, IStatistic DirChange) : base()
+ {
+ Estimator = estimator;
+ ValueChange_Bytes = ByteChange ?? Statistic.Default_Bytes;
+ ValueChange_Files = FileChange ?? Statistic.Default_Files;
+ ValueChange_Directories = DirChange ?? Statistic.Default_Dirs;
+ }
+
+ ///
+ ///
+ ///
+ private IProgressEstimator Estimator { get; }
+
+ ///
+ public IStatistic BytesStatistic => Estimator?.BytesStatistic;
+
+ ///
+ public IStatistic FilesStatistic => Estimator?.FilesStatistic;
+
+ ///
+ public IStatistic DirectoriesStatistic => Estimator?.DirectoriesStatistic;
+
+ /// IStatistic Object that shows how much was added to the { } object during this UI Update
+ public IStatistic ValueChange_Bytes { get; }
+
+ /// IStatistic Object that shows how much was added to the { } object during this UI Update
+ public IStatistic ValueChange_Files { get; }
+
+ /// IStatistic Object that shows how much was added to the { } object during this UI Update
+ public IStatistic ValueChange_Directories { get; }
+
+ }
+}
+
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/ProgressEstimatorCreatedEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/ProgressEstimatorCreatedEventArgs.cs
new file mode 100644
index 0000000..35544e7
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/ProgressEstimatorCreatedEventArgs.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using FSI.Lib.Tools.RoboSharp.Interfaces;
+
+// Do Not change NameSpace here! -> Must be RoboSharp due to prior releases
+namespace FSI.Lib.Tools.RoboSharp.EventArgObjects
+{
+ ///
+ /// Reports that a ProgressEstimator object is now available for binding
+ ///
+ public class ProgressEstimatorCreatedEventArgs : EventArgs
+ {
+ private ProgressEstimatorCreatedEventArgs() : base() { }
+
+ internal ProgressEstimatorCreatedEventArgs(IProgressEstimator estimator) : base()
+ {
+ ResultsEstimate = estimator;
+ }
+
+ ///
+ ///
+ ///
+ public IProgressEstimator ResultsEstimate { get; }
+
+ }
+}
+
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/ResultListUpdatedEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/ResultListUpdatedEventArgs.cs
new file mode 100644
index 0000000..f858470
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/ResultListUpdatedEventArgs.cs
@@ -0,0 +1,24 @@
+using System;
+using FSI.Lib.Tools.RoboSharp.Results;
+using FSI.Lib.Tools.RoboSharp.Interfaces;
+
+namespace FSI.Lib.Tools.RoboSharp.EventArgObjects
+{
+ /// EventArgs for the delegate
+ public class ResultListUpdatedEventArgs : EventArgs
+ {
+ private ResultListUpdatedEventArgs() { }
+
+ /// Create the EventArgs for the delegate
+ /// Results list to present as an interface
+ public ResultListUpdatedEventArgs(IRoboCopyResultsList list)
+ {
+ ResultsList = list;
+ }
+
+ ///
+ /// Read-Only interface to the List that has been updated.
+ ///
+ public IRoboCopyResultsList ResultsList { get; }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboCommandCompletedEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboCommandCompletedEventArgs.cs
new file mode 100644
index 0000000..5842be1
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboCommandCompletedEventArgs.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Security.Cryptography.X509Certificates;
+using FSI.Lib.Tools.RoboSharp.EventArgObjects;
+
+// Do Not change NameSpace here! -> Must be RoboSharp due to prior releases
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ ///
+ ///
+ public class RoboCommandCompletedEventArgs : TimeSpanEventArgs
+ {
+ ///
+ /// Return the Results object
+ ///
+ ///
+ internal RoboCommandCompletedEventArgs(Results.RoboCopyResults results) : base(results.StartTime, results.EndTime, results.TimeSpan)
+ {
+ this.Results = results;
+ }
+
+ ///
+ public Results.RoboCopyResults Results { get; }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCommandStartedEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCommandStartedEventArgs.cs
new file mode 100644
index 0000000..365fc8c
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCommandStartedEventArgs.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace FSI.Lib.Tools.RoboSharp.EventArgObjects
+{
+ ///
+ /// EventArgs to declare when a RoboCommand process starts
+ ///
+ public class RoboQueueCommandStartedEventArgs : EventArgs
+ {
+ private RoboQueueCommandStartedEventArgs() : base() { }
+ internal RoboQueueCommandStartedEventArgs(RoboCommand cmd) : base() { Command = cmd; StartTime = DateTime.Now; }
+
+ ///
+ /// Command that started.
+ ///
+ public RoboCommand Command { get; }
+
+ ///
+ /// Returns TRUE if the command's is available for binding
+ ///
+ public bool ProgressEstimatorAvailable => Command.IsRunning;
+
+ ///
+ /// Local time the command started.
+ ///
+ public DateTime StartTime { get; }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCompletedEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCompletedEventArgs.cs
new file mode 100644
index 0000000..7646870
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCompletedEventArgs.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using FSI.Lib.Tools.RoboSharp.Interfaces;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+namespace FSI.Lib.Tools.RoboSharp.EventArgObjects
+{
+ ///
+ /// EventArgs to declare when a RoboCommand process starts
+ ///
+ public class RoboQueueCompletedEventArgs : TimeSpanEventArgs
+ {
+ internal RoboQueueCompletedEventArgs(RoboQueueResults runResults, bool listOnlyRun) : base(runResults.StartTime, runResults.EndTime, runResults.TimeSpan)
+ {
+ RunResults = runResults;
+ CopyOperation = !listOnlyRun;
+ }
+
+ ///
+ /// RoboQueue Results Object
+ ///
+ public RoboQueueResults RunResults { get; }
+
+ ///
+ /// TRUE if this run was a COPY OPERATION, FALSE is the results were created after a call.
+ ///
+ public bool CopyOperation { get; }
+
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/StatChangedEventArg.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/StatChangedEventArg.cs
new file mode 100644
index 0000000..ae86828
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/StatChangedEventArg.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.ComponentModel;
+using FSI.Lib.Tools.RoboSharp.Results;
+using FSI.Lib.Tools.RoboSharp.Interfaces;
+
+namespace FSI.Lib.Tools.RoboSharp.EventArgObjects
+{
+ ///
+ /// Interface helper for dealing with Statistic Event Args
+ ///
+ public interface IStatisticPropertyChangedEventArg
+ {
+ ///
+ Statistic.StatType StatType { get; }
+
+ /// TRUE if of type . Otherwise false.
+ bool Is_StatChangedEventArg { get; }
+
+ /// TRUE if of type . Otherwise false.
+ bool Is_StatisticPropertyChangedEventArgs { get; }
+
+ ///
+ string PropertyName { get; }
+
+ }
+
+ ///
+ /// EventArgs provided by when any individual property gets modified.
+ ///
+ ///
+ /// Under most circumstances, the 'PropertyName' property will detail which parameter has been updated.
+ /// When the Statistic object has multiple values change via a method call ( Reset / Add / Subtract methods ), then PropertyName will be String.Empty, indicating multiple values have changed.
+ /// If this is the case, then the , , and will report the value from the sender's property.
+ ///
+ public class StatChangedEventArg : PropertyChangedEventArgs, IStatisticPropertyChangedEventArg
+ {
+ private StatChangedEventArg() : base("") { }
+ internal StatChangedEventArg(Statistic stat, long oldValue, long newValue, string PropertyName) : base(PropertyName)
+ {
+ Sender = stat;
+ StatType = stat.Type;
+ OldValue = oldValue;
+ NewValue = newValue;
+ }
+
+ /// This is a reference to the Statistic that generated the EventArg object
+ public IStatistic Sender { get; }
+
+ ///
+ public Statistic.StatType StatType { get; }
+
+ /// Old Value of the object
+ public long OldValue { get; }
+
+ /// Current Value of the object
+ public long NewValue { get; }
+
+ ///
+ /// Result of NewValue - OldValue
+ ///
+ public long Difference => NewValue - OldValue;
+
+ bool IStatisticPropertyChangedEventArg.Is_StatChangedEventArg => true;
+
+ bool IStatisticPropertyChangedEventArg.Is_StatisticPropertyChangedEventArgs => false;
+ }
+
+ ///
+ /// EventArgs provided by
+ ///
+ ///
+ /// Under most circumstances, the 'PropertyName' property will detail which parameter has been updated.
+ /// When the Statistic object has multiple values change via a method call ( Reset / Add / Subtract methods ), then PropertyName will be String.Empty, indicating multiple values have changed.
+ /// If this is the case, then the , , and will report the value from the sender's property.
+ ///
+ public class StatisticPropertyChangedEventArgs : PropertyChangedEventArgs, IStatisticPropertyChangedEventArg
+ {
+ private StatisticPropertyChangedEventArgs() : base("") { }
+ internal StatisticPropertyChangedEventArgs(Statistic stat, Statistic oldValue, string PropertyName) : base(PropertyName)
+ {
+ //Sender = stat;
+ StatType = stat.Type;
+ OldValue = oldValue;
+ NewValue = stat.Clone();
+ Lazydifference = new Lazy(() => Statistic.Subtract(NewValue, OldValue));
+ }
+
+ ///
+ public Statistic.StatType StatType { get; }
+
+ /// Old Value of the object
+ public IStatistic OldValue { get; }
+
+ /// Current Value of the object
+ public IStatistic NewValue { get; }
+
+ ///
+ /// Result of NewValue - OldValue
+ ///
+ public IStatistic Difference => Lazydifference.Value;
+ private Lazy Lazydifference;
+
+ bool IStatisticPropertyChangedEventArg.Is_StatChangedEventArg => false;
+
+ bool IStatisticPropertyChangedEventArg.Is_StatisticPropertyChangedEventArgs => true;
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/EventArgObjects/TimeSpanEventArgs.cs b/FSI.Lib/Tools/RoboSharp/EventArgObjects/TimeSpanEventArgs.cs
new file mode 100644
index 0000000..3413a37
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/EventArgObjects/TimeSpanEventArgs.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace FSI.Lib.Tools.RoboSharp.EventArgObjects
+{
+ ///
+ /// Provide a base class that includes a StartTime, EndTime and will calculate the TimeSpan in between
+ ///
+ public abstract class TimeSpanEventArgs : EventArgs, RoboSharp.Interfaces.ITimeSpan
+ {
+ private TimeSpanEventArgs() : base() { }
+
+ ///
+ /// Create New Args
+ ///
+ ///
+ ///
+ public TimeSpanEventArgs(DateTime startTime, DateTime endTime) : base()
+ {
+ StartTime = startTime;
+ EndTime = endTime;
+ TimeSpan = EndTime.Subtract(StartTime);
+ }
+
+ internal TimeSpanEventArgs(DateTime startTime, DateTime endTime, TimeSpan tSpan) : base()
+ {
+ StartTime = startTime;
+ EndTime = endTime;
+ TimeSpan = tSpan;
+ }
+
+ ///
+ /// Local time the command started.
+ ///
+ public virtual DateTime StartTime { get; }
+
+ ///
+ /// Local time the command stopped.
+ ///
+ public virtual DateTime EndTime { get; }
+
+ ///
+ /// Length of time the process took to run
+ ///
+ public virtual TimeSpan TimeSpan { get; }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/ExtensionMethods.cs b/FSI.Lib/Tools/RoboSharp/ExtensionMethods.cs
new file mode 100644
index 0000000..1be9f33
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/ExtensionMethods.cs
@@ -0,0 +1,244 @@
+using System;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+using System.Diagnostics;
+using System.Collections.Generic;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+ internal static class ExtensionMethods
+ {
+ /// Encase the LogPath in quotes if needed
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ internal static string WrapPath(this string logPath) => (!logPath.StartsWith("\"") && logPath.Contains(" ")) ? $"\"{logPath}\"" : logPath;
+
+ /// Extension method provided by RoboSharp package
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ internal static bool IsNullOrWhiteSpace(this string value) => string.IsNullOrWhiteSpace(value);
+
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ internal static long TryConvertLong(this string val)
+ {
+ try
+ {
+ return Convert.ToInt64(val);
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ internal static int TryConvertInt(this string val)
+ {
+ try
+ {
+ return Convert.ToInt32(val);
+ }
+ catch
+ {
+ return 0;
+ }
+
+ }
+
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ public static string CleanOptionInput(this string option)
+ {
+ // Get rid of forward slashes
+ option = option.Replace("/", "");
+ // Get rid of padding
+ option = option.Trim();
+ return option;
+ }
+
+ public static string CleanDirectoryPath(this string path)
+ {
+ // Get rid of single and double quotes
+ path = path?.Replace("\"", "");
+ path = path?.Replace("\'", "");
+
+ //Validate against null / empty strings.
+ if (string.IsNullOrWhiteSpace(path)) return string.Empty;
+
+ // Get rid of padding
+ path = path.Trim();
+
+ // Get rid of trailing Directory Seperator Chars
+ // Length greater than 3 because E:\ is the shortest valid path
+ while (path.Length > 3 && path.EndsWithDirectorySeperator())
+ {
+ path = path.Substring(0, path.Length - 1);
+ }
+
+ //Sanitize invalid paths -- Convert E: to E:\
+ if (path.Length <= 2)
+ {
+ if (DriveRootRegex.IsMatch(path))
+ return path.ToUpper() + '\\';
+ else
+ return path;
+ }
+
+ // Fix UNC paths that are the root directory of a UNC drive
+ if (Uri.TryCreate(path, UriKind.Absolute, out Uri URI) && URI.IsUnc)
+ {
+ if (path.EndsWith("$"))
+ {
+ path += '\\';
+ }
+ }
+
+ return path;
+ }
+
+ private static readonly Regex DriveRootRegex = new Regex("[A-Za-z]:", RegexOptions.Compiled);
+
+ ///
+ /// Check if the string ends with a directory seperator character
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ public static bool EndsWithDirectorySeperator(this string path) => path.EndsWith(Path.DirectorySeparatorChar.ToString()) || path.EndsWith(Path.AltDirectorySeparatorChar.ToString());
+
+ ///
+ /// Convert into a char[]. Perform a ForEach( Char in strTwo) loop, and append any characters in Str2 to the end of this string if they don't already exist within this string.
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ internal static string CombineCharArr(this string StrOne, string StrTwo)
+ {
+ if (String.IsNullOrWhiteSpace(StrTwo)) return StrOne;
+ if (String.IsNullOrWhiteSpace(StrOne)) return StrTwo ?? StrOne;
+ string ret = StrOne;
+ char[] S2 = StrTwo.ToArray();
+ foreach (char c in S2)
+ {
+ if (!ret.Contains(c))
+ ret += c;
+ }
+ return ret;
+ }
+
+ ///
+ /// Compare the current value to that of the supplied value, and take the greater of the two.
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ internal static int GetGreaterVal(this int i, int i2) => i >= i2 ? i : i2;
+
+ ///
+ /// Evaluate this string. If this string is null or empty, replace it with the supplied string.
+ ///
+ ///
+ ///
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ [DebuggerHidden()]
+ internal static string ReplaceIfEmpty(this string str1, string str2) => String.IsNullOrWhiteSpace(str1) ? str2 ?? String.Empty : str1;
+ }
+}
+
+namespace System.Threading
+
+{
+ ///
+ /// Contains methods for CancelleableSleep and WaitUntil
+ ///
+ internal static class ThreadEx
+ {
+
+ ///
+ /// Wait synchronously until this task has reached the specified
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ public static void WaitUntil(this Task t, TaskStatus status)
+ {
+ while (t.Status < status)
+ Thread.Sleep(100);
+ }
+
+ ///
+ /// Wait asynchronously until this task has reached the specified
+ /// Checks every 100ms
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ public static async Task WaitUntilAsync(this Task t, TaskStatus status)
+ {
+ while (t.Status < status)
+ await Task.Delay(100);
+ }
+
+ ///
+ /// Wait synchronously until this task has reached the specified
+ /// Checks every milliseconds
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ public static async Task WaitUntilAsync(this Task t, TaskStatus status, int interval)
+ {
+ while (t.Status < status)
+ await Task.Delay(interval);
+ }
+
+ /// TimeSpan to sleep the thread
+ ///
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ internal static Task CancellableSleep(TimeSpan timeSpan, CancellationToken token)
+ {
+ return CancellableSleep((int)timeSpan.TotalMilliseconds, token);
+ }
+
+ ///
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ internal static Task CancellableSleep(TimeSpan timeSpan, CancellationToken[] tokens)
+ {
+ return CancellableSleep((int)timeSpan.TotalMilliseconds, tokens);
+ }
+
+ ///
+ /// Use await Task.Delay to sleep the thread.
+ ///
+ /// True if timer has expired (full duration slep), otherwise false.
+ /// Number of milliseconds to wait"/>
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ internal static Task CancellableSleep(int millisecondsTimeout, CancellationToken token)
+ {
+ return Task.Delay(millisecondsTimeout, token).ContinueWith(t => t.Exception == default);
+ }
+
+ ///
+ /// Use await Task.Delay to sleep the thread.
+ /// Supplied tokens are used to create a LinkedToken that can cancel the sleep at any point.
+ ///
+ /// True if slept full duration, otherwise false.
+ /// Number of milliseconds to wait"/>
+ /// Use to create the token used to cancel the delay
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ internal static Task CancellableSleep(int millisecondsTimeout, CancellationToken[] tokens)
+ {
+ var token = CancellationTokenSource.CreateLinkedTokenSource(tokens).Token;
+ return Task.Delay(millisecondsTimeout, token).ContinueWith(t => t.Exception == default);
+ }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/ImpersonateUser.cs b/FSI.Lib/Tools/RoboSharp/ImpersonateUser.cs
new file mode 100644
index 0000000..b246dd9
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/ImpersonateUser.cs
@@ -0,0 +1,95 @@
+#if NET40_OR_GREATER
+
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+
+ ///
+ /// Create an authenticated user to test Source/Destination directory access
+ /// See Issue #43 and PR #45
+ /// This class is not available in NetCoreApp3.1, NetStandard2.0, and NetStandard2.1
+ ///
+ internal class ImpersonatedUser : IDisposable
+ {
+ IntPtr userHandle;
+
+ WindowsImpersonationContext impersonationContext;
+
+ ///
+ ///
+ internal ImpersonatedUser(string user, string domain, string password)
+ {
+ userHandle = IntPtr.Zero;
+
+ bool loggedOn = LogonUser(
+ user,
+ domain,
+ password,
+ LogonType.Interactive,
+ LogonProvider.Default,
+ out userHandle);
+
+ if (!loggedOn)
+ throw new Win32Exception(Marshal.GetLastWin32Error());
+
+ // Begin impersonating the user
+ impersonationContext = WindowsIdentity.Impersonate(userHandle);
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (userHandle != IntPtr.Zero)
+ {
+ CloseHandle(userHandle);
+
+ userHandle = IntPtr.Zero;
+
+ impersonationContext.Undo();
+ }
+ }
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ static extern bool LogonUser(
+
+ string lpszUsername,
+
+ string lpszDomain,
+
+ string lpszPassword,
+
+ LogonType dwLogonType,
+
+ LogonProvider dwLogonProvider,
+
+ out IntPtr phToken
+
+ );
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool CloseHandle(IntPtr hHandle);
+
+ enum LogonType : int
+ {
+ Interactive = 2,
+ Network = 3,
+ Batch = 4,
+ Service = 5,
+ NetworkCleartext = 8,
+ NewCredentials = 9,
+ }
+
+ enum LogonProvider : int
+ {
+ Default = 0,
+ }
+
+ }
+
+}
+
+#endif
\ No newline at end of file
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/IProgressEstimator.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/IProgressEstimator.cs
new file mode 100644
index 0000000..8af1aff
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/IProgressEstimator.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Object that provides objects whose events can be bound to report estimated RoboCommand / RoboQueue progress periodically.
+ ///
+ ///
+ ///
+ ///
+ public interface IProgressEstimator : IResults
+ {
+
+ ///
+ /// Estimate of current number of directories processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy.
+ ///
+ new IStatistic DirectoriesStatistic { get; }
+
+ ///
+ /// Estimate of current number of files processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy.
+ ///
+ new IStatistic FilesStatistic { get; }
+
+ ///
+ /// Estimate of current number of bytes processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy.
+ ///
+ new IStatistic BytesStatistic { get; }
+
+ ///
+ /// Parse this object's stats into a enum.
+ ///
+ RoboCopyExitCodes GetExitCode();
+
+ /// Event that occurs when this IProgressEstimatorObject's IStatistic values have been updated.
+ event ProgressEstimator.UIUpdateEventHandler ValuesUpdated;
+
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/IResults.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/IResults.cs
new file mode 100644
index 0000000..d161840
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/IResults.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Provides objects for File, Directory, and Bytes to allow comparison between ProgressEstimator and RoboCopyResults objects
+ ///
+ ///
+ ///
+ ///
+ public interface IResults
+ {
+ /// Information about number of Directories Copied, Skipped, Failed, etc.
+ IStatistic DirectoriesStatistic { get; }
+
+ /// Information about number of Files Copied, Skipped, Failed, etc.
+ IStatistic FilesStatistic { get; }
+
+ /// Information about number of Bytes processed.
+ IStatistic BytesStatistic { get; }
+
+ ///
+ RoboCopyExitStatus Status { get; }
+
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCommand.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCommand.cs
new file mode 100644
index 0000000..b978217
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCommand.cs
@@ -0,0 +1,114 @@
+using System.Threading.Tasks;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public interface IRoboCommand
+ {
+ #region Properties
+
+ ///
+ string Name { get; }
+
+ ///
+ bool IsPaused { get; }
+
+ ///
+ bool IsRunning { get; }
+
+ ///
+ bool IsScheduled{ get; }
+
+ ///
+ bool IsCancelled { get; }
+
+ ///
+ bool StopIfDisposing { get; }
+
+ ///
+ IProgressEstimator IProgressEstimator { get; }
+
+ ///
+ string CommandOptions { get; }
+
+ ///
+ CopyOptions CopyOptions { get; set; }
+
+ ///
+ SelectionOptions SelectionOptions { get; set; }
+
+ ///
+ RetryOptions RetryOptions { get; set; }
+
+ ///
+ LoggingOptions LoggingOptions { get; set; }
+
+ ///
+ JobOptions JobOptions{ get; }
+
+ ///
+ RoboSharpConfiguration Configuration { get; }
+
+ #endregion Properties
+
+ #region Events
+
+ ///
+ event RoboCommand.FileProcessedHandler OnFileProcessed;
+
+ ///
+ event RoboCommand.CommandErrorHandler OnCommandError;
+
+ ///
+ event RoboCommand.ErrorHandler OnError;
+
+ ///
+ event RoboCommand.CommandCompletedHandler OnCommandCompleted;
+
+ ///
+ event RoboCommand.CopyProgressHandler OnCopyProgressChanged;
+
+ ///
+ event RoboCommand.ProgressUpdaterCreatedHandler OnProgressEstimatorCreated;
+
+ #endregion Events
+
+ #region Methods
+
+ ///
+ void Pause();
+
+ ///
+ void Resume();
+
+ ///
+ Task Start(string domain = "", string username = "", string password = "");
+
+ ///
+ Task Start_ListOnly(string domain = "", string username = "", string password = "");
+
+ ///
+ void Stop();
+
+ ///
+ void Dispose();
+
+#if NET45_OR_GREATER || NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER
+
+ ///
+ Task StartAsync_ListOnly(string domain = "", string username = "", string password = "");
+
+ ///
+ Task StartAsync(string domain = "", string username = "", string password = "");
+
+#endif
+
+
+ #endregion Methods
+ }
+}
\ No newline at end of file
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyCombinedExitStatus.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyCombinedExitStatus.cs
new file mode 100644
index 0000000..72c93dc
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyCombinedExitStatus.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.ComponentModel;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Read-Only interface for
+ ///
+ ///
+ ///
+ ///
+ public interface IRoboCopyCombinedExitStatus : INotifyPropertyChanged, ICloneable
+ {
+ ///
+ bool WasCancelled { get; }
+
+ ///
+ bool AnyNoCopyNoError { get; }
+
+ ///
+ bool AnyWasCancelled { get; }
+
+ ///
+ bool AllSuccessful { get; }
+
+ ///
+ bool AllSuccessful_WithWarnings { get; }
+
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyResultsList.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyResultsList.cs
new file mode 100644
index 0000000..902e2ff
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyResultsList.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Text;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Interface to provide Read-Only access to a
+ /// Implements:
+ /// where T =
+ ///
+ ///
+ ///
+ ///
+ ///
+ public interface IRoboCopyResultsList : IEnumerable, INotifyCollectionChanged
+ {
+ #region < Properties >
+
+ ///
+ /// Get the objects at the specified index.
+ ///
+ ///
+ ///
+ RoboCopyResults this[int i] { get; }
+
+ ///
+ IStatistic DirectoriesStatistic { get; }
+
+ ///
+ IStatistic BytesStatistic { get; }
+
+ ///
+ IStatistic FilesStatistic { get; }
+
+ ///
+ ISpeedStatistic SpeedStatistic { get; }
+
+ ///
+ IRoboCopyCombinedExitStatus Status { get; }
+
+ ///
+ IReadOnlyList Collection { get; }
+
+ ///
+ int Count { get; }
+
+ #endregion
+
+ #region < Methods >
+
+ ///
+ /// Get a snapshot of the ByteStatistics objects from this list.
+ ///
+ /// New array of the ByteStatistic objects
+ IStatistic[] GetByteStatistics();
+
+ ///
+ /// Get a snapshot of the DirectoriesStatistic objects from this list.
+ ///
+ /// New array of the DirectoriesStatistic objects
+ IStatistic[] GetDirectoriesStatistics();
+
+ ///
+ /// Get a snapshot of the FilesStatistic objects from this list.
+ ///
+ /// New array of the FilesStatistic objects
+ IStatistic[] GetFilesStatistics();
+
+ ///
+ /// Get a snapshot of the FilesStatistic objects from this list.
+ ///
+ /// New array of the FilesStatistic objects
+ RoboCopyExitStatus[] GetStatuses();
+
+ ///
+ /// Get a snapshot of the FilesStatistic objects from this list.
+ ///
+ /// New array of the FilesStatistic objects
+ ISpeedStatistic[] GetSpeedStatistics();
+
+ ///
+ /// Combine the into a single array of errors
+ ///
+ /// New array of the ErrorEventArgs objects
+ ErrorEventArgs[] GetErrors();
+
+ #endregion
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueue.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueue.cs
new file mode 100644
index 0000000..4ec5fa6
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueue.cs
@@ -0,0 +1,139 @@
+using FSI.Lib.Tools.RoboSharp.Interfaces;
+using FSI.Lib.Tools.RoboSharp.Results;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Threading.Tasks;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Interface for RoboQueue
+ ///
+ public interface IRoboQueue : IDisposable, INotifyPropertyChanged, IEnumerable
+ {
+ #region < Properties >
+
+ ///
+ bool AnyCancelled { get; }
+
+ ///
+ bool AnyPaused { get; }
+
+ ///
+ bool AnyRunning { get; }
+
+ ///
+ ReadOnlyCollection Commands { get; }
+
+ ///
+ bool CopyOperationCompleted { get; }
+
+ ///
+ bool IsCopyOperationRunning { get; }
+
+ ///
+ bool IsListOnlyRunning { get; }
+
+ ///
+ bool IsPaused { get; }
+
+ ///
+ bool IsRunning { get; }
+
+ ///
+ int JobsComplete { get; }
+
+ ///
+ int JobsCompletedSuccessfully { get; }
+
+ ///
+ int JobsCurrentlyRunning { get; }
+
+ ///
+ int JobsStarted { get; }
+
+ ///
+ int ListCount { get; }
+
+ ///
+ bool ListOnlyCompleted { get; }
+
+ ///
+ IRoboQueueResults ListResults { get; }
+
+ ///
+ int MaxConcurrentJobs { get; set; }
+
+ ///
+ string Name { get; }
+
+ ///
+ IProgressEstimator ProgressEstimator { get; }
+
+ ///
+ IRoboQueueResults RunResults { get; }
+
+ ///
+ bool WasCancelled { get; }
+
+ #endregion
+
+ #region < Events >
+
+
+ ///
+ event RoboCommand.CommandCompletedHandler OnCommandCompleted;
+
+ ///
+ event RoboCommand.CommandErrorHandler OnCommandError;
+
+ ///
+ event RoboQueue.CommandStartedHandler OnCommandStarted;
+
+ ///
+ event RoboCommand.CopyProgressHandler OnCopyProgressChanged;
+
+ ///
+ event RoboCommand.ErrorHandler OnError;
+
+ ///
+ event RoboCommand.FileProcessedHandler OnFileProcessed;
+
+ ///
+ event RoboQueue.ProgressUpdaterCreatedHandler OnProgressEstimatorCreated;
+
+ ///
+ event RoboQueue.RunCompletedHandler RunCompleted;
+
+ ///
+ event RoboCopyResultsList.ResultsListUpdated RunResultsUpdated;
+
+ ///
+ event UnhandledExceptionEventHandler TaskFaulted;
+
+ #endregion
+
+ #region < Methods >
+
+
+ ///
+ void PauseAll();
+
+ ///
+ void ResumeAll();
+
+ ///
+ Task StartAll(string domain = "", string username = "", string password = "");
+
+ ///
+ Task StartAll_ListOnly(string domain = "", string username = "", string password = "");
+
+ ///
+ void StopAll();
+
+ #endregion
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueueResults.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueueResults.cs
new file mode 100644
index 0000000..8325bd8
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueueResults.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Interface for the object.
+ /// Implements
+ ///
+ public interface IRoboQueueResults : IRoboCopyResultsList
+ {
+ ///
+ /// Local time the command started.
+ ///
+ DateTime StartTime { get; }
+
+ ///
+ /// Local time the command stopped.
+ ///
+ DateTime EndTime { get; }
+
+ ///
+ /// Length of time the process took to run
+ ///
+ TimeSpan TimeSpan { get; }
+
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/ISpeedStatistic.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/ISpeedStatistic.cs
new file mode 100644
index 0000000..1ed4fca
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/ISpeedStatistic.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.ComponentModel;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Provide Read-Only access to a SpeedStatistic
+ ///
+ ///
+ ///
+ ///
+ public interface ISpeedStatistic : INotifyPropertyChanged, ICloneable
+ {
+ /// Average Transfer Rate in Bytes/Second
+ decimal BytesPerSec { get; }
+
+ /// Average Transfer Rate in MB/Minute
+ decimal MegaBytesPerMin { get; }
+
+ ///
+ string ToString();
+
+ /// new object
+ ///
+ new SpeedStatistic Clone();
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/IStatistic.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/IStatistic.cs
new file mode 100644
index 0000000..61f7588
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/IStatistic.cs
@@ -0,0 +1,112 @@
+using System;
+using System.ComponentModel;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Provide Read-Only access to a object
+ ///
+ ///
+ ///
+ ///
+ public interface IStatistic : INotifyPropertyChanged, ICloneable
+ {
+
+ #region < Properties >
+
+ ///
+ /// Name of the Statistics Object
+ ///
+ string Name { get; }
+
+ ///
+ ///
+ ///
+ Statistic.StatType Type { get; }
+
+ /// Total Scanned during the run
+ long Total { get; }
+
+ /// Total Copied
+ long Copied { get; }
+
+ /// Total Skipped
+ long Skipped { get; }
+
+ ///
+ long Mismatch { get; }
+
+ /// Total that failed to copy or move
+ long Failed { get; }
+
+ /// Total Extra that exist in the Destination (but are missing from the Source)
+ long Extras { get; }
+
+ ///
+ bool NonZeroValue { get; }
+
+ #endregion
+
+ #region < Events >
+
+ ///
+ new event PropertyChangedEventHandler PropertyChanged;
+
+ /// Occurs when the Property is updated.
+ event Statistic.StatChangedHandler OnTotalChanged;
+
+ /// Occurs when the Property is updated.
+ event Statistic.StatChangedHandler OnCopiedChanged;
+
+ /// Occurs when the Property is updated.
+ event Statistic.StatChangedHandler OnSkippedChanged;
+
+ /// Occurs when the Property is updated.
+ event Statistic.StatChangedHandler OnMisMatchChanged;
+
+ /// Occurs when the Property is updated.
+ event Statistic.StatChangedHandler OnFailedChanged;
+
+ /// Occurs when the Property is updated.
+ event Statistic.StatChangedHandler OnExtrasChanged;
+
+ #endregion
+
+ #region < ToString Methods >
+
+ ///
+ string ToString();
+
+ ///
+ string ToString(bool IncludeType, bool IncludePrefix, string Delimiter, bool DelimiterAfterType = false);
+
+ ///
+ string ToString_Type();
+
+ ///
+ string ToString_Total(bool IncludeType = false, bool IncludePrefix = true);
+
+ ///
+ string ToString_Copied(bool IncludeType = false, bool IncludePrefix = true);
+
+ ///
+ string ToString_Extras(bool IncludeType = false, bool IncludePrefix = true);
+
+ ///
+ string ToString_Failed(bool IncludeType = false, bool IncludePrefix = true);
+
+ ///
+ string ToString_Mismatch(bool IncludeType = false, bool IncludePrefix = true);
+
+ ///
+ string ToString_Skipped(bool IncludeType = false, bool IncludePrefix = true);
+
+ #endregion
+
+ /// new object
+ ///
+ new Statistic Clone();
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Interfaces/ITimeSpan.cs b/FSI.Lib/Tools/RoboSharp/Interfaces/ITimeSpan.cs
new file mode 100644
index 0000000..7331fe4
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Interfaces/ITimeSpan.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace FSI.Lib.Tools.RoboSharp.Interfaces
+{
+ ///
+ /// Interface to normalize all the Start/End/TimeSpan properties of various objects
+ ///
+ internal interface ITimeSpan
+ {
+ ///
+ /// Local time the command started.
+ ///
+ DateTime StartTime { get; }
+
+ ///
+ /// Local time the command stopped.
+ ///
+ DateTime EndTime { get; }
+
+ ///
+ /// Length of time the process took to run
+ ///
+ TimeSpan TimeSpan { get; }
+ }
+
+}
diff --git a/FSI.Lib/Tools/RoboSharp/JobFile.cs b/FSI.Lib/Tools/RoboSharp/JobFile.cs
new file mode 100644
index 0000000..41c9cfe
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/JobFile.cs
@@ -0,0 +1,345 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using FSI.Lib.Tools.RoboSharp.Interfaces;
+using System.Threading.Tasks;
+using FSI.Lib.Tools.RoboSharp.Results;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ /// Represents a single RoboCopy Job File
+ /// Implements:
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public class JobFile : ICloneable, IRoboCommand
+ {
+
+ #region < Constructor >
+
+ ///
+ /// Create a JobFile with Default Options
+ ///
+ public JobFile() { }
+
+ ///
+ /// Constructor for ICloneable Interface
+ ///
+ ///
+ public JobFile(JobFile jobFile)
+ {
+ this.roboCommand = jobFile.roboCommand.Clone();
+ }
+
+ ///
+ /// Clone the RoboCommand's options objects into a new JobFile
+ ///
+ /// RoboCommand whose options shall be cloned
+ /// Optional FilePath to specify for future call to
+ public JobFile(RoboCommand cmd, string filePath = "")
+ {
+ FilePath = filePath ?? "";
+ roboCommand = cmd.Clone();
+ }
+
+ ///
+ /// Constructor for Factory Methods
+ ///
+ private JobFile(string filePath, RoboCommand cmd)
+ {
+ FilePath = filePath;
+ roboCommand = cmd;
+ }
+
+ #endregion
+
+ #region < Factory Methods >
+
+ ///
+ public static JobFile ParseJobFile(string path)
+ {
+ RoboCommand cmd = JobFileBuilder.Parse(path);
+ if (cmd != null) return new JobFile(path, cmd);
+ return null;
+ }
+
+ ///
+ public static JobFile ParseJobFile(StreamReader streamReader)
+ {
+ RoboCommand cmd = JobFileBuilder.Parse(streamReader);
+ if (cmd != null) return new JobFile("", cmd);
+ return null;
+ }
+
+ ///
+ public static JobFile ParseJobFile(FileInfo file)
+ {
+ RoboCommand cmd = JobFileBuilder.Parse(file);
+ if (cmd != null) return new JobFile(file.FullName, cmd);
+ return null;
+ }
+
+ ///
+ public static JobFile ParseJobFile(IEnumerable FileText)
+ {
+ RoboCommand cmd = JobFileBuilder.Parse(FileText);
+ if (cmd != null) return new JobFile("", cmd);
+ return null;
+ }
+
+ #endregion
+
+ #region < ICLONEABLE >
+
+ ///
+ /// Create a clone of this JobFile
+ ///
+ public JobFile Clone() => new JobFile(this);
+
+ object ICloneable.Clone() => Clone();
+
+ #endregion
+
+ #region < Constants >
+
+ ///
+ /// Expected File Extension for Job Files exported from RoboCopy.
+ ///
+ public const string JOBFILE_Extension = ".RCJ";
+
+ ///
+ /// FileFilter to use in an to search for this extension, such as with
+ ///
+ public const string JOBFILE_SearchPattern = "*.RCJ";
+
+ ///
+ /// FileFilter to use in a dialog window, such as the OpenFileDialog window.
+ ///
+
+ public const string JOBFILE_DialogFilter = "RoboCopy Job|*.RCJ";
+ #endregion
+
+ #region < Fields >
+
+ ///
+ /// Options are stored in a RoboCommand object for simplicity.
+ ///
+ protected RoboCommand roboCommand;
+
+ #endregion
+
+ #region < Properties >
+
+ /// FilePath of the Job File
+ public virtual string FilePath { get; set; }
+
+ ///
+ public string Job_Name
+ {
+ get => roboCommand.Name;
+ set => roboCommand.Name = value;
+ }
+
+ ///
+ public CopyOptions CopyOptions => roboCommand.CopyOptions;
+
+ ///
+ public LoggingOptions LoggingOptions => roboCommand.LoggingOptions;
+
+ ///
+ public RetryOptions RetryOptions => roboCommand.RetryOptions;
+
+ ///
+ public SelectionOptions SelectionOptions => roboCommand.SelectionOptions;
+
+ #endregion
+
+ #region < Methods >
+#pragma warning disable CS1573
+
+ ///
+ /// Update the property and save the JobFile to the
+ ///
+ /// Update the property, then save the JobFile to this path.
+ ///
+ ///
+ public async Task Save(string path, bool IncludeSource = false, bool IncludeDestination = false)
+
+ {
+ if (path.IsNullOrWhiteSpace()) throw new ArgumentException("path Property is Empty");
+ FilePath = path;
+ await roboCommand.SaveAsJobFile(FilePath, IncludeSource, IncludeDestination);
+ }
+
+ ///
+ /// Save the JobFile to .
+ /// Source and Destination will be included by default.
+ ///
+ /// If path is null/empty, will throw
+ /// Task that completes when the JobFile has been saved.
+ ///
+ public async Task Save()
+ {
+ if (FilePath.IsNullOrWhiteSpace()) throw new ArgumentException("FilePath Property is Empty");
+ await roboCommand.SaveAsJobFile(FilePath, true, true);
+ }
+
+#pragma warning restore CS1573
+ #endregion
+
+ #region < IRoboCommand Interface >
+
+ #region < Events >
+
+ event RoboCommand.FileProcessedHandler IRoboCommand.OnFileProcessed
+ {
+ add
+ {
+ ((IRoboCommand)roboCommand).OnFileProcessed += value;
+ }
+
+ remove
+ {
+ ((IRoboCommand)roboCommand).OnFileProcessed -= value;
+ }
+ }
+
+ event RoboCommand.CommandErrorHandler IRoboCommand.OnCommandError
+ {
+ add
+ {
+ ((IRoboCommand)roboCommand).OnCommandError += value;
+ }
+
+ remove
+ {
+ ((IRoboCommand)roboCommand).OnCommandError -= value;
+ }
+ }
+
+ event RoboCommand.ErrorHandler IRoboCommand.OnError
+ {
+ add
+ {
+ ((IRoboCommand)roboCommand).OnError += value;
+ }
+
+ remove
+ {
+ ((IRoboCommand)roboCommand).OnError -= value;
+ }
+ }
+
+ event RoboCommand.CommandCompletedHandler IRoboCommand.OnCommandCompleted
+ {
+ add
+ {
+ ((IRoboCommand)roboCommand).OnCommandCompleted += value;
+ }
+
+ remove
+ {
+ ((IRoboCommand)roboCommand).OnCommandCompleted -= value;
+ }
+ }
+
+ event RoboCommand.CopyProgressHandler IRoboCommand.OnCopyProgressChanged
+ {
+ add
+ {
+ ((IRoboCommand)roboCommand).OnCopyProgressChanged += value;
+ }
+
+ remove
+ {
+ ((IRoboCommand)roboCommand).OnCopyProgressChanged -= value;
+ }
+ }
+
+ event RoboCommand.ProgressUpdaterCreatedHandler IRoboCommand.OnProgressEstimatorCreated
+ {
+ add
+ {
+ ((IRoboCommand)roboCommand).OnProgressEstimatorCreated += value;
+ }
+
+ remove
+ {
+ ((IRoboCommand)roboCommand).OnProgressEstimatorCreated -= value;
+ }
+ }
+
+ #endregion
+
+ #region < Properties >
+
+ string IRoboCommand.Name => roboCommand.Name;
+ bool IRoboCommand.IsPaused => roboCommand.IsPaused;
+ bool IRoboCommand.IsRunning => roboCommand.IsRunning;
+ bool IRoboCommand.IsScheduled => roboCommand.IsScheduled;
+ bool IRoboCommand.IsCancelled => roboCommand.IsCancelled;
+ bool IRoboCommand.StopIfDisposing => roboCommand.StopIfDisposing;
+ IProgressEstimator IRoboCommand.IProgressEstimator => roboCommand.IProgressEstimator;
+ SelectionOptions IRoboCommand.SelectionOptions { get => ((IRoboCommand)roboCommand).SelectionOptions; set => ((IRoboCommand)roboCommand).SelectionOptions = value; }
+ RetryOptions IRoboCommand.RetryOptions { get => ((IRoboCommand)roboCommand).RetryOptions; set => ((IRoboCommand)roboCommand).RetryOptions = value; }
+ LoggingOptions IRoboCommand.LoggingOptions { get => roboCommand.LoggingOptions; set => roboCommand.LoggingOptions = value; }
+ CopyOptions IRoboCommand.CopyOptions { get => ((IRoboCommand)roboCommand).CopyOptions; set => ((IRoboCommand)roboCommand).CopyOptions = value; }
+ JobOptions IRoboCommand.JobOptions { get => ((IRoboCommand)roboCommand).JobOptions; }
+ RoboSharpConfiguration IRoboCommand.Configuration => roboCommand.Configuration;
+ string IRoboCommand.CommandOptions => roboCommand.CommandOptions;
+
+ #endregion
+
+ #region < Methods >
+
+ void IRoboCommand.Pause()
+ {
+ ((IRoboCommand)roboCommand).Pause();
+ }
+
+ void IRoboCommand.Resume()
+ {
+ ((IRoboCommand)roboCommand).Resume();
+ }
+
+ Task IRoboCommand.Start(string domain, string username, string password)
+ {
+ return ((IRoboCommand)roboCommand).Start(domain, username, password);
+ }
+
+ void IRoboCommand.Stop()
+ {
+ ((IRoboCommand)roboCommand).Stop();
+ }
+
+ void IRoboCommand.Dispose()
+ {
+ roboCommand.Stop();
+ }
+
+ Task IRoboCommand.Start_ListOnly(string domain, string username, string password)
+ {
+ return roboCommand.Start_ListOnly();
+ }
+
+ Task IRoboCommand.StartAsync_ListOnly(string domain, string username, string password)
+ {
+ return roboCommand.StartAsync_ListOnly();
+ }
+
+ Task IRoboCommand.StartAsync(string domain, string username, string password)
+ {
+ return roboCommand.StartAsync_ListOnly();
+ }
+ #endregion
+
+ #endregion
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/JobFileBuilder.cs b/FSI.Lib/Tools/RoboSharp/JobFileBuilder.cs
new file mode 100644
index 0000000..917d639
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/JobFileBuilder.cs
@@ -0,0 +1,586 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using System.Text.RegularExpressions;
+using System.Runtime.CompilerServices;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+ internal static class JobFileBuilder
+ {
+ #region < Constants & Regex >
+
+ ///
+ /// Any comments within the job file lines will start with this string
+ ///
+ public const string JOBFILE_CommentPrefix = ":: ";
+
+ ///
+ public const string JOBFILE_Extension = JobFile.JOBFILE_Extension;
+
+ ///
+ internal const string JOBFILE_JobName = ":: JOB_NAME: ";
+
+ internal const string JOBFILE_StopIfDisposing = ":: StopIfDisposing: ";
+
+ /// Pattern to Identify the SWITCH, DELIMITER and VALUE section
+ private const string RegString_SWITCH = "\\s*(?\\/[A-Za-z]+[-]{0,1})(?\\s*:?\\s*)(?.+?)";
+ /// Pattern to Identify the SWITCH, DELIMIETER and VALUE section
+ private const string RegString_SWITCH_NumericValue = "\\s*(?\\/[A-Za-z]+[-]{0,1})(?\\s*:?\\s*)(?[0-9]+?)";
+ /// Pattern to Identify COMMENT sections - Throws out white space and comment delimiter '::'
+ private const string RegString_COMMENT = "((?:\\s*[:]{2,}\\s*[:]{0,})(?.*))";
+
+
+ ///
+ /// Regex to check if an entire line is a comment
+ ///
+ ///
+ /// Captured Group Names:
+ /// COMMENT
+ ///
+ private readonly static Regex LINE_IsComment = new Regex("^(?:\\s*[:]{2,}\\s*)(?.*)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ ///
+ /// Regex to check if the string is a flag for RoboCopy - These typically will have comments
+ ///
+ ///
+ /// Captured Group Names:
+ /// SWITCH
+ /// DELIMITER
+ /// VALUE
+ /// COMMENT
+ ///
+ private readonly static Regex LINE_IsSwitch = new Regex($"^{RegString_SWITCH}{RegString_COMMENT}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ private readonly static Regex LINE_IsSwitch_NoComment = new Regex($"^{RegString_SWITCH}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ private readonly static Regex LINE_IsSwitch_NumericValue = new Regex($"^{RegString_SWITCH_NumericValue}{RegString_COMMENT}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ private readonly static Regex LINE_IsSwitch_NumericValue_NoComment = new Regex($"^{RegString_SWITCH_NumericValue}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ ///
+ /// JobName for ROboCommand is not valid parameter for RoboCopy, so we save it into a comment within the file
+ ///
+ ///
+ /// Captured Group Names:
+ /// FLAG
+ /// NAME
+ /// COMMENT
+ ///
+ private readonly static Regex JobNameRegex = new Regex("^\\s*(?:: JOB_NAME:)\\s*(?.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ private readonly static Regex StopIfDisposingRegex = new Regex("^\\s*(?:: StopIfDisposing:)\\s*(?TRUE|FALSE)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ ///
+ /// Regex used for parsing File and Directory filters for /IF /XD and /XF flags
+ ///
+ ///
+ /// Captured Group Names:
+ /// PATH
+ /// COMMENT
+ ///
+ private readonly static Regex DirFileFilterRegex = new Regex($"^\\s*{RegString_FileFilter}{RegString_COMMENT}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ private readonly static Regex DirFileFilterRegex_NoComment = new Regex($"^\\s*{RegString_FileFilter}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ private const string RegString_FileFilter = "(?.+?)";
+
+
+ #region < Copy Options Regex >
+
+ ///
+ /// Regex to find the SourceDirectory within the JobFile
+ ///
+ ///
+ /// Captured Group Names:
+ /// SWITCH
+ /// PATH
+ /// COMMENT
+ ///
+ private readonly static Regex CopyOptionsRegex_SourceDir = new Regex("^\\s*(?/SD:)(?.*)(?::.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ ///
+ /// Regex to find the DestinationDirectory within the JobFile
+ ///
+ ///
+ /// Captured Group Names:
+ /// SWITCH
+ /// PATH
+ /// COMMENT
+ ///
+ private readonly static Regex CopyOptionsRegex_DestinationDir = new Regex("^\\s*(?/DD:)(?.*)(?::.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ ///
+ /// Regex to determine if on the INCLUDE FILES section of the JobFile
+ ///
+ ///
+ /// Each new path / filename should be on its own line
+ ///
+ private readonly static Regex CopyOptionsRegex_IncludeFiles = new Regex("^\\s*(?/IF)\\s*(.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ #endregion
+
+ #region < Selection Options Regex >
+
+ ///
+ /// Regex to determine if on the EXCLUDE FILES section of the JobFile
+ ///
+ ///
+ /// Each new path / filename should be on its own line
+ ///
+ private readonly static Regex SelectionRegex_ExcludeFiles = new Regex("^\\s*(?/XF).*", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ ///
+ /// Regex to determine if on the EXCLUDE DIRECTORIES section of the JobFile
+ ///
+ ///
+ /// Each new path / filename should be on its own line
+ ///
+ private readonly static Regex SelectionRegex_ExcludeDirs = new Regex("^\\s*(?/XD).*", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ #endregion
+
+ #endregion
+
+ #region < Methods that feed Main Parse Routine >
+
+ ///
+ /// Read each line using and attempt to produce a Job File.
+ /// If FileExtension != ".RCJ" -> returns null. Otherwise parses the file.
+ ///
+ /// FileInfo object for some Job File. File Path should end in .RCJ
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ internal static RoboCommand Parse(FileInfo file)
+ {
+ if (file.Extension != JOBFILE_Extension) return null;
+ return Parse(file.OpenText());
+ }
+
+ ///
+ /// Use to read all lines from the supplied file path.
+ /// If FileExtension != ".RCJ" -> returns null. Otherwise parses the file.
+ ///
+ /// File Path to some Job File. File Path should end in .RCJ
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ internal static RoboCommand Parse(string path)
+ {
+ if (Path.GetExtension(path) != JOBFILE_Extension) return null;
+ return Parse(File.OpenText(path));
+ }
+
+ ///
+ /// Read each line from a StreamReader and attempt to produce a Job File.
+ ///
+ /// StreamReader for a file stream that represents a Job File
+ ///
+ [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
+ internal static RoboCommand Parse(StreamReader streamReader)
+ {
+ List Lines = new List();
+ using (streamReader)
+ {
+ while (!streamReader.EndOfStream)
+ Lines.Add(streamReader.ReadLine());
+ }
+ return Parse(Lines);
+ }
+
+ #endregion
+
+ #region <> Main Parse Routine >
+
+ ///
+ /// Parse each line in , and attempt to create a new JobFile object.
+ ///
+ /// String[] read from a JobFile
+ ///
+ internal static RoboCommand Parse(IEnumerable Lines)
+ {
+ //Extract information from the Lines to quicken processing in *OptionsRegex classes
+ List Flags = new List();
+ List ValueFlags = new List();
+ RetryOptions retryOpt = new RetryOptions();
+
+ string JobName = null;
+ bool stopIfDisposing = true;
+
+ foreach (string ln in Lines)
+ {
+ if (ln.IsNullOrWhiteSpace() | ln.Trim() == "::")
+ { }
+ else if (LINE_IsSwitch.IsMatch(ln) || LINE_IsSwitch_NoComment.IsMatch(ln))
+ {
+ var groups = LINE_IsSwitch.Match(ln).Groups;
+
+ //Check RetryOptions inline since it only has 4 properties to check against
+ if (groups["SWITCH"].Value == "/R" && LINE_IsSwitch_NumericValue.IsMatch(ln))
+ {
+ string val = LINE_IsSwitch_NumericValue.Match(ln).Groups["VALUE"].Value;
+ retryOpt.RetryCount = val.IsNullOrWhiteSpace() ? retryOpt.RetryCount : Convert.ToInt32(val);
+ }
+ else if (groups["SWITCH"].Value == "/W")
+ {
+ string val = LINE_IsSwitch_NumericValue.Match(ln).Groups["VALUE"].Value;
+ retryOpt.RetryWaitTime = val.IsNullOrWhiteSpace() ? retryOpt.RetryWaitTime : Convert.ToInt32(val);
+ }
+ else if (groups["SWITCH"].Value == "/REG")
+ {
+ retryOpt.SaveToRegistry = true;
+ }
+ else if (groups["SWITCH"].Value == "/TBD")
+ {
+ retryOpt.WaitForSharenames = true;
+ }
+ //All Other flags
+ else
+ {
+ Flags.Add(groups["SWITCH"]);
+ if (groups["DELIMITER"].Success)
+ ValueFlags.Add(groups);
+ }
+ }
+ else if (JobName == null && JobNameRegex.IsMatch(ln))
+ {
+ JobName = JobNameRegex.Match(ln).Groups["NAME"].Value.Trim();
+ }
+ else if (StopIfDisposingRegex.IsMatch(ln))
+ {
+ stopIfDisposing = Convert.ToBoolean(StopIfDisposingRegex.Match(ln).Groups["VALUE"].Value);
+ }
+
+ }
+
+ CopyOptions copyOpt = Build_CopyOptions(Flags, ValueFlags, Lines);
+ SelectionOptions selectionOpt = Build_SelectionOptions(Flags, ValueFlags, Lines);
+ LoggingOptions loggingOpt = Build_LoggingOptions(Flags, ValueFlags, Lines);
+ JobOptions jobOpt = Build_JobOptions(Flags, ValueFlags, Lines);
+
+ return new RoboCommand(JobName ?? "", StopIfDisposing: stopIfDisposing, source: null, destination: null, configuration: null,
+ copyOptions: copyOpt,
+ selectionOptions: selectionOpt,
+ retryOptions: retryOpt,
+ loggingOptions: loggingOpt,
+ jobOptions: jobOpt);
+ }
+
+ #endregion
+
+ #region < Copy Options >
+
+ ///
+ /// Parser to create CopyOptions object for JobFiles
+ ///
+ private static CopyOptions Build_CopyOptions(IEnumerable Flags, IEnumerable ValueFlags, IEnumerable Lines)
+ {
+ var options = new CopyOptions();
+
+ //Bool Checks
+ options.CheckPerFile = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.CHECK_PER_FILE.Trim());
+ options.CopyAll = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.COPY_ALL.Trim());
+ options.CopyFilesWithSecurity = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.COPY_FILES_WITH_SECURITY.Trim());
+ options.CopySubdirectories = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.COPY_SUBDIRECTORIES.Trim());
+ options.CopySubdirectoriesIncludingEmpty = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.COPY_SUBDIRECTORIES_INCLUDING_EMPTY.Trim());
+ options.CopySymbolicLink = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.COPY_SYMBOLIC_LINK.Trim());
+ options.CreateDirectoryAndFileTree = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.CREATE_DIRECTORY_AND_FILE_TREE.Trim());
+ options.DoNotCopyDirectoryInfo = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.DO_NOT_COPY_DIRECTORY_INFO.Trim());
+ options.DoNotUseWindowsCopyOffload = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.DO_NOT_USE_WINDOWS_COPY_OFFLOAD.Trim());
+ options.EnableBackupMode = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.ENABLE_BACKUP_MODE.Trim());
+ options.EnableEfsRawMode = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.ENABLE_EFSRAW_MODE.Trim());
+ options.EnableRestartMode = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.ENABLE_RESTART_MODE.Trim());
+ options.EnableRestartModeWithBackupFallback = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.ENABLE_RESTART_MODE_WITH_BACKUP_FALLBACK.Trim());
+ options.FatFiles = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.FAT_FILES.Trim());
+ options.FixFileSecurityOnAllFiles = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.FIX_FILE_SECURITY_ON_ALL_FILES.Trim());
+ options.FixFileTimesOnAllFiles = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.FIX_FILE_TIMES_ON_ALL_FILES.Trim());
+ options.Mirror = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.MIRROR.Trim());
+ options.MoveFiles = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.MOVE_FILES.Trim());
+ options.MoveFilesAndDirectories = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.MOVE_FILES_AND_DIRECTORIES.Trim());
+ options.Purge = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.PURGE.Trim());
+ options.RemoveFileInformation = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.REMOVE_FILE_INFORMATION.Trim());
+ options.TurnLongPathSupportOff = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.TURN_LONG_PATH_SUPPORT_OFF.Trim());
+ options.UseUnbufferedIo = Flags.Any(flag => flag.Success && flag.Value == CopyOptions.USE_UNBUFFERED_IO.Trim());
+
+ //int / string values on same line as flag
+ foreach (var match in ValueFlags)
+ {
+ string flag = match["SWITCH"].Value.Trim();
+ string value = match["VALUE"].Value.Trim();
+
+ switch (flag)
+ {
+ case "/A+":
+ options.AddAttributes = value;
+ break;
+ case "/COPY":
+ options.CopyFlags = value;
+ break;
+ case "/LEV":
+ options.Depth = value.TryConvertInt();
+ break;
+ case "/DD":
+ options.Destination = value;
+ break;
+ case "/DCOPY":
+ options.DirectoryCopyFlags = value;
+ break;
+ case "/IPG":
+ options.InterPacketGap = value.TryConvertInt();
+ break;
+ case "/MON":
+ options.MonitorSourceChangesLimit = value.TryConvertInt();
+ break;
+ case "/MOT":
+ options.MonitorSourceTimeLimit = value.TryConvertInt();
+ break;
+ case "/MT":
+ options.MultiThreadedCopiesCount = value.TryConvertInt();
+ break;
+ case "/A-":
+ options.RemoveAttributes = value;
+ break;
+ case "/RH":
+ options.RunHours = value;
+ break;
+ case "/SD":
+ options.Source = value;
+ break;
+ }
+ }
+
+ //Multiple Lines
+ if (Flags.Any(f => f.Value == "/IF"))
+ {
+ bool parsingIF = false;
+ List filters = new List();
+ string path = null;
+ //Find the line that starts with the flag
+ foreach (string ln in Lines)
+ {
+ if (ln.IsNullOrWhiteSpace() || LINE_IsComment.IsMatch(ln))
+ { }
+ else if (LINE_IsSwitch.IsMatch(ln))
+ {
+ if (parsingIF) break; //Moving onto next section -> IF already parsed.
+ parsingIF = ln.Trim().StartsWith("/IF");
+ }
+ else if (parsingIF)
+ {
+ //React to parsing the section - Comments are not expected on these lines
+ path = null;
+ if (DirFileFilterRegex.IsMatch(ln))
+ {
+ path = DirFileFilterRegex.Match(ln).Groups["PATH"].Value;
+ }
+ else if (DirFileFilterRegex_NoComment.IsMatch(ln))
+ {
+ path = DirFileFilterRegex_NoComment.Match(ln).Groups["PATH"].Value;
+ }
+ //Store the value
+ if (!path.IsNullOrWhiteSpace())
+ {
+ filters.Add(path.WrapPath());
+ }
+ }
+ }
+ if (filters.Count > 0) options.FileFilter = filters;
+ } //End of FileFilter section
+
+ return options;
+ }
+
+ #endregion
+
+ #region < Selection Options >
+
+ ///
+ /// Parser to create SelectionOptions object for JobFiles
+ ///
+ private static SelectionOptions Build_SelectionOptions(IEnumerable Flags, IEnumerable ValueFlags, IEnumerable Lines)
+ {
+ var options = new SelectionOptions();
+
+ //Bool Checks
+ options.CompensateForDstDifference = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.COMPENSATE_FOR_DST_DIFFERENCE.Trim());
+ options.ExcludeChanged = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.EXCLUDE_CHANGED.Trim());
+ options.ExcludeExtra = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.EXCLUDE_EXTRA.Trim());
+ options.ExcludeJunctionPoints = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.EXCLUDE_JUNCTION_POINTS.Trim());
+ options.ExcludeJunctionPointsForDirectories = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.EXCLUDE_JUNCTION_POINTS_FOR_DIRECTORIES.Trim());
+ options.ExcludeJunctionPointsForFiles = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.EXCLUDE_JUNCTION_POINTS_FOR_FILES.Trim());
+ options.ExcludeLonely = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.EXCLUDE_LONELY.Trim());
+ options.ExcludeNewer = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.EXCLUDE_NEWER.Trim());
+ options.ExcludeOlder = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.EXCLUDE_OLDER.Trim());
+ options.IncludeSame = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.INCLUDE_SAME.Trim());
+ options.IncludeTweaked = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.INCLUDE_TWEAKED.Trim());
+ options.OnlyCopyArchiveFiles = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.ONLY_COPY_ARCHIVE_FILES.Trim());
+ options.OnlyCopyArchiveFilesAndResetArchiveFlag = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.ONLY_COPY_ARCHIVE_FILES_AND_RESET_ARCHIVE_FLAG.Trim());
+ options.UseFatFileTimes = Flags.Any(flag => flag.Success && flag.Value == SelectionOptions.USE_FAT_FILE_TIMES.Trim());
+
+ //int / string values on same line as flag
+ foreach (var match in ValueFlags)
+ {
+ string flag = match["SWITCH"].Value;
+ string value = match["VALUE"].Value;
+
+ switch (flag)
+ {
+ case "/XA":
+ options.ExcludeAttributes = value;
+ break;
+ case "/IA":
+ options.IncludeAttributes = value;
+ break;
+ case "/MAXAGE":
+ options.MaxFileAge = value;
+ break;
+ case "/MAX":
+ options.MaxFileSize = value.TryConvertLong();
+ break;
+ case "/MAXLAD":
+ options.MaxLastAccessDate = value;
+ break;
+ case "/MINAGE":
+ options.MinFileAge = value;
+ break;
+ case "/MIN":
+ options.MinFileSize = value.TryConvertLong();
+ break;
+ case "/MINLAD":
+ options.MinLastAccessDate = value;
+ break;
+ }
+ }
+
+ //Multiple Lines
+ bool parsingXD = false;
+ bool parsingXF = false;
+ bool xDParsed = false;
+ bool xFParsed = false;
+ string path = null;
+
+ foreach (string ln in Lines)
+ {
+ // Determine if parsing some section
+ if (ln.IsNullOrWhiteSpace() || LINE_IsComment.IsMatch(ln) )
+ { }
+ else if (!xFParsed && !parsingXF && SelectionRegex_ExcludeFiles.IsMatch(ln))
+ {
+ // Paths are not expected to be on this output line
+ parsingXF = true;
+ parsingXD = false;
+ }
+ else if (!xDParsed && !parsingXD && SelectionRegex_ExcludeDirs.IsMatch(ln))
+ {
+ // Paths are not expected to be on this output line
+ parsingXF = false;
+ parsingXD = true;
+ }
+ else if (LINE_IsSwitch.IsMatch(ln) || LINE_IsSwitch_NoComment.IsMatch(ln))
+ {
+ if (parsingXD)
+ {
+ parsingXD = false;
+ xDParsed = true;
+ }
+ if (parsingXF)
+ {
+ parsingXF = false;
+ xFParsed = true;
+ }
+ if (xDParsed && xFParsed) break;
+ }
+ else
+ {
+ //React to parsing the section - Comments are not expected on these lines
+ path = null;
+ if (DirFileFilterRegex.IsMatch(ln))
+ {
+ path = DirFileFilterRegex.Match(ln).Groups["PATH"].Value;
+ }
+ else if (DirFileFilterRegex_NoComment.IsMatch(ln))
+ {
+ path = DirFileFilterRegex_NoComment.Match(ln).Groups["PATH"].Value;
+ }
+ //Store the value
+ if (!path.IsNullOrWhiteSpace())
+ {
+ if (parsingXF)
+ options.ExcludedFiles.Add(path.WrapPath());
+ else if (parsingXD)
+ options.ExcludedDirectories.Add(path.WrapPath());
+ }
+
+ }
+ }
+ return options;
+ }
+
+ #endregion
+
+ #region < Logging Options >
+
+ ///
+ /// Parser to create LoggingOptions object for JobFiles
+ ///
+ private static LoggingOptions Build_LoggingOptions(IEnumerable Flags, IEnumerable ValueFlags, IEnumerable Lines)
+ {
+ var options = new LoggingOptions();
+
+ //Bool Checks
+ options.IncludeFullPathNames = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.INCLUDE_FULL_PATH_NAMES.Trim());
+ options.IncludeSourceTimeStamps = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.INCLUDE_SOURCE_TIMESTAMPS.Trim());
+ options.ListOnly = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.LIST_ONLY.Trim());
+ options.NoDirectoryList = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.NO_DIRECTORY_LIST.Trim());
+ options.NoFileClasses = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.NO_FILE_CLASSES.Trim());
+ options.NoFileList = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.NO_FILE_LIST.Trim());
+ options.NoFileSizes = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.NO_FILE_SIZES.Trim());
+ options.NoJobHeader = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.NO_JOB_HEADER.Trim());
+ options.NoJobSummary = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.NO_JOB_SUMMARY.Trim());
+ options.NoProgress = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.NO_PROGRESS.Trim());
+ options.OutputAsUnicode = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.OUTPUT_AS_UNICODE.Trim());
+ options.OutputToRoboSharpAndLog = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.OUTPUT_TO_ROBOSHARP_AND_LOG.Trim());
+ options.PrintSizesAsBytes = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.PRINT_SIZES_AS_BYTES.Trim());
+ options.ReportExtraFiles = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.REPORT_EXTRA_FILES.Trim());
+ options.ShowEstimatedTimeOfArrival = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.SHOW_ESTIMATED_TIME_OF_ARRIVAL.Trim());
+ options.VerboseOutput = Flags.Any(flag => flag.Success && flag.Value == LoggingOptions.VERBOSE_OUTPUT.Trim());
+
+ //int / string values on same line as flag
+ foreach (var match in ValueFlags)
+ {
+ string flag = match["SWITCH"].Value;
+ string value = match["VALUE"].Value;
+
+ switch (flag)
+ {
+ case "/LOG+":
+ options.AppendLogPath = value;
+ break;
+ case "/UNILOG+":
+ options.AppendUnicodeLogPath = value;
+ break;
+ case "/LOG":
+ options.LogPath = value;
+ break;
+ case "/UNILOG":
+ options.UnicodeLogPath = value;
+ break;
+ }
+ }
+
+ return options;
+ }
+
+ #endregion
+
+ #region < Job Options >
+
+ ///
+ /// Parser to create JobOptions object for JobFiles
+ ///
+ private static JobOptions Build_JobOptions(IEnumerable Flags, IEnumerable ValueFlags, IEnumerable Lines)
+ {
+ return new JobOptions();
+ }
+
+ #endregion
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/JobOptions.cs b/FSI.Lib/Tools/RoboSharp/JobOptions.cs
new file mode 100644
index 0000000..ff1f373
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/JobOptions.cs
@@ -0,0 +1,186 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public class JobOptions : ICloneable
+ {
+ // For more information, a good resource is here: https://adamtheautomator.com/robocopy/#Robocopy_Jobs
+
+ #region < Constructors >
+
+ ///
+ ///
+ ///
+ public JobOptions() { }
+
+ ///
+ /// Constructor for ICloneable Interface
+ ///
+ /// JobOptions object to clone
+ public JobOptions(JobOptions options)
+ {
+ FilePath = options.FilePath;
+ NoDestinationDirectory = options.NoDestinationDirectory;
+ NoSourceDirectory = options.NoSourceDirectory;
+ PreventCopyOperation = options.PreventCopyOperation;
+ }
+
+ #endregion
+
+ #region < ICloneable >
+
+ ///
+ /// Clone this JobOptions object
+ ///
+ /// New JobOptions object
+ public JobOptions Clone() => new JobOptions(this);
+
+ object ICloneable.Clone() => Clone();
+
+ #endregion
+
+ #region < Constants >
+
+ ///
+ /// Take parameters from the named job file
+ ///
+ ///
+ /// Usage: /JOB:"Path\To\File.RCJ"
+ ///
+ internal const string JOB_LOADNAME = " /JOB:";
+
+ ///
+ /// Save parameters to the named job file
+ ///
+ ///
+ /// Usage:
+ /// /SAVE:"Path\To\File" -> Creates Path\To\File.RCJ
+ /// /SAVE:"Path\To\File.txt" -> Creates Path\To\File.txt.RCJ
+ ///
+ internal const string JOB_SAVE = " /SAVE:";
+
+ ///
+ /// Quit after processing command line
+ ///
+ ///
+ /// Used when writing JobFile
+ ///
+ internal const string JOB_QUIT = " /QUIT";
+
+ ///
+ /// No source directory is specified
+ ///
+ internal const string JOB_NoSourceDirectory = " /NOSD";
+
+ ///
+ /// No destination directory is specified
+ ///
+ internal const string JOB_NoDestinationDirectory = " /NODD";
+
+ #endregion
+
+ #region < Properties >
+
+ ///
+ /// FilePath to save the Job Options (.RCJ) file to.
+ /// /SAVE:{FilePath}
+ ///
+ ///
+ /// This causes RoboCopy to generate an RCJ file where the command options are stored to so it can be used later.
+ /// and options are only evaluated if this is set.
+ ///
+ public virtual string FilePath { get; set; } = "";
+
+ ///
+ /// RoboCopy will validate the command, then exit before performing any Move/Copy/List operations.
+ /// /QUIT
+ ///
+ ///
+ /// This option is typically used when generating JobFiles. RoboCopy will exit after saving the Job FIle to the specified
+ ///
+ public virtual bool PreventCopyOperation { get; set; }
+
+ ///
+ /// path will not be saved to the JobFile.
+ /// /NOSD
+ ///
+ ///
+ /// Default value is False, meaning if is set, it will be saved to the JobFile RoboCopy generates.
+ ///
+ public virtual bool NoSourceDirectory { get; set; }
+
+ ///
+ /// path will not be saved to the JobFile.
+ /// /NODD
+ ///
+ ///
+ /// Default value is False, meaning if is set, it will be saved to the JobFile RoboCopy generates.
+ ///
+ public virtual bool NoDestinationDirectory { get; set; }
+ #endregion
+
+ #region < Methods >
+
+ ///
+ /// Parse the properties and return the string
+ ///
+ ///
+ internal string Parse()
+ {
+ string options = "";
+ if (!FilePath.IsNullOrWhiteSpace())
+ {
+ options += $"{JOB_SAVE}{FilePath.WrapPath()}";
+ if (NoSourceDirectory) options += JOB_NoSourceDirectory;
+ if (NoDestinationDirectory) options += JOB_NoDestinationDirectory;
+ }
+ if (PreventCopyOperation) options += JOB_QUIT;
+ return options;
+ }
+
+ ///
+ /// Adds the 'NAME' and other properties into the JobFile
+ ///
+ internal void RunPostProcessing(RoboCommand cmd)
+ {
+ if (FilePath.IsNullOrWhiteSpace()) return;
+ if (!File.Exists(FilePath)) return;
+ var txt = File.ReadAllLines(FilePath).ToList();
+ //Write Options to JobFile
+ txt.InsertRange(6, new string[]
+ {
+ "",
+ "::",
+ ":: Options for RoboSharp.JobFile",
+ "::",
+ JobFileBuilder.JOBFILE_JobName + " " + cmd.Name,
+ JobFileBuilder.JOBFILE_StopIfDisposing + " " + cmd.StopIfDisposing.ToString().ToUpper()
+ });
+ File.WriteAllLines(FilePath, txt);
+ }
+
+ ///
+ /// Combine this object with another RetryOptions object.
+ /// not not be modified.
+ ///
+ ///
+ public void Merge(JobOptions options)
+ {
+ NoSourceDirectory |= options.NoSourceDirectory;
+ NoDestinationDirectory |= options.NoDestinationDirectory;
+ PreventCopyOperation |= options.PreventCopyOperation;
+ }
+
+ #endregion
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/LoggingOptions.cs b/FSI.Lib/Tools/RoboSharp/LoggingOptions.cs
new file mode 100644
index 0000000..fa3009c
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/LoggingOptions.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Text;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ /// Options related to the output logs generated by RoboCopy
+ ///
+ ///
+ ///
+ ///
+ public class LoggingOptions : ICloneable
+ {
+ #region Constructors
+
+ ///
+ /// Create new LoggingOptions with Default Settings
+ ///
+ public LoggingOptions() { }
+
+ ///
+ /// Clone a LoggingOptions Object
+ ///
+ /// LoggingOptions object to clone
+ public LoggingOptions(LoggingOptions options)
+ {
+ ListOnly = options.ListOnly;
+ ReportExtraFiles = options.ReportExtraFiles;
+ VerboseOutput = options.VerboseOutput;
+ IncludeSourceTimeStamps = options.IncludeSourceTimeStamps;
+ IncludeFullPathNames = options.IncludeFullPathNames;
+ PrintSizesAsBytes = options.PrintSizesAsBytes;
+ NoFileSizes = options.NoFileSizes;
+ NoFileClasses = options.NoFileClasses;
+ NoFileList = options.NoFileList;
+ NoDirectoryList = options.NoDirectoryList;
+ NoProgress = options.NoProgress;
+ ShowEstimatedTimeOfArrival = options.ShowEstimatedTimeOfArrival;
+ LogPath = options.LogPath;
+ AppendLogPath = options.AppendLogPath;
+ UnicodeLogPath = options.UnicodeLogPath;
+ AppendUnicodeLogPath = options.AppendUnicodeLogPath;
+ OutputToRoboSharpAndLog = options.OutputToRoboSharpAndLog;
+ NoJobHeader = options.NoJobHeader;
+ NoJobSummary = options.NoJobSummary;
+ OutputAsUnicode = options.OutputAsUnicode;
+
+ }
+
+ ///
+ public LoggingOptions Clone() => new LoggingOptions(this);
+
+ object ICloneable.Clone() => Clone();
+
+ #endregion
+
+ internal const string LIST_ONLY = "/L ";
+ internal const string REPORT_EXTRA_FILES = "/X ";
+ internal const string VERBOSE_OUTPUT = "/V ";
+ internal const string INCLUDE_SOURCE_TIMESTAMPS = "/TS ";
+ internal const string INCLUDE_FULL_PATH_NAMES = "/FP ";
+ internal const string PRINT_SIZES_AS_BYTES = "/BYTES ";
+ internal const string NO_FILE_SIZES = "/NS ";
+ internal const string NO_FILE_CLASSES = "/NC ";
+ internal const string NO_FILE_LIST = "/NFL ";
+ internal const string NO_DIRECTORY_LIST = "/NDL ";
+ internal const string NO_PROGRESS = "/NP ";
+ internal const string SHOW_ESTIMATED_TIME_OF_ARRIVAL = "/ETA ";
+ internal const string LOG_PATH = "/LOG:{0} ";
+ internal const string APPEND_LOG_PATH = "/LOG+:{0} ";
+ internal const string UNICODE_LOG_PATH = "/UNILOG:{0} ";
+ internal const string APPEND_UNICODE_LOG_PATH = "/UNILOG+:{0} ";
+ internal const string OUTPUT_TO_ROBOSHARP_AND_LOG = "/TEE ";
+ internal const string NO_JOB_HEADER = "/NJH ";
+ internal const string NO_JOB_SUMMARY = "/NJS ";
+ internal const string OUTPUT_AS_UNICODE = "/UNICODE ";
+
+ ///
+ /// Do not copy, timestamp or delete any files.
+ /// [/L]
+ ///
+ public virtual bool ListOnly { get; set; }
+ ///
+ /// Report all extra files, not just those selected.
+ /// [X]
+ ///
+ public virtual bool ReportExtraFiles { get; set; }
+ ///
+ /// Produce verbose output, showing skipped files.
+ /// [V]
+ ///
+ public virtual bool VerboseOutput { get; set; } = true;
+ ///
+ /// Include source file time stamps in the output.
+ /// [/TS]
+ ///
+ public virtual bool IncludeSourceTimeStamps { get; set; }
+ ///
+ /// Include full path names of files in the output.
+ /// [/FP]
+ ///
+ public virtual bool IncludeFullPathNames { get; set; }
+ ///
+ /// Print sizes as bytes in the output.
+ /// [/BYTES]
+ ///
+ public virtual bool PrintSizesAsBytes { get; set; }
+ ///
+ /// Do not log file sizes.
+ /// [/NS]
+ ///
+ public virtual bool NoFileSizes { get; set; }
+ ///
+ /// Do not log file classes.
+ /// [/NC]
+ ///
+ public virtual bool NoFileClasses { get; set; }
+ ///
+ /// Do not log file names.
+ /// [/NFL]
+ /// WARNING: If this is set to TRUE then GUI cannot handle showing progress correctly as it can't get information it requires from the log
+ ///
+ public virtual bool NoFileList { get; set; }
+ ///
+ /// Do not log directory names.
+ /// [/NDL]
+ ///
+ public virtual bool NoDirectoryList { get; set; }
+ ///
+ /// Do not log percentage copied.
+ /// [/NP]
+ ///
+ public virtual bool NoProgress { get; set; }
+ ///
+ /// Show estimated time of arrival of copied files.
+ /// [/ETA]
+ ///
+ public virtual bool ShowEstimatedTimeOfArrival { get; set; }
+ ///
+ /// Output status to LOG file (overwrite existing log).
+ /// [/LOG:file]
+ ///
+ public virtual string LogPath { get; set; }
+ ///
+ /// Output status to LOG file (append to existing log).
+ /// [/LOG+:file]
+ ///
+ public virtual string AppendLogPath { get; set; }
+ ///
+ /// Output status to LOG file as UNICODE (overwrite existing log).
+ /// [/UNILOG:file]
+ ///
+ public virtual string UnicodeLogPath { get; set; }
+ ///
+ /// Output status to LOG file as UNICODE (append to existing log).
+ /// [/UNILOG+:file]
+ ///
+ public virtual string AppendUnicodeLogPath { get; set; }
+ ///
+ /// Output to RoboSharp and Log.
+ /// [/TEE]
+ ///
+ public virtual bool OutputToRoboSharpAndLog { get; set; }
+ ///
+ /// Do not output a Job Header.
+ /// [/NJH]
+ ///
+ public virtual bool NoJobHeader { get; set; }
+ ///
+ /// Do not output a Job Summary.
+ /// [/NJS]
+ /// WARNING: If this is set to TRUE then statistics will not work correctly as this information is gathered from the job summary part of the log
+ ///
+ public virtual bool NoJobSummary { get; set; }
+ ///
+ /// Output as UNICODE.
+ /// [/UNICODE]
+ ///
+ public virtual bool OutputAsUnicode { get; set; }
+
+ /// Encase the LogPath in quotes if needed
+ internal string WrapPath(string logPath) => (!logPath.StartsWith("\"") && logPath.Contains(" ")) ? $"\"{logPath}\"" : logPath;
+
+ internal string Parse()
+ {
+ var options = new StringBuilder();
+
+ if (ListOnly)
+ options.Append(LIST_ONLY);
+ if (ReportExtraFiles)
+ options.Append(REPORT_EXTRA_FILES);
+ if (VerboseOutput)
+ options.Append(VERBOSE_OUTPUT);
+ if (IncludeSourceTimeStamps)
+ options.Append(INCLUDE_SOURCE_TIMESTAMPS);
+ if (IncludeFullPathNames)
+ options.Append(INCLUDE_FULL_PATH_NAMES);
+ if (PrintSizesAsBytes)
+ options.Append(PRINT_SIZES_AS_BYTES);
+ if (NoFileSizes)
+ options.Append(NO_FILE_SIZES);
+ if (NoFileClasses)
+ options.Append(NO_FILE_CLASSES);
+ if (NoFileList)
+ options.Append(NO_FILE_LIST);
+ if (NoDirectoryList)
+ options.Append(NO_DIRECTORY_LIST);
+ if (NoProgress)
+ options.Append(NO_PROGRESS);
+ if (ShowEstimatedTimeOfArrival)
+ options.Append(SHOW_ESTIMATED_TIME_OF_ARRIVAL);
+ if (!LogPath.IsNullOrWhiteSpace())
+ options.Append(string.Format(LOG_PATH, WrapPath(LogPath)));
+ if (!AppendLogPath.IsNullOrWhiteSpace())
+ options.Append(string.Format(APPEND_LOG_PATH, WrapPath(AppendLogPath)));
+ if (!UnicodeLogPath.IsNullOrWhiteSpace())
+ options.Append(string.Format(UNICODE_LOG_PATH, WrapPath(UnicodeLogPath)));
+ if (!AppendUnicodeLogPath.IsNullOrWhiteSpace())
+ options.Append(string.Format(APPEND_UNICODE_LOG_PATH, WrapPath(AppendUnicodeLogPath)));
+ if (OutputToRoboSharpAndLog)
+ options.Append(OUTPUT_TO_ROBOSHARP_AND_LOG);
+ if (NoJobHeader)
+ options.Append(NO_JOB_HEADER);
+ if (NoJobSummary)
+ options.Append(NO_JOB_SUMMARY);
+ if (OutputAsUnicode)
+ options.Append(OUTPUT_AS_UNICODE);
+
+ return options.ToString();
+ }
+
+ ///
+ /// Combine this object with another LoggingOptions object.
+ /// Any properties marked as true take priority. IEnumerable items are combined.
+ /// String Values will only be replaced if the primary object has a null/empty value for that property.
+ ///
+ ///
+ public void Merge(LoggingOptions options)
+ {
+ ListOnly |= options.ListOnly;
+ ReportExtraFiles |= options.ReportExtraFiles;
+ VerboseOutput |= options.VerboseOutput;
+ IncludeSourceTimeStamps |= options.IncludeSourceTimeStamps;
+ IncludeFullPathNames |= options.IncludeFullPathNames;
+ PrintSizesAsBytes |= options.PrintSizesAsBytes;
+ NoFileSizes |= options.NoFileSizes;
+ NoFileClasses |= options.NoFileClasses;
+ NoFileList |= options.NoFileList;
+ NoDirectoryList |= options.NoDirectoryList;
+ NoProgress |= options.NoProgress;
+ ShowEstimatedTimeOfArrival |= options.ShowEstimatedTimeOfArrival;
+ OutputToRoboSharpAndLog |= options.OutputToRoboSharpAndLog;
+ NoJobHeader |= options.NoJobHeader;
+ NoJobSummary |= options.NoJobSummary;
+ OutputAsUnicode |= options.OutputAsUnicode;
+
+ LogPath = LogPath.ReplaceIfEmpty(options.LogPath);
+ AppendLogPath = AppendLogPath.ReplaceIfEmpty(options.AppendLogPath);
+ UnicodeLogPath = UnicodeLogPath.ReplaceIfEmpty(options.UnicodeLogPath);
+ AppendUnicodeLogPath = AppendUnicodeLogPath.ReplaceIfEmpty(options.AppendUnicodeLogPath);
+
+ }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/NativeMethods.cs b/FSI.Lib/Tools/RoboSharp/NativeMethods.cs
new file mode 100644
index 0000000..1ef60f5
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/NativeMethods.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ ///
+ /// Native Methods for Pause/Suspend/Resume processes
+ ///
+ public static class NativeMethods
+ {
+ [Flags]
+ public enum ThreadAccess : int
+ {
+ TERMINATE = (0x0001),
+ SUSPEND_RESUME = (0x0002),
+ GET_CONTEXT = (0x0008),
+ SET_CONTEXT = (0x0010),
+ SET_INFORMATION = (0x0020),
+ QUERY_INFORMATION = (0x0040),
+ SET_THREAD_TOKEN = (0x0080),
+ IMPERSONATE = (0x0100),
+ DIRECT_IMPERSONATION = (0x0200)
+ }
+
+ [DllImport("kernel32.dll")]
+ static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
+ [DllImport("kernel32.dll")]
+ static extern uint SuspendThread(IntPtr hThread);
+ [DllImport("kernel32.dll")]
+ static extern int ResumeThread(IntPtr hThread);
+
+ public static bool Suspend(this Process process)
+ {
+ if (process.HasExited) return false;
+ foreach (ProcessThread thread in process.Threads)
+ {
+ var pOpenThread = OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint)thread.Id);
+ if (pOpenThread == IntPtr.Zero)
+ {
+ break;
+ }
+ SuspendThread(pOpenThread);
+ }
+ return true;
+ }
+ public static bool Resume(this Process process)
+ {
+ if (process.HasExited) return false;
+ foreach (ProcessThread thread in process.Threads)
+ {
+ var pOpenThread = OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint)thread.Id);
+ if (pOpenThread == IntPtr.Zero)
+ {
+ break;
+ }
+ ResumeThread(pOpenThread);
+ }
+ return true;
+ }
+ }
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+}
diff --git a/FSI.Lib/Tools/RoboSharp/ObservableList.cs b/FSI.Lib/Tools/RoboSharp/ObservableList.cs
new file mode 100644
index 0000000..6f6efa5
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/ObservableList.cs
@@ -0,0 +1,411 @@
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Collections.Specialized;
+using System.Collections.ObjectModel;
+using System.Threading;
+
+namespace System.Collections.Generic
+{
+ ///
+ /// Extends the Generic class with an event that will fire when the list is updated via standard list methods
+ ///
+ /// Type of object the list will contain
+ ///
+ /// This class is being provided by the RoboSharp DLL
+ ///
+ ///
+ ///
+ public class ObservableList : List, INotifyCollectionChanged
+ {
+ #region < Constructors >
+
+ ///
+ public ObservableList() : base() { }
+
+ ///
+ public ObservableList(int capacity) : base(capacity) { }
+
+ ///
+ public ObservableList(IEnumerable collection) : base(collection) { }
+
+ #endregion
+
+ #region < Events >
+
+ /// This event fires whenever the List's array is updated.
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ ///
+ /// Raise the event.
+ ///
+ /// Override this method to provide post-processing of Added/Removed items within derived classes.
+ ///
+ ///
+ protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+ {
+ // This Syncronization code was taken from here: https://stackoverflow.com/a/54733415/12135042
+ // This ensures that the CollectionChanged event is invoked from the proper thread, no matter where the change came from.
+
+ if (SynchronizationContext.Current == _synchronizationContext)
+ {
+ // Execute the CollectionChanged event on the current thread
+ CollectionChanged?.Invoke(this, e);
+ }
+ else
+ {
+ // Raises the CollectionChanged event on the creator thread
+ _synchronizationContext.Send((callback) => CollectionChanged?.Invoke(this, e), null);
+ }
+ }
+ private SynchronizationContext _synchronizationContext = SynchronizationContext.Current;
+
+ #region < Alternate methods for OnCollectionChanged + reasoning why it wasn't used >
+
+ /*
+ * This standard method cannot be used because RoboQueue is adding results onto the ResultsLists as they complete, which means the events may not be on the original thread that RoboQueue was constructed in.
+ * protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) => CollectionChanged?.Invoke(this, e);
+ */
+
+ // --------------------------------------------------------------------------------------------------------------
+
+ /*
+ * This code was taken from here: https://www.codeproject.com/Articles/64936/Threadsafe-ObservableImmutable-Collection
+ * It works, but its a bit more involved since it loops through all handlers.
+ * This was not used due to being unavailable in some targets. (Same reasoning for creating new class instead of class provided by above link)
+ */
+
+ // ///
+ // /// Raise the event.
+ // /// Override this method to provide post-processing of Added/Removed items within derived classes.
+ // ///
+ // ///
+ // protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+ // {
+ // var notifyCollectionChangedEventHandler = CollectionChanged;
+
+ // if (notifyCollectionChangedEventHandler == null)
+ // return;
+
+ // foreach (NotifyCollectionChangedEventHandler handler in notifyCollectionChangedEventHandler.GetInvocationList())
+ // {
+ //#if NET40_OR_GREATER
+ // var dispatcherObject = handler.Target as DispatcherObject;
+
+ // if (dispatcherObject != null && !dispatcherObject.CheckAccess())
+ // {
+ // dispatcherObject.Dispatcher.Invoke(handler, this, e);
+ // }
+ // else
+ //#endif
+ // handler(this, e); // note : this does not execute handler in target thread's context
+ // }
+ // }
+
+ // --------------------------------------------------------------------------------------------------------------
+ #endregion
+
+ #endregion
+
+ #region < New Methods >
+
+ ///
+ /// Replace an item in the list.
+ ///
+ /// Search for this item in the list. If found, replace it. If not found, return false. will not be added to the list.
+ /// This item will replace the . If was not found, this item does not get added to the list.
+ /// True if the was found in the list and successfully replaced. Otherwise false.
+ public virtual bool Replace(T itemToReplace, T newItem)
+ {
+ if (!this.Contains(itemToReplace)) return false;
+ return Replace(this.IndexOf(itemToReplace), newItem);
+ }
+
+
+ ///
+ /// Replace an item in the list
+ ///
+ /// Index of the item to replace
+ /// This item will replace the item at the specified
+ /// True if the the item was successfully replaced. Otherwise throws.
+ ///
+ public virtual bool Replace(int index, T newItem)
+ {
+ this[index] = newItem;
+ return true;
+ }
+
+ ///
+ /// Replaces the items in this list with the items in supplied collection. If the collection has more items than will be removed from the list, the remaining items will be added to the list.
+ /// EX: List has 10 items, collection has 5 items, index of 8 is specified (which is item 9 on 0-based index) -> Item 9 + 10 are replaced, and remaining 3 items from collection are added to the list.
+ ///
+ /// Index of the item to replace
+ /// Collection of items to insert into the list.
+ /// True if the the collection was successfully inserted into the list. Otherwise throws.
+ ///
+ ///
+ ///
+ public virtual bool Replace(int index, IEnumerable collection)
+ {
+ int collectionCount = collection.Count();
+ int ItemsAfterIndex = this.Count - index; // # of items in the list after the index
+ int CountToReplace = collectionCount <= ItemsAfterIndex ? collectionCount : ItemsAfterIndex; //# if items that will be replaced
+ int AdditionalItems = collectionCount - CountToReplace; //# of additional items that will be added to the list.
+
+ List oldItemsList = this.GetRange(index, CountToReplace);
+
+ //Insert the collection
+ base.RemoveRange(index, CountToReplace);
+ if (AdditionalItems > 0)
+ base.AddRange(collection);
+ else
+ base.InsertRange(index, collection);
+
+ List insertedList = collection.ToList();
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems: insertedList.GetRange(0, CountToReplace), oldItems: oldItemsList, index));
+ if (AdditionalItems > 0)
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, insertedList.GetRange(CountToReplace, AdditionalItems)));
+ return true;
+ }
+
+ #endregion
+
+ #region < Methods that Override List Methods >
+
+ ///
+ /// Get or Set the element at the specified index.
+ ///
+ /// The zero-based index of the item to Get or Set.
+ ///
+ new public T this[int index] {
+ get => base[index];
+ set {
+ if (index >= 0 && index < Count)
+ {
+ //Perform Replace
+ T old = base[index];
+ base[index] = value;
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem: value, oldItem: old, index));
+ }
+ else if (index == Count && index <= Capacity - 1)
+ {
+ //Append value to end only if the capacity doesn't need to be changed
+ base.Add(value);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value, index));
+ }
+ else
+ {
+ base[index] = value; // Generate ArgumentOutOfRangeException exception
+ }
+ }
+ }
+
+ #region < Add >
+
+ ///
+ new public virtual void Add(T item)
+ {
+ base.Add(item);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
+ }
+
+ ///
+ new public virtual void AddRange(IEnumerable collection)
+ {
+ if (collection == null || collection.Count() == 0) return;
+ base.AddRange(collection);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection.ToList()));
+ }
+
+ #endregion
+
+ #region < Insert >
+
+ ///
+ /// Generates event for item that was added and item that was shifted ( Event is raised twice )
+ new public virtual void Insert(int index, T item)
+ {
+ base.Insert(index, item);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index: index));
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, this[index + 1], index + 1, index));
+ }
+
+ ///
+ /// Generates event for items that were added and items that were shifted ( Event is raised twice )
+ new public virtual void InsertRange(int index, IEnumerable collection)
+ {
+ if (collection == null || collection.Count() == 0) return;
+ int i = index + collection.Count() < this.Count ? collection.Count() : this.Count - index;
+ List movedItems = base.GetRange(index, i);
+ base.InsertRange(index, collection);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, changedItems: collection.ToList(), index));
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, changedItems: movedItems, IndexOf(movedItems[0]), index));
+ }
+
+ #endregion
+
+ #region < Remove >
+
+ ///
+ new public virtual void Clear()
+ {
+ base.Clear();
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+
+ ///
+ new public virtual bool Remove(T item)
+ {
+ if (!base.Contains(item)) return false;
+
+ int i = base.IndexOf(item);
+ if (base.Remove(item))
+ {
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, i));
+ return true;
+ }
+ else
+ return false;
+ }
+
+ ///
+ new public virtual void RemoveAt(int index)
+ {
+ T item = base[index];
+ base.RemoveAt(index);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index: index));
+ }
+
+ ///
+ new public virtual void RemoveRange(int index, int count)
+ {
+ List removedItems = base.GetRange(index, count);
+ if (removedItems.Count > 0)
+ {
+ base.RemoveRange(index, count);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems.ToList(), index));
+ }
+ }
+
+ ///
+ new public virtual int RemoveAll(Predicate match)
+ {
+ List removedItems = base.FindAll(match);
+ int ret = removedItems.Count;
+ if (ret > 0)
+ {
+ ret = base.RemoveAll(match);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems));
+ }
+ return ret;
+ }
+
+ #endregion
+
+ #region < Move / Sort >
+
+ #region < Hide base members >
+
+ ///
+ new virtual public void Sort(int index, int count, IComparer comparer) => Sort(index, count, comparer, true);
+ ///
+ new virtual public void Sort(IComparer comparer) => Sort(comparer, true);
+ ///
+ new virtual public void Sort(Comparison comparison) => Sort(comparison, true);
+ ///
+ new virtual public void Sort() => Sort(true);
+ ///
+ new virtual public void Reverse(int index, int count) => Reverse(index, count, true);
+ ///
+ new virtual public void Reverse() => Reverse(true);
+
+ #endregion
+
+ ///
+ ///
+ public virtual void Reverse(bool verbose)
+ {
+ PerformMove(new Action(() => base.Reverse()), this, verbose);
+ }
+
+ ///
+ ///
+ public virtual void Reverse(int index, int count, bool verbose)
+ {
+ List OriginalOrder = base.GetRange(index, count);
+ PerformMove(new Action(() => base.Reverse(index, count)), OriginalOrder, verbose);
+ }
+
+ ///
+ ///
+ public virtual void Sort(bool verbose)
+ {
+ PerformMove(new Action(() => base.Sort()), this, verbose);
+ }
+
+ ///
+ ///
+ public virtual void Sort(Comparison comparison, bool verbose)
+ {
+ PerformMove(new Action(() => base.Sort(comparison)), this, verbose);
+ }
+
+ ///
+ ///
+ public virtual void Sort(IComparer comparer, bool verbose)
+ {
+ PerformMove(new Action(() => base.Sort(comparer)), this, verbose);
+ }
+
+ ///
+ ///
+ public virtual void Sort(int index, int count, IComparer comparer, bool verbose)
+ {
+ List OriginalOrder = base.GetRange(index, count);
+ Action action = new Action(() => base.Sort(index, count, comparer));
+ PerformMove(action, OriginalOrder, verbose);
+ }
+
+ ///
+ /// Per rules, generates event for every item that has moved within the list.
+ /// Set parameter in overload to generate a single event instead.
+ ///
+ /// Action to perform that will rearrange items in the list - should not add, remove or replace!
+ /// List of items that are intended to rearrage - can be whole or subset of list
+ ///
+ /// If TRUE: Create a 'Move' OnCollectionChange event for all items that were moved within the list.
+ /// If FALSE: Generate a single event with
+ ///
+ protected void PerformMove(Action MoveAction, List OriginalOrder, bool verbose)
+ {
+ //Store Old List Order
+ List OldIndexList = this.ToList();
+
+ //Perform the move
+ MoveAction.Invoke();
+
+ //Generate the event
+ foreach (T obj in OriginalOrder)
+ {
+ int oldIndex = OldIndexList.IndexOf(obj);
+ int newIndex = this.IndexOf(obj);
+ if (oldIndex != newIndex)
+ {
+ if (verbose)
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, changedItem: obj, newIndex, oldIndex));
+ else
+ {
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ break;
+ }
+ }
+ }
+
+ //OldIndexList no longer needed
+ OldIndexList.Clear();
+ }
+
+ #endregion
+
+ #endregion
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/PriorityScheduler.cs b/FSI.Lib/Tools/RoboSharp/PriorityScheduler.cs
new file mode 100644
index 0000000..1fad526
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/PriorityScheduler.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FSI.Lib.Tools.RoboSharp
+{
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ ///
+ /// Object derived from TaskScheduler. Assisgns the task to some thread
+ ///
+ public class PriorityScheduler : TaskScheduler
+
+ {
+ /// TaskScheduler for AboveNormal Priority Tasks
+ public static PriorityScheduler AboveNormal = new PriorityScheduler(ThreadPriority.AboveNormal);
+
+ /// TaskScheduler for BelowNormal Priority Tasks
+ public static PriorityScheduler BelowNormal = new PriorityScheduler(ThreadPriority.BelowNormal);
+
+ /// TaskScheduler for the lowest Priority Tasks
+ public static PriorityScheduler Lowest = new PriorityScheduler(ThreadPriority.Lowest);
+
+
+ private BlockingCollection _tasks = new BlockingCollection();
+ private Thread[] _threads;
+ private ThreadPriority _priority;
+ private readonly int _maximumConcurrencyLevel = Math.Max(1, Environment.ProcessorCount);
+
+ public PriorityScheduler(ThreadPriority priority)
+ {
+ _priority = priority;
+ }
+
+ public override int MaximumConcurrencyLevel
+ {
+ get { return _maximumConcurrencyLevel; }
+ }
+
+ protected override IEnumerable GetScheduledTasks()
+ {
+ return _tasks;
+ }
+
+ protected override void QueueTask(Task task)
+ {
+ _tasks.Add(task);
+ bool _executing = false;
+ if (_threads == null)
+ {
+ _threads = new Thread[_maximumConcurrencyLevel];
+ for (int i = 0; i < _threads.Length; i++)
+ {
+ int local = i;
+ _threads[i] = new Thread(() =>
+ {
+ foreach (Task t in _tasks.GetConsumingEnumerable())
+ _executing = base.TryExecuteTask(t);
+ });
+ _threads[i].Name = string.Format("PriorityScheduler: ", i);
+ _threads[i].Priority = _priority;
+ _threads[i].IsBackground = true;
+ _threads[i].Start();
+ }
+ }
+ }
+
+ protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
+ {
+ return false; // we might not want to execute task that should schedule as high or low priority inline
+ }
+ }
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+}
diff --git a/FSI.Lib/Tools/RoboSharp/ProcessedFileInfo.cs b/FSI.Lib/Tools/RoboSharp/ProcessedFileInfo.cs
new file mode 100644
index 0000000..bc4d60c
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/ProcessedFileInfo.cs
@@ -0,0 +1,35 @@
+namespace FSI.Lib.Tools.RoboSharp
+{
+ ///
+ /// Message Type reported by RoboCopy
+ ///
+ public enum FileClassType
+ {
+ /// Details about a Directory
+ NewDir,
+ /// Details about a FILE
+ File,
+ /// Status Message reported by RoboCopy
+ SystemMessage
+ }
+
+ /// Contains information about the current item being processed by RoboCopy
+ public class ProcessedFileInfo
+ {
+ /// Description of the item as reported by RoboCopy
+ public string FileClass { get; set; }
+
+ ///
+ public FileClassType FileClassType { get; set; }
+
+ ///
+ /// File -> File Size
+ /// Directory -> Number files in folder -> Can be negative if PURGE is used
+ /// SystemMessage -> Should be 0
+ ///
+ public long Size { get; set; }
+
+ /// Folder or File Name / Message Text
+ public string Name { get; set; }
+ }
+}
diff --git a/FSI.Lib/Tools/RoboSharp/Results/ProgressEstimator.cs b/FSI.Lib/Tools/RoboSharp/Results/ProgressEstimator.cs
new file mode 100644
index 0000000..cc4e69a
--- /dev/null
+++ b/FSI.Lib/Tools/RoboSharp/Results/ProgressEstimator.cs
@@ -0,0 +1,521 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using FSI.Lib.Tools.RoboSharp.Interfaces;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+using FSI.Lib.Tools.RoboSharp.EventArgObjects;
+
+namespace FSI.Lib.Tools.RoboSharp.Results
+{
+ ///
+ /// Object that provides objects whose events can be bound to report estimated RoboCommand progress periodically.
+ ///
+ /// Note: Only works properly with /V verbose set TRUE.
+ ///
+ ///
+ /// Subscribe to or to be notified when the ProgressEstimator becomes available for binding
+ /// Create event handler to subscribe to the Events you want to handle:
+ ///
+ /// private void OnProgressEstimatorCreated(object sender, Results.ProgressEstimatorCreatedEventArgs e) {
+ /// e.ResultsEstimate.ByteStats.PropertyChanged += ByteStats_PropertyChanged;
+ /// e.ResultsEstimate.DirStats.PropertyChanged += DirStats_PropertyChanged;
+ /// e.ResultsEstimate.FileStats.PropertyChanged += FileStats_PropertyChanged;
+ /// }
+ ///
+ ///
+ ///
+ ///
+ public class ProgressEstimator : IProgressEstimator, IResults
+ {
+ #region < Constructors >
+
+ private ProgressEstimator() { }
+
+ internal ProgressEstimator(RoboCommand cmd)
+ {
+ command = cmd;
+ DirStatField = new Statistic(Statistic.StatType.Directories, "Directory Stats Estimate");
+ FileStatsField = new Statistic(Statistic.StatType.Files, "File Stats Estimate");
+ ByteStatsField = new Statistic(Statistic.StatType.Bytes, "Byte Stats Estimate");
+
+ tmpByte.EnablePropertyChangeEvent = false;
+ tmpFile.EnablePropertyChangeEvent = false;
+ tmpDir.EnablePropertyChangeEvent = false;
+ this.StartUpdateTask(out UpdateTaskCancelSource);
+ }
+
+ #endregion
+
+ #region < Private Members >
+
+ private readonly RoboCommand command;
+ private bool SkippingFile { get; set; }
+ private bool CopyOpStarted { get; set; }
+ internal bool FileFailed { get; set; }
+
+ private RoboSharpConfiguration Config => command?.Configuration;
+
+ // Stat Objects that will be publicly visible
+ private readonly Statistic DirStatField;
+ private readonly Statistic FileStatsField;
+ private readonly Statistic ByteStatsField;
+
+ internal enum WhereToAdd { Copied, Skipped, Extra, MisMatch, Failed }
+
+ // Storage for last entered Directory and File objects
+ /// Used for providing Source Directory in CopyProgressChanged args
+ internal ProcessedFileInfo CurrentDir { get; private set; }
+ /// Used for providing Source Directory in CopyProgressChanged args AND for byte Statistic
+ internal ProcessedFileInfo CurrentFile { get; private set; }
+ /// Marked as TRUE if this is LIST ONLY mode or the file is 0KB -- Value set during 'AddFile' method
+ private bool CurrentFile_SpecialHandling { get; set; }
+
+ //Stat objects to house the data temporarily before writing to publicly visible stat objects
+ readonly Statistic tmpDir =new Statistic(type: Statistic.StatType.Directories);
+ readonly Statistic tmpFile = new Statistic(type: Statistic.StatType.Files);
+ readonly Statistic tmpByte = new Statistic(type: Statistic.StatType.Bytes);
+
+ //UpdatePeriod
+ private const int UpdatePeriod = 150; // Update Period in milliseconds to push Updates to a UI or RoboQueueProgressEstimator
+ private readonly object DirLock = new object(); //Thread Lock for tmpDir
+ private readonly object FileLock = new object(); //Thread Lock for tmpFile and tmpByte
+ private readonly object UpdateLock = new object(); //Thread Lock for NextUpdatePush and UpdateTaskTrgger
+ private DateTime NextUpdatePush = DateTime.Now.AddMilliseconds(UpdatePeriod);
+ private TaskCompletionSource