using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; namespace RobvanderWoude { internal class FileHistory { static readonly string progver = "1.00"; static string sourcedir; static string destination; static string excludepattern; static string regexexcludepattern; static bool excludehidden = false; static bool excludelockedoffice = false; static int Main( string[] args ) { #region Check Command Line if ( args.Length < 2 ) { return ShowHelp( ); } if ( args.Contains( "/?" ) ) { return ShowHelp( ); } sourcedir = args[0]; destination = args[1]; if ( !Directory.Exists( sourcedir ) ) { return ShowHelp( "Source directory not found" ); } if ( !Directory.Exists( destination ) ) { return ShowHelp( "Destination directory not found" ); } foreach ( string filter in args.Skip( 2 ) ) { if ( filter[0] == '/' ) { if ( filter.ToUpper( ) == "/H" ) { if ( excludehidden ) { return ShowHelp( "Duplicate command line switch /H" ); } excludehidden = true; } else if ( filter.ToUpper( ) == "/O" ) { if ( excludelockedoffice ) { return ShowHelp( "Duplicate command line switch /O" ); } excludelockedoffice = true; } else if ( filter.ToUpper( ).StartsWith( "/R:" ) && filter.Length > 3 ) { if ( !string.IsNullOrWhiteSpace( regexexcludepattern ) ) { return ShowHelp( "Duplicate command line switch /R" ); } regexexcludepattern = filter.Substring( 3 ); } else if ( filter.ToUpper( ).StartsWith( "/X:" ) && filter.Length > 3 ) { if ( !string.IsNullOrWhiteSpace( excludepattern ) ) { return ShowHelp( "Duplicate command line switch /X" ); } excludepattern = filter.Substring( 3 ); } else { return ShowHelp( "Invalid command line switch {0}", filter ); } } } #endregion Check Command Line #region Initial Backup Console.WriteLine( "Checking initial backup set . . ." ); foreach ( string filter in args.Skip( 2 ) ) { if ( filter[0] != '/' ) { foreach ( string file in Directory.GetFiles( sourcedir, filter ) ) { if ( Directory.GetFiles( destination, Path.GetFileNameWithoutExtension( file ) + ".*" + Path.GetExtension( file ) ).Length == 0 ) { BackupFile( file ); } } } } #endregion Initial Backup foreach ( string filter in args.Skip( 2 ) ) { if ( filter[0] != '/' ) { FileSystemWatcher watcher = new FileSystemWatcher( sourcedir ) { EnableRaisingEvents = true, NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size, Filter = filter, IncludeSubdirectories = true }; watcher.Changed += OnChanged; watcher.Created += OnCreated; watcher.Deleted += OnDeleted; watcher.Renamed += OnRenamed; watcher.Error += OnError; } } Console.WriteLine( "Press enter to exit.\n" ); Console.ReadLine( ); return 0; } static bool BackupFile( string sourcefile ) { if ( IsExcluded( sourcefile ) ) { return true; } string timestamp = DateTime.Now.ToString( "yyyyMMddHHmmss" ); string relpath = sourcefile.Replace( sourcedir, "" ); if ( relpath[0] == '\\' ) { relpath = relpath.Substring( 1 ); } string reldir = Path.GetDirectoryName( relpath ); string destinationfile = Path.Combine( destination, reldir, Path.GetFileNameWithoutExtension( relpath ) + "." + timestamp + Path.GetExtension( relpath ) ); try { if ( !File.Exists( destinationfile ) ) { File.Copy( sourcefile, destinationfile, true ); Console.WriteLine( $"Copied \"{sourcefile}\" to \"{destinationfile}\"\n" ); } } catch ( Exception ex ) { PrintException( ex ); return false; } return true; } static bool FileCompare( string file1, string file2 ) { if ( new FileInfo( file1 ).Length != new FileInfo( file2 ).Length ) { return false; } byte[] bytes1 = File.ReadAllBytes( file1 ); byte[] bytes2 = File.ReadAllBytes( file2 ); for ( int i = 0; i < bytes1.Length; i++ ) { if ( bytes1[i] != bytes2[i] ) { return false; } } return true; } static string GetLastBackup( string sourcefile ) { string lastbackup; string ext = Path.GetExtension( sourcefile ); string name = Path.GetFileNameWithoutExtension( sourcefile ); string srcdir = Path.GetDirectoryName( sourcefile ); string target = sourcefile.Replace( sourcedir, destination ); string targetdir = Path.GetDirectoryName( target ); List targetfiles = Directory.GetFiles( targetdir, name + ".*" + ext ).ToList( ); targetfiles.Sort( ); lastbackup = targetfiles.LastOrDefault( ); return lastbackup; } static bool IsExcluded( string file ) { if ( excludehidden ) { try { if ( File.GetAttributes( file ).HasFlag( FileAttributes.Hidden ) ) { return true; } } catch { // ignore } } if ( excludelockedoffice ) { string ext = Path.GetExtension( file ); if ( Regex.IsMatch( ext, @"^\.(doc|ppt|xls)x?$", RegexOptions.IgnoreCase ) ) { string parentdir = Path.GetDirectoryName( file ); string name = Path.GetFileName( file ); string lockfile = Path.Combine( parentdir, @"~$" + name ); if ( File.Exists( lockfile ) ) { return true; } } } if ( !string.IsNullOrWhiteSpace( excludepattern ) ) { foreach ( string pattern in excludepattern.Split( ";".ToCharArray( ) ) ) { if ( file.ToUpper( ).Contains( pattern.ToUpper( ) ) ) { return true; } } } if ( !string.IsNullOrWhiteSpace( regexexcludepattern ) ) { Regex regex = new Regex( regexexcludepattern, RegexOptions.IgnoreCase ); if ( regex.IsMatch( file ) ) { return true; } } return false; } private static void OnChanged( object sender, FileSystemEventArgs e ) { if ( e.ChangeType != WatcherChangeTypes.Changed ) { return; } if ( IsExcluded( e.FullPath ) ) { return; } Console.WriteLine( $"Changed: {e.FullPath}" ); BackupFile( e.FullPath ); } private static void OnCreated( object sender, FileSystemEventArgs e ) { if ( IsExcluded( e.FullPath ) ) { return; } string value = $"Created: {e.FullPath}"; Console.WriteLine( value ); BackupFile( e.FullPath ); } private static void OnDeleted( object sender, FileSystemEventArgs e ) { #region Office lock file string ext = Path.GetExtension( e.FullPath ); string name = Path.GetFileName( e.FullPath ); string parentdir = Path.GetDirectoryName( e.FullPath ); if ( name.StartsWith( "~$" ) && Regex.IsMatch( ext, @"^\.(doc|ppt|xls)x?$", RegexOptions.IgnoreCase ) ) { if ( excludelockedoffice ) { name = name.Substring( 2 ); string lastbackup = GetLastBackup( Path.Combine( parentdir, name ) ); if ( string.IsNullOrWhiteSpace( lastbackup ) || !FileCompare( lastbackup, Path.Combine( parentdir, name ) ) ) { OnChanged( sender, new FileSystemEventArgs( WatcherChangeTypes.Changed, parentdir, name ) ); } return; } } #endregion Office lock file if ( IsExcluded( e.FullPath ) ) { return; } Console.WriteLine( $"Deleted: {e.FullPath}" ); } private static void OnRenamed( object sender, RenamedEventArgs e ) { if ( IsExcluded( e.FullPath ) ) { return; } Console.WriteLine( $"Renamed:" ); Console.WriteLine( $" Old: {e.OldFullPath}" ); Console.WriteLine( $" New: {e.FullPath}" ); BackupFile( e.FullPath ); } private static void OnError( object sender, ErrorEventArgs e ) { PrintException( e.GetException( ) ); } private static void PrintException( Exception ex ) { if ( ex != null ) { Console.WriteLine( $"Message: {ex.Message}" ); Console.WriteLine( "Stacktrace:" ); Console.WriteLine( ex.StackTrace ); Console.WriteLine( ); PrintException( ex.InnerException ); } } public static int ShowHelp( params string[] errmsg ) { #region Error Message if ( errmsg.Length > 0 ) { List errargs = new List( errmsg ); errargs.RemoveAt( 0 ); Console.Error.WriteLine( ); Console.ForegroundColor = ConsoleColor.Red; Console.Error.Write( "ERROR:\t" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( errmsg[0], errargs.ToArray( ) ); Console.ResetColor( ); } #endregion Error Message #region Help Text /* FileHistory.exe, Version 1.00 Save a timestamped copy of any monitored file when it is changed Usage: FileHistory sourcedir destination [ filters ] [ options ] Where: sourcedir is the directory to monitor destination is the directory where backups will be kept filters optional file type filter(s), e.g. "*.txt" (default: "*.*" i.e. all files) Options: /H exclude Hidden files /O exclude locked MS-Office Word, Excel, PowerPoint files /R:pattern exclude files matching regular expressions pattern /X:pattern exclude files matching DOS files pattern Notes: Unlike Microsoft's built-in File History, this program can monitor ANY directory, not just the user libraries. Specified source and destination directories must both exist. You may use as many filters as you like, e.g. *.doc* *.xls* *.ppt* to monitor MS-Office Word, Excel and PowerPoint files. When using /O to exclude locked MS-Office files, a backup copy will be saved as soon as the locked MS-Office file is closed, unless it is identical to the last backup of that file. Use /R to specify a regular expressions pattern for files to be excluded, or /X to specify (multiple) DOS file pattern(s). Multiple DOS file patterns can be combined by using a semicolon for separator and embedding the combined pattern in doublequotes, e.g. /X:"~$*.doc*;~$*.xls*" to exclude MS-Office Word and Excel lock files. Command line switches /R and /X may be used simultaneously. This program will keep running until the Enter key is pressed. Return code is 0 when all goes well, or -1 on (command line) errors. Written by Rob van der Woude https://www.robvanderwoude.com */ #endregion Help Text #region Display Help Text Console.Error.WriteLine( ); Console.Error.WriteLine( "FileHistory.exe, Version {0}", progver ); Console.Error.WriteLine( "Save a timestamped copy of any monitored file when it is changed" ); Console.Error.WriteLine( ); Console.Error.Write( "Usage: " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( "FileHistory sourcedir destination [ filters ] [ options ]" ); Console.ResetColor( ); Console.Error.WriteLine( ); Console.Error.Write( "Where: " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "sourcedir" ); Console.ResetColor( ); Console.Error.WriteLine( " is the directory to monitor" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " destination" ); Console.ResetColor( ); Console.Error.WriteLine( " is the directory where backups will be kept" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " filters" ); Console.ResetColor( ); Console.Error.WriteLine( " optional file type filter(s), e.g. \"*.txt\"" ); Console.Error.WriteLine( " (default: \"*.*\" i.e. all files)" ); Console.Error.WriteLine( ); Console.Error.Write( "Options: " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/H" ); Console.ResetColor( ); Console.Error.Write( " exclude " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "H" ); Console.ResetColor( ); Console.Error.WriteLine( "idden files" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /O" ); Console.ResetColor( ); Console.Error.Write( " exclude locked MS-" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "O" ); Console.ResetColor( ); Console.Error.WriteLine( "ffice Word, Excel, PowerPoint files" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /R:pattern" ); Console.ResetColor( ); Console.Error.Write( " exclude files matching regular expressions " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( "pattern" ); Console.Error.Write( " /X:pattern" ); Console.ResetColor( ); Console.Error.Write( " exclude files matching DOS files " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( "pattern" ); Console.ResetColor( ); Console.Error.WriteLine( ); Console.Error.WriteLine( "Notes: Unlike Microsoft's built-in File History, this program can monitor" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " ANY" ); Console.ResetColor( ); Console.Error.WriteLine( " directory, not just the user libraries." ); Console.Error.WriteLine( " Specified source and destination directories must both exist." ); Console.Error.Write( " You may use as many " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "filters" ); Console.ResetColor( ); Console.Error.Write( " as you like, e.g. " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( "*.doc* *.xls* *.ppt*" ); Console.ResetColor( ); Console.Error.WriteLine( " to monitor MS-Office Word, Excel and PowerPoint files." ); Console.Error.Write( " When using " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/O" ); Console.ResetColor( ); Console.Error.WriteLine( " to exclude locked MS-Office files, a backup copy will" ); Console.Error.WriteLine( " be saved as soon as the locked MS-Office file is closed, unless it" ); Console.Error.WriteLine( " is identical to the last backup of that file." ); Console.Error.Write( " Use " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/R" ); Console.ResetColor( ); Console.Error.WriteLine( " to specify a regular expressions pattern for files to be" ); Console.Error.Write( " excluded, or " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/X" ); Console.ResetColor( ); Console.Error.WriteLine( " to specify (multiple) DOS file pattern(s)." ); Console.Error.WriteLine( " Multiple DOS file patterns can be combined by using a semicolon for" ); Console.Error.WriteLine( " separator and embedding the combined pattern in doublequotes, e.g." ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( " /X:\"~$*.doc*;~$*.xls*\"" ); Console.ResetColor( ); Console.Error.WriteLine( " to exclude MS-Office Word and Excel lock files." ); Console.Error.Write( " Command line switches " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/R" ); Console.ResetColor( ); Console.Error.Write( " and " ); Console.ForegroundColor = ConsoleColor.White; Console.Error.Write( "/X" ); Console.ResetColor( ); Console.Error.WriteLine( " may be used simultaneously." ); Console.Error.WriteLine( " This program will keep running until the Enter key is pressed." ); Console.Error.WriteLine( " Return code is 0 when all goes well, or -1 on (command line) errors." ); Console.Error.WriteLine( ); Console.Error.WriteLine( "Written by Rob van der Woude" ); Console.Error.WriteLine( "https://www.robvanderwoude.com" ); #endregion Display Help Text return -1; } } }