forked from cgvr/DeltaVR
		
	
		
			
				
	
	
		
			243 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
/************************************************************************************
 | 
						|
 | 
						|
Copyright   :   Copyright (c) Facebook Technologies, LLC and its affiliates. All rights reserved.
 | 
						|
 | 
						|
Licensed under the Oculus SDK License Version 3.4.1 (the "License");
 | 
						|
you may not use the Oculus SDK except in compliance with the License,
 | 
						|
which is provided at the time of installation or download, or which
 | 
						|
otherwise accompanies this software in either electronic or hard copy form.
 | 
						|
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
https://developer.oculus.com/licenses/sdk-3.4.1
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, the Oculus SDK
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
 | 
						|
************************************************************************************/
 | 
						|
using System.Collections.Generic;
 | 
						|
using System.Diagnostics.CodeAnalysis;
 | 
						|
using System.IO;
 | 
						|
using System.Linq;
 | 
						|
using System.Text.RegularExpressions;
 | 
						|
using System;
 | 
						|
 | 
						|
public class DirectorySyncer
 | 
						|
{
 | 
						|
	public delegate void SyncResultDelegate(SyncResult syncResult);
 | 
						|
 | 
						|
	public readonly string Source;
 | 
						|
	public readonly string Target;
 | 
						|
	public SyncResultDelegate WillPerformOperations;
 | 
						|
	private readonly Regex _ignoreExpression;
 | 
						|
 | 
						|
	// helper classes to simplify transition beyond .NET runtime 3.5
 | 
						|
	public abstract class CancellationToken
 | 
						|
	{
 | 
						|
		protected abstract bool _IsCancellationRequested();
 | 
						|
 | 
						|
		public virtual bool IsCancellationRequested
 | 
						|
		{
 | 
						|
			get { return _IsCancellationRequested(); }
 | 
						|
		}
 | 
						|
 | 
						|
		public void ThrowIfCancellationRequested()
 | 
						|
		{
 | 
						|
			if (IsCancellationRequested)
 | 
						|
			{
 | 
						|
				throw new Exception("Operation Cancelled");
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		public static readonly CancellationToken None = new CancellationTokenNone();
 | 
						|
 | 
						|
		private class CancellationTokenNone : CancellationToken
 | 
						|
		{
 | 
						|
			protected override bool _IsCancellationRequested()
 | 
						|
			{
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	public class CancellationTokenSource : CancellationToken
 | 
						|
	{
 | 
						|
		private bool _isCancelled;
 | 
						|
 | 
						|
		protected override bool _IsCancellationRequested()
 | 
						|
		{
 | 
						|
			return _isCancelled;
 | 
						|
		}
 | 
						|
 | 
						|
		public void Cancel()
 | 
						|
		{
 | 
						|
			_isCancelled = true;
 | 
						|
		}
 | 
						|
 | 
						|
		public CancellationToken Token
 | 
						|
		{
 | 
						|
			get { return this; }
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	private static string EnsureTrailingDirectorySeparator(string path)
 | 
						|
	{
 | 
						|
		return path.EndsWith("" + Path.DirectorySeparatorChar)
 | 
						|
			? path
 | 
						|
			: path + Path.DirectorySeparatorChar;
 | 
						|
	}
 | 
						|
 | 
						|
	private static string CheckedDirectory(string nameInExceptionText, string directory)
 | 
						|
	{
 | 
						|
		directory = Path.GetFullPath(directory);
 | 
						|
		if (!Directory.Exists(directory))
 | 
						|
		{
 | 
						|
			throw new ArgumentException(string.Format("{0} is not a valid directory for argument ${1}", directory,
 | 
						|
				nameInExceptionText));
 | 
						|
		}
 | 
						|
 | 
						|
		return EnsureTrailingDirectorySeparator(directory);
 | 
						|
	}
 | 
						|
 | 
						|
	public DirectorySyncer(string source, string target, string ignoreRegExPattern = null)
 | 
						|
	{
 | 
						|
		Source = CheckedDirectory("source", source);
 | 
						|
		Target = CheckedDirectory("target", target);
 | 
						|
		if (Source.StartsWith(Target, StringComparison.OrdinalIgnoreCase) ||
 | 
						|
			Target.StartsWith(Source, StringComparison.OrdinalIgnoreCase))
 | 
						|
		{
 | 
						|
			throw new ArgumentException(string.Format("Paths must not contain each other (source: {0}, target: {1}",
 | 
						|
				Source, Target));
 | 
						|
		}
 | 
						|
 | 
						|
		ignoreRegExPattern = ignoreRegExPattern ?? "^$";
 | 
						|
		_ignoreExpression = new Regex(ignoreRegExPattern, RegexOptions.IgnoreCase);
 | 
						|
	}
 | 
						|
 | 
						|
	public class SyncResult
 | 
						|
	{
 | 
						|
		public readonly IEnumerable<string> Created;
 | 
						|
		public readonly IEnumerable<string> Updated;
 | 
						|
		public readonly IEnumerable<string> Deleted;
 | 
						|
 | 
						|
		public SyncResult(IEnumerable<string> created, IEnumerable<string> updated, IEnumerable<string> deleted)
 | 
						|
		{
 | 
						|
			Created = created;
 | 
						|
			Updated = updated;
 | 
						|
			Deleted = deleted;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	public bool RelativeFilePathIsRelevant(string relativeFilename)
 | 
						|
	{
 | 
						|
		return !_ignoreExpression.IsMatch(relativeFilename);
 | 
						|
	}
 | 
						|
 | 
						|
	public bool RelativeDirectoryPathIsRelevant(string relativeDirName)
 | 
						|
	{
 | 
						|
		// Since our ignore patterns look at file names, they may contain trailing path separators
 | 
						|
		// In order for paths to match those rules, we add a path separator here
 | 
						|
		return !_ignoreExpression.IsMatch(EnsureTrailingDirectorySeparator(relativeDirName));
 | 
						|
	}
 | 
						|
 | 
						|
	private HashSet<string> RelevantRelativeFilesBeneathDirectory(string path, CancellationToken cancellationToken)
 | 
						|
	{
 | 
						|
		return new HashSet<string>(Directory.GetFiles(path, "*", SearchOption.AllDirectories)
 | 
						|
			.TakeWhile((s) => !cancellationToken.IsCancellationRequested)
 | 
						|
			.Select(p => PathHelper.MakeRelativePath(path, p)).Where(RelativeFilePathIsRelevant));
 | 
						|
	}
 | 
						|
 | 
						|
	private HashSet<string> RelevantRelativeDirectoriesBeneathDirectory(string path,
 | 
						|
		CancellationToken cancellationToken)
 | 
						|
	{
 | 
						|
		return new HashSet<string>(Directory.GetDirectories(path, "*", SearchOption.AllDirectories)
 | 
						|
			.TakeWhile((s) => !cancellationToken.IsCancellationRequested)
 | 
						|
			.Select(p => PathHelper.MakeRelativePath(path, p)).Where(RelativeDirectoryPathIsRelevant));
 | 
						|
	}
 | 
						|
 | 
						|
	public SyncResult Synchronize()
 | 
						|
	{
 | 
						|
		return Synchronize(CancellationToken.None);
 | 
						|
	}
 | 
						|
 | 
						|
	private void DeleteOutdatedFilesFromTarget(SyncResult syncResult, CancellationToken cancellationToken)
 | 
						|
	{
 | 
						|
		var outdatedFiles = syncResult.Updated.Union(syncResult.Deleted);
 | 
						|
		foreach (var fileName in outdatedFiles)
 | 
						|
		{
 | 
						|
			File.Delete(Path.Combine(Target, fileName));
 | 
						|
			cancellationToken.ThrowIfCancellationRequested();
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	[SuppressMessage("ReSharper", "ParameterTypeCanBeEnumerable.Local")]
 | 
						|
	private void DeleteOutdatedEmptyDirectoriesFromTarget(HashSet<string> sourceDirs, HashSet<string> targetDirs,
 | 
						|
		CancellationToken cancellationToken)
 | 
						|
	{
 | 
						|
		var deleted = targetDirs.Except(sourceDirs).OrderByDescending(s => s);
 | 
						|
 | 
						|
		// By sorting in descending order above, we delete leaf-first,
 | 
						|
		// this is simpler than collapsing the list above (which would also allow us to run these ops in parallel).
 | 
						|
		// Assumption is that there are few empty folders to delete
 | 
						|
		foreach (var dir in deleted)
 | 
						|
		{
 | 
						|
			Directory.Delete(Path.Combine(Target, dir));
 | 
						|
			cancellationToken.ThrowIfCancellationRequested();
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	[SuppressMessage("ReSharper", "ParameterTypeCanBeEnumerable.Local")]
 | 
						|
	private void CreateRelevantDirectoriesAtTarget(HashSet<string> sourceDirs, HashSet<string> targetDirs,
 | 
						|
		CancellationToken cancellationToken)
 | 
						|
	{
 | 
						|
		var created = sourceDirs.Except(targetDirs);
 | 
						|
		foreach (var dir in created)
 | 
						|
		{
 | 
						|
			Directory.CreateDirectory(Path.Combine(Target, dir));
 | 
						|
			cancellationToken.ThrowIfCancellationRequested();
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	private void MoveRelevantFilesToTarget(SyncResult syncResult, CancellationToken cancellationToken)
 | 
						|
	{
 | 
						|
		// step 3: we move all new files to target
 | 
						|
		var newFiles = syncResult.Created.Union(syncResult.Updated);
 | 
						|
		foreach (var fileName in newFiles)
 | 
						|
		{
 | 
						|
			var sourceFileName = Path.Combine(Source, fileName);
 | 
						|
			var destFileName = Path.Combine(Target, fileName);
 | 
						|
			// target directory exists due to step CreateRelevantDirectoriesAtTarget()
 | 
						|
			File.Move(sourceFileName, destFileName);
 | 
						|
			cancellationToken.ThrowIfCancellationRequested();
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	public SyncResult Synchronize(CancellationToken cancellationToken)
 | 
						|
	{
 | 
						|
		var sourceDirs = RelevantRelativeDirectoriesBeneathDirectory(Source, cancellationToken);
 | 
						|
		var targetDirs = RelevantRelativeDirectoriesBeneathDirectory(Target, cancellationToken);
 | 
						|
		var sourceFiles = RelevantRelativeFilesBeneathDirectory(Source, cancellationToken);
 | 
						|
		var targetFiles = RelevantRelativeFilesBeneathDirectory(Target, cancellationToken);
 | 
						|
 | 
						|
		var created = sourceFiles.Except(targetFiles).OrderBy(s => s).ToList();
 | 
						|
		var updated = sourceFiles.Intersect(targetFiles).OrderBy(s => s).ToList();
 | 
						|
		var deleted = targetFiles.Except(sourceFiles).OrderBy(s => s).ToList();
 | 
						|
		var syncResult = new SyncResult(created, updated, deleted);
 | 
						|
 | 
						|
		if (WillPerformOperations != null)
 | 
						|
		{
 | 
						|
			WillPerformOperations.Invoke(syncResult);
 | 
						|
		}
 | 
						|
 | 
						|
		DeleteOutdatedFilesFromTarget(syncResult, cancellationToken);
 | 
						|
		DeleteOutdatedEmptyDirectoriesFromTarget(sourceDirs, targetDirs, cancellationToken);
 | 
						|
		CreateRelevantDirectoriesAtTarget(sourceDirs, targetDirs, cancellationToken);
 | 
						|
		MoveRelevantFilesToTarget(syncResult, cancellationToken);
 | 
						|
 | 
						|
		return syncResult;
 | 
						|
	}
 | 
						|
}
 |