context: /*
      Copyright 2008,2009,2010,2011 Chris Capon.
  
      This file is part of KeepBack.
  
      KeepBack is free software: you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
      the Free Software Foundation, either version 3 of the License, or
      (at your option) any later version.
  
      KeepBack is distributed in the hope that it will be useful,
      but WITHOUT ANY WARRANTY; without even the implied warranty of
      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      GNU General Public License for more details.
  
      You should have received a copy of the GNU General Public License
      along with KeepBack.  If not, see <http://www.gnu.org/licenses/>.
  
  */
  
  /*
  	When compiling under Mono - include reference to Mono.Posix
  */
  
  #if true //tracking a bug in history processing
  #define CHECK_HISTORY_BUG
  #endif
  
  using System;
  using System.Collections.Generic;
  using System.Globalization;
  using System.IO;
  using System.Reflection;
  using System.Text;
  
  namespace KeepBack
  {
  #if false
  	public class Archive : IDisposable
  	{
  		/* Backup proceedure
  		 * -----------------
  		 * - An Archive is a folder with a list of dated folders underneath.
  		 *   - each dated folder represents one backup
  		 *   - the most recent dated folder holds a complete backup of everything
  		 *   - all previous dated folders contain just the delta differences
  		 *     for files and folders which changed or were deleted between the
  		 *     times of the two backups.
  		 * - When a backup is processed:
  		 *   - "history" is set to the most recent dated folder (if one exists)
  		 *   - "current" is set to the name of a new dated folder based on the current time.
  		 *   - the "history" folder is renamed to "current".
  		 *   - a new "history" folder is created to contain old versions of any file which has changed.
  		 *   - this way, the "current" folder always contains the most recent copies of all files.
  		 */
  
  		//--- define ----------------------------
  
  		const string         SECTION_DETAIL  = @"           ";
  		const char           BLANK           = '-';
  		public const string  EXTENSION       = "keep";
  
  		public enum Action
  		{
  			Info,
  			Directory,
  			File,
  			Change,
  			Skip,
  		}
  		public enum Reason
  		{
  			Created   = 1,
  			Modified  = 2,
  			Deleted   = 3,
  			Skipped   = 4,
  		}
  		public enum MergeLevel
  		{
  			Year,
  			Month,
  			Day,
  			Hour,
  			Minute,
  		}
  		enum Function
  		{
  			TD, //TestDrive
  			LD, //ListDirectories
  			LF, //ListFiles
  			DC, //DirectoryCreate
  			DM, //DirectoryMove
  			DD, //DirectoryDelete
  			FC, //FileCopy
  			FM, //FileMove
  			FD, //FileDelete
  			FI, //FileInfo
  		}
  
  		public delegate void ActionDelegate( Action action, string text );
  
  		//--- field -----------------------------
  	
  		ActionDelegate    action;
  		Ctrl              ctrl;
  		StreamWriter      log           = null;
  
  		bool              debug         = false;
  		bool              cancel        = false;
  
  		bool              caseSensitive = false;             //indicates whether archive filesystem distinguishes between upper and lower case or not
  		DateTime          minDateUtc    = DateTime.MinValue; //minimum LastWriteUDC date the archive filesystem will accept
  		DateTime          maxDateUtc    = DateTime.MaxValue; //maximum LastWriteUDC date the archive filesystem will accept
  
  		DateTime          start         = DateTime.MinValue;
  		string            current       = null;
  		string            history       = null;
  
  		string            curr          = null;
  		string            hist          = null;
  		string            fold          = null;
  
  		MatchSet          inc           = null;
  		MatchSet          exc           = null;
  		MatchSet          his           = null;
  
  		readonly string[] ReasonNames   = Enum.GetNames ( typeof(Reason          ) );
  		readonly Array    ReasonValues  = Enum.GetValues( typeof(Reason          ) );
  		readonly string[] MatchNames    = Enum.GetNames ( typeof(MatchSet.SetType) );
  		readonly Array    MatchValues   = Enum.GetValues( typeof(MatchSet.SetType) );
  
  		//--- property --------------------------
  
  		public bool Cancel { get { return cancel; } set { cancel = value; } }
  
  		//--- constructor -----------------------
  
  		public Archive( ActionDelegate action, Ctrl ctrl, bool debug )
  		{
  			this.action   = action;
  			this.ctrl     = ctrl;
  			this.debug    = debug;
  
  			this.start    = DateTime.Now;
  
  			this.log = new StreamWriter( PathCombine( this.ctrl.Path, DateFolderSecond( start ) + ".log" ), false, Encoding.ASCII );
  			LogInfo( "---------------" );
  			LogInfo( Status( "Archive", this.ctrl.Path ) );
  		}
  
  		//--- method ----------------------------
  
  		bool TestArchiveDriveProperties( string path )
  		{
  			try
  			{
  				/* Check if filesystem is case sensitive or not
  				 * by creating a temporary file then attempting
  				 * to access it using a different case name.
  				 */
  				string p = PathCombine( path, "__TemporaryFile__.txt" );
  				if( File.Exists( p.ToLower() ) )
  				{
  					File.Delete( p.ToLower() );
  				}
  				using( StreamWriter sw = File.CreateText( p ) )
  				{
  					sw.WriteLine( p );
  				}
  				this.caseSensitive = ! File.Exists( p.ToLower() );
  
  				/* Find the supported date range for file LastWriteTimeUtc.
  				 * Use a binary search to find the min and max dates allowed.
  				 */
  				DateTime lo = DateTime.MinValue;
  				DateTime hi = DateTime.UtcNow;
  				while( (minDateUtc = hi.AddTicks( (lo.Ticks - hi.Ticks) / 2 )) < hi )
  				{
  					try
  					{
  						File.SetLastWriteTimeUtc( p, minDateUtc );
  						hi = minDateUtc;
  					}
  					catch( Exception )
  					{
  						lo = minDateUtc;
  					}
  				}
  				lo = DateTime.UtcNow;
  				hi = DateTime.MaxValue;
  				while( (maxDateUtc = lo.AddTicks( (hi.Ticks - lo.Ticks) / 2 )) > lo )
  				{
  					try
  					{
  						File.SetLastWriteTimeUtc( p, maxDateUtc );
  						lo = maxDateUtc;
  					}
  					catch( Exception )
  					{
  						hi = maxDateUtc;
  					}
  				}
  
  				File.Delete( p );
  				if( debug )
  				{
  					Log( "  filesystem " + (this.caseSensitive ? "distinguishes" : "does not distinguish") + " between upper and lower case." );
  					Log( "  filesystem date range is from " + DisplayDate( minDateUtc ) + " to " + DisplayDate( maxDateUtc ) + "." );
  				}
  				return true;
  			}
  			catch( Exception ex )
  			{
  				LogException( Function.TD, DisplayDirectory( path ), ex );
  			}
  			return false;
  		}
  
  		public void Backup( string archiveFilePath )
  		{
  			Log( "" );
  			LogInfo( "Backup..." );
  			if( ! Directory.Exists( ctrl.Path ) )
  			{
  				LogInfo( "The archive path [ " + ctrl.Path + " ] is not accessible." );
  				return;
  			}
  			current = DateFolderMinute( start );
  			history = null;
  			{
  				string[] a = ctrl.HistoryFolders();
  				if( a.Length > 0 )
  				{
  					history = a[a.Length - 1];
  					if( string.Compare( current, history, true ) <= 0 )
  					{
  						current = DateFolderSecond( start );
  						if( string.Compare( current, history, true ) <= 0 )
  						{
  							LogInfo( "A backup was started at " + current + " which is earlier than your previous backup at " + history + ".  Please try again later." );
  							return;
  						}
  					}
  				}
  			}
  			current = PathCombine( ctrl.Path, current );
  			if( history != null )
  			{
  				history = PathCombine( ctrl.Path, history );
  				LogInfo( Status( "  previous", history ) );
  				if( ! DirectoryMove( history, current ) )
  				{
  					LogInfo( "The latest backup folder can not be renamed.  Maybe another application is using it." );
  					return;
  				}
  			}
  			else
  			{
  				if( ! DirectoryCreateDirectory( current ) )
  				{
  					LogInfo( "A folder for this backup can not be created at [ " + current + " ]." );
  					return;
  				}
  			}
  			LogInfo( Status( "  current", current ) );
  
  			if( ! TestArchiveDriveProperties( ctrl.Path ) )
  			{
  				LogInfo( "Was not able to test archive filesystem properties." );
  				return;
  			}
  
  			Log( "" );
  			LogLegend();
  			Log( "" );
  			{
  				/* move copy of control file in to backup folder
  				 */
  				string fp = Path.GetDirectoryName( archiveFilePath );
  				string n  = Path.GetFileName     ( archiveFilePath );
  				BackupFile( fp, current, history, n, n, true );
  				foreach( string s in ListFiles( current ) )
  				{
  					if( string.Compare( s, n, true ) != 0 )
  					{
  						RemoveFile( current, history, s, s, true );
  					}
  				}
  			}
  
  			CtrlArchive archive = ctrl.Archive;
  			if( archive != null )
  			{
  				/* backup each folder in archive set
  				 */
  				List<string> a = new List<string>( ListDirectories( current ) );
  
  				if( archive.Folders != null )
  				{
  					foreach( CtrlFolder folder in archive.Folders )
  					{
  						if( cancel ) { break; }
  						Log( "" );
  						Log( "Folder" );
  						LogInfo( Status( folder.Name, folder.Path ) );
  						Backup( folder );
  						foreach( string s in ListDirectories( current, folder.Name ) )
  						{
  							a.Remove( s );
  						}
  					}
  				}
  				if( ! cancel && (a.Count > 0) && (history != null) )
  				{
  					/* move any existing folders not in archive set to history
  					 */
  					Log( "" );
  					Log( "Remove" );
  					DirectoryCreateDirectory( history );
  					foreach( string n in a )
  					{
  						LogReason( Reason.Deleted, n, DirectoryMove( PathCombine( current, n ), PathCombine( history, n ) ) );
  					}
  				}
  			}
  
  			action( Action.Directory, "" );
  			action( Action.File, "" );
  			TimeSpan x = DateTime.Now - start;
  			Log( "" );
  			LogInfo( Status( ".." + (cancel ? "cancelled" : "done     "), DisplayTime( x ) ) );
  		}
  
  		void Backup( CtrlFolder folder )
  		{
  			if( string.IsNullOrEmpty( folder.Name ) || string.IsNullOrEmpty( folder.Path ) )
  			{
  				LogInfo( "  incomplete folder definition" );
  				return;
  			}
  			this.curr  = PathCombine( current, folder.Name );
  			this.hist  = (history == null) ? null : PathCombine( history, folder.Name );
  			this.fold  = folder.Path;
  			MatchSet.MatchDelegate md = new MatchSet.MatchDelegate( LogMatch );
  			this.inc   = new MatchSet( MatchSet.SetType.Include, folder.Include, md );
  			this.exc   = new MatchSet( MatchSet.SetType.Exclude, folder.Exclude, md );
  			this.his   = new MatchSet( MatchSet.SetType.History, folder.History, md );
  			BackupCompareFolder( "", false, true );
  		}
  		bool BackupCompareFolder( string path, bool include, bool history )
  		{
  			/* The 'include' flag indicates an item has specifically been included
  			 * by a filter.
  			 * The 'history' flag indicates an item has specifically been marked for
  			 * a single backup only (no historical copies).
  			 * If a folder is specifically excluded with a filter, then no further
  			 * analysis is done on it.
  			 * If the folder has not specifically been included or excluded, then a
  			 * deeper analysis is done.  The folder and its descendants are scanned
  			 * for files and folders which do match an include filter.
  			 */
  			if( ! include && inc.MatchDirectory( path ) ) { include = true ; }
  			if(              exc.MatchDirectory( path ) ) { return    false; }
  			if(   history && his.MatchDirectory( path ) ) { history = false; }
  
  			if( cancel ) { return include; }
  			string cp = PathCombine( curr, path );
  			if( ! Directory.Exists( cp ) )
  			{
  				//..if directory doesn't exist in current, then the entire tree can simply be copied.
  				return CopyFolder( path, include );
  			}
  			bool         rc = include;
  			List<string> a  = new List<string>( ListDirectories( cp ) );
  			string       fp = PathCombine( fold, path );
  			string       hp = (hist == null) ? null : PathCombine( hist, path );
  			foreach( string n in ListDirectories( fp ) )
  			{
  				if( cancel ) { return rc; }
  				//..check each sub-directory in folder.
  #if __MonoCS__
  				{
  					string fpn = PathCombine( fp, n );
  					Mono.Unix.UnixSymbolicLinkInfo i = new Mono.Unix.UnixSymbolicLinkInfo( fpn );
  					if( ! i.Exists || ! i.IsDirectory )
  					{
  						LogReason( Reason.Skipped, fpn, false );
  						continue;
  					}
  				}
  #endif
  				if( BackupCompareFolder( PathCombine( path, n ), include, history ) )
  				{
  					foreach( string s in ListDirectories( cp, n ) )
  					{
  						a.Remove( s );
  					}
  					rc = true;
  				}
  			}
  			action( Action.Directory, path );
  //			action( Action.File, "" );
  			if( a.Count > 0 )
  			{
  				//..move deleted, renamed or excluded directories from current to history.
  				if( ! history || ((hp != null) && DirectoryCreateDirectory( hp )) )
  				{
  					foreach( string n in a )
  					{
  						if( cancel ) { return rc; }
  						action( Action.File, DisplayDirectory( n ) );
  						string pn = PathCombine( path, n );
  						if( ! history || his.MatchDirectory( pn ) )
  						{
  							LogReason( Reason.Deleted, DisplayDirectory( pn ), DirectoryDelete( PathCombine( cp, n ), true ) );
  						}
  						else
  						{
  							LogReason( Reason.Deleted, DisplayDirectory( pn ), DirectoryMove( PathCombine( cp, n ), PathCombine( hp, n ) ) );
  						}
  					}
  				}
  			}
  
  			a = new List<string>( ListFiles( cp ) );
  			foreach( string n in ListFiles( fp ) )
  			{
  				if( cancel ) { return rc; }
  				string  pn  = PathCombine( path, n );
  				if( (include || inc.MatchFile( pn )) && ! exc.MatchFile( pn ) )
  				{
  					foreach( string s in ListFiles( cp, n ) )
  					{
  						a.Remove( s );
  					}
  					BackupFile( fp, cp, hp, pn, n, history && ! his.MatchFile( pn ) );
  					rc = true;
  				}
  			}
  			if( a.Count > 0 )
  			{
  				//..move deleted, renamed or excluded files from current to history.
  				if( ! history || ((hp != null) && DirectoryCreateDirectory( hp )) )
  				{
  					foreach( string n in a )
  					{
  						if( cancel ) { return rc; }
  						string pn = PathCombine( path, n );
  						RemoveFile( cp, hp, pn, n, history && ! his.MatchFile( pn ) );
  					}
  				}
  			}
  			return rc;
  		}
  
  		/// <summary>
  		///		Copy a folder and all sub-folders as quickly as possible without
  		///		checking their contents first.
  		///		Before calling this function, make sure the destination folder
  		///		does not already exist.
  		/// </summary>
  		bool CopyFolder( string path, bool include )
  		{
  			if( cancel ) { return include; }
  			if( ! include && inc.MatchDirectory( path ) ) { include = true; }
  			if(              exc.MatchDirectory( path ) ) { return false; }
  			bool   rc = include;
  			string cp = PathCombine( curr, path );
  			if( ! include || DirectoryCreateDirectory( cp ) )
  			{
  				if( include )
  				{
  					LogReason( Reason.Created, DisplayDirectory( path ), true );
  					rc = true;
  				}
  				string fp = PathCombine( fold, path );
  				foreach( string n in ListDirectories( fp ) )
  				{
  					if( cancel ) { return rc; }
  #if __MonoCS__
  					{
  						string fpn = PathCombine( fp, n );
  						Mono.Unix.UnixSymbolicLinkInfo i = new Mono.Unix.UnixSymbolicLinkInfo( fpn );
  						if( ! i.Exists || ! i.IsDirectory )
  						{
  							LogReason( Reason.Skipped, fpn, false );
  							continue;
  						}
  					}
  #endif
  					rc |= CopyFolder( PathCombine( path, n ), include );
  				}
  				action( Action.Directory, path );
  //				action( Action.File, "" );
  				foreach( string n in ListFiles( fp ) )
  				{
  					if( cancel ) { return rc; }
  #if __MonoCS__
  					{
  						string fpn = PathCombine( fp, n );
  						Mono.Unix.UnixSymbolicLinkInfo i = new Mono.Unix.UnixSymbolicLinkInfo( fpn );
  						if( ! i.Exists || ! i.IsRegularFile )
  						{
  							LogReason( Reason.Skipped, fpn, false );
  							continue;
  						}
  					}
  #endif
  					string pn = PathCombine( path, n );
  					if( (include || inc.MatchFile( pn )) && ! exc.MatchFile( pn ) )
  					{
  						if( include || DirectoryCreateDirectory( cp ) )
  						{
  							action( Action.File, n );
  							LogReason( Reason.Created, pn, FileCopy( PathCombine( fp, n ), PathCombine( cp, n ), false ) );
  							rc = true;
  						}
  					}
  				}
  			}
  			return rc;
  		}
  
  		void BackupFile( string fp, string cp, string hp, string pn, string n, bool history )
  		{
  			string   fpn  = PathCombine( fp, n );
  			string   cpn  = PathCombine( cp, n );
  
  			FileInfo ffi  = FileInfo( fpn );
  			FileInfo cfi  = FileInfo( cpn );
  			if( (ffi == null) || (cfi == null) )
  			{
  				return;
  			}
  #if __MonoCS__
  			{
  				Mono.Unix.UnixSymbolicLinkInfo i = new Mono.Unix.UnixSymbolicLinkInfo( fpn );
  				if( ! i.Exists || ! i.IsRegularFile )
  				{
  					LogReason( Reason.Skipped, fpn, false );
  					return;
  				}
  			}
  #endif
  			if( ! cfi.Exists )
  			{
  				//..file does not exist in current, simply copy
  				action( Action.File, n );
  				LogReason( Reason.Created, pn, FileCopy( fpn, cpn, false ) );
  				return;
  			}
  			/* ..check for file property changes
  			 * The date of the file is adjusted to fit within the min and max dates the archive filesystem can support.
  			 * The dates are checked to be within 1 second of each other to avoid slight inconsistencies at the millisecond level.
  			 * The file length must match.
  			 */
  			if( 
  				   DateCompare( DateAdjust( ffi.LastWriteTimeUtc ), DateAdjust( cfi.LastWriteTimeUtc ) )
  				&& (ffi.Length == cfi.Length )
  				)
  			{
  				//..file has not changed, ignore
  				return;
  			}
  
gt: 			action( Action.File, n );