diff --git a/FSI.Lib/FSI.Lib/FSI.Lib.csproj b/FSI.Lib/FSI.Lib/FSI.Lib.csproj index 5551135..99f14e6 100644 --- a/FSI.Lib/FSI.Lib/FSI.Lib.csproj +++ b/FSI.Lib/FSI.Lib/FSI.Lib.csproj @@ -9,6 +9,10 @@ 3.0 + + + + @@ -28,6 +32,7 @@ + diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/ApplicationConstants.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/ApplicationConstants.cs new file mode 100644 index 0000000..54f3303 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/CopyOptions.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/CopyOptions.cs new file mode 100644 index 0000000..5ba33e3 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Debugger.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Debugger.cs new file mode 100644 index 0000000..e2dfdda --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_DE.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_DE.cs new file mode 100644 index 0000000..dca8989 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/DefaultConfigurations/RoboSharpConfig_EN.cs new file mode 100644 index 0000000..cb23a40 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/CommandErrorEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/CommandErrorEventArgs.cs new file mode 100644 index 0000000..bf91ef5 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/CopyProgressEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/CopyProgressEventArgs.cs new file mode 100644 index 0000000..d99c2ec --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/ErrorEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/ErrorEventArgs.cs new file mode 100644 index 0000000..29a0829 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/FileProcessedEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/FileProcessedEventArgs.cs new file mode 100644 index 0000000..7ba605d --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/IProgressEstimatorUpdateEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/IProgressEstimatorUpdateEventArgs.cs new file mode 100644 index 0000000..2ac7083 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/ProgressEstimatorCreatedEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/ProgressEstimatorCreatedEventArgs.cs new file mode 100644 index 0000000..35544e7 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/ResultListUpdatedEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/ResultListUpdatedEventArgs.cs new file mode 100644 index 0000000..f858470 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboCommandCompletedEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboCommandCompletedEventArgs.cs new file mode 100644 index 0000000..5842be1 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCommandStartedEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCommandStartedEventArgs.cs new file mode 100644 index 0000000..365fc8c --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCompletedEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/RoboQueueCompletedEventArgs.cs new file mode 100644 index 0000000..7646870 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/StatChangedEventArg.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/StatChangedEventArg.cs new file mode 100644 index 0000000..ae86828 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/EventArgObjects/TimeSpanEventArgs.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/EventArgObjects/TimeSpanEventArgs.cs new file mode 100644 index 0000000..3413a37 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/ExtensionMethods.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/ExtensionMethods.cs new file mode 100644 index 0000000..1be9f33 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/ImpersonateUser.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/ImpersonateUser.cs new file mode 100644 index 0000000..b246dd9 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/IProgressEstimator.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/IProgressEstimator.cs new file mode 100644 index 0000000..8af1aff --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/IResults.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/IResults.cs new file mode 100644 index 0000000..d161840 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCommand.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCommand.cs new file mode 100644 index 0000000..b978217 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyCombinedExitStatus.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyCombinedExitStatus.cs new file mode 100644 index 0000000..72c93dc --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyResultsList.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboCopyResultsList.cs new file mode 100644 index 0000000..902e2ff --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueue.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueue.cs new file mode 100644 index 0000000..4ec5fa6 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueueResults.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/IRoboQueueResults.cs new file mode 100644 index 0000000..8325bd8 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/ISpeedStatistic.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/ISpeedStatistic.cs new file mode 100644 index 0000000..1ed4fca --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/IStatistic.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/IStatistic.cs new file mode 100644 index 0000000..61f7588 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Interfaces/ITimeSpan.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Interfaces/ITimeSpan.cs new file mode 100644 index 0000000..7331fe4 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/JobFile.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/JobFile.cs new file mode 100644 index 0000000..41c9cfe --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/JobFileBuilder.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/JobFileBuilder.cs new file mode 100644 index 0000000..917d639 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/JobOptions.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/JobOptions.cs new file mode 100644 index 0000000..ff1f373 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/LoggingOptions.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/LoggingOptions.cs new file mode 100644 index 0000000..fa3009c --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/NativeMethods.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/NativeMethods.cs new file mode 100644 index 0000000..1ef60f5 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/ObservableList.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/ObservableList.cs new file mode 100644 index 0000000..6f6efa5 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/PriorityScheduler.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/PriorityScheduler.cs new file mode 100644 index 0000000..1fad526 --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/ProcessedFileInfo.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/ProcessedFileInfo.cs new file mode 100644 index 0000000..bc4d60c --- /dev/null +++ b/FSI.Lib/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/FSI.Lib/Tools/RoboSharp/Results/ProgressEstimator.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/ProgressEstimator.cs new file mode 100644 index 0000000..cc4e69a --- /dev/null +++ b/FSI.Lib/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 UpdateTaskTrigger; // TCS that the UpdateTask awaits on + private CancellationTokenSource UpdateTaskCancelSource; // While !Cancelled, UpdateTask continues looping + + #endregion + + #region < Public Properties > + + /// + /// Estimate of current number of directories processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy. + ///
+ public IStatistic DirectoriesStatistic => DirStatField; + + /// + /// Estimate of current number of files processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy. + ///
+ public IStatistic FilesStatistic => FileStatsField; + + /// + /// Estimate of current number of bytes processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy. + ///
+ public IStatistic BytesStatistic => ByteStatsField; + + RoboCopyExitStatus IResults.Status => new RoboCopyExitStatus((int)GetExitCode()); + + /// + public delegate void UIUpdateEventHandler(IProgressEstimator sender, IProgressEstimatorUpdateEventArgs e); + + /// + public event UIUpdateEventHandler ValuesUpdated; + + #endregion + + #region < Public Methods > + + /// + /// Parse this object's stats into a enum. + /// + /// + public RoboCopyExitCodes GetExitCode() + { + Results.RoboCopyExitCodes code = 0; + + //Files Copied + if (FileStatsField.Copied > 0) + code |= Results.RoboCopyExitCodes.FilesCopiedSuccessful; + + //Extra + if (DirStatField.Extras > 0 | FileStatsField.Extras > 0) + code |= Results.RoboCopyExitCodes.ExtraFilesOrDirectoriesDetected; + + //MisMatch + if (DirStatField.Mismatch > 0 | FileStatsField.Mismatch > 0) + code |= Results.RoboCopyExitCodes.MismatchedDirectoriesDetected; + + //Failed + if (DirStatField.Failed > 0 | FileStatsField.Failed > 0) + code |= Results.RoboCopyExitCodes.SomeFilesOrDirectoriesCouldNotBeCopied; + + return code; + + } + + #endregion + + #region < Get RoboCopyResults Object ( Internal ) > + + /// + /// Repackage the statistics into a new object + /// + /// + /// Used by ResultsBuilder as starting point for the results. + /// Should not be used anywhere else, as it kills the worker thread that calculates the Statistics objects. + /// + /// + internal RoboCopyResults GetResults() + { + //Stop the Update Task + UpdateTaskCancelSource?.Cancel(); + UpdateTaskTrigger?.TrySetResult(null); + + // - if copy operation wasn't completed, register it as failed instead. + // - if file was to be marked as 'skipped', then register it as skipped. + + ProcessPreviousFile(); + PushUpdate(); // Perform Final calculation before generating the Results Object + + // Package up + return new RoboCopyResults() + { + BytesStatistic = (Statistic)BytesStatistic, + DirectoriesStatistic = (Statistic)DirectoriesStatistic, + FilesStatistic = (Statistic)FilesStatistic, + SpeedStatistic = new SpeedStatistic(), + }; + } + + #endregion + + #region < Calculate Dirs (Internal) > + + /// Increment + internal void AddDir(ProcessedFileInfo currentDir, bool CopyOperation) + { + + WhereToAdd? whereTo = null; + bool SetCurrentDir = false; + if (currentDir.FileClass.Equals(Config.LogParsing_ExistingDir, StringComparison.CurrentCultureIgnoreCase)) // Existing Dir + { + whereTo = WhereToAdd.Skipped; + SetCurrentDir = true; + } + else if (currentDir.FileClass.Equals(Config.LogParsing_NewDir, StringComparison.CurrentCultureIgnoreCase)) //New Dir + { + whereTo = WhereToAdd.Copied; + SetCurrentDir = true; + } + else if (currentDir.FileClass.Equals(Config.LogParsing_ExtraDir, StringComparison.CurrentCultureIgnoreCase)) //Extra Dir + { + whereTo = WhereToAdd.Extra; + SetCurrentDir = false; + } + else if (currentDir.FileClass.Equals(Config.LogParsing_DirectoryExclusion, StringComparison.CurrentCultureIgnoreCase)) //Excluded Dir + { + whereTo = WhereToAdd.Skipped; + SetCurrentDir = false; + } + //Store CurrentDir under various conditions + if (SetCurrentDir) CurrentDir = currentDir; + + lock (DirLock) + { + switch (whereTo) + { + case WhereToAdd.Copied: tmpDir.Total++; tmpDir.Copied++;break; + case WhereToAdd.Extra: tmpDir.Extras++; break; //Extras do not count towards total + case WhereToAdd.Failed: tmpDir.Total++; tmpDir.Failed++; break; + case WhereToAdd.MisMatch: tmpDir.Total++; tmpDir.Mismatch++; break; + case WhereToAdd.Skipped: tmpDir.Total++; tmpDir.Skipped++; break; + } + } + + + //Check if the UpdateTask should push an update to the public fields + if (Monitor.TryEnter(UpdateLock)) + { + if (NextUpdatePush <= DateTime.Now) + UpdateTaskTrigger?.TrySetResult(null); + Monitor.Exit(UpdateLock); + } + } + + #endregion + + #region < Calculate Files (Internal) > + + /// + /// Performs final processing of the previous file if needed + /// + private void ProcessPreviousFile() + { + if (CurrentFile != null) + { + if (FileFailed) + { + PerformByteCalc(CurrentFile, WhereToAdd.Failed); + } + else if (CopyOpStarted && CurrentFile_SpecialHandling) + { + PerformByteCalc(CurrentFile, WhereToAdd.Copied); + } + else if (SkippingFile) + { + PerformByteCalc(CurrentFile, WhereToAdd.Skipped); + } + else if (UpdateTaskCancelSource?.IsCancellationRequested ?? true) + { + //Default marks as failed - This should only occur during the 'GetResults()' method due to the if statement above. + PerformByteCalc(CurrentFile, WhereToAdd.Failed); + } + } + } + + /// Increment + internal void AddFile(ProcessedFileInfo currentFile, bool CopyOperation) + { + ProcessPreviousFile(); + + CurrentFile = currentFile; + SkippingFile = false; + CopyOpStarted = false; + FileFailed = false; + + // Flag to perform checks during a ListOnly operation OR for 0kb files (They won't get Progress update, but will be created) + bool SpecialHandling = !CopyOperation || currentFile.Size == 0; + CurrentFile_SpecialHandling = SpecialHandling; + + // EXTRA FILES + if (currentFile.FileClass.Equals(Config.LogParsing_ExtraFile, StringComparison.CurrentCultureIgnoreCase)) + { + PerformByteCalc(currentFile, WhereToAdd.Extra); + } + //MisMatch + else if (currentFile.FileClass.Equals(Config.LogParsing_MismatchFile, StringComparison.CurrentCultureIgnoreCase)) + { + PerformByteCalc(currentFile, WhereToAdd.MisMatch); + } + //Failed Files + else if (currentFile.FileClass.Equals(Config.LogParsing_FailedFile, StringComparison.CurrentCultureIgnoreCase)) + { + PerformByteCalc(currentFile, WhereToAdd.Failed); + } + + //Files to be Copied/Skipped + else + { + SkippingFile = CopyOperation;//Assume Skipped, adjusted when CopyProgress is updated + if (currentFile.FileClass.Equals(Config.LogParsing_NewFile, StringComparison.CurrentCultureIgnoreCase)) // New File + { + //Special handling for 0kb files & ListOnly -> They won't get Progress update, but will be created + if (SpecialHandling) + { + SetCopyOpStarted(); + } + } + else if (currentFile.FileClass.Equals(Config.LogParsing_SameFile, StringComparison.CurrentCultureIgnoreCase)) //Identical Files + { + if (command.SelectionOptions.IncludeSame) + { + if (SpecialHandling) SetCopyOpStarted(); // Only add to Copied if ListOnly / 0-bytes + } + else + PerformByteCalc(currentFile, WhereToAdd.Skipped); + } + else if (SpecialHandling) // These checks are always performed during a ListOnly operation + { + + switch (true) + { + //Skipped Or Copied Conditions + case true when currentFile.FileClass.Equals(Config.LogParsing_NewerFile, StringComparison.CurrentCultureIgnoreCase): // ExcludeNewer + SkippedOrCopied(currentFile, command.SelectionOptions.ExcludeNewer); + break; + case true when currentFile.FileClass.Equals(Config.LogParsing_OlderFile, StringComparison.CurrentCultureIgnoreCase): // ExcludeOlder + SkippedOrCopied(currentFile, command.SelectionOptions.ExcludeOlder); + break; + case true when currentFile.FileClass.Equals(Config.LogParsing_ChangedExclusion, StringComparison.CurrentCultureIgnoreCase): //ExcludeChanged + SkippedOrCopied(currentFile, command.SelectionOptions.ExcludeChanged); + break; + case true when currentFile.FileClass.Equals(Config.LogParsing_TweakedInclusion, StringComparison.CurrentCultureIgnoreCase): //IncludeTweaked + SkippedOrCopied(currentFile, !command.SelectionOptions.IncludeTweaked); + break; + + //Mark As Skip Conditions + case true when currentFile.FileClass.Equals(Config.LogParsing_FileExclusion, StringComparison.CurrentCultureIgnoreCase): //FileExclusion + case true when currentFile.FileClass.Equals(Config.LogParsing_AttribExclusion, StringComparison.CurrentCultureIgnoreCase): //AttributeExclusion + case true when currentFile.FileClass.Equals(Config.LogParsing_MaxFileSizeExclusion, StringComparison.CurrentCultureIgnoreCase): //MaxFileSizeExclusion + case true when currentFile.FileClass.Equals(Config.LogParsing_MinFileSizeExclusion, StringComparison.CurrentCultureIgnoreCase): //MinFileSizeExclusion + case true when currentFile.FileClass.Equals(Config.LogParsing_MaxAgeOrAccessExclusion, StringComparison.CurrentCultureIgnoreCase): //MaxAgeOrAccessExclusion + case true when currentFile.FileClass.Equals(Config.LogParsing_MinAgeOrAccessExclusion, StringComparison.CurrentCultureIgnoreCase): //MinAgeOrAccessExclusion + PerformByteCalc(currentFile, WhereToAdd.Skipped); + break; + } + } + } + } + + /// + /// Method meant only to be called from AddFile method while SpecialHandling is true - helps normalize code and avoid repetition + /// + private void SkippedOrCopied(ProcessedFileInfo currentFile, bool MarkSkipped) + { + if (MarkSkipped) + PerformByteCalc(currentFile, WhereToAdd.Skipped); + else + { + SetCopyOpStarted(); + //PerformByteCalc(currentFile, WhereToAdd.Copied); + } + } + + /// Catch start copy progress of large files + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] + internal void SetCopyOpStarted() + { + SkippingFile = false; + CopyOpStarted = true; + } + + /// Increment .Copied ( Triggered when copy progress = 100% ) + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] + internal void AddFileCopied(ProcessedFileInfo currentFile) + { + PerformByteCalc(currentFile, WhereToAdd.Copied); + } + + /// + /// Perform the calculation for the ByteStatistic + /// + private void PerformByteCalc(ProcessedFileInfo file, WhereToAdd where) + { + if (file == null) return; + + //Reset Flags + SkippingFile = false; + CopyOpStarted = false; + FileFailed = false; + CurrentFile = null; + CurrentFile_SpecialHandling = false; + + //Perform Math + lock (FileLock) + { + //Extra files do not contribute towards Copy Total. + if (where == WhereToAdd.Extra) + { + tmpFile.Extras++; + tmpByte.Extras += file.Size; + } + else + { + tmpFile.Total++; + tmpByte.Total += file.Size; + + switch (where) + { + case WhereToAdd.Copied: + tmpFile.Copied++; + tmpByte.Copied += file.Size; + break; + case WhereToAdd.Extra: + break; + case WhereToAdd.Failed: + tmpFile.Failed++; + tmpByte.Failed += file.Size; + break; + case WhereToAdd.MisMatch: + tmpFile.Mismatch++; + tmpByte.Mismatch += file.Size; + break; + case WhereToAdd.Skipped: + tmpFile.Skipped++; + tmpByte.Skipped += file.Size; + break; + } + } + } + //Check if the UpdateTask should push an update to the public fields + if (Monitor.TryEnter(UpdateLock)) + { + if (NextUpdatePush <= DateTime.Now) + UpdateTaskTrigger?.TrySetResult(null); + Monitor.Exit(UpdateLock); + } + } + + #endregion + + #region < PushUpdate to Public Stat Objects > + + /// + /// Creates a LongRunning task that is meant to periodically push out Updates to the UI on a thread isolated from the event thread. + /// + /// + /// + private Task StartUpdateTask(out CancellationTokenSource CancelSource) + { + CancelSource = new CancellationTokenSource(); + var CS = CancelSource; + return Task.Run(async () => + { + while (!CS.IsCancellationRequested) + { + lock(UpdateLock) + { + PushUpdate(); + UpdateTaskTrigger = new TaskCompletionSource(); + NextUpdatePush = DateTime.Now.AddMilliseconds(UpdatePeriod); + } + await UpdateTaskTrigger.Task; + } + //Cleanup + CS?.Dispose(); + UpdateTaskTrigger = null; + UpdateTaskCancelSource = null; + }, CS.Token); + } + + /// + /// Push the update to the public Stat Objects + /// + private void PushUpdate() + { + //Lock the Stat objects, clone, reset them, then push the update to the UI. + Statistic TD = null; + Statistic TB = null; + Statistic TF = null; + lock (DirLock) + { + if (tmpDir.NonZeroValue) + { + TD = tmpDir.Clone(); + tmpDir.Reset(); + } + } + lock (FileLock) + { + if (tmpFile.NonZeroValue) + { + TF = tmpFile.Clone(); + tmpFile.Reset(); + } + if (tmpByte.NonZeroValue) + { + TB = tmpByte.Clone(); + tmpByte.Reset(); + } + } + //Push UI update after locks are released, to avoid holding up the other thread for too long + if (TD != null) DirStatField.AddStatistic(TD); + if (TB != null) ByteStatsField.AddStatistic(TB); + if (TF != null) FileStatsField.AddStatistic(TF); + //Raise the event if any of the values have been updated + if (TF != null || TD != null || TB != null) + { + ValuesUpdated?.Invoke(this, new IProgressEstimatorUpdateEventArgs(this, TB, TF, TD)); + } + } + + #endregion + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/ResultsBuilder.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/ResultsBuilder.cs new file mode 100644 index 0000000..e9747dd --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/ResultsBuilder.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// Helper class to build a object. + /// + /// + /// + /// + internal class ResultsBuilder + { + private ResultsBuilder() { } + + internal ResultsBuilder(RoboCommand roboCommand) { + RoboCommand = roboCommand; + Estimator = new ProgressEstimator(roboCommand); + } + + #region < Private Members > + + ///Reference back to the RoboCommand that spawned this object + private readonly RoboCommand RoboCommand; + + private readonly List outputLines = new List(); + + /// This is the last line that was logged. + internal string LastLine => outputLines.Count > 0 ? outputLines.Last() : ""; + + #endregion + + #region < Command Options Properties > + + /// + internal string CommandOptions { get; set; } + + /// + internal string Source { get; set; } + + /// + internal string Destination { get; set; } + + internal List RoboCopyErrors { get; } = new List(); + + #endregion + + #region < Counters in case cancellation > + + /// + internal ProgressEstimator Estimator { get; } + + #endregion + + /// + /// Add a LogLine reported by RoboCopy to the LogLines list. + /// + internal void AddOutput(string output) + { + if (output == null) + return; + + if (Regex.IsMatch(output, @"^\s*[\d\.,]+%\s*$", RegexOptions.Compiled)) //Ignore Progress Indicators + return; + + outputLines.Add(output); + } + + /// + /// Builds the results from parsing the logLines. + /// + /// + /// This is used by the ProgressUpdateEventArgs to ignore the loglines when generating the estimate + /// + internal RoboCopyResults BuildResults(int exitCode, bool IsProgressUpdateEvent = false) + { + var res = Estimator.GetResults(); //Start off with the estimated results, and replace if able + + res.JobName = RoboCommand.Name; + res.Status = new RoboCopyExitStatus(exitCode); + + var statisticLines = IsProgressUpdateEvent ? new List() : GetStatisticLines(); + + //Dir Stats + if (exitCode >= 0 && statisticLines.Count >= 1) + res.DirectoriesStatistic = Statistic.Parse(Statistic.StatType.Directories, statisticLines[0]); + + //File Stats + if (exitCode >= 0 && statisticLines.Count >= 2) + res.FilesStatistic = Statistic.Parse(Statistic.StatType.Files, statisticLines[1]); + + //Bytes + if (exitCode >= 0 && statisticLines.Count >= 3) + res.BytesStatistic = Statistic.Parse(Statistic.StatType.Bytes, statisticLines[2]); + + //Speed Stats + if (exitCode >= 0 && statisticLines.Count >= 6) + res.SpeedStatistic = SpeedStatistic.Parse(statisticLines[4], statisticLines[5]); + + res.LogLines = outputLines.ToArray(); + res.RoboCopyErrors = this.RoboCopyErrors.ToArray(); + res.Source = this.Source; + res.Destination = this.Destination; + res.CommandOptions = this.CommandOptions; + return res; + } + + private List GetStatisticLines() + { + var res = new List(); + for (var i = outputLines.Count-1; i > 0; i--) + { + var line = outputLines[i]; + if (line.StartsWith("-----------------------")) + break; + + if (line.Contains(":") && !line.Contains("\\")) + res.Add(line); + } + + res.Reverse(); + return res; + } + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyExitCodes.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyExitCodes.cs new file mode 100644 index 0000000..d08a22e --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyExitCodes.cs @@ -0,0 +1,42 @@ +using System; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// RoboCopy Exit Codes + /// + /// + [Flags] + public enum RoboCopyExitCodes + { + /// No Files Copied, No Errors Occured + NoErrorNoCopy = 0x0, + /// One or more files were copied successfully + FilesCopiedSuccessful = 0x1, + /// + /// Some Extra files or directories were detected.
+ /// Examine the output log for details. + ///
+ ExtraFilesOrDirectoriesDetected = 0x2, + /// + /// Some Mismatched files or directories were detected.
+ /// Examine the output log. Housekeeping might be required. + ///
+ MismatchedDirectoriesDetected = 0x4, + /// + /// Some files or directories could not be copied
+ /// (copy errors occurred and the retry limit was exceeded). + /// Check these errors further. + ///
+ SomeFilesOrDirectoriesCouldNotBeCopied = 0x8, + /// + /// Serious error. Robocopy did not copy any files.
+ /// Either a usage error or an error due to insufficient access privileges on the source or destination directories. + ///
+ SeriousErrorOccurred = 0x10, + /// + /// The Robocopy process exited prior to completion + /// + Cancelled = -1, + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResults.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResults.cs new file mode 100644 index 0000000..30ef47d --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResults.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using FSI.Lib.Tools.RoboSharp.Interfaces; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// Results provided by the RoboCopy command. Includes the Log, Exit Code, and statistics parsed from the log. + /// + /// + /// + /// + public class RoboCopyResults : IResults, ITimeSpan + { + internal RoboCopyResults() { } + + #region < Properties > + + /// + public string Source { get; internal set; } + + /// + public string Destination { get; internal set; } + + /// + public string CommandOptions { get; internal set; } + + /// + public string JobName { get; internal set; } + + /// + /// All Errors that were generated by RoboCopy during the run. + /// + public ErrorEventArgs[] RoboCopyErrors{ get; internal set; } + + /// + public RoboCopyExitStatus Status { get; internal set; } + + /// Information about number of Directories Copied, Skipped, Failed, etc. + /// + /// If the job was cancelled, or run without a Job Summary, this will attempt to provide approximate results based on the Process.StandardOutput from Robocopy.
+ /// Results should only be treated as accurate if .ExitCodeValue >= 0 and the job was run with = FALSE + ///
+ public Statistic DirectoriesStatistic { get; internal set; } + + /// Information about number of Files Copied, Skipped, Failed, etc. + /// + /// If the job was cancelled, or run without a Job Summary, this will attempt to provide approximate results based on the Process.StandardOutput from Robocopy.
+ /// Results should only be treated as accurate if .ExitCodeValue >= 0 and the job was run with = FALSE + ///
+ public Statistic FilesStatistic { get; internal set; } + + /// Information about number of Bytes processed. + /// + /// If the job was cancelled, or run without a Job Summary, this will attempt to provide approximate results based on the Process.StandardOutput from Robocopy.
+ /// Results should only be treated as accurate if .ExitCodeValue >= 0 and the job was run with = FALSE + ///
+ public Statistic BytesStatistic { get; internal set; } + + /// + public SpeedStatistic SpeedStatistic { get; internal set; } + + /// Output Text reported by RoboCopy + public string[] LogLines { get; internal set; } + + /// Time the RoboCopy process was started + public DateTime StartTime { get; internal set; } + + /// Time the RoboCopy process was completed / cancelled. + public DateTime EndTime { get; internal set; } + + /// Length of Time the RoboCopy Process ran + public TimeSpan TimeSpan { get; internal set; } + + #endregion + + #region < IResults > + + IStatistic IResults.BytesStatistic => BytesStatistic; + IStatistic IResults.DirectoriesStatistic => DirectoriesStatistic; + IStatistic IResults.FilesStatistic => FilesStatistic; + + #endregion + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + public override string ToString() + { + string str = $"ExitCode: {Status.ExitCode}, Directories: {DirectoriesStatistic?.Total.ToString() ?? "Unknown"}, Files: {FilesStatistic?.Total.ToString() ?? "Unknown"}, Bytes: {BytesStatistic?.Total.ToString() ?? "Unknown"}"; + + if (SpeedStatistic != null) + { + str += $", Speed: {SpeedStatistic.BytesPerSec} Bytes/sec"; + } + + return str; + } + } +} \ No newline at end of file diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsList.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsList.cs new file mode 100644 index 0000000..08e221f --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsList.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +//using System.Linq; +using System.Text; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using FSI.Lib.Tools.RoboSharp.EventArgObjects; +using FSI.Lib.Tools.RoboSharp.Interfaces; +using StatType = FSI.Lib.Tools.RoboSharp.Results.Statistic.StatType; +using System.Collections; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// Object used to represent results from multiple s.
+ /// As are added to this object, it will update the Totals and Averages accordingly. + /// Implements: + ///
+ ///
where T = RoboCopyResults + ///
+ ///
+ /// + /// + /// + public sealed class RoboCopyResultsList : IRoboCopyResultsList, IList, INotifyCollectionChanged + { + #region < Constructors > + + /// + public RoboCopyResultsList() { InitCollection(null); Init(); } + + /// Populate the new List object with this result as the first item. + /// + public RoboCopyResultsList(RoboCopyResults result) { ResultsList.Add(result); InitCollection(null); Init(); } + + /// + public RoboCopyResultsList(IEnumerable collection) { InitCollection(collection); Init(); } + + /// + /// Clone a RoboCopyResultsList into a new object + /// + public RoboCopyResultsList(RoboCopyResultsList resultsList) + { + InitCollection(resultsList); + Total_DirStatsField = GetLazyStat(resultsList.Total_DirStatsField, GetLazyFunc(GetDirectoriesStatistics, StatType.Directories)); + Total_FileStatsField = GetLazyStat(resultsList.Total_FileStatsField, GetLazyFunc(GetFilesStatistics, StatType.Files)); + Total_ByteStatsField = GetLazyStat(resultsList.Total_ByteStatsField, GetLazyFunc(GetByteStatistics, StatType.Bytes)); + Average_SpeedStatsField = GetLazyStat(resultsList.Average_SpeedStatsField, GetLazyAverageSpeedFunc()); + ExitStatusSummaryField = GetLazyStat(resultsList.ExitStatusSummaryField, GetLazCombinedStatusFunc()); + } + + #region < Constructor Helper Methods > + + private void InitCollection(IEnumerable collection) + { + ResultsList.AddRange(collection); + ResultsList.CollectionChanged += OnCollectionChanged; + } + + private void Init() + { + Total_DirStatsField = new Lazy(GetLazyFunc(GetDirectoriesStatistics, StatType.Directories)); + Total_FileStatsField = new Lazy(GetLazyFunc(GetFilesStatistics, StatType.Files)); + Total_ByteStatsField = new Lazy(GetLazyFunc(GetByteStatistics, StatType.Bytes)); + Average_SpeedStatsField = new Lazy(GetLazyAverageSpeedFunc()); + ExitStatusSummaryField = new Lazy(GetLazCombinedStatusFunc()); + } + + private Func GetLazyFunc(Func Action, StatType StatType) => new Func(() => Statistic.AddStatistics(Action.Invoke(), StatType)); + private Func GetLazyAverageSpeedFunc() => new Func(() => AverageSpeedStatistic.GetAverage(GetSpeedStatistics())); + private Func GetLazCombinedStatusFunc() => new Func(() => RoboCopyCombinedExitStatus.CombineStatuses(GetStatuses())); + private Lazy GetLazyStat(Lazy lazyStat, Func action) where T : ICloneable + { + if (lazyStat.IsValueCreated) + { + var clone = lazyStat.Value.Clone(); + return new Lazy(() => (T)clone); + } + else + { + return new Lazy(action); + } + } + + #endregion + + #endregion + + #region < Fields > + + //These objects are the underlying Objects that may be bound to by consumers. + //The values are updated upon request of the associated property. + //This is so that properties are not returning new objects every request (which would break bindings) + //If the statistic is never requested, then Lazy<> allows the list to skip performing the math against that statistic. + + private Lazy Total_DirStatsField; + private Lazy Total_ByteStatsField; + private Lazy Total_FileStatsField; + private Lazy Average_SpeedStatsField; + private Lazy ExitStatusSummaryField; + private readonly ObservableList ResultsList = new ObservableList(); + + #endregion + + #region < Events > + + /// + /// Delegate for objects to send notification that the list behind an interface has been updated + /// + public delegate void ResultsListUpdated(object sender, ResultListUpdatedEventArgs e); + + #endregion + + #region < Public Properties > + + /// Sum of all DirectoryStatistics objects + /// Underlying value is Lazy{Statistic} object - Initial value not calculated until first request. + public IStatistic DirectoriesStatistic => Total_DirStatsField?.Value; + + /// Sum of all ByteStatistics objects + /// Underlying value is Lazy{Statistic} object - Initial value not calculated until first request. + public IStatistic BytesStatistic => Total_ByteStatsField?.Value; + + /// Sum of all FileStatistics objects + /// Underlying value is Lazy{Statistic} object - Initial value not calculated until first request. + public IStatistic FilesStatistic => Total_FileStatsField?.Value; + + /// Average of all SpeedStatistics objects + /// Underlying value is Lazy{SpeedStatistic} object - Initial value not calculated until first request. + public ISpeedStatistic SpeedStatistic => Average_SpeedStatsField?.Value; + + /// Sum of all RoboCopyExitStatus objects + /// Underlying value is Lazy object - Initial value not calculated until first request. + public IRoboCopyCombinedExitStatus Status => ExitStatusSummaryField?.Value; + + /// The Collection of RoboCopy Results. Add/Removal of objects must be performed through this object's methods, not on the list directly. + public IReadOnlyList Collection => ResultsList; + + /// + public int Count => ResultsList.Count; + + /// + /// Get or Set the element at the specified index. + /// + /// The zero-based index of the item to Get or Set. + /// + public RoboCopyResults this[int index] { get => ResultsList[index]; set => ResultsList[index] = value; } + + #endregion + + #region < Get Array Methods ( Public ) > + + /// + /// Get a snapshot of the ByteStatistics objects from this list. + /// + /// New array of the ByteStatistic objects + public IStatistic[] GetByteStatistics() + { + List tmp = new List { }; + foreach (RoboCopyResults r in this) + tmp.Add(r?.BytesStatistic); + return tmp.ToArray(); + } + + /// + /// Get a snapshot of the DirectoriesStatistic objects from this list. + /// + /// New array of the DirectoriesStatistic objects + public IStatistic[] GetDirectoriesStatistics() + { + List tmp = new List { }; + foreach (RoboCopyResults r in this) + tmp.Add(r?.DirectoriesStatistic); + return tmp.ToArray(); + } + + /// + /// Get a snapshot of the FilesStatistic objects from this list. + /// + /// New array of the FilesStatistic objects + public IStatistic[] GetFilesStatistics() + { + List tmp = new List { }; + foreach (RoboCopyResults r in this) + tmp.Add(r?.FilesStatistic); + return tmp.ToArray(); + } + + /// + /// Get a snapshot of the FilesStatistic objects from this list. + /// + /// New array of the FilesStatistic objects + public RoboCopyExitStatus[] GetStatuses() + { + List tmp = new List { }; + foreach (RoboCopyResults r in this) + tmp.Add(r?.Status); + return tmp.ToArray(); + } + + /// + /// Get a snapshot of the FilesStatistic objects from this list. + /// + /// New array of the FilesStatistic objects + public ISpeedStatistic[] GetSpeedStatistics() + { + List tmp = new List { }; + foreach (RoboCopyResults r in this) + tmp.Add(r?.SpeedStatistic); + return tmp.ToArray(); + } + + /// + /// Combine the into a single array of errors + /// + /// New array of the ErrorEventArgs objects + public ErrorEventArgs[] GetErrors() + { + List tmp = new List { }; + foreach (RoboCopyResults r in this) + tmp.AddRange(r?.RoboCopyErrors); + return tmp.ToArray(); + } + + #endregion + + #region < INotifyCollectionChanged > + + /// + /// + /// + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// Process the Added/Removed items, then fire the event + /// + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Move) goto RaiseEvent; // Sorting causes no change in math -> Simply raise the event + + //Reset means a drastic change -> Recalculate everything, then goto RaiseEvent + if (e.Action == NotifyCollectionChangedAction.Reset) + { + //Bytes + if (Total_ByteStatsField.IsValueCreated) + { + Total_ByteStatsField.Value.Reset(false); + Total_ByteStatsField.Value.AddStatistic(GetByteStatistics()); + } + //Directories + if (Total_DirStatsField.IsValueCreated) + { + Total_DirStatsField.Value.Reset(false); + Total_DirStatsField.Value.AddStatistic(GetDirectoriesStatistics()); + } + //Files + if (Total_FileStatsField.IsValueCreated) + { + Total_FileStatsField.Value.Reset(false); + Total_FileStatsField.Value.AddStatistic(GetFilesStatistics()); + } + //Exit Status + if (ExitStatusSummaryField.IsValueCreated) + { + ExitStatusSummaryField.Value.Reset(false); + ExitStatusSummaryField.Value.CombineStatus(GetStatuses()); + } + //Speed + if (Average_SpeedStatsField.IsValueCreated) + { + Average_SpeedStatsField.Value.Reset(false); + Average_SpeedStatsField.Value.Average(GetSpeedStatistics()); + } + + goto RaiseEvent; + } + + //Process New Items + if (e.NewItems != null) + { + int i = 0; + int i2 = e.NewItems.Count; + foreach (RoboCopyResults r in e?.NewItems) + { + i++; + bool RaiseValueChangeEvent = (e.OldItems == null || e.OldItems.Count == 0) && (i == i2); //Prevent raising the event if more calculation needs to be performed either from NewItems or from OldItems + //Bytes + if (Total_ByteStatsField.IsValueCreated) + Total_ByteStatsField.Value.AddStatistic(r?.BytesStatistic, RaiseValueChangeEvent); + //Directories + if (Total_DirStatsField.IsValueCreated) + Total_DirStatsField.Value.AddStatistic(r?.DirectoriesStatistic, RaiseValueChangeEvent); + //Files + if (Total_FileStatsField.IsValueCreated) + Total_FileStatsField.Value.AddStatistic(r?.FilesStatistic, RaiseValueChangeEvent); + //Exit Status + if (ExitStatusSummaryField.IsValueCreated) + ExitStatusSummaryField.Value.CombineStatus(r?.Status, RaiseValueChangeEvent); + //Speed + if (Average_SpeedStatsField.IsValueCreated) + { + Average_SpeedStatsField.Value.Add(r?.SpeedStatistic); + if (RaiseValueChangeEvent) Average_SpeedStatsField.Value.CalculateAverage(); + } + } + } + + //Process Removed Items + if (e.OldItems != null) + { + int i = 0; + int i2 = e.OldItems.Count; + foreach (RoboCopyResults r in e?.OldItems) + { + i++; + bool RaiseValueChangeEvent = i == i2; + //Bytes + if (Total_ByteStatsField.IsValueCreated) + Total_ByteStatsField.Value.Subtract(r?.BytesStatistic, RaiseValueChangeEvent); + //Directories + if (Total_DirStatsField.IsValueCreated) + Total_DirStatsField.Value.Subtract(r?.DirectoriesStatistic, RaiseValueChangeEvent); + //Files + if (Total_FileStatsField.IsValueCreated) + Total_FileStatsField.Value.Subtract(r?.FilesStatistic, RaiseValueChangeEvent); + //Exit Status + if (ExitStatusSummaryField.IsValueCreated && RaiseValueChangeEvent) + { + ExitStatusSummaryField.Value.Reset(false); + ExitStatusSummaryField.Value.CombineStatus(GetStatuses()); + } + //Speed + if (Average_SpeedStatsField.IsValueCreated) + { + if (this.Count == 0) + Average_SpeedStatsField.Value.Reset(); + else + Average_SpeedStatsField.Value.Subtract(r.SpeedStatistic); + if (RaiseValueChangeEvent) Average_SpeedStatsField.Value.CalculateAverage(); + } + } + } + + RaiseEvent: + //Raise the CollectionChanged event + CollectionChanged?.Invoke(this, e); + } + + #endregion + + #region < ICloneable > + + /// Clone this object to a new RoboCopyResultsList + public RoboCopyResultsList Clone() => new RoboCopyResultsList(this); + + #endregion + + #region < IList{T} Implementation > + + bool ICollection.IsReadOnly => false; + + /// + public int IndexOf(RoboCopyResults item) => ResultsList.IndexOf(item); + + /// + public void Insert(int index, RoboCopyResults item) => ResultsList.Insert(index, item); + + /// + public void RemoveAt(int index) => ResultsList.RemoveAt(index); + + /// + public void Add(RoboCopyResults item) => ResultsList.Add(item); + + /// + public void Clear() => ResultsList.Clear(); + + /// + public bool Contains(RoboCopyResults item) => ResultsList.Contains(item); + + /// + public void CopyTo(RoboCopyResults[] array, int arrayIndex) => ResultsList.CopyTo(array, arrayIndex); + + /// + public bool Remove(RoboCopyResults item) => ResultsList.Remove(item); + + /// + public IEnumerator GetEnumerator() => ResultsList.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ResultsList.GetEnumerator(); + + #endregion + + } + +} + diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsStatus.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsStatus.cs new file mode 100644 index 0000000..6f6a824 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsStatus.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using FSI.Lib.Tools.RoboSharp.Interfaces; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// Object that evaluates the ExitCode reported after RoboCopy finishes executing. + /// + /// + /// + /// + public class RoboCopyExitStatus + { + /// + /// Initializes a new instance of the class. + /// + public RoboCopyExitStatus(int exitCodeValue) + { + ExitCodeValue = exitCodeValue; + } + + /// ExitCode as reported by RoboCopy + public int ExitCodeValue { get; protected set; } + + /// ExitCode reported by RoboCopy converted into the Enum + public RoboCopyExitCodes ExitCode => (RoboCopyExitCodes)ExitCodeValue; + + /// + public bool Successful => !WasCancelled && ExitCodeValue < 0x10; + + /// + public bool HasWarnings => ExitCodeValue >= 0x4; + + /// + public bool HasErrors => ExitCodeValue >= 0x10; + + /// + public virtual bool WasCancelled => ExitCodeValue < 0x0; + + /// + /// Returns a string that represents the current object. + /// + public override string ToString() + { + return $"ExitCode: {ExitCodeValue} ({ExitCode})"; + } + + } + + /// + /// Represents the combination of multiple Exit Statuses + /// + public sealed class RoboCopyCombinedExitStatus : RoboCopyExitStatus, IRoboCopyCombinedExitStatus + { + #region < Constructor > + + /// + /// Initializes a new instance of the class. + /// + public RoboCopyCombinedExitStatus() : base(0) { } + + /// + /// Initializes a new instance of the class. + /// + public RoboCopyCombinedExitStatus(int exitCodeValue) : base(exitCodeValue) { } + + /// + /// Clone this into a new instance + /// + /// + public RoboCopyCombinedExitStatus(RoboCopyCombinedExitStatus obj) :base((int)obj.ExitCode) + { + wascancelled = obj.wascancelled; + noCopyNoError = obj.noCopyNoError; + } + + #endregion + + #region < Fields and Event > + + //Private bools for the Combine methods + private bool wascancelled; + private bool noCopyNoError; + private bool EnablePropertyChangeEvent = true; + + /// This event when the ExitStatus summary has changed + public event PropertyChangedEventHandler PropertyChanged; + + #endregion + + #region < Public Properties > + + /// Overides + /// + public override bool WasCancelled => AnyWasCancelled; + + /// + /// Atleast one objects combined into this result resulted in no errors and no files/directories copied. + /// + public bool AnyNoCopyNoError => noCopyNoError || ExitCodeValue == 0x0; + + /// + /// Atleast one object combined into this result had been cancelled / exited prior to completion. + /// + public bool AnyWasCancelled => wascancelled || ExitCodeValue < 0x0; + + /// + /// All jobs completed without errors or warnings. + /// + public bool AllSuccessful => !WasCancelled && (ExitCodeValue == 0x0 || ExitCodeValue == 0x1); + + /// + /// All jobs completed without errors or warnings, but Extra Files/Folders were detected. + /// + public bool AllSuccessful_WithWarnings => !WasCancelled && Successful && ExitCodeValue > 0x1; + + #endregion + + #region < RaiseEvent Methods > + +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + private object[] StoreCurrentValues() + { + return new object[10] + { + WasCancelled, AnyNoCopyNoError, AnyWasCancelled, AllSuccessful, AllSuccessful_WithWarnings, HasErrors, HasWarnings, Successful, ExitCode, ExitCodeValue + }; + } + +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + private void CompareAndRaiseEvents(object[] OriginalValues) + { + object[] CurrentValues = StoreCurrentValues(); + if (CurrentValues[0] != OriginalValues[0]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("WasCancelled")); + if (CurrentValues[1] != OriginalValues[1]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AnyNoCopyNoError")); + if (CurrentValues[2] != OriginalValues[2]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AnyWasCancelled")); + if (CurrentValues[3] != OriginalValues[3]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AllSuccessful")); + if (CurrentValues[4] != OriginalValues[4]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AllSuccessful_WithWarnings")); + if (CurrentValues[5] != OriginalValues[5]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("HasErrors")); + if (CurrentValues[6] != OriginalValues[6]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("HasWarnings")); + if (CurrentValues[7] != OriginalValues[7]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Successful")); + if (CurrentValues[8] != OriginalValues[8]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ExitCode")); + if (CurrentValues[9] != OriginalValues[9]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ExitCodeValue")); + } + + #endregion + + /// + /// Combine the RoboCopyExitCodes of the supplied ExitStatus with this ExitStatus. + /// + /// If any were Cancelled, set the WasCancelled property to TRUE. Otherwise combine the exit codes. + /// ExitStatus to combine with +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + public void CombineStatus(RoboCopyExitStatus status) + { + if (status == null) return; + object[] OriginalValues = EnablePropertyChangeEvent ? StoreCurrentValues() : null; + //Combine the status + if (status.WasCancelled) + { + wascancelled = true; + } + else + { + if (status.ExitCode == 0x0) this.noCopyNoError = true; //0x0 is lost if any other values have been added, so this logs the state + RoboCopyExitCodes code = this.ExitCode | status.ExitCode; + this.ExitCodeValue = (int)code; + } + //Raise Property Change Events + if (EnablePropertyChangeEvent) CompareAndRaiseEvents(OriginalValues); + } + + internal void CombineStatus(RoboCopyExitStatus status, bool enablePropertyChangeEvent) + { + EnablePropertyChangeEvent = enablePropertyChangeEvent; + CombineStatus(status); + EnablePropertyChangeEvent = enablePropertyChangeEvent; + } + + /// + /// Combine all the RoboCopyExitStatuses together. + /// + /// Array or List of ExitStatuses to combine. + public void CombineStatus(IEnumerable status) + { + foreach (RoboCopyExitStatus s in status) + { + EnablePropertyChangeEvent = s == status.Last(); + CombineStatus(s); + } + } + + /// + /// Combine all the RoboCopyExitStatuses together. + /// + /// Array or List of ExitStatuses to combine. + /// new RoboCopyExitStatus object + public static RoboCopyCombinedExitStatus CombineStatuses(IEnumerable statuses) + { + RoboCopyCombinedExitStatus ret = new RoboCopyCombinedExitStatus(0); + ret.CombineStatus(statuses); + return ret; + } + + /// + /// Reset the value of the object + /// +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + public void Reset() + { + object[] OriginalValues = EnablePropertyChangeEvent ? StoreCurrentValues() : null; + this.wascancelled = false; + this.noCopyNoError = false; + this.ExitCodeValue = 0; + if (EnablePropertyChangeEvent) CompareAndRaiseEvents(OriginalValues); + } + + /// + /// Reset the value of the object + /// + internal void Reset(bool enablePropertyChangeEvent) + { + EnablePropertyChangeEvent = enablePropertyChangeEvent; + Reset(); + EnablePropertyChangeEvent = true; + } + + /// + public RoboCopyCombinedExitStatus Clone() => new RoboCopyCombinedExitStatus(this); + + object ICloneable.Clone() => Clone(); + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboQueueProgressEstimator.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboQueueProgressEstimator.cs new file mode 100644 index 0000000..4dcb0b1 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboQueueProgressEstimator.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using FSI.Lib.Tools.RoboSharp.Interfaces; +using FSI.Lib.Tools.RoboSharp.EventArgObjects; +using FSI.Lib.Tools.RoboSharp.Results; +using WhereToAdd = FSI.Lib.Tools.RoboSharp.Results.ProgressEstimator.WhereToAdd; +using System.Runtime.CompilerServices; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// Updates the Statistics every 250ms + /// + /// + /// + /// + internal class RoboQueueProgressEstimator : IProgressEstimator, IResults, IDisposable + { + #region < Constructors > + + internal RoboQueueProgressEstimator() + { + tmpDirs = new Statistic(Statistic.StatType.Directories); + tmpFiles = new Statistic(Statistic.StatType.Files); + tmpBytes = new Statistic(Statistic.StatType.Bytes); + + tmpDirs.EnablePropertyChangeEvent = false; + tmpFiles.EnablePropertyChangeEvent = false; + tmpBytes.EnablePropertyChangeEvent = false; + } + + #endregion + + #region < Private Members > + + //ThreadSafe Bags/Queues + private readonly ConcurrentDictionary SubscribedStats = new ConcurrentDictionary(); + + //Stats + private readonly Statistic DirStatField = new Statistic(Statistic.StatType.Directories, "Directory Stats Estimate"); + private readonly Statistic FileStatsField = new Statistic(Statistic.StatType.Files, "File Stats Estimate"); + private readonly Statistic ByteStatsField = new Statistic(Statistic.StatType.Bytes, "Byte Stats Estimate"); + + //Add Tasks + private int UpdatePeriodInMilliSecond = 250; + private readonly Statistic tmpDirs; + private readonly Statistic tmpFiles; + private readonly Statistic tmpBytes; + private DateTime NextUpdate = DateTime.Now; + private readonly object StatLock = new object(); + private readonly object UpdateLock = new object(); + private bool disposedValue; + + #endregion + + #region < Public Properties > + + /// + /// Estimate of current number of directories processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy. + ///
+ public IStatistic DirectoriesStatistic => DirStatField; + + /// + /// Estimate of current number of files processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy. + ///
+ public IStatistic FilesStatistic => FileStatsField; + + /// + /// Estimate of current number of bytes processed while the job is still running.
+ /// Estimate is provided by parsing of the LogLines produces by RoboCopy. + ///
+ public IStatistic BytesStatistic => ByteStatsField; + + RoboCopyExitStatus IResults.Status => new RoboCopyExitStatus((int)GetExitCode()); + + /// + public event ProgressEstimator.UIUpdateEventHandler ValuesUpdated; + + #endregion + + #region < Public Methods > + + /// + /// Parse this object's stats into a enum. + /// + /// + public RoboCopyExitCodes GetExitCode() + { + Results.RoboCopyExitCodes code = 0; + + //Files Copied + if (FilesStatistic.Copied > 0) + code |= Results.RoboCopyExitCodes.FilesCopiedSuccessful; + + //Extra + if (DirectoriesStatistic.Extras > 0 || FilesStatistic.Extras > 0) + code |= Results.RoboCopyExitCodes.ExtraFilesOrDirectoriesDetected; + + //MisMatch + if (DirectoriesStatistic.Mismatch > 0 || FilesStatistic.Mismatch > 0) + code |= Results.RoboCopyExitCodes.MismatchedDirectoriesDetected; + + //Failed + if (DirectoriesStatistic.Failed > 0 || FilesStatistic.Failed > 0) + code |= Results.RoboCopyExitCodes.SomeFilesOrDirectoriesCouldNotBeCopied; + + return code; + + } + + #endregion + + #region < Counting Methods ( private ) > + + /// + /// Subscribe to the update events of a object + /// + internal void BindToProgressEstimator(IProgressEstimator estimator) + { + if (!SubscribedStats.ContainsKey(estimator)) + { + SubscribedStats.TryAdd(estimator, null); + estimator.ValuesUpdated += Estimator_ValuesUpdated; + } + } + + private void Estimator_ValuesUpdated(IProgressEstimator sender, IProgressEstimatorUpdateEventArgs e) + { + Statistic bytes = null; + Statistic files = null; + Statistic dirs = null; + bool updateReqd = false; + + //Wait indefinitely until the tmp stats are released -- Previously -> lock(StatLock) { } + Monitor.Enter(StatLock); + + //Update the Temp Stats + tmpBytes.AddStatistic(e.ValueChange_Bytes); + tmpFiles.AddStatistic(e.ValueChange_Files); + tmpDirs.AddStatistic(e.ValueChange_Directories); + + //Check if an update should be pushed to the public fields + //If another thread already has this locked, then it is pushing out an update -> This thread exits the method after releasing StatLock + if (Monitor.TryEnter(UpdateLock)) + { + updateReqd = DateTime.Now >= NextUpdate && (tmpFiles.NonZeroValue || tmpDirs.NonZeroValue || tmpBytes.NonZeroValue); + if (updateReqd) + { + if (tmpBytes.NonZeroValue) + { + bytes = tmpBytes.Clone(); + tmpBytes.Reset(); + } + + if (tmpFiles.NonZeroValue) + { + files = tmpFiles.Clone(); + tmpFiles.Reset(); + } + + if (tmpDirs.NonZeroValue) + { + dirs = tmpDirs.Clone(); + tmpDirs.Reset(); + } + } + } + + Monitor.Exit(StatLock); //Release the tmp Stats lock + + if (updateReqd) + { + // Perform the Add Events + if (bytes != null) ByteStatsField.AddStatistic(bytes); + if (files != null) FileStatsField.AddStatistic(files); + if (dirs != null) DirStatField.AddStatistic(dirs); + //Raise the event, then update the NextUpdate time period + ValuesUpdated?.Invoke(this, new IProgressEstimatorUpdateEventArgs(this, bytes, files, dirs)); + NextUpdate = DateTime.Now.AddMilliseconds(UpdatePeriodInMilliSecond); + } + + //Release the UpdateLock + if (Monitor.IsEntered(UpdateLock)) + Monitor.Exit(UpdateLock); + } + + /// + /// Unsubscribe from all bound Statistic objects + /// + internal void UnBind() + { + if (SubscribedStats != null) + { + foreach (var est in SubscribedStats.Keys) + { + est.ValuesUpdated -= Estimator_ValuesUpdated; + } + } + } + + #endregion + + #region < CancelTasks & DisposePattern > + + /// + /// Unbind all the ProgressEstimators + /// + internal void CancelTasks() => CancelTasks(true); + + private void CancelTasks(bool RunUpdateTask) + { + //Preventn any additional events coming through + UnBind(); + //Push the last update out after a short delay to allow any pending events through + if (RunUpdateTask) + { + Task.Run( async () => { + lock (UpdateLock) + NextUpdate = DateTime.Now.AddMilliseconds(124); + await Task.Delay(125); + Estimator_ValuesUpdated(null, IProgressEstimatorUpdateEventArgs.DummyArgs); + }); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + //Cancel the tasks + CancelTasks(false); + disposedValue = true; + } + } + + // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~RoboQueueProgressEstimator() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboQueueResults.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboQueueResults.cs new file mode 100644 index 0000000..f6045e6 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/RoboQueueResults.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FSI.Lib.Tools.RoboSharp.Interfaces; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// Object returned by RoboQueue when a run has completed. + /// + public sealed class RoboQueueResults : IRoboQueueResults, IRoboCopyResultsList, ITimeSpan + { + internal RoboQueueResults() + { + collection = new RoboCopyResultsList(); + StartTime = DateTime.Now; + QueueProcessRunning = true; + } + + private RoboCopyResultsList collection { get; } + private DateTime EndTimeField; + private TimeSpan TimeSpanField; + private bool QueueProcessRunning; + + /// + /// Add a result to the collection + /// + internal void Add(RoboCopyResults result) => collection.Add(result); + + #region < IRoboQueueResults > + + /// Time the RoboQueue task was started + public DateTime StartTime { get; } + + /// Time the RoboQueue task was completed / cancelled. + /// Should Only considered valid if = true. + public DateTime EndTime + { + get => EndTimeField; + internal set + { + EndTimeField = value; + TimeSpanField = value.Subtract(StartTime); + QueueProcessRunning = false; + } + } + + /// Length of Time RoboQueue was running + /// Should Only considered valid if = true. + public TimeSpan TimeSpan => TimeSpanField; + + /// TRUE if the RoboQueue object that created this results set has not finished running yet. + public bool QueueRunning => QueueProcessRunning; + + /// TRUE if the RoboQueue object that created this results has completed running, or has been cancelled. + public bool QueueComplete => !QueueProcessRunning; + + #endregion + + #region < IRoboCopyResultsList Implementation > + + /// + public IStatistic DirectoriesStatistic => ((IRoboCopyResultsList)collection).DirectoriesStatistic; + + /// + public IStatistic BytesStatistic => ((IRoboCopyResultsList)collection).BytesStatistic; + + /// + public IStatistic FilesStatistic => ((IRoboCopyResultsList)collection).FilesStatistic; + + /// + public ISpeedStatistic SpeedStatistic => ((IRoboCopyResultsList)collection).SpeedStatistic; + + /// + public IRoboCopyCombinedExitStatus Status => ((IRoboCopyResultsList)collection).Status; + + /// + public IReadOnlyList Collection => ((IRoboCopyResultsList)collection).Collection; + + /// + public int Count => ((IRoboCopyResultsList)collection).Count; + + public RoboCopyResults this[int i] => ((IRoboCopyResultsList)collection)[i]; + + /// + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add + { + ((INotifyCollectionChanged)collection).CollectionChanged += value; + } + + remove + { + ((INotifyCollectionChanged)collection).CollectionChanged -= value; + } + } + + /// + public IStatistic[] GetByteStatistics() + { + return ((IRoboCopyResultsList)collection).GetByteStatistics(); + } + + /// + public IStatistic[] GetDirectoriesStatistics() + { + return ((IRoboCopyResultsList)collection).GetDirectoriesStatistics(); + } + + /// + public IEnumerator GetEnumerator() + { + return ((IEnumerable)collection).GetEnumerator(); + } + + /// + public IStatistic[] GetFilesStatistics() + { + return ((IRoboCopyResultsList)collection).GetFilesStatistics(); + } + + /// + public ISpeedStatistic[] GetSpeedStatistics() + { + return ((IRoboCopyResultsList)collection).GetSpeedStatistics(); + } + + /// + public RoboCopyExitStatus[] GetStatuses() + { + return ((IRoboCopyResultsList)collection).GetStatuses(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)collection).GetEnumerator(); + } + + /// + public ErrorEventArgs[] GetErrors() + { + return ((IRoboCopyResultsList)collection).GetErrors(); + } + #endregion + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/SpeedStatistic.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/SpeedStatistic.cs new file mode 100644 index 0000000..7ffac70 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/SpeedStatistic.cs @@ -0,0 +1,387 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using FSI.Lib.Tools.RoboSharp.Interfaces; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// Contains information regarding average Transfer Speed.
+ /// Note: Runs that do not perform any copy operations or that exited prematurely ( ) will result in a null object. + ///
+ /// + /// + /// + public class SpeedStatistic : INotifyPropertyChanged, ISpeedStatistic + { + /// + /// Create new SpeedStatistic + /// + public SpeedStatistic() { } + + /// + /// Clone a SpeedStatistic + /// + public SpeedStatistic(SpeedStatistic stat) + { + BytesPerSec = stat.BytesPerSec; + MegaBytesPerMin = stat.MegaBytesPerMin; + } + + #region < Private & Protected Members > + + private decimal BytesPerSecField = 0; + private decimal MegaBytesPerMinField = 0; + + /// This toggle Enables/Disables firing the Event to avoid firing it when doing multiple consecutive changes to the values + protected bool EnablePropertyChangeEvent { get; set; } = true; + + #endregion + + #region < Public Properties & Events > + + /// This event will fire when the value of the SpeedStatistic is updated + public event PropertyChangedEventHandler PropertyChanged; + + /// Raise Property Change Event + protected void OnPropertyChange(string PropertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName)); + + /// + public virtual decimal BytesPerSec + { + get => BytesPerSecField; + protected set + { + if (BytesPerSecField != value) + { + BytesPerSecField = value; + if (EnablePropertyChangeEvent) OnPropertyChange("MegaBytesPerMin"); + } + } + } + + /// + public virtual decimal MegaBytesPerMin + { + get => MegaBytesPerMinField; + protected set + { + if (MegaBytesPerMinField != value) + { + MegaBytesPerMinField = value; + if (EnablePropertyChangeEvent) OnPropertyChange("MegaBytesPerMin"); + } + } + } + + #endregion + + #region < Methods > + + /// + /// Returns a string that represents the current object. + /// + public override string ToString() + { + return $"Speed: {BytesPerSec} Bytes/sec{Environment.NewLine}Speed: {MegaBytesPerMin} MegaBytes/min"; + } + + /// + public virtual SpeedStatistic Clone() => new SpeedStatistic(this); + + object ICloneable.Clone() => Clone(); + + internal static SpeedStatistic Parse(string line1, string line2) + { + var res = new SpeedStatistic(); + + var pattern = new Regex(@"\d+([\.,]\d+)?"); + Match match; + + match = pattern.Match(line1); + if (match.Success) + { + res.BytesPerSec = Convert.ToDecimal(match.Value.Replace(',', '.'), CultureInfo.InvariantCulture); + } + + match = pattern.Match(line2); + if (match.Success) + { + res.MegaBytesPerMin = Convert.ToDecimal(match.Value.Replace(',', '.'), CultureInfo.InvariantCulture); + } + + return res; + } + + #endregion + + } + + /// + /// This object represents the Average of several objects, and contains + /// methods to facilitate that functionality. + /// + public sealed class AverageSpeedStatistic : SpeedStatistic + { + #region < Constructors > + + /// + /// Initialize a new object with the default values. + /// + public AverageSpeedStatistic() : base() { } + + /// + /// Initialize a new object.
+ /// Values will be set to the return values of and
+ ///
+ /// + /// Either a or a object.
+ /// If a is passed into this constructor, it wil be treated as the base instead. + /// + public AverageSpeedStatistic(ISpeedStatistic speedStat) : base() + { + Divisor = 1; + Combined_BytesPerSec = speedStat.BytesPerSec; + Combined_MegaBytesPerMin = speedStat.MegaBytesPerMin; + CalculateAverage(); + } + + /// + /// Initialize a new object using .
+ ///
+ /// + /// + public AverageSpeedStatistic(IEnumerable speedStats) : base() + { + Average(speedStats); + } + + /// + /// Clone an AverageSpeedStatistic + /// + public AverageSpeedStatistic(AverageSpeedStatistic stat) : base(stat) + { + Divisor = stat.Divisor; + Combined_BytesPerSec = stat.BytesPerSec; + Combined_MegaBytesPerMin = stat.MegaBytesPerMin; + } + + #endregion + + #region < Fields > + + /// Sum of all + private decimal Combined_BytesPerSec = 0; + + /// Sum of all + private decimal Combined_MegaBytesPerMin = 0; + + /// Total number of SpeedStats that were combined to produce the Combined_* values + private long Divisor = 0; + + #endregion + + #region < Public Methods > + + /// + public override SpeedStatistic Clone() => new AverageSpeedStatistic(this); + + #endregion + + #region < Reset Value Methods > + + /// + /// Set the values for this object to 0 + /// +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + public void Reset() + { + Combined_BytesPerSec = 0; + Combined_MegaBytesPerMin = 0; + Divisor = 0; + BytesPerSec = 0; + MegaBytesPerMin = 0; + } + + /// + /// Set the values for this object to 0 + /// +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + internal void Reset(bool enablePropertyChangeEvent) + { + EnablePropertyChangeEvent = enablePropertyChangeEvent; + Reset(); + EnablePropertyChangeEvent = true; + } + + #endregion + + // Add / Subtract methods are internal to allow usage within the RoboCopyResultsList object. + // The 'Average' Methods will first Add the statistics to the current one, then recalculate the average. + // Subtraction is only used when an item is removed from a RoboCopyResultsList + // As such, public consumers should typically not require the use of subtract methods + + #region < ADD ( internal ) > + + /// + /// Add the results of the supplied SpeedStatistic objects to this object.
+ /// Does not automatically recalculate the average, and triggers no events. + ///
+ /// + /// If any supplied Speedstat object is actually an object, default functionality will combine the private fields + /// used to calculate the average speed instead of using the publicly reported speeds.
+ /// This ensures that combining the average of multiple objects returns the correct value.
+ /// Ex: One object with 2 runs and one with 3 runs will return the average of all 5 runs instead of the average of two averages. + ///
+ /// SpeedStatistic Item to add + /// + /// Setting this to TRUE will instead combine the calculated average of the , treating it as a single object.
+ /// Ignore the private fields, and instead use the calculated speeds) + /// +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + internal void Add(ISpeedStatistic stat, bool ForceTreatAsSpeedStat = false) + { + if (stat == null) return; + bool IsAverageStat = !ForceTreatAsSpeedStat && stat.GetType() == typeof(AverageSpeedStatistic); + AverageSpeedStatistic AvgStat = IsAverageStat ? (AverageSpeedStatistic)stat : null; + Divisor += IsAverageStat ? AvgStat.Divisor : 1; + Combined_BytesPerSec += IsAverageStat ? AvgStat.Combined_BytesPerSec : stat.BytesPerSec; + Combined_MegaBytesPerMin += IsAverageStat ? AvgStat.Combined_MegaBytesPerMin : stat.MegaBytesPerMin; + } + + + /// + /// Add the supplied SpeedStatistic collection to this object. + /// + /// SpeedStatistic collection to add + /// + /// +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + internal void Add(IEnumerable stats, bool ForceTreatAsSpeedStat = false) + { + foreach (SpeedStatistic stat in stats) + Add(stat, ForceTreatAsSpeedStat); + } + + #endregion + + #region < Subtract ( internal ) > + + /// + /// Subtract the results of the supplied SpeedStatistic objects from this object.
+ ///
+ /// Statistics Item to add + /// +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + internal void Subtract(SpeedStatistic stat, bool ForceTreatAsSpeedStat = false) + { + if (stat == null) return; + bool IsAverageStat = !ForceTreatAsSpeedStat && stat.GetType() == typeof(AverageSpeedStatistic); + AverageSpeedStatistic AvgStat = IsAverageStat ? (AverageSpeedStatistic)stat : null; + Divisor -= IsAverageStat ? AvgStat.Divisor : 1; + //Combine the values if Divisor is still valid + if (Divisor >= 1) + { + Combined_BytesPerSec -= IsAverageStat ? AvgStat.Combined_BytesPerSec : stat.BytesPerSec; + Combined_MegaBytesPerMin -= IsAverageStat ? AvgStat.Combined_MegaBytesPerMin : stat.MegaBytesPerMin; + } + //Cannot have negative speeds or divisors -> Reset all values + if (Divisor < 1 || Combined_BytesPerSec < 0 || Combined_MegaBytesPerMin < 0) + { + Combined_BytesPerSec = 0; + Combined_MegaBytesPerMin = 0; + Divisor = 0; + } + } + + /// + /// Subtract the supplied SpeedStatistic collection from this object. + /// + /// SpeedStatistic collection to subtract + /// +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + internal void Subtract(IEnumerable stats, bool ForceTreatAsSpeedStat = false) + { + foreach (SpeedStatistic stat in stats) + Subtract(stat, ForceTreatAsSpeedStat); + } + + #endregion + + #region < AVERAGE ( public ) > + + /// + /// Immediately recalculate the BytesPerSec and MegaBytesPerMin values + /// +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + internal void CalculateAverage() + { + EnablePropertyChangeEvent = false; + //BytesPerSec + var i = Divisor < 1 ? 0 : Math.Round(Combined_BytesPerSec / Divisor, 3); + bool TriggerBPS = BytesPerSec != i; + BytesPerSec = i; + //MegaBytes + i = Divisor < 1 ? 0 : Math.Round(Combined_MegaBytesPerMin / Divisor, 3); + bool TriggerMBPM = MegaBytesPerMin != i; + MegaBytesPerMin = i; + //Trigger Events + EnablePropertyChangeEvent = true; + if (TriggerBPS) OnPropertyChange("BytesPerSec"); + if (TriggerMBPM) OnPropertyChange("MegaBytesPerMin"); + } + + /// + /// Combine the supplied objects, then get the average. + /// + /// Stats object + /// + public void Average(ISpeedStatistic stat) + { + Add(stat); + CalculateAverage(); + } + + /// + /// Combine the supplied objects, then get the average. + /// + /// Collection of objects + /// + public void Average(IEnumerable stats) + { + Add(stats); + CalculateAverage(); + } + + /// New Statistics Object + /// + public static AverageSpeedStatistic GetAverage(IEnumerable stats) + { + AverageSpeedStatistic stat = new AverageSpeedStatistic(); + stat.Average(stats); + return stat; + } + + #endregion + + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/Statistic.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/Statistic.cs new file mode 100644 index 0000000..782ac16 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/Results/Statistic.cs @@ -0,0 +1,816 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using FSI.Lib.Tools.RoboSharp.Interfaces; +using FSI.Lib.Tools.RoboSharp.EventArgObjects; + +namespace FSI.Lib.Tools.RoboSharp.Results +{ + /// + /// Information about number of items Copied, Skipped, Failed, etc. + /// + /// will not typically raise any events, but this object is used for other items, such as and to present results whose values may update periodically. + /// + /// + /// + /// + public class Statistic : IStatistic + { + internal static IStatistic Default_Bytes = new Statistic(type: StatType.Bytes); + internal static IStatistic Default_Files = new Statistic(type: StatType.Files); + internal static IStatistic Default_Dirs = new Statistic(type: StatType.Directories); + + #region < Constructors > + + /// Create a new Statistic object of + [Obsolete("Statistic Types require Initialization with a StatType")] + private Statistic() { } + + /// Create a new Statistic object + public Statistic(StatType type) { Type = type; } + + /// Create a new Statistic object + public Statistic(StatType type, string name) { Type = type; Name = name; } + + /// Clone an existing Statistic object + public Statistic(Statistic stat) + { + Type = stat.Type; + NameField = stat.Name; + TotalField = stat.Total; + CopiedField = stat.Copied; + SkippedField = stat.Skipped; + MismatchField = stat.Mismatch; + FailedField = stat.Failed; + ExtrasField = stat.Extras; + } + + /// Clone an existing Statistic object + internal Statistic(StatType type, string name, long total, long copied, long skipped, long mismatch, long failed, long extras) + { + Type = type; + NameField = name; + TotalField = total; + CopiedField = copied; + SkippedField = skipped; + MismatchField = mismatch; + FailedField = failed; + ExtrasField = extras; + } + + /// Describe the Type of Statistics Object + public enum StatType + { + /// Statistics object represents count of Directories + Directories, + /// Statistics object represents count of Files + Files, + /// Statistics object represents a Size ( number of bytes ) + Bytes + } + + #endregion + + #region < Fields > + + private string NameField = ""; + private long TotalField = 0; + private long CopiedField = 0; + private long SkippedField = 0; + private long MismatchField = 0; + private long FailedField = 0; + private long ExtrasField = 0; + + #endregion + + #region < Events > + + /// This toggle Enables/Disables firing the Event to avoid firing it when doing multiple consecutive changes to the values + internal bool EnablePropertyChangeEvent = true; + private bool PropertyChangedListener => EnablePropertyChangeEvent && PropertyChanged != null; + private bool Listener_TotalChanged => EnablePropertyChangeEvent && OnTotalChanged != null; + private bool Listener_CopiedChanged => EnablePropertyChangeEvent && OnCopiedChanged != null; + private bool Listener_SkippedChanged => EnablePropertyChangeEvent && OnSkippedChanged != null; + private bool Listener_MismatchChanged => EnablePropertyChangeEvent && OnMisMatchChanged != null; + private bool Listener_FailedChanged => EnablePropertyChangeEvent && OnFailedChanged != null; + private bool Listener_ExtrasChanged => EnablePropertyChangeEvent && OnExtrasChanged != null; + + /// + /// This event will fire when the value of the statistic is updated via Adding / Subtracting methods.
+ /// Provides object. + ///
+ /// + /// Allows use with both binding to controls and binding.
+ /// EventArgs can be passed into after casting. + ///
+ public event PropertyChangedEventHandler PropertyChanged; + + /// Handles any value changes + public delegate void StatChangedHandler(Statistic sender, StatChangedEventArg e); + + /// Occurs when the Property is updated. + public event StatChangedHandler OnTotalChanged; + + /// Occurs when the Property is updated. + public event StatChangedHandler OnCopiedChanged; + + /// Occurs when the Property is updated. + public event StatChangedHandler OnSkippedChanged; + + /// Occurs when the Property is updated. + public event StatChangedHandler OnMisMatchChanged; + + /// Occurs when the Property is updated. + public event StatChangedHandler OnFailedChanged; + + /// Occurs when the Property is updated. + public event StatChangedHandler OnExtrasChanged; + + #endregion + + #region < Properties > + + /// + /// Checks all values and determines if any of them are != 0. + /// + public bool NonZeroValue => TotalField != 0 || CopiedField != 0 || SkippedField != 0 || MismatchField != 0 || FailedField != 0 || ExtrasField != 0; + + /// + /// Name of the Statistics Object + /// + public string Name + { + get => NameField; + set + { + if (value != NameField) + { + NameField = value ?? ""; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name")); + } + } + } + + + /// + /// + /// + public StatType Type { get; } + + /// + public long Total { + get => TotalField; + internal set + { + if (TotalField != value) + { + var e = PrepEventArgs(TotalField, value, "Total"); + TotalField = value; + if (e != null) + { + PropertyChanged?.Invoke(this, e.Value.Item2); + OnTotalChanged?.Invoke(this, e.Value.Item1); + } + } + } + } + + /// + public long Copied { + get => CopiedField; + internal set + { + if (CopiedField != value) + { + var e = PrepEventArgs(CopiedField, value, "Copied"); + CopiedField = value; + if (e != null) + { + PropertyChanged?.Invoke(this, e.Value.Item2); + OnCopiedChanged?.Invoke(this, e.Value.Item1); + } + } + } + } + + /// + public long Skipped { + get => SkippedField; + internal set + { + if (SkippedField != value) + { + var e = PrepEventArgs(SkippedField, value, "Skipped"); + SkippedField = value; + if (e != null) + { + PropertyChanged?.Invoke(this, e.Value.Item2); + OnSkippedChanged?.Invoke(this, e.Value.Item1); + } + } + } + } + + /// + public long Mismatch { + get => MismatchField; + internal set + { + if (MismatchField != value) + { + var e = PrepEventArgs(MismatchField, value, "Mismatch"); + MismatchField = value; + if (e != null) + { + PropertyChanged?.Invoke(this, e.Value.Item2); + OnMisMatchChanged?.Invoke(this, e.Value.Item1); + } + } + } + } + + /// + public long Failed { + get => FailedField; + internal set + { + if (FailedField != value) + { + var e = PrepEventArgs(FailedField, value, "Failed"); + FailedField = value; + if (e != null) + { + PropertyChanged?.Invoke(this, e.Value.Item2); + OnFailedChanged?.Invoke(this, e.Value.Item1); + } + } + } + } + + /// + public long Extras { + get => ExtrasField; + internal set + { + if (ExtrasField != value) + { + var e = PrepEventArgs(ExtrasField, value, "Extras"); + ExtrasField = value; + if (e != null) + { + PropertyChanged?.Invoke(this, e.Value.Item2); + OnExtrasChanged?.Invoke(this, e.Value.Item1); + } + } + } + } + + #endregion + + #region < ToString Methods > + + /// + /// Returns a string that represents the current object. + /// + public override string ToString() => ToString(false, true, ", "); + + /// + /// Customize the returned string + /// + /// Include string representation of + /// Include "Total:" / "Copied:" / etc in the string to identify the values + /// Value Delimieter + /// + /// Include the delimiter after the 'Type' - Only used if us also true.
+ /// When is true, a space always exist after the type string. This would add delimiter instead of the space. + /// + /// + /// TRUE, TRUE, "," --> $"{Type} Total: {Total}, Copied: {Copied}, Skipped: {Skipped}, Mismatch: {Mismatch}, Failed: {Failed}, Extras: {Extras}" + /// FALSE, TRUE, "," --> $"Total: {Total}, Copied: {Copied}, Skipped: {Skipped}, Mismatch: {Mismatch}, Failed: {Failed}, Extras: {Extras}" + /// FALSE, FALSE, "," --> $"{Total}, {Copied}, {Skipped}, {Mismatch}, {Failed}, {Extras}" + /// + public string ToString(bool IncludeType, bool IncludePrefix, string Delimiter, bool DelimiterAfterType = false) + { + return $"{ToString_Type(IncludeType, DelimiterAfterType)}" + $"{(IncludeType && DelimiterAfterType ? Delimiter : "")}" + + $"{ToString_Total(false, IncludePrefix)}{Delimiter}" + + $"{ToString_Copied(false, IncludePrefix)}{Delimiter}" + + $"{ToString_Skipped(false, IncludePrefix)}{Delimiter}" + + $"{ToString_Mismatch(false, IncludePrefix)}{Delimiter}" + + $"{ToString_Failed(false, IncludePrefix)}{Delimiter}" + + $"{ToString_Extras(false, IncludePrefix)}"; + } + + /// Get the as a string + public string ToString_Type() => ToString_Type(true).Trim(); + private string ToString_Type(bool IncludeType, bool Trim = false) => IncludeType ? $"{Type}{(Trim? "" : " ")}" : ""; + + /// Get the string describing the + /// + /// + public string ToString_Total(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix? "Total: " : "")}{Total}"; + + /// Get the string describing the + /// + public string ToString_Copied(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Copied: " : "")}{Copied}"; + + /// Get the string describing the + /// + public string ToString_Extras(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Extras: " : "")}{Extras}"; + + /// Get the string describing the + /// + public string ToString_Failed(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Failed: " : "")}{Failed}"; + + /// Get the string describing the + /// + public string ToString_Mismatch(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Mismatch: " : "")}{Mismatch}"; + + /// Get the string describing the + /// + public string ToString_Skipped(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Skipped: " : "")}{Skipped}"; + + #endregion + + #region < Parsing Methods > + + /// + /// Parse a string and for the tokens reported by RoboCopy + /// + /// Statistic Type to produce + /// LogLine produced by RoboCopy in Summary Section + /// New Statistic Object + public static Statistic Parse(StatType type, string line) + { + var res = new Statistic(type); + + var tokenNames = new[] { nameof(Total), nameof(Copied), nameof(Skipped), nameof(Mismatch), nameof(Failed), nameof(Extras) }; + var patternBuilder = new StringBuilder(@"^.*:"); + + foreach (var tokenName in tokenNames) + { + var tokenPattern = GetTokenPattern(tokenName); + patternBuilder.Append(@"\s+").Append(tokenPattern); + } + + var pattern = patternBuilder.ToString(); + var match = Regex.Match(line, pattern); + if (!match.Success) + return res; + + var props = res.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var tokenName in tokenNames) + { + var prop = props.FirstOrDefault(x => x.Name == tokenName); + if (prop == null) + continue; + + var tokenString = match.Groups[tokenName].Value; + var tokenValue = ParseTokenString(tokenString); + prop.SetValue(res, tokenValue, null); + } + + return res; + } + + private static string GetTokenPattern(string tokenName) + { + return $@"(?<{tokenName}>[\d\.]+(\s\w)?)"; + } + + private static long ParseTokenString(string tokenString) + { + if (string.IsNullOrWhiteSpace(tokenString)) + return 0; + + tokenString = tokenString.Trim(); + if (Regex.IsMatch(tokenString, @"^\d+$", RegexOptions.Compiled)) + return long.Parse(tokenString); + + var match = Regex.Match(tokenString, @"(?[\d\.,]+)(\.(?\d+))\s(?\w)", RegexOptions.Compiled); + if (match.Success) + { + var mains = match.Groups["Mains"].Value.Replace(".", "").Replace(",", ""); + var fraction = match.Groups["Fraction"].Value; + var unit = match.Groups["Unit"].Value.ToLower(); + + var number = double.Parse($"{mains}.{fraction}", NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + switch (unit) + { + case "k": + // Byte = kBytes * 1024 + number *= Math.Pow(1024, 1); + break; + case "m": + // Byte = MBytes * 1024 * 1024 + number *= Math.Pow(1024, 2); + break; + case "g": + // Byte = GBytes * 1024 * 1024 * 1024 + number *= Math.Pow(1024, 3); + break; + case "t": + // Byte = TBytes * 1024 * 1024 * 1024 * 1024 + number *= Math.Pow(1024, 4); + break; + } + + return Convert.ToInt64(number); + } + + return 0; + } + + #endregion + + #region < Reset Method > + + /// + /// Set the values for this object to 0 + /// + public void Reset(bool enablePropertyChangeEvent) + { + EnablePropertyChangeEvent = enablePropertyChangeEvent; + Reset(); + EnablePropertyChangeEvent = true; + } + + + /// + /// Reset all values to Zero ( 0 ) -- Used by for the properties + /// + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] + public void Reset() + { + Statistic OriginalValues = PropertyChangedListener && NonZeroValue ? this.Clone() : null; + + //Total + var eTotal = ConditionalPrepEventArgs(Listener_TotalChanged, TotalField, 0, "Total"); + TotalField = 0; + //Copied + var eCopied = ConditionalPrepEventArgs(Listener_CopiedChanged, CopiedField, 0, "Copied"); + CopiedField = 0; + //Extras + var eExtras = ConditionalPrepEventArgs(Listener_ExtrasChanged, ExtrasField, 0, "Extras"); + ExtrasField = 0; + //Failed + var eFailed = ConditionalPrepEventArgs(Listener_FailedChanged, FailedField, 0, "Failed"); + FailedField = 0; + //Mismatch + var eMismatch = ConditionalPrepEventArgs(Listener_MismatchChanged, MismatchField, 0, "Mismatch"); + MismatchField = 0; + //Skipped + var eSkipped = ConditionalPrepEventArgs(Listener_SkippedChanged, SkippedField, 0, "Skipped"); + SkippedField = 0; + + //Trigger Events + TriggerDeferredEvents(OriginalValues, eTotal, eCopied, eExtras, eFailed, eMismatch, eSkipped); + } + + + #endregion + + #region < Trigger Deferred Events > + + /// + /// Prep Event Args for SETTERS of the properties + /// + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] + private Lazy> PrepEventArgs(long OldValue, long NewValue, string PropertyName) + { + if (!EnablePropertyChangeEvent) return null; + var old = this.Clone(); + return new Lazy>(() => + { + StatChangedEventArg e1 = new StatChangedEventArg(this, OldValue, NewValue, PropertyName); + var e2 = new StatisticPropertyChangedEventArgs(this, old, PropertyName); + return new Tuple(e1, e2); + }); + } + + /// + /// Prep event args for the ADD and RESET methods + /// + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] + private StatChangedEventArg ConditionalPrepEventArgs(bool Listener, long OldValue, long NewValue, string PropertyName)=> !Listener || OldValue == NewValue ? null : new StatChangedEventArg(this, OldValue, NewValue, PropertyName); + + /// + /// Raises the events that were deferred while item was object was still being calculated by ADD / RESET + /// + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] + private void TriggerDeferredEvents(Statistic OriginalValues, StatChangedEventArg eTotal, StatChangedEventArg eCopied, StatChangedEventArg eExtras, StatChangedEventArg eFailed, StatChangedEventArg eMismatch, StatChangedEventArg eSkipped) + { + //Perform Events + int i = 0; + if (eTotal != null) + { + i = 1; + OnTotalChanged?.Invoke(this, eTotal); + } + if (eCopied != null) + { + i += 2; + OnCopiedChanged?.Invoke(this, eCopied); + } + if (eExtras != null) + { + i += 4; + OnExtrasChanged?.Invoke(this, eExtras); + } + if (eFailed != null) + { + i += 8; + OnFailedChanged?.Invoke(this, eFailed); + } + if (eMismatch != null) + { + i += 16; + OnMisMatchChanged?.Invoke(this, eMismatch); + } + if (eSkipped != null) + { + i += 32; + OnSkippedChanged?.Invoke(this, eSkipped); + } + + //Trigger PropertyChangeEvent + if (OriginalValues != null) + { + switch (i) + { + case 1: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eTotal.PropertyName)); return; + case 2: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eCopied.PropertyName)); return; + case 4: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eExtras.PropertyName)); return; + case 8: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eFailed.PropertyName)); return; + case 16: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eMismatch.PropertyName)); return; + case 32: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eSkipped.PropertyName)); return; + default: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, String.Empty)); return; + } + } + } + + #endregion + + #region < ADD Methods > + + /// + /// Add the supplied values to this Statistic object.
+ /// Events are defered until all the fields have been added together. + ///
+ /// + /// + /// + /// + /// + /// + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] + public void Add(long total = 0, long copied = 0, long extras = 0, long failed = 0, long mismatch = 0, long skipped = 0) + { + //Store the original object values for the event args + Statistic originalValues = PropertyChangedListener ? this.Clone() : null; + + //Total + long i = TotalField; + TotalField += total; + var eTotal = ConditionalPrepEventArgs(Listener_TotalChanged, i, TotalField, "Total"); + + //Copied + i = CopiedField; + CopiedField += copied; + var eCopied = ConditionalPrepEventArgs(Listener_CopiedChanged, i, CopiedField, "Copied"); + + //Extras + i = ExtrasField; + ExtrasField += extras; + var eExtras = ConditionalPrepEventArgs(Listener_ExtrasChanged, i, ExtrasField, "Extras"); + + //Failed + i = FailedField; + FailedField += failed; + var eFailed = ConditionalPrepEventArgs(Listener_FailedChanged, i, FailedField, "Failed"); + + //Mismatch + i = MismatchField; + MismatchField += mismatch; + var eMismatch = ConditionalPrepEventArgs(Listener_MismatchChanged, i, MismatchField, "Mismatch"); + + //Skipped + i = SkippedField; + SkippedField += skipped; + var eSkipped = ConditionalPrepEventArgs(Listener_SkippedChanged, i, SkippedField, "Skipped"); + + //Trigger Events + if (EnablePropertyChangeEvent) + TriggerDeferredEvents(originalValues, eTotal, eCopied, eExtras, eFailed, eMismatch, eSkipped); + } + + /// + /// Add the results of the supplied Statistics object to this Statistics object.
+ /// Events are defered until all the fields have been added together. + ///
+ /// Statistics Item to add + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] + public void AddStatistic(IStatistic stat) + { + if (stat != null && stat.Type == this.Type && stat.NonZeroValue) + Add(stat.Total, stat.Copied, stat.Extras, stat.Failed, stat.Mismatch, stat.Skipped); + } + + + #pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) + /// + /// + internal void AddStatistic(IStatistic stats, bool enablePropertyChangedEvent) + { + EnablePropertyChangeEvent = enablePropertyChangedEvent; + AddStatistic(stats); + EnablePropertyChangeEvent = true; + + } + #pragma warning restore CS1573 + + + /// + /// Add the results of the supplied Statistics objects to this Statistics object. + /// + /// Statistics Item to add + public void AddStatistic(IEnumerable stats) + { + foreach (Statistic stat in stats) + { + EnablePropertyChangeEvent = stat == stats.Last(); + AddStatistic(stat); + } + } + + /// + /// Adds to the appropriate property based on the 'PropertyChanged' value.
+ /// Will only add the value if the == . + ///
+ /// Arg provided by either or a Statistic's object's On*Changed events + public void AddStatistic(PropertyChangedEventArgs eventArgs) + { + //Only process the args if of the proper type + var e = eventArgs as IStatisticPropertyChangedEventArg; + if (e == null) return; + + if (e.StatType != this.Type || (e.Is_StatisticPropertyChangedEventArgs && e.Is_StatChangedEventArg)) + { + // INVALID! + } + else if (e.Is_StatisticPropertyChangedEventArgs) + { + var e1 = (StatisticPropertyChangedEventArgs)eventArgs; + AddStatistic(e1.Difference); + } + else if (e.Is_StatChangedEventArg) + { + var e2 = (StatChangedEventArg)eventArgs; + switch (e.PropertyName) + { + case "": //String.Empty means all fields have changed + AddStatistic(e2.Sender); + break; + case "Copied": + this.Copied += e2.Difference; + break; + case "Extras": + this.Extras += e2.Difference; + break; + case "Failed": + this.Failed += e2.Difference; + break; + case "Mismatch": + this.Mismatch += e2.Difference; + break; + case "Skipped": + this.Skipped += e2.Difference; + break; + case "Total": + this.Total += e2.Difference; + break; + } + } + } + + /// + /// Combine the results of the supplied statistics objects of the specified type. + /// + /// Collection of objects + /// Create a new Statistic object of this type. + /// New Statistics Object + public static Statistic AddStatistics(IEnumerable stats, StatType statType) + { + Statistic ret = new Statistic(statType); + ret.AddStatistic(stats.Where(s => s.Type == statType) ); + return ret; + } + + + #endregion ADD + + #region < AVERAGE Methods > + + /// + /// Combine the supplied objects, then get the average. + /// + /// Array of Stats objects + public void AverageStatistic(IEnumerable stats) + { + this.AddStatistic(stats); + int cnt = stats.Count() + 1; + Total /= cnt; + Copied /= cnt; + Extras /= cnt; + Failed /= cnt; + Mismatch /= cnt; + Skipped /= cnt; + + } + + /// New Statistics Object + /// + public static Statistic AverageStatistics(IEnumerable stats, StatType statType) + { + Statistic stat = AddStatistics(stats, statType); + int cnt = stats.Count(s => s.Type == statType); + if (cnt > 1) + { + stat.Total /= cnt; + stat.Copied /= cnt; + stat.Extras /= cnt; + stat.Failed /= cnt; + stat.Mismatch /= cnt; + stat.Skipped /= cnt; + } + return stat; + } + + #endregion AVERAGE + + #region < Subtract Methods > + + /// + /// Subtract Method used by
+ /// Events are deferred until all value changes have completed. + ///
+ /// Statistics Item to subtract +#if !NET40 + [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] +#endif + public void Subtract(IStatistic stat) + { + if (stat.Type == this.Type && stat.NonZeroValue) + Add(-stat.Total, -stat.Copied, -stat.Extras, -stat.Failed, -stat.Mismatch, -stat.Skipped); + } + + #pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) + /// + /// + internal void Subtract(IStatistic stats, bool enablePropertyChangedEvent) + { + EnablePropertyChangeEvent = enablePropertyChangedEvent; + Subtract(stats); + EnablePropertyChangeEvent = true; + } + #pragma warning restore CS1573 + + /// + /// Subtract the results of the supplied Statistics objects to this Statistics object. + /// + /// Statistics Item to subtract + public void Subtract(IEnumerable stats) + { + foreach (Statistic stat in stats) + { + EnablePropertyChangeEvent = stat == stats.Last(); + Subtract(stat); + } + } + + /// Statistics object to clone + /// + /// Clone of the object with the subtracted from it. + /// + public static Statistic Subtract(IStatistic MainStat, IStatistic stats) + { + var ret = MainStat.Clone(); + ret.Subtract(stats); + return ret; + } + + #endregion Subtract + + + /// + public Statistic Clone() => new Statistic(this); + + object ICloneable.Clone() => new Statistic(this); + + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/RetryOptions.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RetryOptions.cs new file mode 100644 index 0000000..063bb9e --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RetryOptions.cs @@ -0,0 +1,106 @@ +using System; +using System.Text; + +namespace FSI.Lib.Tools.RoboSharp +{ + /// + /// RoboCopy switches for how to react if a copy/move operation errors + /// + /// + /// + /// + public class RetryOptions : ICloneable + { + #region Constructors + + /// + /// Create new RetryOptions with Default Settings + /// + public RetryOptions() { } + + /// + /// Clone a RetryOptions Object + /// + /// RetryOptions object to clone + public RetryOptions(RetryOptions options) + { + WaitForSharenames = options.WaitForSharenames; + SaveToRegistry = options.SaveToRegistry; + RetryWaitTime = options.RetryWaitTime; + RetryCount = options.RetryCount; + } + + /// + public RetryOptions Clone() => new RetryOptions(this); + + object ICloneable.Clone() => Clone(); + + #endregion + + internal const string RETRY_COUNT = "/R:{0} "; + internal const string RETRY_WAIT_TIME = "/W:{0} "; + internal const string SAVE_TO_REGISTRY = "/REG "; + internal const string WAIT_FOR_SHARENAMES = "/TBD "; + + private int retryCount = 0; + private int retryWaitTime = 30; + + /// + /// Specifies the number of retries N on failed copies (default is 0). + /// [/R:N] + /// + public virtual int RetryCount + { + get { return retryCount; } + set { retryCount = value; } + } + /// + /// Specifies the wait time N in seconds between retries (default is 30). + /// [/W:N] + /// + public virtual int RetryWaitTime + { + get { return retryWaitTime; } + set { retryWaitTime = value; } + } + /// + /// Saves RetryCount and RetryWaitTime in the Registry as default settings. + /// [/REG] + /// + public virtual bool SaveToRegistry { get; set; } + /// + /// Wait for sharenames to be defined. + /// [/TBD] + /// + public virtual bool WaitForSharenames { get; set; } + + internal string Parse() + { + var options = new StringBuilder(); + + options.Append(string.Format(RETRY_COUNT, RetryCount)); + options.Append(string.Format(RETRY_WAIT_TIME, RetryWaitTime)); + + if (SaveToRegistry) + options.Append(SAVE_TO_REGISTRY); + if (WaitForSharenames) + options.Append(WAIT_FOR_SHARENAMES); + + return options.ToString(); + } + + /// + /// Combine this object with another RetryOptions 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(RetryOptions options) + { + RetryCount = RetryCount.GetGreaterVal(options.RetryCount); + RetryWaitTime = RetryWaitTime.GetGreaterVal(options.RetryWaitTime); + WaitForSharenames |= options.WaitForSharenames; + SaveToRegistry |= options.SaveToRegistry; + } + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboCommand.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboCommand.cs new file mode 100644 index 0000000..8969431 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboCommand.cs @@ -0,0 +1,865 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using FSI.Lib.Tools.RoboSharp.Interfaces; +using FSI.Lib.Tools.RoboSharp.EventArgObjects; +using FSI.Lib.Tools.RoboSharp; + +namespace FSI.Lib.Tools.RoboSharp +{ + /// + /// Wrapper for the RoboCopy process + /// + /// + /// + /// + public class RoboCommand : IDisposable, IRoboCommand, ICloneable + { + #region < Constructors > + + /// Create a new RoboCommand object + public RoboCommand() + { + InitClassProperties(); + Init(); + } + + /// + public RoboCommand(string name, bool stopIfDisposing = true) + { + InitClassProperties(); + Init(name, stopIfDisposing); + } + + /// + public RoboCommand(string source, string destination, bool stopIfDisposing = true) + { + InitClassProperties(); + Init("", stopIfDisposing, source, destination); + + } + /// + public RoboCommand(string source, string destination, string name, bool stopIfDisposing = true) + { + InitClassProperties(); + Init(name, stopIfDisposing, source, destination); + } + + /// Each of the Options objects can be specified within this constructor. If left = null, a new object will be generated using the default options for that object. + /// + public RoboCommand(string name, string source = null, string destination = null, bool StopIfDisposing = true, RoboSharpConfiguration configuration = null, CopyOptions copyOptions = null, SelectionOptions selectionOptions = null, RetryOptions retryOptions = null, LoggingOptions loggingOptions = null, JobOptions jobOptions = null) + { + this.configuration = configuration ?? new RoboSharpConfiguration(); + this.copyOptions = copyOptions ?? new CopyOptions(); + this.selectionOptions = selectionOptions ?? new SelectionOptions(); + this.retryOptions = retryOptions ?? new RetryOptions(); + this.loggingOptions = loggingOptions ?? new LoggingOptions(); + this.jobOptions = jobOptions ?? new JobOptions(); + Init(name, StopIfDisposing, source ?? CopyOptions.Source, destination ?? CopyOptions.Destination); + } + + /// + /// Create a new RoboCommand with identical options at this RoboCommand + /// + /// + /// If Desired, the new RoboCommand object will share some of the same Property objects as the input . + /// For Example, that means that if a SelectionOption property changes, it will affect both RoboCommand objects since the property is shared between them.
+ /// If the Link* options are set to FALSE (default), then it will create new property objects whose settings match the current settings of . + /// Properties that can be linked:
+ /// ( Linked by default )
+ /// ( Linked by default )
+ ///
+ ///
+ ///
+ ///
+ /// RoboCommand 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 + /// Link the of the two commands ( True Default ) + /// Link the of the two commands + /// Link the of the two commands ( True Default ) + /// Link the of the two commands + /// Link the of the two commands + public RoboCommand(RoboCommand command, string NewSource = null, string NewDestination = null, bool LinkConfiguration = true, bool LinkRetryOptions = true, bool LinkSelectionOptions = false, bool LinkLoggingOptions = false, bool LinkJobOptions = false) + { + Name = command.Name; + StopIfDisposing = command.StopIfDisposing; + + configuration = LinkConfiguration ? command.configuration : command.configuration.Clone(); + copyOptions = new CopyOptions(command.CopyOptions, NewSource, NewDestination); + JobOptions = LinkJobOptions ? command.jobOptions : command.jobOptions.Clone(); + loggingOptions = LinkLoggingOptions ? command.loggingOptions : command.loggingOptions.Clone(); + retryOptions = LinkRetryOptions ? command.retryOptions : command.retryOptions.Clone(); + selectionOptions = LinkSelectionOptions ? command.selectionOptions : command.SelectionOptions.Clone(); + } + + /// Create a new RoboCommand object + /// + /// + /// + /// + private void Init(string name = "", bool stopIfDisposing = true, string source = "", string destination = "") + { + Name = name; + StopIfDisposing = stopIfDisposing; + CopyOptions.Source = source; + CopyOptions.Destination = destination; + } + + private void InitClassProperties() + { + copyOptions = new CopyOptions(); + selectionOptions = new SelectionOptions(); + retryOptions = new RetryOptions(); + loggingOptions = new LoggingOptions(); + configuration = new RoboSharpConfiguration(); + jobOptions = new JobOptions(); + } + + /// + public RoboCommand Clone(string NewSource = null, string NewDestination = null, bool LinkConfiguration = true, bool LinkRetryOptions = true, bool LinkSelectionOptions = false, bool LinkLoggingOptions = false, bool LinkJobOptions = false) + => new RoboCommand(this, NewSource, NewDestination, LinkConfiguration, LinkRetryOptions, LinkSelectionOptions, LinkLoggingOptions, LinkJobOptions); + + object ICloneable.Clone() => new RoboCommand(this, null, null, false, false, false, false, false); + + #endregion + + #region < Private Vars > + + // set up in Constructor + private CopyOptions copyOptions; + private SelectionOptions selectionOptions; + private RetryOptions retryOptions; + private LoggingOptions loggingOptions; + private RoboSharpConfiguration configuration; + private JobOptions jobOptions; + + // Modified while running + private Process process; + private Task backupTask; + private bool hasError; + //private bool hasExited; //No longer evaluated + private bool isPaused; + private bool isRunning; + private bool isCancelled; + private Results.ResultsBuilder resultsBuilder; + private Results.RoboCopyResults results; + /// Stores the LastData processed by + private string LastDataReceived = ""; + + #endregion Private Vars + + #region < Public Vars > + + /// ID Tag for the job - Allows consumers to find/sort/remove/etc commands within a list via string comparison + public string Name { get; set; } + /// Value indicating if process is currently paused + public bool IsPaused { get { return isPaused; } } + /// Value indicating if process is currently running + public bool IsRunning { get { return isRunning; } } + /// Value indicating if process was Cancelled + public bool IsCancelled { get { return isCancelled; } } + /// TRUE if is set up (Copy Operation is scheduled to only operate within specified timeframe). Otherwise False. + public bool IsScheduled { get => !String.IsNullOrWhiteSpace(CopyOptions.RunHours); } + /// Get the parameters string passed to RoboCopy based on the current setup + public string CommandOptions { get { return GenerateParameters(); } } + /// + public CopyOptions CopyOptions + { + get { return copyOptions; } + set { copyOptions = value ?? copyOptions; } + } + /// + public SelectionOptions SelectionOptions + { + get { return selectionOptions; } + set { selectionOptions = value ?? selectionOptions; } + } + /// + public RetryOptions RetryOptions + { + get { return retryOptions; } + set { retryOptions = value ?? retryOptions; } + } + /// + public LoggingOptions LoggingOptions + { + get { return loggingOptions; } + set { loggingOptions = value ?? loggingOptions; } + } + /// + public JobOptions JobOptions + { + get { return jobOptions; } + set { jobOptions = value ?? jobOptions; } + } + /// + public RoboSharpConfiguration Configuration + { + get { return configuration; } + } + /// + /// + /// A new object is created every time the method is called, but will not be created until called for the first time. + /// + internal Results.ProgressEstimator ProgressEstimator { get; private set; } + + /// + public IProgressEstimator IProgressEstimator => this.ProgressEstimator; + + /// + /// Value indicating if the process should be killed when the method is called.
+ /// For example, if the RoboCopy process should exit when the program exits, this should be set to TRUE (default). + ///
+ public bool StopIfDisposing { get; set; } = true; + + #endregion Public Vars + + #region < Events > + + /// Handles + public delegate void FileProcessedHandler(RoboCommand sender, FileProcessedEventArgs e); + /// Occurs each time a new item has started processing + public event FileProcessedHandler OnFileProcessed; + + /// Handles + public delegate void CommandErrorHandler(RoboCommand sender, CommandErrorEventArgs e); + /// Occurs when an error occurs while generating the command that prevents the RoboCopy process from starting. + public event CommandErrorHandler OnCommandError; + + /// Handles + public delegate void ErrorHandler(RoboCommand sender, ErrorEventArgs e); + /// Occurs an error is detected by RoboCopy + public event ErrorHandler OnError; + + /// Handles + public delegate void CommandCompletedHandler(RoboCommand sender, RoboCommandCompletedEventArgs e); + /// Occurs when the RoboCopy process has finished executing and results are available. + public event CommandCompletedHandler OnCommandCompleted; + + /// Handles + public delegate void CopyProgressHandler(RoboCommand sender, CopyProgressEventArgs e); + /// Occurs each time the current item's progress is updated + public event CopyProgressHandler OnCopyProgressChanged; + + /// Handles + public delegate void ProgressUpdaterCreatedHandler(RoboCommand sender, ProgressEstimatorCreatedEventArgs e); + /// + /// Occurs when a is created during , allowing binding to occur within the event subscriber.
+ /// This event will occur once per Start. + ///
+ public event ProgressUpdaterCreatedHandler OnProgressEstimatorCreated; + + /// + /// Occurs if the RoboCommand task is stopped due to an unhandled exception. Occurs instead of + /// + public event UnhandledExceptionEventHandler TaskFaulted; + + #endregion + + #region < Pause / Stop / Resume > + + /// Pause execution of the RoboCopy process when == false + public virtual void Pause() + { + if (process != null && !process.HasExited && isPaused == false) + { + Debugger.Instance.DebugMessage("RoboCommand execution paused."); + isPaused = process.Suspend(); + } + } + + /// Resume execution of the RoboCopy process when == true + public virtual void Resume() + { + if (process != null && !process.HasExited && isPaused == true) + { + Debugger.Instance.DebugMessage("RoboCommand execution resumed."); + process.Resume(); + isPaused = false; + } + } + + /// Immediately Kill the RoboCopy process + public virtual void Stop() => Stop(false); + + private void Stop(bool DisposeProcess) + { + //Note: This previously checked for CopyOptions.RunHours.IsNullOrWhiteSpace() == TRUE prior to issuing the stop command + //If the removal of that check broke your application, please create a new issue thread on the repo. + if (process != null) + { + if (!isCancelled && (!process?.HasExited ?? true)) + { + process?.Kill(); + isCancelled = true; + } + //hasExited = true; + if (DisposeProcess) + { + process?.Dispose(); + process = null; + } + } + isPaused = false; + } + + #endregion + + #region < Start Methods > + +#if NET45_OR_GREATER || NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER + /// + /// awaits then returns the results. + /// + /// Returns the RoboCopy results once RoboCopy has finished executing. + /// + public virtual async Task StartAsync(string domain = "", string username = "", string password = "") + { + await Start(domain, username, password); + return GetResults(); + } + + /// awaits then returns the results. + /// Returns the List-Only results once RoboCopy has finished executing. + /// + public virtual async Task StartAsync_ListOnly(string domain = "", string username = "", string password = "") + { + await Start_ListOnly(domain, username, password); + return GetResults(); + } + +#endif + + /// + /// Run the currently selected options in ListOnly mode by setting = TRUE + /// + /// Task that awaits , then resets the ListOnly option to original value. + /// + public virtual async Task Start_ListOnly(string domain = "", string username = "", string password = "") + { + bool _listOnly = LoggingOptions.ListOnly; + LoggingOptions.ListOnly = true; + await Start(domain, username, password); + LoggingOptions.ListOnly = _listOnly; + return; + } + + /// + /// Start the RoboCopy Process. + /// + /// + /// If overridden by a derived class, the override affects all Start* methods within RoboCommand. Base.Start() must be called to start the robocopy process. + /// + /// + /// + /// + /// Returns a task that reports when the RoboCopy process has finished executing. + /// + public virtual Task Start(string domain = "", string username = "", string password = "") + { + if (process != null | IsRunning) throw new InvalidOperationException("RoboCommand.Start() method cannot be called while process is already running / IsRunning = true."); + Debugger.Instance.DebugMessage("RoboCommand started execution."); + hasError = false; + isCancelled = false; + isPaused = false; + isRunning = true; + + resultsBuilder = new Results.ResultsBuilder(this); + results = null; + + #region Check Source and Destination + +#if NET40_OR_GREATER + // Authenticate on Target Server -- Create user if username is provided, else null + ImpersonatedUser impersonation = username.IsNullOrWhiteSpace() ? null : impersonation = new ImpersonatedUser(username, domain, password); +#endif + // make sure source path is valid + if (!Directory.Exists(CopyOptions.Source)) + { + Debugger.Instance.DebugMessage("The Source directory does not exist."); + hasError = true; + OnCommandError?.Invoke(this, new CommandErrorEventArgs(new DirectoryNotFoundException("The Source directory does not exist."))); + Debugger.Instance.DebugMessage("RoboCommand execution stopped due to error."); + } + + #region Create Destination Directory + + //Check that the Destination Drive is accessible insteead [fixes #106] + try + { + //Check if the destination drive is accessible -> should not cause exception [Fix for #106] + DirectoryInfo dInfo = new DirectoryInfo(CopyOptions.Destination).Root; + if (!dInfo.Exists) + { + Debugger.Instance.DebugMessage("The destination drive does not exist."); + hasError = true; + OnCommandError?.Invoke(this, new CommandErrorEventArgs(new DirectoryNotFoundException("The Destination Drive is invalid."))); + Debugger.Instance.DebugMessage("RoboCommand execution stopped due to error."); + } + //If not list only, verify that drive has write access -> should cause exception if no write access [Fix #101] + if (!LoggingOptions.ListOnly & !hasError) + { + dInfo = Directory.CreateDirectory(CopyOptions.Destination); + if (!dInfo.Exists) + { + Debugger.Instance.DebugMessage("The destination directory does not exist."); + hasError = true; + OnCommandError?.Invoke(this, new CommandErrorEventArgs(new DirectoryNotFoundException("Unable to create Destination Folder. Check Write Access."))); + Debugger.Instance.DebugMessage("RoboCommand execution stopped due to error."); + } + } + } + catch (Exception ex) + { + Debugger.Instance.DebugMessage(ex.Message); + hasError = true; + OnCommandError?.Invoke(this, new CommandErrorEventArgs("The Destination directory is invalid.", ex)); + Debugger.Instance.DebugMessage("RoboCommand execution stopped due to error."); + } + + #endregion + +#if NET40_OR_GREATER + //Dispose Authentification + impersonation?.Dispose(); +#endif + + #endregion + + if (hasError) + { + isRunning = false; + return Task.Delay(5); + } + else + { + //Raise EstimatorCreatedEvent to alert consumers that the Estimator can now be bound to + ProgressEstimator = resultsBuilder.Estimator; + OnProgressEstimatorCreated?.Invoke(this, new ProgressEstimatorCreatedEventArgs(resultsBuilder.Estimator)); + return GetRoboCopyTask(resultsBuilder, domain, username, password); + } + } + + /// + /// Start the RoboCopy process and the watcher task + /// + /// The continuation task that cleans up after the task that watches RoboCopy has finished executing. + /// + private Task GetRoboCopyTask(Results.ResultsBuilder resultsBuilder, string domain = "", string username = "", string password = "") + { + if (process != null) throw new InvalidOperationException("Cannot start a new RoboCopy Process while this RoboCommand is already running."); + + isRunning = true; + DateTime StartTime = DateTime.Now; + + backupTask = Task.Run( async () => + { + + process = new Process(); + + //Declare Encoding + process.StartInfo.StandardOutputEncoding = Configuration.StandardOutputEncoding; + process.StartInfo.StandardErrorEncoding = Configuration.StandardErrorEncoding; + + if (!string.IsNullOrEmpty(domain)) + { + Debugger.Instance.DebugMessage(string.Format("RoboCommand running under domain - {0}", domain)); + process.StartInfo.Domain = domain; + } + + if (!string.IsNullOrEmpty(username)) + { + Debugger.Instance.DebugMessage(string.Format("RoboCommand running under username - {0}", username)); + process.StartInfo.UserName = username; + } + + if (!string.IsNullOrEmpty(password)) + { + Debugger.Instance.DebugMessage("RoboCommand password entered."); + var ssPwd = new System.Security.SecureString(); + + for (int x = 0; x < password.Length; x++) + { + ssPwd.AppendChar(password[x]); + } + + process.StartInfo.Password = ssPwd; + } + + Debugger.Instance.DebugMessage("Setting RoboCopy process up..."); + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.FileName = Configuration.RoboCopyExe; + if (resultsBuilder != null) + { + resultsBuilder.Source = CopyOptions.Source; + resultsBuilder.Destination = CopyOptions.Destination; + resultsBuilder.CommandOptions = GenerateParameters(); + } + process.StartInfo.Arguments = resultsBuilder?.CommandOptions ?? GenerateParameters(); + process.OutputDataReceived += process_OutputDataReceived; + process.ErrorDataReceived += process_ErrorDataReceived; + process.EnableRaisingEvents = true; + + //Setup the WaitForExitAsync Task + //hasExited = false; + var ProcessExitedAsync = new TaskCompletionSource(); + process.Exited += (sender, args) => + { + process.WaitForExit(); //This looks counter-intuitive, but is required to ensure all output lines have been read before building results. + //hasExited = true; + ProcessExitedAsync.TrySetResult(null); + }; + + //Start the Task + Debugger.Instance.DebugMessage("RoboCopy process started."); + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + _ = await ProcessExitedAsync.Task; //This allows task to release the thread to perform other work + if (resultsBuilder != null) // Only replace results if a ResultsBuilder was supplied (Not supplied when saving as a JobFile) + { + results = resultsBuilder.BuildResults(process?.ExitCode ?? -1); + } + Debugger.Instance.DebugMessage("RoboCopy process exited."); + }, CancellationToken.None); + + Task continueWithTask = backupTask.ContinueWith((continuation) => // this task always runs + { + bool WasCancelled = process.ExitCode == -1; + Stop(true); //Ensure process is disposed of - Sets IsRunning flags to false + + //Run Post-Processing of the Generated JobFile if one was created. + JobOptions.RunPostProcessing(this); + + isRunning = false; //Now that all processing is complete, IsRunning should be reported as false. + + if (continuation.IsFaulted && !WasCancelled) // If some fault occurred while processing, throw the exception to caller + { + TaskFaulted?.Invoke(this, new UnhandledExceptionEventArgs(continuation.Exception, true)); + throw continuation.Exception; + } + //Raise event announcing results are available + if (!hasError && resultsBuilder != null) + { + results.StartTime = StartTime; + results.EndTime = DateTime.Now; + results.TimeSpan = results.EndTime.Subtract(results.StartTime); + OnCommandCompleted?.Invoke(this, new RoboCommandCompletedEventArgs(results)); + } + }, CancellationToken.None); + + return continueWithTask; + } + + /// + /// Save this RoboCommand's options to a new RoboCopyJob ( *.RCJ ) file.
+ /// Note: This will not save the path submitted into . + ///
+ /// + /// Job Files don't care if the Source/Destination are invalid, since they just save the command values to a file. + /// + /// + /// Save into the RCJ file. + /// Save into the RCJ file. + /// +#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) + public async Task SaveAsJobFile(string path, bool IncludeSource = false, bool IncludeDestination = false, string domain = "", string username = "", string password = "") +#pragma warning restore CS1573 + { + //If currently running and this is called, clone the command, then run the save method against the clone. + if (process != null) + { + var cmd = this.Clone(); + cmd.StopIfDisposing = true; + try + { + await cmd.SaveAsJobFile(path, IncludeSource, IncludeDestination, domain, username, password); + } + catch(Exception Fault) + { + cmd.Dispose(); + throw Fault; + } + cmd.Dispose(); + return; + } + + bool _QUIT = JobOptions.PreventCopyOperation; + string _PATH = JobOptions.FilePath; + bool _NODD = JobOptions.NoDestinationDirectory; + bool _NOSD = JobOptions.NoSourceDirectory; + + JobOptions.FilePath = path; + JobOptions.NoSourceDirectory = !IncludeSource; + JobOptions.NoDestinationDirectory = !IncludeDestination; + JobOptions.PreventCopyOperation = true; + Exception e = null; + try + { + await GetRoboCopyTask(null, domain, username, password); //This should take approximately 1-2 seconds at most + } + catch (Exception Fault) + { + e = Fault; + } + finally + { + //Restore Original Settings + JobOptions.FilePath = _PATH; + JobOptions.NoSourceDirectory = _NOSD; + JobOptions.NoDestinationDirectory = _NODD; + JobOptions.PreventCopyOperation = _QUIT; + //If an exception occured, rethrow it. + if (e != null) throw e; + } + } + + + #endregion + + #region < Process Event Handlers > + + /// Occurs when the Process reports an error prior to starting the robocopy process, not an 'error' from Robocopy + void process_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + if (OnCommandError != null && !e.Data.IsNullOrWhiteSpace()) + { + hasError = true; + OnCommandError(this, new CommandErrorEventArgs(e.Data, null)); + } + } + + /// React to Process.StandardOutput + void process_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + var lastData = resultsBuilder.LastLine; + resultsBuilder?.AddOutput(e.Data); + + if (e.Data == null) return; // Nothing to do + var data = e.Data.Trim().Replace("\0", ""); // ? + if (data.IsNullOrWhiteSpace()) return; // Nothing to do + if (LastDataReceived == data) return; // Sometimes RoboCopy reports same item multiple times - Typically for Progress indicators + LastDataReceived = data; + + if (Regex.IsMatch(data, "^[0-9]+[.]?[0-9]*%", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnorePatternWhitespace)) + { + var currentFile = resultsBuilder?.Estimator?.CurrentFile; + var currentDir = resultsBuilder?.Estimator?.CurrentDir; + + //Increment ProgressEstimator + if (data == "100%") + resultsBuilder?.Estimator?.AddFileCopied(currentFile); + else + resultsBuilder?.Estimator?.SetCopyOpStarted(); + + // copy progress data -> Use the CurrentFile and CurrentDir from the ResultsBuilder + OnCopyProgressChanged?.Invoke(this, + new CopyProgressEventArgs( + Convert.ToDouble(data.Replace("%", ""), CultureInfo.InvariantCulture), + currentFile, currentDir + )); + + } + else + { + //Parse the string to determine which event to raise + var splitData = data.Split(new char[] { '\t' }, StringSplitOptions.RemoveEmptyEntries); + + if (splitData.Length == 2) // Directory + { + // Regex to parse the string for FileCount, Path, and Type (Description) + Regex DirRegex = new Regex("^(?\\*?[a-zA-Z]{0,10}\\s?[a-zA-Z]{0,3})\\s*(?[-]{0,1}[0-9]{1,100})\\t(?.+)", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + var file = new ProcessedFileInfo(); + file.FileClassType = FileClassType.NewDir; + + if (DirRegex.IsMatch(data)) + { + //New Method - Parsed using Regex + GroupCollection MatchData = DirRegex.Match(data).Groups; + file.FileClass = MatchData["Type"].Value.Trim(); + if (file.FileClass == "") file.FileClass = configuration.LogParsing_ExistingDir; + long.TryParse(MatchData["FileCount"].Value, out long size); + file.Size = size; + file.Name = MatchData["Path"].Value.Trim(); + } + else + { + //Old Method -> Left Intact for other language compatibilty / unforseen cases + file.FileClass = "New Dir"; + long.TryParse(splitData[0].Replace("New Dir", "").Trim(), out long size); + file.Size = size; + file.Name = splitData[1]; + } + + resultsBuilder?.Estimator?.AddDir(file, !this.LoggingOptions.ListOnly); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(file)); + } + else if (splitData.Length == 3) // File + { + var file = new ProcessedFileInfo(); + file.FileClass = splitData[0].Trim(); + file.FileClassType = FileClassType.File; + long size = 0; + long.TryParse(splitData[1].Trim(), out size); + file.Size = size; + file.Name = splitData[2]; + resultsBuilder?.Estimator?.AddFile(file, !LoggingOptions.ListOnly); + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(file)); + } + else if (Configuration.ErrorTokenRegex.IsMatch(data)) // Error Message - Mark the current file as FAILED immediately - Don't raise OnError event until error description comes in though + { + /* + * Mark the current file as Failed + * TODO: This data may have to be parsed to determine if it involved the current file's filename, or some other error. At time of writing, it appears that it doesn't require this check. + * */ + + ProgressEstimator.FileFailed = true; + } + else if (Configuration.ErrorTokenRegex.IsMatch(lastData)) // Error Message - Uses previous data instead since RoboCopy reports errors onto line 1, then description onto line 2. + { + ErrorEventArgs args = new ErrorEventArgs(lastData, data, Configuration.ErrorTokenRegex); + resultsBuilder.RoboCopyErrors.Add(args); + + //Check to Raise the event + OnError?.Invoke(this, args); + } + else if (!data.StartsWith("----------")) // System Message + { + // Do not log errors that have already been logged + var errorCode = ApplicationConstants.ErrorCodes.FirstOrDefault(x => data == x.Value); + if (errorCode.Key == null) + { + var file = new ProcessedFileInfo(); + file.FileClass = "System Message"; + file.FileClassType = FileClassType.SystemMessage; + file.Size = 0; + file.Name = data; + OnFileProcessed?.Invoke(this, new FileProcessedEventArgs(file)); + } + } + } + } + + #endregion + + #region < Other Public Methods > + + /// + /// The RoboCopyResults object from the last run + public Results.RoboCopyResults GetResults() + { + return results; + } + + /// + /// Set the results to null - This is to prevent adding results from a previous run being added to the results list by RoboQueue + /// + internal void ResetResults() + { + results = null; + } + + /// + /// Generate the Parameters and Switches to execute RoboCopy with based on the configured settings + /// + /// + private string GenerateParameters() + { + Debugger.Instance.DebugMessage("Generating parameters..."); + Debugger.Instance.DebugMessage(CopyOptions); + var parsedCopyOptions = CopyOptions.Parse(); + var parsedSelectionOptions = SelectionOptions.Parse(); + Debugger.Instance.DebugMessage("SelectionOptions parsed."); + var parsedRetryOptions = RetryOptions.Parse(); + Debugger.Instance.DebugMessage("RetryOptions parsed."); + var parsedLoggingOptions = LoggingOptions.Parse(); + Debugger.Instance.DebugMessage("LoggingOptions parsed."); + var parsedJobOptions = JobOptions.Parse(); + Debugger.Instance.DebugMessage("LoggingOptions parsed."); + //var systemOptions = " /V /R:0 /FP /BYTES /W:0 /NJH /NJS"; + + return string.Format("{0}{1}{2}{3} /BYTES {4}", parsedCopyOptions, parsedSelectionOptions, + parsedRetryOptions, parsedLoggingOptions, parsedJobOptions); + } + + /// + public override string ToString() + { + return GenerateParameters(); + } + + /// + /// Combine this object's options with that of some JobFile + /// + /// + public void MergeJobFile(JobFile jobFile) + { + Name = Name.ReplaceIfEmpty(jobFile.Job_Name); + copyOptions.Merge(jobFile.CopyOptions); + LoggingOptions.Merge(jobFile.LoggingOptions); + RetryOptions.Merge(jobFile.RetryOptions); + SelectionOptions.Merge(jobFile.SelectionOptions); + JobOptions.Merge(((IRoboCommand)jobFile).JobOptions); + //this.StopIfDisposing |= ((IRoboCommand)jobFile).StopIfDisposing; + } + + #endregion + + #region < IDisposable Implementation > + + bool disposed = false; + + /// Dispose of this object. Kills RoboCopy process if == true && == false. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Finalizer -> Cleans up resources when garbage collected + /// + ~RoboCommand() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + /// IDisposable Implementation + protected virtual void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) + { + + } + + if (StopIfDisposing && !IsScheduled) + { + Stop(true); + } + + //Release any hooks to the process, but allow it to continue running + process?.Dispose(); + process = null; + + disposed = true; + } + + #endregion IDisposable Implementation + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboQueue.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboQueue.cs new file mode 100644 index 0000000..a772fbc --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboQueue.cs @@ -0,0 +1,946 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.ObjectModel; +using System.Collections.Concurrent; +using FSI.Lib.Tools.RoboSharp.EventArgObjects; +using FSI.Lib.Tools.RoboSharp.Interfaces; +using FSI.Lib.Tools.RoboSharp.Results; + +namespace FSI.Lib.Tools.RoboSharp +{ + /// + /// Contains a private List{RoboCommand} object with controlled methods for access to it.
+ /// Attempting to modify the list while = true results in being thrown. + /// Implements the following:
+ ///
+ /// -- Allow enumerating through the collection that is stored in a private list -- Also see
+ /// -- Allow subscription to collection changes against the list
+ /// -- Most properties will trigger events when updated.
+ /// -- Allow disposal of all objects in the list. + ///
+ /// + /// + /// + public sealed class RoboQueue : IRoboQueue, IDisposable, INotifyPropertyChanged, IEnumerable, INotifyCollectionChanged + { + #region < Constructors > + + /// + /// Initialize a new (empty) object. + /// + public RoboQueue() + { + Init(); + Commands = new ReadOnlyCollection(CommandList); + } + + /// + /// Initialize a new (empty) object with a specificed Name. + /// + /// + public RoboQueue(string name, int maxConcurrentJobs = 1) + { + Init(name, maxConcurrentJobs); + Commands = new ReadOnlyCollection(CommandList); + } + + /// + /// Initialize a new object that contains the supplied . + /// + /// + public RoboQueue(RoboCommand roboCommand, string name = "", int maxConcurrentJobs = 1) + { + CommandList.Add(roboCommand); + Init(name, maxConcurrentJobs); + Commands = new ReadOnlyCollection(CommandList); + } + + /// + /// Initialize a new object that contains the supplied collection. + /// + /// RoboCommand(s) to populate the list with. + /// + /// + public RoboQueue(IEnumerable roboCommand, string name = "", int maxConcurrentJobs = 1) + { + CommandList.AddRange(roboCommand); + Init(name, maxConcurrentJobs); + Commands = new ReadOnlyCollection(CommandList); + } + + private void Init(string name = "", int maxConcurrentJobs = 1) + { + NameField = name; + MaxConcurrentJobsField = maxConcurrentJobs; + } + + #endregion + + #region < Fields > + + private readonly ObservableList CommandList = new ObservableList(); + private RoboQueueProgressEstimator Estimator; + private bool disposedValue; + private CancellationTokenSource TaskCancelSource; + private string NameField; + + private bool WasCancelledField = false; + private bool IsPausedField = false; + private bool IsCopyOperationRunningField = false; + private bool IsListOperationRunningField = false; + private bool ListOnlyCompletedField = false; + private bool CopyOpCompletedField = false; + + private int MaxConcurrentJobsField; + private int JobsStartedField; + private int JobsCompleteField; + private int JobsCompletedSuccessfullyField; + + #endregion + + #region < Properties Dependent on CommandList > + + /// + /// Checks property of all items in the list. + ///
INotifyPropertyChanged is not raised when this property changes. + ///
+ public bool AnyRunning => CommandList.Any(c => c.IsRunning); + + /// + /// Checks property of all items in the list. + ///
INotifyPropertyChanged is not raised when this property changes. + ///
+ public bool AnyPaused => CommandList.Any(c => c.IsPaused); + + /// + /// Checks property of all items in the list. + ///
INotifyPropertyChanged is not raised when this property changes. + ///
+ public bool AnyCancelled => CommandList.Any(c => c.IsCancelled); + + /// + /// Check the list and get the count of RoboCommands that are either in the 'Run' or 'Paused' state.
+ /// (Paused state is included since these can be resumed at any time) + ///
+ public int JobsCurrentlyRunning => CommandList.Where((C) => C.IsRunning | C.IsPaused).Count(); + + /// Number of RoboCommands in the list + public int ListCount => CommandList.Count; + + #endregion + + #region < Properties > + + /// + /// Name of this collection of RoboCommands + /// + public string Name + { + get => NameField; + private set + { + if (value != NameField) + { + NameField = value; + OnPropertyChanged("Name"); + } + } + } + + /// + /// Wraps the private into a ReadOnlyCollection for public consumption and data binding. + /// + public ReadOnlyCollection Commands { get; } + + /// + /// + /// This object will produce the sum of all the ProgressEstimator objects generated by the commands within the list. + /// After the first request, the values will be updated every 250ms while the Queue is still running. + /// + public IProgressEstimator ProgressEstimator => Estimator; + + /// + /// Indicates if a task is currently running or paused.
+ /// When true, prevents starting new tasks and prevents modication of the list. + ///
+ public bool IsRunning => IsCopyOperationRunning || IsListOnlyRunning; + + /// + /// This is set true when is called while any of the items in the list were running, and set false when or is called. + /// + public bool IsPaused + { + get => IsPausedField; + private set + { + if (value != IsPausedField) + { + IsPausedField = value; + OnPropertyChanged("IsPaused"); + } + } + } + + /// + /// Flag is set to TRUE if the 'Stop' command is issued. Reset to False when starting a new operation. + /// + public bool WasCancelled + { + get => WasCancelledField; + private set + { + if (value != WasCancelledField) + { + WasCancelledField = value; + OnPropertyChanged("WasCancelled"); + } + } + } + + /// Indicates if the StartAll task is currently running. + public bool IsCopyOperationRunning + { + get => IsCopyOperationRunningField; + private set + { + if (value != IsCopyOperationRunningField) + { + bool running = IsRunning; + IsCopyOperationRunningField = value; + OnPropertyChanged("IsCopyOperationRunning"); + if (IsRunning != running) OnPropertyChanged("IsRunning"); + } + } + } + + /// Indicates if the StartAll_ListOnly task is currently running. + public bool IsListOnlyRunning + { + get => IsListOperationRunningField; + private set + { + if (value != IsListOperationRunningField) + { + bool running = IsRunning; + IsListOperationRunningField = value; + OnPropertyChanged("IsListOnlyRunning"); + if (IsRunning != running) OnPropertyChanged("IsRunning"); + } + } + } + + /// Indicates if the StartAll_ListOnly() operation has been completed. + public bool ListOnlyCompleted + { + get => ListOnlyCompletedField; + private set + { + if (value != ListOnlyCompletedField) + { + ListOnlyCompletedField = value; + OnPropertyChanged("ListOnlyCompleted"); + } + } + } + + /// Indicates if the StartAll() operation has been completed. + public bool CopyOperationCompleted + { + get => CopyOpCompletedField; + private set + { + if (value != CopyOpCompletedField) + { + CopyOpCompletedField = value; + OnPropertyChanged("CopyOperationCompleted"); + } + } + } + + /// + /// Specify the max number of RoboCommands to execute at the same time.
+ /// Set Value to 0 to allow infinite number of jobs (Will issue all start commands at same time)
+ /// Default Value = 1;
+ ///
+ public int MaxConcurrentJobs + { + get => MaxConcurrentJobsField; + set + { + int newVal = value > 0 ? value : IsRunning & MaxConcurrentJobsField > 0 ? 1 : 0; //Allow > 0 at all times //If running, set value to 1 + if (newVal != MaxConcurrentJobsField) + { + MaxConcurrentJobsField = newVal; + OnPropertyChanged("MaxConcurrentJobs"); + } + } + } + + /// + /// Report how many tasks has completed during the run.
+ /// This value is reset to 0 when a new run starts, and increments as each job exits. + ///
+ public int JobsComplete + { + get => JobsCompleteField; + private set + { + if (value != JobsCompleteField) + { + JobsCompleteField = value; + OnPropertyChanged("JobsComplete"); + } + } + } + + /// + /// Report how many tasks has completed successfully during the run.
+ /// This value is reset to 0 when a new run starts, and increments as each job exits. + ///
+ public int JobsCompletedSuccessfully + { + get => JobsCompletedSuccessfullyField; + private set + { + if (value != JobsCompletedSuccessfullyField) + { + JobsCompletedSuccessfullyField = value; + OnPropertyChanged("JobsCompletedSuccessfully"); + } + } + } + + /// + /// Report how many tasks have been started during the run.
+ /// This value is reset to 0 when a new run starts, and increments as each job starts. + ///
+ public int JobsStarted + { + get => JobsStartedField; + private set + { + if (value != JobsStartedField) + { + JobsStartedField = value; + OnPropertyChanged("JobsStarted"); + } + } + } + + /// + /// Contains the results from the most recent run started via + /// Any time StartALL_ListOnly is called, a new RoboQueueResults object will be created.
+ ///
+ public IRoboQueueResults ListResults => ListResultsObj; + private RoboQueueResults ListResultsObj { get; set; } + + /// + /// Contains the results from the most recent run started via + /// Any time StartALL is called, a new RoboQueueResults object will be created.
+ ///
+ public IRoboQueueResults RunResults => RunResultsObj; + private RoboQueueResults RunResultsObj { get; set; } + + /* + * Possible To-Do: Code in ConcurrentQueue objects if issues arise with items being added to the ResultsObj lists. + * private ConcurrentQueue ListResultsQueue = new ConcurrentQueue(); + * private ConcurrentQueue RunResultsQueue = new ConcurrentQueue(); + */ + + #endregion + + #region < Events > + + #region < RoboCommand Events > + + /// + /// This bind to every RoboCommand in the list. + public event RoboCommand.FileProcessedHandler OnFileProcessed; + + /// + /// This bind to every RoboCommand in the list. + public event RoboCommand.CommandErrorHandler OnCommandError; + + /// + /// This bind to every RoboCommand in the list. + public event RoboCommand.ErrorHandler OnError; + + /// + /// This will occur for every RoboCommand in the list. + public event RoboCommand.CommandCompletedHandler OnCommandCompleted; + + /// + /// This bind to every RoboCommand in the list. + public event RoboCommand.CopyProgressHandler OnCopyProgressChanged; + + #endregion + + #region < ListUpdated Events > + + /// Occurs when the gets updated + public event RoboCopyResultsList.ResultsListUpdated ListResultsUpdated; + + /// Occurs when the gets updated + public event RoboCopyResultsList.ResultsListUpdated RunResultsUpdated; + + #endregion + + #region < ProgressUpdater Event > + + /// Handles + public delegate void ProgressUpdaterCreatedHandler(RoboQueue sender, ProgressEstimatorCreatedEventArgs e); + /// + /// Occurs when a is created when starting a new task, allowing binding to occur within the event subscriber.
+ /// This event will occur once per Start. See notes on for more details. + ///
+ public event ProgressUpdaterCreatedHandler OnProgressEstimatorCreated; + + #endregion + + #region < CommandStarted Event > + + /// Handles + public delegate void CommandStartedHandler(RoboQueue sender, RoboQueueCommandStartedEventArgs e); + /// + /// Occurs each time a Command has started succesfully + /// + public event CommandStartedHandler OnCommandStarted; + + #endregion + + #region < RunComplete Event > + + /// Handles + public delegate void RunCompletedHandler(RoboQueue sender, RoboQueueCompletedEventArgs e); + /// + /// Occurs after when the task started by the StartAll and StartAll_ListOnly methods has finished executing. + /// + public event RunCompletedHandler RunCompleted; + + #endregion + + #region < UnhandledException Fault > + + /// + /// Occurs if the RoboQueue task is stopped due to an unhandled exception. Occurs instead of + ///
Also occurs if any of the RoboCommand objects raise + ///
+ public event UnhandledExceptionEventHandler TaskFaulted; + + #endregion + + #endregion + + #region < Methods > + + /// + /// Get the current instance of the object + /// + /// New instance of the list. + public RoboQueueResults GetListResults() => ListResultsObj; + + /// + /// Get the current of the object + /// + /// New instance of the list. + public RoboQueueResults GetRunResults() => RunResultsObj; + + /// + /// Run against all items in the list. + /// + public void StopAll() + { + //If a TaskCancelSource is present, request cancellation. The continuation tasks null the value out then call this method to ensure everything stopped once they complete. + if (TaskCancelSource != null && !TaskCancelSource.IsCancellationRequested) + { + IsPaused = false; + TaskCancelSource.Cancel(); // Cancel the RoboCommand Task + //RoboCommand Continuation Task will call StopAllTask() method to ensure all processes are stopped & diposed. + } + else if (TaskCancelSource == null) + { + //This is supplied to allow stopping all commands if consumer manually looped through the list instead of using the Start* methods. + CommandList.ForEach((c) => c.Stop()); + IsCopyOperationRunning = false; + IsListOnlyRunning = false; + IsPaused = false; + } + WasCancelled = true; + } + + /// + /// Loop through the items in the list and issue on any commands where is true. + /// + public void PauseAll() + { + CommandList.ForEach((c) => { if (c.IsRunning) c.Pause(); }); + IsPaused = IsRunning || AnyPaused; + } + + /// + /// Loop through the items in the list and issue on any commands where is true. + /// + public void ResumeAll() + { + CommandList.ForEach((c) => { if (c.IsPaused) c.Resume(); }); + IsPaused = false; + } + + #endregion + + #region < Run List-Only Mode > + + /// + /// Set all RoboCommand objects to ListOnly mode, run them, then set all RoboCommands back to their previous ListOnly mode setting. + /// + /// + public Task StartAll_ListOnly(string domain = "", string username = "", string password = "") + { + if (IsRunning) throw new InvalidOperationException("Cannot start a new RoboQueue Process while this RoboQueue is already running."); + IsListOnlyRunning = true; + ListOnlyCompleted = false; + + ListResultsObj = new RoboQueueResults(); + ListResultsUpdated?.Invoke(this, new ResultListUpdatedEventArgs(ListResults)); + + //Run the commands + Task Run = StartJobs(domain, username, password, true); + Task ResultsTask = Run.ContinueWith((continuation) => + { + //Set Flags + IsListOnlyRunning = false; + IsPaused = false; + ListOnlyCompleted = !WasCancelled && !continuation.IsFaulted; + + // If some fault occurred while processing, throw the exception to caller + if (continuation.IsFaulted) + { + TaskFaulted?.Invoke(this, new UnhandledExceptionEventArgs(continuation.Exception, true)); + throw continuation.Exception; + } + ListResultsObj.EndTime= DateTime.Now; + RunCompleted?.Invoke(this, new RoboQueueCompletedEventArgs(ListResultsObj, true)); + return (IRoboQueueResults)ListResultsObj; + }, CancellationToken.None + ); + return ResultsTask; + } + + #endregion + + #region < Run User-Set Parameters > + + /// + public Task StartAll(string domain = "", string username = "", string password = "") + { + if (IsRunning) throw new InvalidOperationException("Cannot start a new RoboQueue Process while this RoboQueue is already running."); + + IsCopyOperationRunning = true; + CopyOperationCompleted = false; + + RunResultsObj = new RoboQueueResults(); + RunResultsUpdated?.Invoke(this, new ResultListUpdatedEventArgs(RunResults)); + + Task Run = StartJobs(domain, username, password, false); + Task ResultsTask = Run.ContinueWith((continuation) => + { + IsCopyOperationRunning = false; + IsPaused = false; + CopyOperationCompleted = !WasCancelled && !continuation.IsFaulted; + + // If some fault occurred while processing, throw the exception to caller + if (continuation.IsFaulted) + { + TaskFaulted?.Invoke(this, new UnhandledExceptionEventArgs(continuation.Exception, true)); + throw continuation.Exception; + } + + RunResultsObj.EndTime = DateTime.Now; + RunCompleted?.Invoke(this, new RoboQueueCompletedEventArgs(RunResultsObj, false)); + return (IRoboQueueResults)RunResultsObj; + }, CancellationToken.None + ); + return ResultsTask; + } + + #endregion + + #region < StartJobs Method > + + /// + /// Create Task that Starts all RoboCommands. + /// + /// , , and are applied to all RoboCommand objects during this run. + /// New Task that finishes after all RoboCommands have stopped executing + private Task StartJobs(string domain = "", string username = "", string password = "", bool ListOnlyMode = false) + { + Debugger.Instance.DebugMessage("Starting Parallel execution of RoboQueue"); + + TaskCancelSource = new CancellationTokenSource(); + CancellationToken cancellationToken = TaskCancelSource.Token; + var SleepCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken).Token; + + List TaskList = new List(); + JobsStarted = 0; + JobsComplete = 0; + JobsCompletedSuccessfully = 0; + + WasCancelled = false; + IsPaused = false; + + //Create a Task to Start all the RoboCommands + Task StartAll = Task.Factory.StartNew(async () => + { + //Reset results of all commands in the list + foreach (RoboCommand cmd in CommandList) + cmd.ResetResults(); + + Estimator = new RoboQueueProgressEstimator(); + OnProgressEstimatorCreated?.Invoke(this, new ProgressEstimatorCreatedEventArgs(Estimator)); + + //Start all commands, running as many as allowed + foreach (RoboCommand cmd in CommandList) + { + if (cancellationToken.IsCancellationRequested) break; + + //Assign the events + RoboCommand.CommandCompletedHandler handler = (o, e) => RaiseCommandCompleted(o, e, ListOnlyMode); + cmd.OnCommandCompleted += handler; + cmd.OnCommandError += this.OnCommandError; + cmd.OnCopyProgressChanged += this.OnCopyProgressChanged; + cmd.OnError += this.OnError; + cmd.OnFileProcessed += this.OnFileProcessed; + cmd.OnProgressEstimatorCreated += Cmd_OnProgressEstimatorCreated; + cmd.TaskFaulted += TaskFaulted; + + //Start the job + //Once the job ends, unsubscribe events + Task C = !ListOnlyMode ? cmd.Start(domain, username, password) : cmd.Start_ListOnly(domain, username, password); + Task T = C.ContinueWith((t) => + { + cmd.OnCommandCompleted -= handler; + cmd.OnCommandError -= this.OnCommandError; + cmd.OnCopyProgressChanged -= this.OnCopyProgressChanged; + cmd.OnError -= this.OnError; + cmd.OnFileProcessed -= this.OnFileProcessed; + if (t.IsFaulted) throw t.Exception; // If some fault occurred while processing, throw the exception to caller + }, CancellationToken.None); + + TaskList.Add(T); //Add the continuation task to the list. + + //Raise Events + JobsStarted++; OnPropertyChanged("JobsStarted"); + if (cmd.IsRunning) OnCommandStarted?.Invoke(this, new RoboQueueCommandStartedEventArgs(cmd)); //Declare that a new command in the queue has started. + OnPropertyChanged("JobsCurrentlyRunning"); //Notify the Property Changes + + //Check if more jobs are allowed to run + if (IsPaused) cmd.Pause(); //Ensure job that just started gets paused if Pausing was requested + while (!cancellationToken.IsCancellationRequested && (IsPaused || (MaxConcurrentJobs > 0 && JobsCurrentlyRunning >= MaxConcurrentJobs && TaskList.Count < CommandList.Count))) + await ThreadEx.CancellableSleep(500, SleepCancelToken); + + } //End of ForEachLoop + + //Asynchronous wait for either cancellation is requested OR all jobs to finish. + //- Task.WaitAll is blocking -> not ideal, and also throws if cancellation is requested -> also not ideal. + //- Task.WhenAll is awaitable, but does not provide allow cancellation + //- If Cancelled, the 'WhenAll' task continues to run, but the ContinueWith task here will stop all tasks, thus completing the WhenAll task + if (!cancellationToken.IsCancellationRequested) + { + var tcs = new TaskCompletionSource(); + _ = cancellationToken.Register(() => tcs.TrySetResult(null)); + _ = await Task.WhenAny(Task.WhenAll(TaskList.ToArray()), tcs.Task); + } + + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).Unwrap(); + + //Continuation Task return results to caller + Task ContinueWithTask = StartAll.ContinueWith(async (continuation) => + { + Estimator?.CancelTasks(); + if (cancellationToken.IsCancellationRequested) + { + //If cancellation was requested -> Issue the STOP command to all commands in the list + Debugger.Instance.DebugMessage("RoboQueue Task Was Cancelled"); + await StopAllTask(TaskList); + } + else if (continuation.IsFaulted) + { + Debugger.Instance.DebugMessage("RoboQueue Task Faulted"); + await StopAllTask(TaskList); + throw continuation.Exception; + } + else + { + Debugger.Instance.DebugMessage("RoboQueue Task Completed"); + } + + TaskCancelSource?.Dispose(); + TaskCancelSource = null; + + }, CancellationToken.None).Unwrap(); + + return ContinueWithTask; + } + + private async Task StopAllTask(IEnumerable StartedTasks) + { + CommandList.ForEach((c) => c.Stop()); + await Task.WhenAll(StartedTasks); + + IsCopyOperationRunning = false; + IsListOnlyRunning = false; + IsPaused = false; + + TaskCancelSource.Dispose(); + TaskCancelSource = null; + } + + private void Cmd_OnProgressEstimatorCreated(RoboCommand sender, ProgressEstimatorCreatedEventArgs e) + { + Estimator?.BindToProgressEstimator(e.ResultsEstimate); + sender.OnProgressEstimatorCreated -= Cmd_OnProgressEstimatorCreated; + } + + /// + /// Intercept OnCommandCompleted from each RoboCommand, react, then raise this object's OnCommandCompleted event + /// + private void RaiseCommandCompleted(RoboCommand sender, RoboCommandCompletedEventArgs e, bool ListOnlyBinding) + { + if (ListOnlyBinding) + { + ListResultsObj.Add(sender.GetResults()); + ListResultsUpdated?.Invoke(this, new ResultListUpdatedEventArgs(ListResults)); + } + else + { + RunResultsObj.Add(sender.GetResults()); + RunResultsUpdated?.Invoke(this, new ResultListUpdatedEventArgs(RunResults)); + } + + //Notify the Property Changes + if (!sender.IsCancelled) + { + JobsCompletedSuccessfully++; + } + JobsComplete++; + OnPropertyChanged("JobsCurrentlyRunning"); + OnCommandCompleted?.Invoke(sender, e); + } + + #endregion + + #region < IDisposable Implementation > + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + Estimator?.UnBind(); + + //RoboCommand objects attach to a process, so must be in the 'unmanaged' section. + foreach (RoboCommand cmd in CommandList) + cmd.Dispose(); + CommandList.Clear(); + } + + disposedValue = true; + } + } + + /// + /// Finalizer -> Ensures that all RoboCommand objects get disposed of properly when program exits + /// + ~RoboQueue() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + /// + /// Dispose all RoboCommand objects contained in the list. - This will kill any Commands that have = true (default)
+ ///
+ public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + #region < INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable > + + /// + public event PropertyChangedEventHandler PropertyChanged; + private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + /// + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add { CommandList.CollectionChanged += value; } + remove { CommandList.CollectionChanged -= value; } + } + + /// + /// Gets the enumerator for the enumeating through this object's objects + /// + public IEnumerator GetEnumerator() + { + return Commands.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)Commands).GetEnumerator(); + } + + #endregion + + #region < List Access Methods > + + /// + /// Exception thrown when attempting to run a method accesses the list backing a RoboQueue object while the tasks are in progress. + /// + public class ListAccessDeniedException : Exception + { + /// This functionality is disabled if == true. + /// + private const string StandardMsg = "Running methods that modify the list of RoboCommands methods while RoboQueue.IsRunning = TRUE is prohibited."; + internal ListAccessDeniedException() : base(StandardMsg) { } + internal ListAccessDeniedException(string message) : base($"{StandardMsg}\n{message}") { } + internal ListAccessDeniedException(string message, Exception innerException) : base(message, innerException) { } + } + + #region < Add > + + /// + /// + public void AddCommand(RoboCommand item) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.Add(item); + OnPropertyChanged("ListCount"); + OnPropertyChanged("Commands"); + + } + + /// + /// + public void AddCommand(int index, RoboCommand item) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.Insert(index, item); + OnPropertyChanged("ListCount"); + OnPropertyChanged("Commands"); + } + + /// + /// + public void AddCommand(IEnumerable collection) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.AddRange(collection); + OnPropertyChanged("ListCount"); + OnPropertyChanged("Commands"); + } + + #endregion + + #region < Remove > + + /// + /// + public void RemoveCommand(RoboCommand item) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.Remove(item); + OnPropertyChanged("ListCount"); + OnPropertyChanged("Commands"); + } + + /// + /// + public void RemoveCommand(int index) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.RemoveAt(index); + OnPropertyChanged("ListCount"); + OnPropertyChanged("Commands"); + } + + /// + /// + public void RemoveCommand(int index, int count) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.RemoveRange(index, count); + OnPropertyChanged("ListCount"); + OnPropertyChanged("Commands"); + } + + /// + /// + public void RemovCommand(Predicate match) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.RemoveAll(match); + OnPropertyChanged("ListCount"); + OnPropertyChanged("Commands"); + } + + /// + /// + public void ClearCommandList() + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.Clear(); + OnPropertyChanged("ListCount"); + OnPropertyChanged("Commands"); + } + + /// Performs then + public void ReplaceCommand(RoboCommand item, int index) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.Replace(index, item); + } + + #endregion + + #region < Find / Contains / Etc > + + /// + public bool Contains(IRoboCommand item) => CommandList.Contains(item); + + /// + /// + public void ForEach(Action action) + { + if (IsRunning) throw new ListAccessDeniedException(); + CommandList.ForEach(action); + } + + /// + public List FindAll(Predicate predicate) => CommandList.FindAll(predicate); + + /// + public IRoboCommand Find(Predicate predicate) => CommandList.Find(predicate); + + /// + public int IndexOf(IRoboCommand item) => CommandList.IndexOf(item); + + #endregion + + #endregion + + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboSharp.xml b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboSharp.xml new file mode 100644 index 0000000..a8974ee --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboSharp.xml @@ -0,0 +1,1042 @@ + + + + RoboSharp + + + + + Describes an error that occured when generating the command + + + + + Error Description + + + + + 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. +
+
+ + + + + + + + + + + + Exception to data to pass to the event handler + + + + Source, Destination, and options for how to move or copy files. + + + + + The source file path where the RoboCommand is copying files from. + + + + + + + + The destination file path where the RoboCommand is copying files to. + + + + + + + + Allows you to supply a set of files to copy or use wildcard characters (* or ?). + + + + + Copies subdirectories. Note that this option excludes empty directories. + [/S] + + + + + Copies subdirectories. Note that this option includes empty directories. + [/E] + + + + + Copies only the top N levels of the source directory tree. The default is + zero which does not limit the depth. + [/LEV:N] + + + + + Copies files in Restart mode. + [/Z] + + + + + Copies files in Backup mode. + [/B] + + + + + Uses Restart mode. If access is denied, this option uses Backup mode. + [/ZB] + + + + + Copy using unbuffered I/O (recommended for large files). + [/J] + + + + + Copies all encrypted files in EFS RAW mode. + [/EFSRAW] + + + + + 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] + + + + + Copies files with security (equivalent to /copy:DAT). + [/SEC] + + + + + Copies all file information (equivalent to /copy:DATSOU). + [/COPYALL] + + + + + Copies no file information (useful with Purge option). + [/NOCOPY] + + + + + Fixes file security on all files, even skipped ones. + [/SECFIX] + + + + + Fixes file times on all files, even skipped ones. + [/TIMFIX] + + + + + Deletes destination files and directories that no longer exist in the source. + [/PURGE] + + + + + Mirrors a directory tree (equivalent to CopySubdirectoriesIncludingEmpty plus Purge). + [/MIR] + + + + + Moves files, and deletes them from the source after they are copied. + [/MOV] + + + + + Moves files and directories, and deletes them from the source after they are copied. + [/MOVE] + + + + + 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] + + + + + 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] + + + + + Creates a directory tree and zero-length files only. + [/CREATE] + + + + + Creates destination files by using 8.3 character-length FAT file names only. + [/FAT] + + + + + Turns off support for very long paths (longer than 256 characters). + [/256] + + + + + 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] + + + + + 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] + + + + + Specifies run times when new copies may be started. + [/rh:hhmm-hhmm] + + + + + Checks run times on a per-file (not per-pass) basis. + [/PF] + + + + + 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] + + + + + Copies the symbolic link instead of the target. + [/SL] + + + + + 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] + + + + + What to copy for directories (default is DA). + (copyflags: D=Data, A=Attributes, T=Timestamps). + [/DCOPY:copyflags] + + + + + Do not copy any directory info. + [/NODCOPY] + + + + + Copy files without using the Windows Copy Offload mechanism. + [/NOOFFLOAD] + + + + + Current File Progress reported as + + + + + Current File Progress Percentage + + + + + + + + + Information about an Error reported by the RoboCopy process + + + + + Error Description + + + + + Error Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Options related to the output logs generated by RoboCopy + + + + + Do not copy, timestamp or delete any files. + [/L] + + + + + Report all extra files, not just those selected. + [X] + + + + + Produce verbose output, showing skipped files. + [V] + + + + + Include source file time stamps in the output. + [/TS] + + + + + Include full path names of files in the output. + [/FP] + + + + + Print sizes as bytes in the output. + [/BYTES] + + + + + Do not log file sizes. + [/NS] + + + + + Do not log file classes. + [/NC] + + + + + 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 + + + + + Do not log directory names. + [/NDL] + + + + + Do not log percentage copied. + [/NP] + + + + + Show estimated time of arrival of copied files. + [/ETA] + + + + + Output status to LOG file (overwrite existing log). + [/LOG:file] + + + + + Output status to LOG file (append to existing log). + [/LOG+:file] + + + + + Output status to LOG file as UNICODE (overwrite existing log). + [/UNILOG:file] + + + + + Output status to LOG file as UNICODE (append to existing log). + [/UNILOG+:file] + + + + + Output to RoboSharp and Log. + [/TEE] + + + + + Do not output a Job Header. + [/NJH] + + + + + 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 + + + + + Output as UNICODE. + [/UNICODE] + + + + Encase the LogPath in quotes if needed + + + + Message Type reported by RoboCopy + + + + Details about a FOLDER + + + Details about a FILE + + + Status Message reported by RoboCopy + + + Contains information about the current item being processed by RoboCopy + + + Description of the item as reported by RoboCopy + + + + + + File Size + + + Folder or File Name / Message Text + + + + Helper class to build a object. + + + + + RoboCopy Exit Codes + + + + + No Files Copied, No Errors Occured + + + One or more files were copied successfully + + + + Some Extra files or directories were detected.
+ Examine the output log for details. +
+
+ + + Some Mismatched files or directories were detected.
+ Examine the output log. Housekeeping might be required. +
+
+ + + Some files or directories could not be copied
+ (copy errors occurred and the retry limit was exceeded). + Check these errors further. +
+
+ + + Serious error. Robocopy did not copy any files.
+ Either a usage error or an error due to insufficient access privileges on the source or destination directorie +
+
+ + + Results provided by the RoboCopy command. Includes the Log, Exit Code, and statistics parsed from the log. + + + + + + + Information about number of Directories Copied, Skipped, Failed, etc. + + + Information about number of Files Copied, Skipped, Failed, etc. + + + Information about number of Bytes processed. + + + + + + Output Text reported by RoboCopy + + + + Returns a string that represents the current object. + + + A string that represents the current object. + + + + + Object that evaluates the ExitCode reported after RoboCopy finishes executing. + + + + + Initializes a new instance of the class. + + + + ExitCode as reported by RoboCopy + + + ExitCode reported by RoboCopy converted into the Enum + + + + + + + + + + + + + Returns a string that represents the current object. + + + + Contains information regarding average Transfer Speed + + + Average Transfer Rate in Bytes/Second + + + Average Transfer Rate in MB/Minute + + + + Returns a string that represents the current object. + + + + + Information about number of items Copied, Skipped, Failed, etc. + + + + Total Scanned during the run + + + Total Copied + + + Total Skipped + + + + + + Total that failed to copy or move + + + Total Extra that exist in the Destination (but are missing from the Source) + + + + Returns a string that represents the current object. + + + + + Parse a string and for the tokens reported by RoboCopy + + + New Statistic Object + + + + Add the results of the supplied Statistics object to this Statistics object. + + Statistics Item to add + + + + Add the results of the supplied Statistics objects to this Statistics object. + + Statistics Item to add + + + + Combine the results of the supplied statistics objects + + Statistics Item to add + New Statistics Object + + + + Combine the supplied objects, then get the average. + + Array of Stats objects + + + New Statistics Object + + + + + RoboCopy switches for how to react if a copy/move operation errors + + + + + Specifies the number of retries N on failed copies (default is 0). + [/R:N] + + + + + Specifies the wait time N in seconds between retries (default is 30). + [/W:N] + + + + + Saves RetryCount and RetryWaitTime in the Registry as default settings. + [/REG] + + + + + Wait for sharenames to be defined. + [/TBD] + + + + + Wrapper for the RoboCopy process + + + + Value indicating if process is currently paused + + + Value indicating if process is currently running + + + Value indicating if process was Cancelled + + + + + + + + + + + + + + + + + + + + + + Value indicating if the process should be killed when the method is called.
+ For example, if the RoboCopy process should exit when the program exits, this should be set to TRUE. +
+
+ + Handles + + + Occurs each time a new item has started processing + + + Handles + + + Occurs when an error occurs while generating the command + + + Handles + + + Occurs when the command exits due to an error + + + Handles + + + Occurs when the command exits + + + Handles + + + Occurs each time the current item's progress is updated + + + Create a new RoboCommand object + + + Pause execution of the RoboCopy process when == false + + + Resume execution of the RoboCopy process when == true + + + + Start the RoboCopy Process. + + + + + Returns a task that reports when the RoboCopy process has finished executing. + + + Kill the process + + + + The RoboCopyResults object from the last run + + + > + + + IDisposable Implementation + + + + + + + + + Return the Results object + + + + + + + + + Setup the ErrorToken and the path to RoboCopy.exe. + + + + + + + + + + Specify the path to RoboCopy.exe here. If not set, use the default copy. + + + + + RoboCopy Switches that determine which folders and files are selected for copying/moving + + + + + Copies only files for which the Archive attribute is set. + [/A] + + + + + Copies only files for which the Archive attribute is set, and resets the Archive attribute. + [/M] + + + + + This property should be set to a string consisting of all the attributes to include (eg. AH; RASHCNETO). + Includes only files for which any of the specified attributes are set. + [/IA:attributes] + + + + + This property should be set to a string consisting of all the attributes to exclude (eg. AH; RASHCNETO). + Excludes files for which any of the specified attributes are set. + [/XA:attributes] + + + + + Files should be separated by spaces. + Excludes files that match the specified names or paths. Note that FileName can include wildcard characters (* and ?). + [/XF File File ...] + + + + + Directories should be separated by spaces. + Excludes directories that match the specified names or paths. + [/XD Directory Directory ...] + + + + + Excludes changed files. + [/XC] + + + + + Excludes newer files. + [/XN] + + + + + Excludes older files. + [/XO] + + + + + Excludes extra files and directories. + [/XX] + + + + + Excludes lonely files and directories. + [/XL] + + + + + Includes the same files. + [/IS] + + + + + Includes tweaked files. + [/IT] + + + + + Zero indicates that this feature is turned off. + Specifies the maximum file size (to exclude files bigger than N bytes). + [/MAX:N] + + + + + Zero indicates that this feature is turned off. + Specifies the minimum file size (to exclude files smaller than N bytes). + [/MIN:N] + + + + + Specifies the maximum file age (to exclude files older than N days or date). + [/MAXAGE:N OR YYYYMMDD] + + + + + Specifies the minimum file age (exclude files newer than N days or date). + [/MINAGE:N OR YYYYMMDD] + + + + + Specifies the maximum last access date (excludes files unused since Date). + [/MAXLAD:YYYYMMDD] + + + + + Specifies the minimum last access date (excludes files used since N) If N is less + than 1900, N specifies the number of days. Otherwise, N specifies a date + in the format YYYYMMDD. + [/MAXLAD:N or YYYYMMDD] + + + + + Excludes junction points, which are normally included by default. + [/XJ] + + + + + Assumes FAT file times (two-second precision). + [/FFT] + + + + + Compensates for one-hour DST time differences. + [/DST] + + + + + Excludes junction points for directories. + [/XJD] + + + + + Excludes junction points for files. + [/XJF] + + + + + taken from https://stackoverflow.com/a/49641055 + + + + +
+
diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboSharpConfiguration.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboSharpConfiguration.cs new file mode 100644 index 0000000..afd3d01 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/RoboSharpConfiguration.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using FSI.Lib.Tools.RoboSharp.DefaultConfigurations; + +namespace FSI.Lib.Tools.RoboSharp +{ + /// + /// Setup the ErrorToken and the path to RoboCopy.exe. + /// + /// + /// + /// + public class RoboSharpConfiguration : ICloneable + { + #region Constructors + + /// + /// Create new LoggingOptions with Default Settings + /// + public RoboSharpConfiguration() { } + + /// + /// Clone a RoboSharpConfiguration Object + /// + /// RoboSharpConfiguration object to clone + public RoboSharpConfiguration(RoboSharpConfiguration options) + { + errorToken = options.errorToken; + errorTokenRegex = options.errorTokenRegex; + roboCopyExe = options.roboCopyExe; + + #region < File Tokens > + newFileToken = options.newFileToken; + olderToken = options.olderToken; + newerToken = options.newerToken; + sameToken = options.sameToken; + extraToken = options.extraToken; + mismatchToken = options.mismatchToken; + failedToken = options.failedToken; + #endregion + + #region < Directory Tokens > + newerDirToken = options.newerDirToken; + extraDirToken = options.extraDirToken; + existingDirToken = options.existingDirToken; + #endregion + + } + + /// + public RoboSharpConfiguration Clone() => new RoboSharpConfiguration(this); + + object ICloneable.Clone() => Clone(); + + #endregion + + private static readonly IDictionary + defaultConfigurations = new Dictionary() + { + {"en", new RoboSharpConfig_EN() }, //en uses Defaults for LogParsing properties + {"de", new RoboSharpConfig_DE() }, + }; + + /// + /// Error Token Identifier -- EN = "ERROR", DE = "FEHLER", etc
+ /// Leave as / Set to null to use system default. + ///
+ public string ErrorToken + { + get { return errorToken ?? GetDefaultConfiguration().ErrorToken; } + set + { + if (value != errorToken) ErrRegexInitRequired = true; + errorToken = value; + } + } + /// field backing property - Protected to allow DefaultConfig derived classes to set within constructor + protected string errorToken = null; + + /// + /// Regex to identify Error Tokens with during LogLine parsing + /// + public Regex ErrorTokenRegex + { + get + { + if (ErrRegexInitRequired) goto RegenRegex; //Regex Generation Required + else if (errorTokenRegex != null) return errorTokenRegex; //field already assigned -> return the field + else + { + //Try get default, if default has regex defined, use that. + errorTokenRegex = GetDefaultConfiguration().errorTokenRegex; + if (errorTokenRegex != null) return errorTokenRegex; + } + // Generate a new Regex Statement + RegenRegex: + errorTokenRegex = ErrorTokenRegexGenerator(ErrorToken); //new Regex($" {this.ErrorToken} " + @"(\d{1,3}) \(0x\d{8}\) "); + ErrRegexInitRequired = false; + return errorTokenRegex; + } + } + /// Field backing property - Protected to allow DefaultConfig derived classes to set within constructor + protected Regex errorTokenRegex; + private bool ErrRegexInitRequired = false; + + /// + /// Generate a new ErrorTokenRegex object from by insterting the into a standardized pattern. + /// + /// Language Specific + /// + internal static Regex ErrorTokenRegexGenerator(string errorToken) + { + Regex BaseErrTokenRegex = new Regex("(?.*?)\\s+IDENTIFIER\\s+(?[0-9]+)\\s+(?\\([0-9Xx]+\\))\\s+(?[\\w\\s]+(?!:))(?.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture); + string pattern = BaseErrTokenRegex.ToString().Replace("IDENTIFIER", errorToken); + return new Regex(pattern, BaseErrTokenRegex.Options); + } + + + #region < Tokens for Log Parsing > + + #region < File Tokens > + + /// + /// Log Lines starting with this string indicate : New File -> Source FILE Exists, Destination does not + /// + public string LogParsing_NewFile + { + get { return newFileToken ?? GetDefaultConfiguration().newFileToken ?? "New File"; } + set { newFileToken = value; } + } + private string newFileToken; + + /// + /// Log Lines starting with this string indicate : Destination File newer than Source + /// + public string LogParsing_OlderFile + { + get { return olderToken ?? GetDefaultConfiguration().olderToken ?? "Older"; } + set { olderToken = value; } + } + private string olderToken; + + /// + /// Log Lines starting with this string indicate : Source File newer than Destination + /// + public string LogParsing_NewerFile + { + get { return newerToken ?? GetDefaultConfiguration().newerToken ?? "Newer"; } + set { newerToken = value; } + } + private string newerToken; + + /// + /// Log Lines starting with this string indicate : Source FILE is identical to Destination File + /// + public string LogParsing_SameFile + { + get { return sameToken ?? GetDefaultConfiguration().sameToken ?? "same"; } + set { sameToken = value; } + } + private string sameToken; + + /// + /// Log Lines starting with this string indicate : EXTRA FILE -> Destination Exists, but Source does not + /// + public string LogParsing_ExtraFile + { + get { return extraToken ?? GetDefaultConfiguration().extraToken ?? "*EXTRA File"; } + set { extraToken = value; } + } + private string extraToken; + + /// + /// Log Lines starting with this string indicate : MISMATCH FILE + /// + public string LogParsing_MismatchFile + { + get { return mismatchToken ?? GetDefaultConfiguration().mismatchToken ?? "*Mismatch"; } // TODO: Needs Verification + set { mismatchToken = value; } + } + private string mismatchToken; + + /// + /// Log Lines starting with this string indicate : File Failed to Copy + /// + public string LogParsing_FailedFile + { + get { return failedToken ?? GetDefaultConfiguration().failedToken ?? "*Failed"; } // TODO: Needs Verification + set { failedToken = value; } + } + private string failedToken; + + /// + /// Log Lines starting with this string indicate : File was excluded by filters + /// + public string LogParsing_FileExclusion + { + get { return fileExcludedToken ?? GetDefaultConfiguration().fileExcludedToken ?? "named"; } // TODO: Needs Verification + set { fileExcludedToken = value; } + } + private string fileExcludedToken; + + /// + /// Log Lines starting with this string indicate : File was excluded by or filters + /// + public string LogParsing_AttribExclusion + { + get { return attribExcludedToken ?? GetDefaultConfiguration().attribExcludedToken ?? "attrib"; } + set { attribExcludedToken = value; } + } + private string attribExcludedToken; + + /// + /// Log Lines starting with this string indicate : File was excluded by filters + /// + public string LogParsing_MaxFileSizeExclusion + { + get { return maxfilesizeExcludedToken ?? GetDefaultConfiguration().maxfilesizeExcludedToken ?? "large"; } + set { maxfilesizeExcludedToken = value; } + } + private string maxfilesizeExcludedToken; + + /// + /// Log Lines starting with this string indicate : File was excluded by filters + /// + public string LogParsing_MinFileSizeExclusion + { + get { return minfilesizeExcludedToken ?? GetDefaultConfiguration().minfilesizeExcludedToken ?? "small"; } + set { minfilesizeExcludedToken = value; } + } + private string minfilesizeExcludedToken; + + /// + /// Log Lines starting with this string indicate : File was excluded by or filters + /// + public string LogParsing_MaxAgeOrAccessExclusion + { + get { return maxageoraccessExcludedToken ?? GetDefaultConfiguration().maxageoraccessExcludedToken ?? "too old"; } + set { maxageoraccessExcludedToken = value; } + } + private string maxageoraccessExcludedToken; + + /// + /// Log Lines starting with this string indicate : File was excluded by or filters + /// + public string LogParsing_MinAgeOrAccessExclusion + { + get { return minageoraccessExcludedToken ?? GetDefaultConfiguration().minageoraccessExcludedToken ?? "too new"; } + set { minageoraccessExcludedToken = value; } + } + private string minageoraccessExcludedToken; + + /// + /// Log Lines starting with this string indicate : File was excluded by filters + /// + public string LogParsing_ChangedExclusion + { + get { return changedExcludedToken ?? GetDefaultConfiguration().changedExcludedToken ?? "changed"; } + set { changedExcludedToken = value; } + } + private string changedExcludedToken; + + /// + /// Log Lines starting with this string indicate : File was included by filters + /// + public string LogParsing_TweakedInclusion + { + get { return tweakedIncludedToken ?? GetDefaultConfiguration().tweakedIncludedToken ?? "tweaked"; } + set { tweakedIncludedToken = value; } + } + private string tweakedIncludedToken; + + #endregion + + #region < Directory Tokens > + + /// + /// Log Lines starting with this string indicate : New Dir -> Directory will be copied to Destination + /// + public string LogParsing_NewDir + { + get { return newerDirToken ?? GetDefaultConfiguration().newerDirToken ?? "New Dir"; } + set { newerDirToken = value; } + } + private string newerDirToken; + + /// + /// Log Lines starting with this string indicate : Extra Dir -> Does not exist in source + /// + public string LogParsing_ExtraDir + { + get { return extraDirToken ?? GetDefaultConfiguration().extraDirToken ?? "*EXTRA Dir"; } + set { extraDirToken = value; } + } + private string extraDirToken; + + /// + /// Existing Dirs do not have an identifier on the line. Instead, this string will be used when creating the object to indicate an Existing Directory. + /// + public string LogParsing_ExistingDir + { + get { return existingDirToken ?? GetDefaultConfiguration().existingDirToken ?? "Existing Dir"; } + set { existingDirToken = value; } + } + private string existingDirToken; + + /// + /// Log Lines starting with this string indicate : Folder was excluded by filters + /// + public string LogParsing_DirectoryExclusion + { + get { return dirExcludedToken ?? GetDefaultConfiguration().dirExcludedToken ?? "named"; } // TODO: Needs Verification + set { dirExcludedToken = value; } + } + private string dirExcludedToken; + + #endregion + + #endregion + + /// + /// Specify the path to RoboCopy.exe here. If not set, use the default copy. + /// + public string RoboCopyExe + { + get { return roboCopyExe ?? "Robocopy.exe"; } + set { roboCopyExe = value; } + } + private string roboCopyExe = null; + + /// Default is retrieved from the OEMCodePage + /// + public System.Text.Encoding StandardOutputEncoding { get; set; } = System.Text.Encoding.GetEncoding(System.Globalization.CultureInfo.CurrentCulture.TextInfo.OEMCodePage); + + /// Default is retrieved from the OEMCodePage + /// + public System.Text.Encoding StandardErrorEncoding { get; set; } = System.Text.Encoding.GetEncoding(System.Globalization.CultureInfo.CurrentCulture.TextInfo.OEMCodePage); + + + private RoboSharpConfiguration defaultConfig = null; + private RoboSharpConfiguration GetDefaultConfiguration() + { + if (defaultConfig != null) return defaultConfig; + + // check for default with language Tag xx-YY (e.g. en-US) + var currentLanguageTag = System.Globalization.CultureInfo.CurrentUICulture.IetfLanguageTag; + if (defaultConfigurations.ContainsKey(currentLanguageTag)) + { + defaultConfig = defaultConfigurations[currentLanguageTag]; + } + else + { + // check for default with language Tag xx (e.g. en) + var match = Regex.Match(currentLanguageTag, @"^\w+", RegexOptions.Compiled); + if (match.Success) + { + var currentMainLanguageTag = match.Value; + if (defaultConfigurations.ContainsKey(currentMainLanguageTag)) + { + defaultConfig = defaultConfigurations[currentMainLanguageTag]; + } + } + } + + // no match, fallback to en + return defaultConfig ?? defaultConfigurations["en"]; + } + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/SelectionOptions.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/SelectionOptions.cs new file mode 100644 index 0000000..6f0b164 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/SelectionOptions.cs @@ -0,0 +1,469 @@ +using System; +using System.Linq; +using System.Text; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; +using System.IO; + +namespace FSI.Lib.Tools.RoboSharp +{ + /// + /// RoboCopy Switches that determine which folders and files are selected for copying/moving + /// + /// + /// + /// + public class SelectionOptions : ICloneable + { + #region Constructors + + /// + /// Create new SelectionOptions with Default Settings + /// + public SelectionOptions() { } + + /// + /// Clone a SelectionOptions Object + /// + public SelectionOptions(SelectionOptions options) + { + OnlyCopyArchiveFiles = options.OnlyCopyArchiveFiles; + OnlyCopyArchiveFilesAndResetArchiveFlag = options.OnlyCopyArchiveFilesAndResetArchiveFlag; + IncludeAttributes = options.IncludeAttributes; + ExcludeAttributes = options.ExcludeAttributes; + ExcludedFiles.AddRange(options.ExcludedFiles); + ExcludedDirectories.AddRange(options.ExcludedDirectories); + ExcludeChanged = options.ExcludeChanged; + ExcludeNewer = options.ExcludeNewer; + ExcludeOlder = options.ExcludeOlder; + ExcludeExtra = options.ExcludeExtra; + ExcludeLonely = options.ExcludeLonely; + IncludeSame = options.IncludeSame; + IncludeTweaked = options.IncludeTweaked; + MaxFileSize = options.MaxFileSize; + MinFileSize = options.MinFileSize; + MaxFileAge = options.MaxFileAge; + MinFileAge = options.MinFileAge; + MaxLastAccessDate = options.MaxLastAccessDate; + MinLastAccessDate = options.MinLastAccessDate; + ExcludeJunctionPoints = options.ExcludeJunctionPoints; + UseFatFileTimes = options.UseFatFileTimes; + CompensateForDstDifference = options.CompensateForDstDifference; ; + ExcludeJunctionPointsForFiles = options.ExcludeJunctionPointsForFiles; + + } + + /// + /// Clone this SelectionOptions Object + /// + public SelectionOptions Clone() => new SelectionOptions(this); + + object ICloneable.Clone() => Clone(); + + #endregion + + #region Option Constants + + internal const string ONLY_COPY_ARCHIVE_FILES = "/A "; + internal const string ONLY_COPY_ARCHIVE_FILES_AND_RESET_ARCHIVE_FLAG = "/M "; + internal const string INCLUDE_ATTRIBUTES = "/IA:{0} "; + internal const string EXCLUDE_ATTRIBUTES = "/XA:{0} "; + internal const string EXCLUDE_FILES = "/XF {0} "; + internal const string EXCLUDE_DIRECTORIES = "/XD {0} "; + internal const string EXCLUDE_CHANGED = "/XC "; + internal const string EXCLUDE_NEWER = "/XN "; + internal const string EXCLUDE_OLDER = "/XO "; + internal const string EXCLUDE_EXTRA = "/XX "; + internal const string EXCLUDE_LONELY = "/XL "; + internal const string INCLUDE_SAME = "/IS "; + internal const string INCLUDE_TWEAKED = "/IT "; + internal const string MAX_FILE_SIZE = "/MAX:{0} "; + internal const string MIN_FILE_SIZE = "/MIN:{0} "; + internal const string MAX_FILE_AGE = "/MAXAGE:{0} "; + internal const string MIN_FILE_AGE = "/MINAGE:{0} "; + internal const string MAX_LAST_ACCESS_DATE = "/MAXLAD:{0} "; + internal const string MIN_LAST_ACCESS_DATE = "/MINLAD:{0} "; + internal const string EXCLUDE_JUNCTION_POINTS = "/XJ "; + internal const string USE_FAT_FILE_TIMES = "/FFT "; + internal const string COMPENSATE_FOR_DST_DIFFERENCE = "/DST "; + internal const string EXCLUDE_JUNCTION_POINTS_FOR_DIRECTORIES = "/XJD "; + internal const string EXCLUDE_JUNCTION_POINTS_FOR_FILES = "/XJF "; + + #endregion Option Constants + + #region < ExcludedDirs and ExcludedFiles > + + private readonly List excludedDirs = new List(); + private readonly List excludedFiles = new List(); + + /// + /// This regex is used when the { } and { } properties are set in order to split the input string to a List{string} + /// + /// + /// Regex Tester to use with to get all the matches from a string. + /// + public static Regex FileFolderNameRegexSplitter = new Regex("(?\".+?\"|[^\\s\\,\"\\|]+)", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + /// + /// Use { } to split the , then add the matches to the suppplied . + /// + /// String to perform against + /// List to add regex matches to + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ParseAndAddToList(string inputString, List list) + { + MatchCollection collection = FileFolderNameRegexSplitter.Matches(inputString); + if (collection.Count == 0) return; + foreach (Match c in collection) + { + string s = c.Groups["VALUE"].Value; + list.Add(s); + } + } + + #endregion + + #region Public Properties + + /// + /// Copies only files for which the Archive attribute is set. + /// [/A] + /// + public virtual bool OnlyCopyArchiveFiles { get; set; } + /// + /// Copies only files for which the Archive attribute is set, and resets the Archive attribute. + /// [/M] + /// + public virtual bool OnlyCopyArchiveFilesAndResetArchiveFlag { get; set; } + /// + /// This property should be set to a string consisting of all the attributes to include (eg. AH; RASHCNETO). + /// Includes only files for which any of the specified attributes are set. + /// [/IA:attributes] + /// + public virtual string IncludeAttributes { get; set; } + /// + /// This property should be set to a string consisting of all the attributes to exclude (eg. AH; RASHCNETO). + /// Excludes files for which any of the specified attributes are set. + /// [/XA:attributes] + /// + public virtual string ExcludeAttributes { get; set; } + /// + /// Files should be separated by spaces. + /// Excludes files that match the specified names or paths. Note that FileName can include wildcard characters (* and ?). + /// [/XF File File ...] + /// + /// + /// This property is now backed by the ExcludedFiles List{String} property.
+ /// Get -> Ensures all strings in { } are wrapped in quotes if needed, and concats the items into a single string.
+ /// Set -- Clears ExcludedFiles and splits this list using a regex to populate the list. + ///
+ [Obsolete("This property is now backed by the ExcludedFiles List property. \n Both Get/Set accessors still work similar to previous:\n" + + "- 'Get' sanitizies then Joins all strings in the list into a single output string that is passed into RoboCopy.\n" + + "- 'Set' clears the ExcludedFiles list, then splits the input string using regex to repopulate the list." + )] + public string ExcludeFiles + { + get + { + string RetString = ""; + foreach (string s in excludedFiles) + { + RetString += s.WrapPath() + " "; + } + return RetString.Trim(); + } + set + { + excludedFiles.Clear(); + if (value.IsNullOrWhiteSpace()) return; + ParseAndAddToList(value, excludedFiles); + } + } + + /// + /// 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 List ExcludedFiles + { + get + { + return excludedFiles; + } + } + /// + /// Directories should be separated by spaces. + /// Excludes directories that match the specified names or paths. + /// [/XD Directory Directory ...] + /// + /// + /// This property is now backed by the ExcludedDirectories List{String} property.
+ /// Get -> Ensures all strings in { } are wrapped in quotes if needed, and concats the items into a single string.
+ /// Set -> Clears ExcludedDirs and splits this list using a regex to populate the list. + ///
+ [Obsolete("This property is now backed by the ExcludedDirectories List property. \n Both Get/Set accessors still work similar to previous:\n" + + "- 'Get' sanitizies then Joins all strings in the list into a single output string that is passed into RoboCopy.\n" + + "- 'Set' clears the ExcludedDirectories list, then splits the input string using regex to repopulate the list." + )] + public string ExcludeDirectories + { + get + { + string RetString = ""; + foreach (string s in excludedDirs) + { + RetString += s.WrapPath() + " "; + } + return RetString.Trim(); + } + set + { + excludedDirs.Clear(); + if (value.IsNullOrWhiteSpace()) return; + ParseAndAddToList(value, excludedDirs); + } + } + /// + /// 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 List ExcludedDirectories + { + get + { + return excludedDirs; + } + } + /// + /// Excludes changed files. + /// [/XC] + /// + public virtual bool ExcludeChanged { get; set; } + /// + /// Excludes newer files. + /// [/XN] + /// + public virtual bool ExcludeNewer { get; set; } + /// + /// Excludes older files. + /// [/XO] + /// + public virtual bool ExcludeOlder { get; set; } + /// + /// Excludes extra files and directories. + /// [/XX] + /// + public virtual bool ExcludeExtra { get; set; } + /// + /// Excludes lonely files and directories. + /// [/XL] + /// + public virtual bool ExcludeLonely { get; set; } + /// + /// Includes the same files. + /// [/IS] + /// + public virtual bool IncludeSame { get; set; } + /// + /// Includes tweaked files. + /// [/IT] + /// + public virtual bool IncludeTweaked { get; set; } + /// + /// Zero indicates that this feature is turned off. + /// Specifies the maximum file size (to exclude files bigger than N bytes). + /// [/MAX:N] + /// + public virtual long MaxFileSize { get; set; } + /// + /// Zero indicates that this feature is turned off. + /// Specifies the minimum file size (to exclude files smaller than N bytes). + /// [/MIN:N] + /// + public virtual long MinFileSize { get; set; } + /// + /// Specifies the maximum file age (to exclude files older than N days or date). + /// [/MAXAGE:N OR YYYYMMDD] + /// + public virtual string MaxFileAge { get; set; } + /// + /// Specifies the minimum file age (exclude files newer than N days or date). + /// [/MINAGE:N OR YYYYMMDD] + /// + public virtual string MinFileAge { get; set; } + /// + /// Specifies the maximum last access date (excludes files unused since Date). + /// [/MAXLAD:YYYYMMDD] + /// + public virtual string MaxLastAccessDate { get; set; } + /// + /// Specifies the minimum last access date (excludes files used since N) If N is less + /// than 1900, N specifies the number of days. Otherwise, N specifies a date + /// in the format YYYYMMDD. + /// [/MINLAD:N or YYYYMMDD] + /// + public virtual string MinLastAccessDate { get; set; } + /// + /// Excludes junction points, which are normally included by default. + /// [/XJ] + /// + public virtual bool ExcludeJunctionPoints { get; set; } + /// + /// Assumes FAT file times (two-second precision). + /// [/FFT] + /// + public virtual bool UseFatFileTimes { get; set; } + /// + /// Compensates for one-hour DST time differences. + /// [/DST] + /// + public virtual bool CompensateForDstDifference { get; set; } + /// + /// Excludes junction points for directories. + /// [/XJD] + /// + public virtual bool ExcludeJunctionPointsForDirectories { get; set; } + /// + /// Excludes junction points for files. + /// [/XJF] + /// + public virtual bool ExcludeJunctionPointsForFiles { get; set; } + + #endregion Public Properties + + /// + /// + public void SetIncludedAttributes(FileAttributes? AttributesToInclude) => this.IncludeAttributes = ConvertFileAttrToString(AttributesToInclude); + + /// + /// + public void SetExcludedAttributes(FileAttributes? AttributesToExclude) => this.ExcludeAttributes = ConvertFileAttrToString(AttributesToExclude); + + /// + /// Converts a enum to its RASHCNETO string. + /// + /// + /// Accepts: ReadOnly, Archive, System, Hidden, Compressed, NotContentIndexed, Encrypted, Temporary, Offline
+ /// Ignores: All Other Attributes
+ /// Pass in NULL value to return empty string. + /// + /// RASHCNETO depending on submitted enum + public static string ConvertFileAttrToString(FileAttributes? attributes) + { + if (attributes is null) return String.Empty; + string s = ""; + var Attr = (FileAttributes)attributes; + if (Attr.HasFlag(FileAttributes.ReadOnly)) s += "R"; + if (Attr.HasFlag(FileAttributes.Archive)) s += "A"; + if (Attr.HasFlag(FileAttributes.System)) s += "S"; + if (Attr.HasFlag(FileAttributes.Hidden)) s += "H"; + if (Attr.HasFlag(FileAttributes.Compressed)) s += "C"; + if (Attr.HasFlag(FileAttributes.NotContentIndexed)) s += "N"; + if (Attr.HasFlag(FileAttributes.Encrypted)) s += "E"; + if (Attr.HasFlag(FileAttributes.Temporary)) s += "T"; + if (Attr.HasFlag(FileAttributes.Offline)) s += "O"; + return s; + } + + internal string Parse() + { + var options = new StringBuilder(); + + #region Set Options + + if (OnlyCopyArchiveFiles) + options.Append(ONLY_COPY_ARCHIVE_FILES); + if (OnlyCopyArchiveFilesAndResetArchiveFlag) + options.Append(ONLY_COPY_ARCHIVE_FILES_AND_RESET_ARCHIVE_FLAG); + if (!IncludeAttributes.IsNullOrWhiteSpace()) + options.Append(string.Format(INCLUDE_ATTRIBUTES, IncludeAttributes.CleanOptionInput())); + if (!ExcludeAttributes.IsNullOrWhiteSpace()) + options.Append(string.Format(EXCLUDE_ATTRIBUTES, ExcludeAttributes.CleanOptionInput())); +#pragma warning disable CS0618 // Marked as Obsolete for consumers, but it originally functionality is still intact, so this still works properly. + if (!ExcludeFiles.IsNullOrWhiteSpace()) + options.Append(string.Format(EXCLUDE_FILES, ExcludeFiles)); + if (!ExcludeDirectories.IsNullOrWhiteSpace()) + options.Append(string.Format(EXCLUDE_DIRECTORIES, ExcludeDirectories)); +#pragma warning restore CS0618 + if (ExcludeChanged) + options.Append(EXCLUDE_CHANGED); + if (ExcludeNewer) + options.Append(EXCLUDE_NEWER); + if (ExcludeOlder) + options.Append(EXCLUDE_OLDER); + if (ExcludeExtra) + options.Append(EXCLUDE_EXTRA); + if (ExcludeLonely) + options.Append(EXCLUDE_LONELY); + if (IncludeSame) + options.Append(INCLUDE_SAME); + if (IncludeTweaked) + options.Append(INCLUDE_TWEAKED); + if (MaxFileSize > 0) + options.Append(string.Format(MAX_FILE_SIZE, MaxFileSize)); + if (MinFileSize > 0) + options.Append(string.Format(MIN_FILE_SIZE, MinFileSize)); + if (!MaxFileAge.IsNullOrWhiteSpace()) + options.Append(string.Format(MAX_FILE_AGE, MaxFileAge.CleanOptionInput())); + if (!MinFileAge.IsNullOrWhiteSpace()) + options.Append(string.Format(MIN_FILE_AGE, MinFileAge.CleanOptionInput())); + if (!MaxLastAccessDate.IsNullOrWhiteSpace()) + options.Append(string.Format(MAX_LAST_ACCESS_DATE, MaxLastAccessDate.CleanOptionInput())); + if (!MinLastAccessDate.IsNullOrWhiteSpace()) + options.Append(string.Format(MIN_LAST_ACCESS_DATE, MinLastAccessDate.CleanOptionInput())); + if (ExcludeJunctionPoints) + options.Append(EXCLUDE_JUNCTION_POINTS); + if (ExcludeJunctionPointsForDirectories) + options.Append(EXCLUDE_JUNCTION_POINTS_FOR_DIRECTORIES); + if (ExcludeJunctionPointsForFiles) + options.Append(EXCLUDE_JUNCTION_POINTS_FOR_FILES); + if (UseFatFileTimes) + options.Append(USE_FAT_FILE_TIMES); + if (CompensateForDstDifference) + options.Append(COMPENSATE_FOR_DST_DIFFERENCE); + + #endregion Set Options + + return options.ToString(); + } + + /// + /// Combine this object with another RetryOptions 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(SelectionOptions options) + { + //File Attributes + IncludeAttributes = IncludeAttributes.CombineCharArr(options.IncludeAttributes); + ExcludeAttributes = ExcludeAttributes.CombineCharArr(options.ExcludeAttributes); + + //File Age + MaxFileAge = MaxFileAge.ReplaceIfEmpty(options.MaxFileAge); + MinFileAge = MaxFileAge.ReplaceIfEmpty(options.MinFileAge); + MaxLastAccessDate = MaxFileAge.ReplaceIfEmpty(options.MaxLastAccessDate); + MinLastAccessDate = MaxFileAge.ReplaceIfEmpty(options.MinLastAccessDate); + + //Bools + OnlyCopyArchiveFiles |= options.OnlyCopyArchiveFiles; + OnlyCopyArchiveFilesAndResetArchiveFlag |= options.OnlyCopyArchiveFilesAndResetArchiveFlag; + ExcludedFiles.AddRange(options.ExcludedFiles); + ExcludedDirectories.AddRange(options.ExcludedDirectories); + ExcludeChanged |= options.ExcludeChanged; + ExcludeNewer |= options.ExcludeNewer; + ExcludeOlder |= options.ExcludeOlder; + ExcludeExtra |= options.ExcludeExtra; + ExcludeLonely |= options.ExcludeLonely; + IncludeSame |= options.IncludeSame; + IncludeTweaked |= options.IncludeTweaked; + MaxFileSize |= options.MaxFileSize; + MinFileSize |= options.MinFileSize; + ExcludeJunctionPoints |= options.ExcludeJunctionPoints; + UseFatFileTimes |= options.UseFatFileTimes; + CompensateForDstDifference |= options.CompensateForDstDifference; ; + ExcludeJunctionPointsForFiles |= options.ExcludeJunctionPointsForFiles; + } + } +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/VersionManager.cs b/FSI.Lib/FSI.Lib/Tools/RoboSharp/VersionManager.cs new file mode 100644 index 0000000..33f431a --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/VersionManager.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Threading.Tasks; + +namespace FSI.Lib.Tools.RoboSharp +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class VersionManager + { + public enum VersionCheckType + { + UseRtlGetVersion, + UseWMI + } + + public static VersionCheckType VersionCheck { get; set; } = VersionManager.VersionCheckType.UseRtlGetVersion; + + + private static double? version; + public static double Version + { + get + { + if (version == null) + { + if (VersionCheck == VersionCheckType.UseWMI) + { + var v = GetOsVersion(); + version = GetOsVersionNumber(v); + return version.Value; + } + else + { + var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) }; + RtlGetVersion(ref osVersionInfo); + var versionString = $"{osVersionInfo.MajorVersion}.{osVersionInfo.MinorVersion}{osVersionInfo.BuildNumber}"; + version = GetOsVersionNumber(versionString); + return version.Value; + } + } + else + { + return version.Value; + } + } + } + + static VersionManager() + { + System.Globalization.CultureInfo customCulture = (System.Globalization.CultureInfo)System.Threading.Thread.CurrentThread.CurrentCulture.Clone(); + customCulture.NumberFormat.NumberDecimalSeparator = "."; + System.Threading.Thread.CurrentThread.CurrentCulture = customCulture; + } + + + private static string GetOsVersion() + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + using (var session = Microsoft.Management.Infrastructure.CimSession.Create(".")) + + { + var win32OperatingSystemCimInstance = session.QueryInstances("root\\cimv2", "WQL", "SELECT Version FROM Win32_OperatingSystem").FirstOrDefault(); + + if (win32OperatingSystemCimInstance?.CimInstanceProperties["Version"] != null) + { + return win32OperatingSystemCimInstance.CimInstanceProperties["Version"].Value.ToString(); + } + } +#endif +#if NET40_OR_GREATER + using (System.Management.ManagementObjectSearcher objMOS = new System.Management.ManagementObjectSearcher("SELECT * FROM Win32_OperatingSystem")) + { + foreach (System.Management.ManagementObject objManagement in objMOS.Get()) + { + var version = objManagement.GetPropertyValue("Version"); + + if (version != null) + { + return version.ToString(); + } + } + } +#endif + + return Environment.OSVersion.Version.ToString(); + } + + private static double GetOsVersionNumber(string version) + { + if (version.IsNullOrWhiteSpace()) + return 0; + + var segments = version.Split(new char[] { '.' }); + var major = Convert.ToDouble(segments[0]); + var otherSegments = segments.Skip(1).ToArray(); + var dec = Convert.ToDouble("." + string.Join("", otherSegments), CultureInfo.InvariantCulture); + return major + dec; + } + + /// + /// taken from https://stackoverflow.com/a/49641055 + /// + /// + /// + [SecurityCritical] + [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern int RtlGetVersion(ref OSVERSIONINFOEX versionInfo); + [StructLayout(LayoutKind.Sequential)] + private struct OSVERSIONINFOEX + { + // The OSVersionInfoSize field must be set to Marshal.SizeOf(typeof(OSVERSIONINFOEX)) + internal int OSVersionInfoSize; + internal int MajorVersion; + internal int MinorVersion; + internal int BuildNumber; + internal int PlatformId; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + internal string CSDVersion; + internal ushort ServicePackMajor; + internal ushort ServicePackMinor; + internal short SuiteMask; + internal byte ProductType; + internal byte Reserved; + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/FSI.Lib/FSI.Lib/Tools/RoboSharp/readme.md b/FSI.Lib/FSI.Lib/Tools/RoboSharp/readme.md new file mode 100644 index 0000000..b600211 --- /dev/null +++ b/FSI.Lib/FSI.Lib/Tools/RoboSharp/readme.md @@ -0,0 +1,37 @@ +# RoboSharp + +Link: https://github.com/tjscience/RoboSharp + +### Available on NuGet - https://www.nuget.org/packages/RoboSharp/ +### Wiki - in progress - https://github.com/tjscience/RoboSharp/wiki + +## About + +RoboSharp is a .NET wrapper for the awesome Robocopy windows application. + +Robocopy is a very extensive file copy application written by microsoft and included in modern versions of Windows. To learn more about Robocopy, visit the documentation page at http://technet.microsoft.com/en-us/library/cc733145.aspx. + +RoboSharp came out of a need to manipulate Robocopy in a c# backup application that I was writing. It has helped me tremendously so I thought that I would share it! It exposes all of the switches available in RoboCopy as descriptive properties. With RoboSharp, you can subscribe to events that fire when files are processed, errors occur and even as the progress of a file copy changes. Another really nice feature of RoboSharp is that you can pause and resume a copy that is in progress which is a feature that I though was lacking in Robocopy. + +In the project, you will find the RoboSharp library as well as a recently updated sample backup application that shows off many (but not all) of the options. + +If you like the project, please rate it! + +## Examples + +See the [Wiki](https://github.com/tjscience/RoboSharp/wiki) for examples and code snippets + + + + + +--- + +|

[RoboSharp Going Forward...](https://github.com/tjscience/RoboSharp/issues/63)

| [![image](https://user-images.githubusercontent.com/3706870/44311401-a9064000-a3b4-11e8-96a3-d308f52aeec1.png)](https://github.com/tjscience/RoboSharp/issues/63) | +| ------ | ----------- | + +--- + +# Contributing to RoboSharp + +First off, thanks! Please go through the [guidelines](CONTRIBUTING.md). \ No newline at end of file