From d61ff810a47d9743792ed261b981e75a57cce904 Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Sat, 23 Jan 2016 15:14:28 -0800 Subject: [PATCH] Add command line arguments and make behavior configurable. You can now turn any step on or off (whether to copy new files, whether to update existing files, whether to delete extra remaining files). You can choose two new steps: delete changed files or delete identical files. Who knows where it can be useful. --- src/ContentSync.Tests/Tests.cs | 4 +- src/ContentSync/Arguments.cs | 203 +++++++++++++++++++++++++++++ src/ContentSync/ContentSync.csproj | 2 + src/ContentSync/FileSystem.cs | 67 ++++++++++ src/ContentSync/Folders.cs | 14 +- src/ContentSync/Log.cs | 33 ++++- src/ContentSync/Program.cs | 85 +++++++++--- src/ContentSync/Sync.cs | 103 +++++++++------ 8 files changed, 439 insertions(+), 72 deletions(-) create mode 100644 src/ContentSync/Arguments.cs create mode 100644 src/ContentSync/FileSystem.cs diff --git a/src/ContentSync.Tests/Tests.cs b/src/ContentSync.Tests/Tests.cs index 5c361af..3dfc67a 100644 --- a/src/ContentSync.Tests/Tests.cs +++ b/src/ContentSync.Tests/Tests.cs @@ -72,7 +72,9 @@ private void T(Folder source, Folder destination) source.CreateOnDisk(root); destination.CreateOnDisk(root); - Sync.Directories(Path.Combine(root, source.Name), Path.Combine(root, destination.Name)); + var sourcePath = Path.Combine(root, source.Name); + var destinationPath = Path.Combine(root, destination.Name); + Sync.Directories(sourcePath, destinationPath, new Arguments(sourcePath, destinationPath)); var actual = Folder.FromDisk(Path.Combine(root, destination.Name)); diff --git a/src/ContentSync/Arguments.cs b/src/ContentSync/Arguments.cs new file mode 100644 index 0000000..cb3a6f7 --- /dev/null +++ b/src/ContentSync/Arguments.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GuiLabs.FileUtilities +{ + public class Arguments + { + private string[] args; + + public string Source { get; set; } + public string Destination { get; set; } + public string Pattern { get; set; } = "*"; + + public bool CopyLeftOnlyFiles { get; private set; } = true; + public bool UpdateChangedFiles { get; private set; } = true; + public bool DeleteRightOnlyFiles { get; private set; } = true; + public bool CopyEmptyDirectories { get; private set; } = true; + public bool DeleteRightOnlyDirectories { get; private set; } = true; + public bool DeleteSameFiles { get; private set; } + public bool DeleteChangedFiles { get; private set; } + public bool WhatIf { get; set; } + public bool Quiet { get; set; } + public bool Help { get; set; } + + public string Error { get; set; } = ""; + + public Arguments() + { + args = new string[0]; + } + + public Arguments(params string[] args) + { + this.args = args; + Parse(); + } + + private void Parse() + { + var switches = new List(); + var paths = new List(); + + foreach (var arg in args) + { + if (arg.StartsWith("/") || arg.StartsWith("-")) + { + switches.Add(arg.Substring(1).ToLowerInvariant()); + } + else + { + paths.Add(TrimQuotes(arg)); + } + } + + if (switches.Any()) + { + if (switches.Where(s => s != "q" && s != "whatif").Any()) + { + // if any of c, u, d, ds or dc are specified, they're in explicit mode, assume all defaults false + CopyLeftOnlyFiles = false; + UpdateChangedFiles = false; + DeleteRightOnlyFiles = false; + CopyEmptyDirectories = false; + DeleteRightOnlyDirectories = false; + } + + foreach (var key in switches) + { + switch (key) + { + case "c": + CopyLeftOnlyFiles = true; + break; + case "u": + UpdateChangedFiles = true; + break; + case "cu": + CopyLeftOnlyFiles = true; + UpdateChangedFiles = true; + break; + case "d": + DeleteRightOnlyFiles = true; + break; + case "cd": + CopyLeftOnlyFiles = true; + DeleteRightOnlyFiles = true; + break; + case "cud": + CopyLeftOnlyFiles = true; + UpdateChangedFiles = true; + DeleteRightOnlyFiles = true; + break; + case "ds": + DeleteSameFiles = true; + break; + case "dc": + DeleteChangedFiles = true; + break; + case "q": + Quiet = true; + Log.Quiet = true; + break; + case "h": + case "help": + case "?": + Help = true; + Error = "Help argument cannot be combined with any other arguments"; + return; + case "whatif": + WhatIf = true; + if (switches.Count == 1) + { + // if whatif is the only switch, assume the defaults + CopyLeftOnlyFiles = true; + UpdateChangedFiles = true; + DeleteRightOnlyFiles = true; + CopyEmptyDirectories = true; + DeleteRightOnlyDirectories = true; + } + + break; + default: + Error += "Unrecognized argument: " + key + Environment.NewLine; + return; + } + } + } + + if (Quiet && WhatIf) + { + Error = "-q and -whatif are incompatible. Choose one or the other."; + return; + } + + if (DeleteChangedFiles && UpdateChangedFiles) + { + Error = "Incompatible options: -u and -dc (can't update and delete changed files at the same time"; + return; + } + + if (paths.Count < 2) + { + Error = "Two paths need to be specified (source and destination)"; + return; + } + + if (paths.Count > 3) + { + Error = "Unable to process extra argument: " + paths[3]; + return; + } + + if (paths.Count == 3) + { + var patterns = paths.Where(p => p.Contains("*") || p.Contains("?")).ToArray(); + if (patterns.Length != 1) + { + Error = $"Expected exactly one file pattern, {patterns.Length} was specified"; + return; + } + + if (paths[2] != patterns[0]) + { + Error = "File pattern should be specified after Source and Destination arguments"; + return; + } + + Pattern = paths[2]; + } + + if (Pattern != "*") + { + // when we're not comparing all files, don't synchronize directories + CopyEmptyDirectories = false; + DeleteRightOnlyDirectories = false; + } + + if (!CopyLeftOnlyFiles) + { + CopyEmptyDirectories = false; + } + + if (!DeleteRightOnlyFiles) + { + DeleteRightOnlyDirectories = false; + } + + Source = paths[0]; + Destination = paths[1]; + } + + private static string TrimQuotes(string path) + { + if (path.Length > 2 && path[0] == '"' && path[path.Length - 1] == '"') + { + path = path.Substring(1, path.Length - 2); + } + + return path; + } + } +} diff --git a/src/ContentSync/ContentSync.csproj b/src/ContentSync/ContentSync.csproj index dc28e42..fced344 100644 --- a/src/ContentSync/ContentSync.csproj +++ b/src/ContentSync/ContentSync.csproj @@ -37,6 +37,8 @@ + + diff --git a/src/ContentSync/FileSystem.cs b/src/ContentSync/FileSystem.cs new file mode 100644 index 0000000..1305ff7 --- /dev/null +++ b/src/ContentSync/FileSystem.cs @@ -0,0 +1,67 @@ +using System.IO; + +namespace GuiLabs.FileUtilities +{ + public class FileSystem + { + public static void CopyFile(string source, string destination, bool speculative) + { + if (speculative) + { + Log.WriteLine($"Would copy {source} to {destination}"); + } + else + { + Log.WriteLine($"Copy {source} to {destination}"); + var destinationFolder = Path.GetDirectoryName(destination); + Directory.CreateDirectory(destinationFolder); + File.Copy(source, destination, overwrite: true); + } + } + + public static void DeleteFile(string deletedFilePath, bool speculative) + { + if (speculative) + { + Log.WriteLine("Would delete " + deletedFilePath); + } + else + { + Log.WriteLine("Delete " + deletedFilePath); + var attributes = File.GetAttributes(deletedFilePath); + if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + File.SetAttributes(deletedFilePath, attributes & ~FileAttributes.ReadOnly); + } + + File.Delete(deletedFilePath); + } + } + + public static void CreateDirectory(string newFolder, bool speculative) + { + if (speculative) + { + Log.WriteLine("Would create " + newFolder); + } + else + { + Log.WriteLine("Create " + newFolder); + Directory.CreateDirectory(newFolder); + } + } + + public static void DeleteDirectory(string deletedFolderPath, bool speculative) + { + if (speculative) + { + Log.WriteLine("Would delete " + deletedFolderPath); + } + else + { + Log.WriteLine("Delete " + deletedFolderPath); + Directory.Delete(deletedFolderPath, recursive: true); + } + } + } +} diff --git a/src/ContentSync/Folders.cs b/src/ContentSync/Folders.cs index 3944f8a..cee9631 100644 --- a/src/ContentSync/Folders.cs +++ b/src/ContentSync/Folders.cs @@ -12,11 +12,11 @@ public class Folders /// /// Assumes both leftRoot and rightRoot are existing folders. /// - public static FolderDiffResults DiffFolders(string leftRoot, string rightRoot) + public static FolderDiffResults DiffFolders(string leftRoot, string rightRoot, string pattern) { - var leftRelativePaths = GetRelativePathsOfAllFiles(leftRoot); + var leftRelativePaths = GetRelativePathsOfAllFiles(leftRoot, pattern); var leftOnlyFolders = GetRelativePathsOfAllFolders(leftRoot); - var rightRelativePaths = GetRelativePathsOfAllFiles(rightRoot); + var rightRelativePaths = GetRelativePathsOfAllFiles(rightRoot, pattern); var rightOnlyFolders = GetRelativePathsOfAllFolders(rightRoot); var leftOnlyFiles = new List(); @@ -93,18 +93,18 @@ public static FolderDiffResults DiffFolders(string leftRoot, string rightRoot) } } - public static HashSet GetRelativePathsOfAllFiles(string rootFolder) + public static HashSet GetRelativePathsOfAllFiles(string rootFolder, string pattern) { - using (Log.MeasureTime("Scanning files in " + rootFolder)) + using (Log.MeasureTime("Scanning files")) { - var files = Directory.GetFiles(rootFolder, "*", SearchOption.AllDirectories); + var files = Directory.GetFiles(rootFolder, pattern, SearchOption.AllDirectories); return GetRelativePaths(rootFolder, files); } } public static HashSet GetRelativePathsOfAllFolders(string rootFolder) { - using (Log.MeasureTime("Scanning folders in " + rootFolder)) + using (Log.MeasureTime("Scanning folders")) { var folders = Directory.GetDirectories(rootFolder, "*", SearchOption.AllDirectories); return GetRelativePaths(rootFolder, folders); diff --git a/src/ContentSync/Log.cs b/src/ContentSync/Log.cs index 10e5ec6..b48df2f 100644 --- a/src/ContentSync/Log.cs +++ b/src/ContentSync/Log.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text; namespace GuiLabs.FileUtilities @@ -7,10 +9,17 @@ namespace GuiLabs.FileUtilities public class Log { private static readonly object consoleLock = new object(); - private static readonly StringBuilder finalReport = new StringBuilder(); + private static readonly List> finalReportEntries = new List>(); + + public static bool Quiet { get; internal set; } public static void Write(string message, ConsoleColor color = ConsoleColor.Gray) { + if (Quiet) + { + return; + } + lock (consoleLock) { var oldColor = Console.ForegroundColor; @@ -25,6 +34,11 @@ public static void Write(string message, ConsoleColor color = ConsoleColor.Gray) public static void WriteLine(string message, ConsoleColor color = ConsoleColor.Gray) { + if (Quiet) + { + return; + } + lock (consoleLock) { var oldColor = Console.ForegroundColor; @@ -37,14 +51,23 @@ public static void WriteLine(string message, ConsoleColor color = ConsoleColor.G } } - public static void AddFinalReportEntry(string message) + public static void AddFinalReportEntry(string title, string elapsedTime) { - finalReport.AppendLine(message); + finalReportEntries.Add(Tuple.Create(title, elapsedTime)); } public static void PrintFinalReport() { - Write(finalReport.ToString(), ConsoleColor.DarkGray); + var leftColumnWidth = finalReportEntries.Max(e => e.Item1.Length) + 2; + + var sb = new StringBuilder(); + foreach (var entry in finalReportEntries) + { + var message = (entry.Item1 + ":").PadRight(leftColumnWidth) + entry.Item2; + sb.AppendLine(message); + } + + Write(sb.ToString(), ConsoleColor.DarkGray); } public static IDisposable MeasureTime(string operationTitle) @@ -67,7 +90,7 @@ public void Dispose() var elapsedTime = stopwatch.Elapsed.ToString(@"hh\:mm\:ss\.fff"); if (elapsedTime != "00:00:00.000") { - AddFinalReportEntry(title + ": " + elapsedTime); + AddFinalReportEntry(title, elapsedTime); } } } diff --git a/src/ContentSync/Program.cs b/src/ContentSync/Program.cs index 7ac042d..395bcad 100644 --- a/src/ContentSync/Program.cs +++ b/src/ContentSync/Program.cs @@ -5,21 +5,28 @@ namespace GuiLabs.FileUtilities { internal class ContentSync { - private static void Main(string[] args) + private static int Main(string[] args) { - string source = null; - string destination = null; - if (args.Length == 2) + var arguments = new Arguments(args); + if (arguments.Help || args.Length == 0) { - source = args[0]; - destination = args[1]; + PrintUsage(); + return 0; } - else + + if (!string.IsNullOrEmpty(arguments.Error)) { + var oldColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine("Invalid arguments:" + Environment.NewLine + arguments.Error + Environment.NewLine); + Console.ForegroundColor = oldColor; PrintUsage(); - return; + return 1; } + string source = arguments.Source; + string destination = arguments.Destination; + if (Directory.Exists(source)) { source = Path.GetFullPath(source); @@ -30,36 +37,74 @@ private static void Main(string[] args) using (Log.MeasureTime("Total time")) { - Sync.Directories(source, destination); + Sync.Directories(source, destination, arguments); } Log.PrintFinalReport(); - return; + return 0; } if (File.Exists(source) && Directory.Exists(destination)) { - Sync.Files(source, Path.Combine(destination, Path.GetFileName(source))); - return; + destination = Path.Combine(destination, Path.GetFileName(source)); + Sync.Files(source, destination, arguments); + return 0; } if (File.Exists(source)) { - Sync.Files(source, destination); - return; + Sync.Files(source, destination, arguments); + return 0; } - Console.Error.WriteLine($"Cannot sync {source} to {destination}"); + Console.Error.WriteLine($"Cannot find file or directory: {source}"); + return 2; } private static void PrintUsage() { - Console.WriteLine(@"Usage: ContentSync.exe - Copy/mirror/sync the destination folder to look exactly like source folder. - Copies missing files, deletes files from destination that are not in source, - overwrites files in destination that have different contents than in source. - Doesn't take into account file and date timestamps, works off content only."); + Console.WriteLine(@"Usage: ContentSync.exe [] [-c] [-u] [-d] + [-dc] [-ds] + [-whatif] [-q] + [-h] + + Copy/mirror/sync the destination folder to look exactly like source folder. + Copies missing files, deletes files from destination that are not in source, + overwrites files in destination that have different contents than in source. + Doesn't take into account file and date timestamps, works off content only. + + -c Copy files from source that don't exist in destination (left-only). + + -u Update files that have changed between source and destination. This + only overwrites the destination file if the contents are different. + + -d Delete right-only files (that are in destination but not in + source). + + -ds Delete same files (that exist in source and destination and are + same). + + -dc Delete changed files from destination (can't be used with -u). + + -whatif Print what would have been done (without changing anything). + + -q Quiet mode. Do not output anything to the console. + + -h Display this help and exit. + + Default is: -c -u -d (and if the pattern is not specified, it also syncs + empty directories (creates empty directories that are in the source and + deletes empty directories that are in the destination). + + Explicit mode: If any of -c -u -d -ds or -dc are specified explicitly, + all the defaults for other arguments are reset to false. For instance + if you specify -u then -c and -d default to false (and you have to + specify them explicitly). But if no arguments are specified, -c -u -d + default to true. + + Project page: https://github.com/KirillOsenkov/ContentSync +"); } } } \ No newline at end of file diff --git a/src/ContentSync/Sync.cs b/src/ContentSync/Sync.cs index 9f44814..ec9b116 100644 --- a/src/ContentSync/Sync.cs +++ b/src/ContentSync/Sync.cs @@ -9,82 +9,107 @@ public class Sync /// /// Assumes source directory exists. destination may or may not exist. /// - public static void Directories(string source, string destination) + public static void Directories(string source, string destination, Arguments arguments) { if (!Directory.Exists(destination)) { - Directory.CreateDirectory(destination); + FileSystem.CreateDirectory(destination, arguments.WhatIf); } source = Paths.TrimSeparator(source); destination = Paths.TrimSeparator(destination); - var diff = Folders.DiffFolders(source, destination); + var diff = Folders.DiffFolders(source, destination, arguments.Pattern); - using (Log.MeasureTime("Copying new files")) + if (arguments.CopyLeftOnlyFiles) { - foreach (var leftOnly in diff.LeftOnlyFiles) + using (Log.MeasureTime("Copying new files")) { - var destinationFilePath = destination + leftOnly; - var destinationFolder = Path.GetDirectoryName(destinationFilePath); - Directory.CreateDirectory(destinationFolder); - File.Copy(source + leftOnly, destinationFilePath); - Console.WriteLine("Copy " + destinationFilePath); + foreach (var leftOnly in diff.LeftOnlyFiles) + { + var destinationFilePath = destination + leftOnly; + FileSystem.CopyFile(source + leftOnly, destinationFilePath, arguments.WhatIf); + } } } - using (Log.MeasureTime("Overwriting changed files")) + if (arguments.UpdateChangedFiles) + { + using (Log.MeasureTime("Updating changed files")) + { + foreach (var changed in diff.ChangedFiles) + { + var destinationFilePath = destination + changed; + FileSystem.CopyFile(source + changed, destinationFilePath, arguments.WhatIf); + } + } + } + else if (arguments.DeleteChangedFiles) { - foreach (var changed in diff.ChangedFiles) + using (Log.MeasureTime("Deleting changed files")) { - var destinationFilePath = destination + changed; - File.Copy(source + changed, destinationFilePath, overwrite: true); - Console.WriteLine("Overwrite " + destinationFilePath); + foreach (var changed in diff.ChangedFiles) + { + var destinationFilePath = destination + changed; + FileSystem.DeleteFile(destinationFilePath, arguments.WhatIf); + } } } - using (Log.MeasureTime("Deleting extra files")) + if (arguments.DeleteSameFiles) { - foreach (var rightOnly in diff.RightOnlyFiles) + using (Log.MeasureTime("Deleting identical files")) { - var deletedFilePath = destination + rightOnly; - var attributes = File.GetAttributes(deletedFilePath); - if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + foreach (var same in diff.IdenticalFiles) { - File.SetAttributes(deletedFilePath, attributes & ~FileAttributes.ReadOnly); + var destinationFilePath = destination + same; + FileSystem.DeleteFile(destinationFilePath, arguments.WhatIf); } + } + } - File.Delete(deletedFilePath); - Console.WriteLine("Delete " + deletedFilePath); + if (arguments.DeleteRightOnlyFiles) + { + using (Log.MeasureTime("Deleting extra files")) + { + foreach (var rightOnly in diff.RightOnlyFiles) + { + var deletedFilePath = destination + rightOnly; + FileSystem.DeleteFile(deletedFilePath, arguments.WhatIf); + } } } int foldersCreated = 0; - using (Log.MeasureTime("Creating folders")) + if (arguments.CopyEmptyDirectories) { - foreach (var leftOnlyFolder in diff.LeftOnlyFolders) + using (Log.MeasureTime("Creating folders")) { - var newFolder = destination + leftOnlyFolder; - if (!Directory.Exists(newFolder)) + foreach (var leftOnlyFolder in diff.LeftOnlyFolders) { - Directory.CreateDirectory(newFolder); - Console.WriteLine("Create " + newFolder); - foldersCreated++; + var newFolder = destination + leftOnlyFolder; + if (!Directory.Exists(newFolder)) + { + FileSystem.CreateDirectory(newFolder, arguments.WhatIf); + foldersCreated++; + } } } } int foldersDeleted = 0; - using (Log.MeasureTime("Deleting folders")) + if (arguments.DeleteRightOnlyDirectories) { - foreach (var rightOnlyFolder in diff.RightOnlyFolders) + using (Log.MeasureTime("Deleting folders")) { - var deletedFolderPath = destination + rightOnlyFolder; - if (Directory.Exists(deletedFolderPath)) + foreach (var rightOnlyFolder in diff.RightOnlyFolders) { - Directory.Delete(deletedFolderPath, recursive: true); - Console.WriteLine("Delete " + deletedFolderPath); - foldersDeleted++; + var deletedFolderPath = destination + rightOnlyFolder; + if (Directory.Exists(deletedFolderPath)) + { + FileSystem.DeleteDirectory(deletedFolderPath, arguments.WhatIf); + foldersDeleted++; + } } } } @@ -126,14 +151,14 @@ public static void Directories(string source, string destination) /// If it exists and is different, it is overwritten. /// If it doesn't exist, source is copied. /// - public static void Files(string source, string destination) + public static void Files(string source, string destination, Arguments arguments) { if (File.Exists(destination) && FileUtilities.Files.AreContentsIdentical(source, destination)) { return; } - File.Copy(source, destination, overwrite: true); + FileSystem.CopyFile(source, destination, arguments.WhatIf); } } } \ No newline at end of file