[SuppressMessage("ReSharper", "AccessToDisposedClosure")] // Progress bar inside parallel code private static void SyncFiles( [NotNull] IReadOnlyDictionary <string, IReadOnlyCollection <string> > map, [NotNull] string source, [NotNull] string target, [NotNull] string id, [NotNull] StatisticsManager statistics, int threads) { using (AsciiProgressBar bar = new AsciiProgressBar()) { int progress = 0, total = map.Values.Sum(l => l.Count); string name = Path.GetFileName(source); // Get the name of the source folder // Copy the files in parallel, one task for each subdirectory in the source tree IReadOnlyList <KeyValuePair <string, IReadOnlyCollection <string> > > files = map.ToArray(); Parallel.For(0, files.Count, new ParallelOptions { MaxDegreeOfParallelism = threads }, i => { // Create the target directory KeyValuePair <string, IReadOnlyCollection <string> > pair = files[i]; string relative = pair.Key.Substring(source.Length), folder = string.IsNullOrEmpty(relative) ? Path.Join(target, $"{name}{id}") : Path.Join(target, $"{name}{id}", relative); Directory.CreateDirectory(folder); // Copy the original files, when needed foreach (string file in pair.Value) { string copy = Path.Join(folder, Path.GetFileName(file)); try { if (!File.Exists(copy)) { File.Copy(file, copy); statistics.AddOperation(copy, FileUpdateType.Add); } else if (File.GetLastWriteTimeUtc(file).CompareTo(File.GetLastWriteTimeUtc(copy)) > 0) { if (File.GetAttributes(copy).HasFlag(FileAttributes.ReadOnly)) { File.SetAttributes(copy, FileAttributes.Normal); // In the case the original file was locked } File.Copy(file, copy, true); statistics.AddOperation(copy, FileUpdateType.Update); } } catch (Exception e) when(e is UnauthorizedAccessException || e is IOException) { // Log the failure and carry on statistics.AddOperation(copy, FileUpdateType.Failure); } bar.Report((double)Interlocked.Increment(ref progress) / total); } }); } }
/// <summary> /// Cleans up the backup directory, removing empty folders and unnecessary files /// </summary> /// <param name="map">The map of files to sync</param> /// <param name="source">The original source directory</param> /// <param name="target">The root target directory</param> /// <param name="statistics">The statistics instance to track the performed operations</param> private static void Cleanup( [NotNull] IReadOnlyDictionary <string, IReadOnlyCollection <string> > map, [NotNull] string source, [NotNull] string target, [NotNull] string id, [NotNull] StatisticsManager statistics) { string name = Path.GetFileName(source), root = Path.Join(target, $"{name}{id}"); void Cleanup(string directory) { // Post-order cleanup for unnecessary files string[] subdirectories = Directory.GetDirectories(directory); foreach (string subdirectory in subdirectories) { Cleanup(subdirectory); } // Delete the files that don't belong to the source folder with the current settings string relative = directory.Substring(root.Length), key = Path.Join(source, relative); IReadOnlyCollection <string> files = map.TryGetValue(key, out IReadOnlyCollection <string> paths) ? new HashSet <string>(paths.Select(Path.GetFileName)) : null; foreach (string file in Directory.GetFiles(directory)) { if (files?.Contains(Path.GetFileName(file)) != true) { try { File.Delete(file); statistics.AddOperation(file, FileUpdateType.Remove); } catch (UnauthorizedAccessException) { // Can happen in rare situations } } } // Delete the subfolders, if necessary foreach (string subdirectory in subdirectories) { if (!Directory.EnumerateFiles(subdirectory).Any() && !Directory.EnumerateDirectories(subdirectory).Any()) { Directory.Delete(subdirectory); } } } Cleanup(root); }