Rob van der Woude's Scripting Pages
Powered by GeSHi

Source code for filehistory.cs

(view source code of filehistory.cs as plain text)

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text.RegularExpressions;
  6.  
  7.  
  8. namespace RobvanderWoude
  9. {
  10. 	internal class FileHistory
  11. 	{
  12. 		static readonly string progver = "1.00";
  13.  
  14.  
  15. 		static string sourcedir;
  16. 		static string destination;
  17. 		static string excludepattern;
  18. 		static string regexexcludepattern;
  19. 		static bool excludehidden = false;
  20. 		static bool excludelockedoffice = false;
  21.  
  22.  
  23. 		static int Main( string[] args )
  24. 		{
  25. 			#region Check Command Line
  26.  
  27. 			if ( args.Length < 2 )
  28. 			{
  29. 				return ShowHelp( );
  30. 			}
  31.  
  32. 			if ( args.Contains( "/?" ) )
  33. 			{
  34. 				return ShowHelp( );
  35. 			}
  36.  
  37. 			sourcedir = args[0];
  38. 			destination = args[1];
  39.  
  40. 			if ( !Directory.Exists( sourcedir ) )
  41. 			{
  42. 				return ShowHelp( "Source directory not found" );
  43. 			}
  44.  
  45. 			if ( !Directory.Exists( destination ) )
  46. 			{
  47. 				return ShowHelp( "Destination directory not found" );
  48. 			}
  49.  
  50. 			foreach ( string filter in args.Skip( 2 ) )
  51. 			{
  52. 				if ( filter[0] == '/' )
  53. 				{
  54. 					if ( filter.ToUpper( ) == "/H" )
  55. 					{
  56. 						if ( excludehidden )
  57. 						{
  58. 							return ShowHelp( "Duplicate command line switch /H" );
  59. 						}
  60. 						excludehidden = true;
  61. 					}
  62. 					else if ( filter.ToUpper( ) == "/O" )
  63. 					{
  64. 						if ( excludelockedoffice )
  65. 						{
  66. 							return ShowHelp( "Duplicate command line switch /O" );
  67. 						}
  68. 						excludelockedoffice = true;
  69. 					}
  70. 					else if ( filter.ToUpper( ).StartsWith( "/R:" ) && filter.Length > 3 )
  71. 					{
  72. 						if ( !string.IsNullOrWhiteSpace( regexexcludepattern ) )
  73. 						{
  74. 							return ShowHelp( "Duplicate command line switch /R" );
  75. 						}
  76. 						regexexcludepattern = filter.Substring( 3 );
  77. 					}
  78. 					else if ( filter.ToUpper( ).StartsWith( "/X:" ) && filter.Length > 3 )
  79. 					{
  80. 						if ( !string.IsNullOrWhiteSpace( excludepattern ) )
  81. 						{
  82. 							return ShowHelp( "Duplicate command line switch /X" );
  83. 						}
  84. 						excludepattern = filter.Substring( 3 );
  85. 					}
  86. 					else
  87. 					{
  88. 						return ShowHelp( "Invalid command line switch {0}", filter );
  89. 					}
  90. 				}
  91. 			}
  92.  
  93. 			#endregion Check Command Line
  94.  
  95.  
  96. 			#region Initial Backup
  97.  
  98. 			Console.WriteLine( "Checking initial backup set . . ." );
  99. 			foreach ( string filter in args.Skip( 2 ) )
  100. 			{
  101. 				if ( filter[0] != '/' )
  102. 				{
  103. 					foreach ( string file in Directory.GetFiles( sourcedir, filter ) )
  104. 					{
  105. 						if ( Directory.GetFiles( destination, Path.GetFileNameWithoutExtension( file ) + ".*" + Path.GetExtension( file ) ).Length == 0 )
  106. 						{
  107. 							BackupFile( file );
  108. 						}
  109. 					}
  110. 				}
  111. 			}
  112.  
  113. 			#endregion Initial Backup
  114.  
  115.  
  116. 			foreach ( string filter in args.Skip( 2 ) )
  117. 			{
  118. 				if ( filter[0] != '/' )
  119. 				{
  120. 					FileSystemWatcher watcher = new FileSystemWatcher( sourcedir )
  121. 					{
  122. 						EnableRaisingEvents = true,
  123. 						NotifyFilter = NotifyFilters.Attributes
  124. 									 | NotifyFilters.CreationTime
  125. 									 | NotifyFilters.DirectoryName
  126. 									 | NotifyFilters.FileName
  127. 									 | NotifyFilters.LastAccess
  128. 									 | NotifyFilters.LastWrite
  129. 									 | NotifyFilters.Security
  130. 									 | NotifyFilters.Size,
  131. 						Filter = filter,
  132. 						IncludeSubdirectories = true
  133. 					};
  134. 					watcher.Changed += OnChanged;
  135. 					watcher.Created += OnCreated;
  136. 					watcher.Deleted += OnDeleted;
  137. 					watcher.Renamed += OnRenamed;
  138. 					watcher.Error += OnError;
  139. 				}
  140. 			}
  141.  
  142. 			Console.WriteLine( "Press enter to exit.\n" );
  143. 			Console.ReadLine( );
  144. 			return 0;
  145. 		}
  146.  
  147.  
  148. 		static bool BackupFile( string sourcefile )
  149. 		{
  150. 			if ( IsExcluded( sourcefile ) )
  151. 			{
  152. 				return true;
  153. 			}
  154. 			string timestamp = DateTime.Now.ToString( "yyyyMMddHHmmss" );
  155. 			string relpath = sourcefile.Replace( sourcedir, "" );
  156. 			if ( relpath[0] == '\\' )
  157. 			{
  158. 				relpath = relpath.Substring( 1 );
  159. 			}
  160. 			string reldir = Path.GetDirectoryName( relpath );
  161. 			string destinationfile = Path.Combine( destination, reldir, Path.GetFileNameWithoutExtension( relpath ) + "." + timestamp + Path.GetExtension( relpath ) );
  162. 			try
  163. 			{
  164. 				if ( !File.Exists( destinationfile ) )
  165. 				{
  166. 					File.Copy( sourcefile, destinationfile, true );
  167. 					Console.WriteLine( $"Copied \"{sourcefile}\" to \"{destinationfile}\"\n" );
  168. 				}
  169. 			}
  170. 			catch ( Exception ex )
  171. 			{
  172. 				PrintException( ex );
  173. 				return false;
  174. 			}
  175. 			return true;
  176. 		}
  177.  
  178.  
  179. 		static bool FileCompare( string file1, string file2 )
  180. 		{
  181. 			if ( new FileInfo( file1 ).Length != new FileInfo( file2 ).Length )
  182. 			{
  183. 				return false;
  184. 			}
  185. 			byte[] bytes1 = File.ReadAllBytes( file1 );
  186. 			byte[] bytes2 = File.ReadAllBytes( file2 );
  187. 			for ( int i = 0; i < bytes1.Length; i++ )
  188. 			{
  189. 				if ( bytes1[i] != bytes2[i] )
  190. 				{
  191. 					return false;
  192. 				}
  193. 			}
  194. 			return true;
  195. 		}
  196.  
  197.  
  198. 		static string GetLastBackup( string sourcefile )
  199. 		{
  200. 			string lastbackup;
  201. 			string ext = Path.GetExtension( sourcefile );
  202. 			string name = Path.GetFileNameWithoutExtension( sourcefile );
  203. 			string srcdir = Path.GetDirectoryName( sourcefile );
  204. 			string target = sourcefile.Replace( sourcedir, destination );
  205. 			string targetdir = Path.GetDirectoryName( target );
  206. 			List<string> targetfiles = Directory.GetFiles( targetdir, name + ".*" + ext ).ToList( );
  207. 			targetfiles.Sort( );
  208. 			lastbackup = targetfiles.LastOrDefault( );
  209. 			return lastbackup;
  210. 		}
  211.  
  212.  
  213. 		static bool IsExcluded( string file )
  214. 		{
  215. 			if ( excludehidden )
  216. 			{
  217. 				try
  218. 				{
  219. 					if ( File.GetAttributes( file ).HasFlag( FileAttributes.Hidden ) )
  220. 					{
  221. 						return true;
  222. 					}
  223. 				}
  224. 				catch
  225. 				{
  226. 					// ignore
  227. 				}
  228. 			}
  229. 			if ( excludelockedoffice )
  230. 			{
  231. 				string ext = Path.GetExtension( file );
  232. 				if ( Regex.IsMatch( ext, @"^\.(doc|ppt|xls)x?$", RegexOptions.IgnoreCase ) )
  233. 				{
  234. 					string parentdir = Path.GetDirectoryName( file );
  235. 					string name = Path.GetFileName( file );
  236. 					string lockfile = Path.Combine( parentdir, @"~$" + name );
  237. 					if ( File.Exists( lockfile ) )
  238. 					{
  239. 						return true;
  240. 					}
  241. 				}
  242. 			}
  243. 			if ( !string.IsNullOrWhiteSpace( excludepattern ) )
  244. 			{
  245. 				foreach ( string pattern in excludepattern.Split( ";".ToCharArray( ) ) )
  246. 				{
  247. 					if ( file.ToUpper( ).Contains( pattern.ToUpper( ) ) )
  248. 					{
  249. 						return true;
  250. 					}
  251. 				}
  252. 			}
  253. 			if ( !string.IsNullOrWhiteSpace( regexexcludepattern ) )
  254. 			{
  255. 				Regex regex = new Regex( regexexcludepattern, RegexOptions.IgnoreCase );
  256. 				if ( regex.IsMatch( file ) )
  257. 				{
  258. 					return true;
  259. 				}
  260. 			}
  261. 			return false;
  262. 		}
  263.  
  264.  
  265. 		private static void OnChanged( object sender, FileSystemEventArgs e )
  266. 		{
  267. 			if ( e.ChangeType != WatcherChangeTypes.Changed )
  268. 			{
  269. 				return;
  270. 			}
  271. 			if ( IsExcluded( e.FullPath ) )
  272. 			{
  273. 				return;
  274. 			}
  275. 			Console.WriteLine( $"Changed: {e.FullPath}" );
  276. 			BackupFile( e.FullPath );
  277. 		}
  278.  
  279.  
  280. 		private static void OnCreated( object sender, FileSystemEventArgs e )
  281. 		{
  282. 			if ( IsExcluded( e.FullPath ) )
  283. 			{
  284. 				return;
  285. 			}
  286. 			string value = $"Created: {e.FullPath}";
  287. 			Console.WriteLine( value );
  288. 			BackupFile( e.FullPath );
  289. 		}
  290.  
  291.  
  292. 		private static void OnDeleted( object sender, FileSystemEventArgs e )
  293. 		{
  294. 			#region Office lock file
  295. 			string ext = Path.GetExtension( e.FullPath );
  296. 			string name = Path.GetFileName( e.FullPath );
  297. 			string parentdir = Path.GetDirectoryName( e.FullPath );
  298. 			if ( name.StartsWith( "~$" ) && Regex.IsMatch( ext, @"^\.(doc|ppt|xls)x?$", RegexOptions.IgnoreCase ) )
  299. 			{
  300. 				if ( excludelockedoffice )
  301. 				{
  302. 					name = name.Substring( 2 );
  303. 					string lastbackup = GetLastBackup( Path.Combine( parentdir, name ) );
  304. 					if ( string.IsNullOrWhiteSpace( lastbackup ) || !FileCompare( lastbackup, Path.Combine( parentdir, name ) ) )
  305. 					{
  306. 						OnChanged( sender, new FileSystemEventArgs( WatcherChangeTypes.Changed, parentdir, name ) );
  307. 					}
  308. 					return;
  309. 				}
  310. 			}
  311. 			#endregion Office lock file
  312.  
  313. 			if ( IsExcluded( e.FullPath ) )
  314. 			{
  315. 				return;
  316. 			}
  317. 			Console.WriteLine( $"Deleted: {e.FullPath}" );
  318. 		}
  319.  
  320.  
  321. 		private static void OnRenamed( object sender, RenamedEventArgs e )
  322. 		{
  323. 			if ( IsExcluded( e.FullPath ) )
  324. 			{
  325. 				return;
  326. 			}
  327. 			Console.WriteLine( $"Renamed:" );
  328. 			Console.WriteLine( $"    Old: {e.OldFullPath}" );
  329. 			Console.WriteLine( $"    New: {e.FullPath}" );
  330. 			BackupFile( e.FullPath );
  331. 		}
  332.  
  333.  
  334. 		private static void OnError( object sender, ErrorEventArgs e )
  335. 		{
  336. 			PrintException( e.GetException( ) );
  337. 		}
  338.  
  339.  
  340. 		private static void PrintException( Exception ex )
  341. 		{
  342. 			if ( ex != null )
  343. 			{
  344. 				Console.WriteLine( $"Message: {ex.Message}" );
  345. 				Console.WriteLine( "Stacktrace:" );
  346. 				Console.WriteLine( ex.StackTrace );
  347. 				Console.WriteLine( );
  348. 				PrintException( ex.InnerException );
  349. 			}
  350. 		}
  351.  
  352.  
  353. 		public static int ShowHelp( params string[] errmsg )
  354. 		{
  355. 			#region Error Message
  356.  
  357. 			if ( errmsg.Length > 0 )
  358. 			{
  359. 				List<string> errargs = new List<string>( errmsg );
  360. 				errargs.RemoveAt( 0 );
  361. 				Console.Error.WriteLine( );
  362. 				Console.ForegroundColor = ConsoleColor.Red;
  363. 				Console.Error.Write( "ERROR:\t" );
  364. 				Console.ForegroundColor = ConsoleColor.White;
  365. 				Console.Error.WriteLine( errmsg[0], errargs.ToArray( ) );
  366. 				Console.ResetColor( );
  367. 			}
  368.  
  369. 			#endregion Error Message
  370.  
  371.  
  372. 			#region Help Text
  373.  
  374. 			/*
  375. 			FileHistory.exe,  Version 1.00
  376. 			Save a timestamped copy of any monitored file when it is changed
  377.  
  378. 			Usage:    FileHistory   sourcedir  destination  [ filters ]  [ options ]
  379.  
  380. 			Where:    sourcedir     is the directory to monitor
  381. 			          destination   is the directory where backups will be kept
  382. 			          filters       optional file type filter(s), e.g. "*.txt"
  383. 			                        (default: "*.*" i.e. all files)
  384.  
  385. 			Options:  /H            exclude Hidden files
  386. 			          /O            exclude locked MS-Office Word, Excel, PowerPoint files
  387. 			          /R:pattern    exclude files matching regular expressions pattern
  388. 			          /X:pattern    exclude files matching DOS files pattern
  389.  
  390. 			Notes:    Unlike Microsoft's built-in File History, this program can monitor
  391. 			          ANY directory, not just the user libraries.
  392. 			          Specified source and destination directories must both exist.
  393. 			          You may use as many filters as you like, e.g. *.doc* *.xls* *.ppt*
  394. 			          to monitor MS-Office Word, Excel and PowerPoint files.
  395. 			          When using /O to exclude locked MS-Office files, a backup copy will
  396. 			          be saved as soon as the locked MS-Office file is closed, unless it
  397. 			          is identical to the last backup of that file.
  398. 			          Use /R to specify a regular expressions pattern for files to be
  399. 			          excluded, or /X to specify (multiple) DOS file pattern(s).
  400. 			          Multiple DOS file patterns can be combined by using a semicolon for
  401. 			          separator and embedding the combined pattern in doublequotes, e.g.
  402. 			          /X:"~$*.doc*;~$*.xls*" to exclude MS-Office Word and Excel lock files.
  403. 			          Command line switches /R and /X may be used simultaneously.
  404. 			          This program will keep running until the Enter key is pressed.
  405. 			          Return code is 0 when all goes well, or -1 on (command line) errors.
  406.  
  407. 			Written by Rob van der Woude
  408. 			https://www.robvanderwoude.com
  409. 			*/
  410.  
  411. 			#endregion Help Text
  412.  
  413.  
  414. 			#region Display Help Text
  415.  
  416. 			Console.Error.WriteLine( );
  417.  
  418. 			Console.Error.WriteLine( "FileHistory.exe,  Version {0}", progver );
  419.  
  420. 			Console.Error.WriteLine( "Save a timestamped copy of any monitored file when it is changed" );
  421.  
  422. 			Console.Error.WriteLine( );
  423.  
  424. 			Console.Error.Write( "Usage:    " );
  425. 			Console.ForegroundColor = ConsoleColor.White;
  426. 			Console.Error.WriteLine( "FileHistory   sourcedir  destination  [ filters ]  [ options ]" );
  427. 			Console.ResetColor( );
  428.  
  429. 			Console.Error.WriteLine( );
  430.  
  431. 			Console.Error.Write( "Where:    " );
  432. 			Console.ForegroundColor = ConsoleColor.White;
  433. 			Console.Error.Write( "sourcedir" );
  434. 			Console.ResetColor( );
  435. 			Console.Error.WriteLine( "     is the directory to monitor" );
  436.  
  437. 			Console.ForegroundColor = ConsoleColor.White;
  438. 			Console.Error.Write( "          destination" );
  439. 			Console.ResetColor( );
  440. 			Console.Error.WriteLine( "   is the directory where backups will be kept" );
  441.  
  442. 			Console.ForegroundColor = ConsoleColor.White;
  443. 			Console.Error.Write( "          filters" );
  444. 			Console.ResetColor( );
  445. 			Console.Error.WriteLine( "       optional file type filter(s), e.g. \"*.txt\"" );
  446.  
  447. 			Console.Error.WriteLine( "                        (default: \"*.*\" i.e. all files)" );
  448.  
  449. 			Console.Error.WriteLine( );
  450.  
  451. 			Console.Error.Write( "Options:  " );
  452. 			Console.ForegroundColor = ConsoleColor.White;
  453. 			Console.Error.Write( "/H" );
  454. 			Console.ResetColor( );
  455. 			Console.Error.Write( "            exclude " );
  456. 			Console.ForegroundColor = ConsoleColor.White;
  457. 			Console.Error.Write( "H" );
  458. 			Console.ResetColor( );
  459. 			Console.Error.WriteLine( "idden files" );
  460.  
  461. 			Console.ForegroundColor = ConsoleColor.White;
  462. 			Console.Error.Write( "          /O" );
  463. 			Console.ResetColor( );
  464. 			Console.Error.Write( "            exclude locked MS-" );
  465. 			Console.ForegroundColor = ConsoleColor.White;
  466. 			Console.Error.Write( "O" );
  467. 			Console.ResetColor( );
  468. 			Console.Error.WriteLine( "ffice Word, Excel, PowerPoint files" );
  469.  
  470. 			Console.ForegroundColor = ConsoleColor.White;
  471. 			Console.Error.Write( "          /R:pattern" );
  472. 			Console.ResetColor( );
  473. 			Console.Error.Write( "    exclude files matching regular expressions " );
  474. 			Console.ForegroundColor = ConsoleColor.White;
  475. 			Console.Error.WriteLine( "pattern" );
  476.  
  477. 			Console.Error.Write( "          /X:pattern" );
  478. 			Console.ResetColor( );
  479. 			Console.Error.Write( "    exclude files matching DOS files " );
  480. 			Console.ForegroundColor = ConsoleColor.White;
  481. 			Console.Error.WriteLine( "pattern" );
  482. 			Console.ResetColor( );
  483.  
  484. 			Console.Error.WriteLine( );
  485.  
  486. 			Console.Error.WriteLine( "Notes:    Unlike Microsoft's built-in File History, this program can monitor" );
  487.  
  488. 			Console.ForegroundColor = ConsoleColor.White;
  489. 			Console.Error.Write( "          ANY" );
  490. 			Console.ResetColor( );
  491. 			Console.Error.WriteLine( " directory, not just the user libraries." );
  492.  
  493. 			Console.Error.WriteLine( "          Specified source and destination directories must both exist." );
  494.  
  495. 			Console.Error.Write( "          You may use as many " );
  496. 			Console.ForegroundColor = ConsoleColor.White;
  497. 			Console.Error.Write( "filters" );
  498. 			Console.ResetColor( );
  499. 			Console.Error.Write( " as you like, e.g. " );
  500. 			Console.ForegroundColor = ConsoleColor.White;
  501. 			Console.Error.WriteLine( "*.doc* *.xls* *.ppt*" );
  502. 			Console.ResetColor( );
  503.  
  504. 			Console.Error.WriteLine( "          to monitor MS-Office Word, Excel and PowerPoint files." );
  505.  
  506. 			Console.Error.Write( "          When using " );
  507. 			Console.ForegroundColor = ConsoleColor.White;
  508. 			Console.Error.Write( "/O" );
  509. 			Console.ResetColor( );
  510. 			Console.Error.WriteLine( " to exclude locked MS-Office files, a backup copy will" );
  511.  
  512. 			Console.Error.WriteLine( "          be saved as soon as the locked MS-Office file is closed, unless it" );
  513.  
  514. 			Console.Error.WriteLine( "          is identical to the last backup of that file." );
  515.  
  516. 			Console.Error.Write( "          Use " );
  517. 			Console.ForegroundColor = ConsoleColor.White;
  518. 			Console.Error.Write( "/R" );
  519. 			Console.ResetColor( );
  520. 			Console.Error.WriteLine( " to specify a regular expressions pattern for files to be" );
  521.  
  522. 			Console.Error.Write( "          excluded, or " );
  523. 			Console.ForegroundColor = ConsoleColor.White;
  524. 			Console.Error.Write( "/X" );
  525. 			Console.ResetColor( );
  526. 			Console.Error.WriteLine( " to specify (multiple) DOS file pattern(s)." );
  527.  
  528. 			Console.Error.WriteLine( "          Multiple DOS file patterns can be combined by using a semicolon for" );
  529.  
  530. 			Console.Error.WriteLine( "          separator and embedding the combined pattern in doublequotes, e.g." );
  531.  
  532. 			Console.ForegroundColor = ConsoleColor.White;
  533. 			Console.Error.Write( "          /X:\"~$*.doc*;~$*.xls*\"" );
  534. 			Console.ResetColor( );
  535. 			Console.Error.WriteLine( " to exclude MS-Office Word and Excel lock files." );
  536.  
  537. 			Console.Error.Write( "          Command line switches " );
  538. 			Console.ForegroundColor = ConsoleColor.White;
  539. 			Console.Error.Write( "/R" );
  540. 			Console.ResetColor( );
  541. 			Console.Error.Write( " and " );
  542. 			Console.ForegroundColor = ConsoleColor.White;
  543. 			Console.Error.Write( "/X" );
  544. 			Console.ResetColor( );
  545. 			Console.Error.WriteLine( " may be used simultaneously." );
  546.  
  547. 			Console.Error.WriteLine( "          This program will keep running until the Enter key is pressed." );
  548.  
  549. 			Console.Error.WriteLine( "          Return code is 0 when all goes well, or -1 on (command line) errors." );
  550.  
  551. 			Console.Error.WriteLine( );
  552.  
  553. 			Console.Error.WriteLine( "Written by Rob van der Woude" );
  554.  
  555. 			Console.Error.WriteLine( "https://www.robvanderwoude.com" );
  556.  
  557. 			#endregion Display Help Text
  558.  
  559.  
  560. 			return -1;
  561. 		}
  562. 	}
  563. }

page last modified: 2024-04-16; loaded in 0.0171 seconds