|
/************************************************************************************
|
|
|
|
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;
|
|
}
|
|
}
|