void FswRename(SyncJob syncJob, RenamedEventArgs e) { lock (syncOpLock) { bool isA = e.FullPath.StartsWith(syncJob.PathA); string oldKey = Path.GetRelativePath(isA ? syncJob.PathA : syncJob.PathB, e.OldFullPath); string key = Path.GetRelativePath(isA ? syncJob.PathA : syncJob.PathB, e.FullPath); var oldItemStatus = syncJob.StatusLines.Where(sl => sl.Key == oldKey).SingleOrDefault(); var itemStatus = syncJob.StatusLines.Where(sl => sl.Key == key).SingleOrDefault(); if (isA) { File.Move(Path.Join(syncJob.PathB, oldKey), Path.Join(syncJob.PathB, key)); } else { File.Move(Path.Join(syncJob.PathA, oldKey), Path.Join(syncJob.PathA, key)); } syncJob.StatusLines.Remove(oldItemStatus); syncJob.StatusLines.Add(new FileStatusLine { Key = key, LastModified = new FileInfo(e.FullPath).LastWriteTimeUtc }); } }
static void NewSyncJob(NewOptions options) { if (!Directory.Exists(options.PathA)) { throw new ArgumentException($"PathA ({options.PathA}) does not exist"); } if (!Directory.Exists(options.PathB)) { throw new ArgumentException($"PathB ({options.PathB}) does not exist"); } SyncJob syncJob, oldSyncJob; if (File.Exists(options.SyncJobFile)) { oldSyncJob = SyncJob.Load(options.SyncJobFile); syncJob = new SyncJob(options.PathA, options.PathB, oldSyncJob.StatusLines, options.LogDirectory, oldSyncJob.CurrentPid, options.LogFileLimit); } else { syncJob = new SyncJob(options.PathA, options.PathB, options.LogDirectory, options.LogFileLimit); } syncJob.Save(options.SyncJobFile); }
FileSystemWatcher CreateFsWatcher(SyncJob syncJob, string syncJobFile, string path) { var fsw = new FileSystemWatcher(path); fsw.IncludeSubdirectories = true; fsw.EnableRaisingEvents = true; fsw.Changed += (sender, e) => FswUpdate(syncJob, syncJobFile, e); fsw.Created += (sender, e) => FswUpdate(syncJob, syncJobFile, e); fsw.Deleted += (sender, e) => FswUpdate(syncJob, syncJobFile, e); fsw.Renamed += (sender, e) => FswRename(syncJob, e); fsw.Error += (sender, e) => logger.LogError(e.GetException(), "Error receiving file system events"); return(fsw); }
void FswUpdate(SyncJob syncJob, string syncJobFile, FileSystemEventArgs e) { logger.LogInformation($"FileSystem Event: {e.ChangeType.ToString()} {e.FullPath}"); lock (syncOpLock) { bool isA = e.FullPath.StartsWith(syncJob.PathA); string key = Path.GetRelativePath(isA ? syncJob.PathA : syncJob.PathB, e.FullPath); string counterPath = Path.Join(isA ? syncJob.PathB : syncJob.PathA, key); SyncItem <FileStatusLine> itemEvent = null; if (e.ChangeType != WatcherChangeTypes.Deleted) { itemEvent = new SyncItem <FileStatusLine>(e.FullPath, key, new FileStatusLine { Key = key, LastModified = (new FileInfo(e.FullPath)).LastWriteTimeUtc }); } SyncItem <FileStatusLine> itemCounter = null; logger.LogDebug($"counterPath = {counterPath}"); if (File.Exists(counterPath)) { logger.LogDebug($"counterPath exists"); itemCounter = new SyncItem <FileStatusLine>(counterPath, key, new FileStatusLine { Key = key, LastModified = (new FileInfo(counterPath)).LastWriteTimeUtc }); } SyncItem <FileStatusLine> itemStatus = null; var statusLine = syncJob.StatusLines.Where(sl => sl.Key == key).SingleOrDefault(); if (statusLine != null) { itemStatus = new SyncItem <FileStatusLine>("", key, statusLine); } SyncItem <FileStatusLine> itemA, itemB; if (isA) { itemA = itemEvent; itemB = itemCounter; } else { itemA = itemCounter; itemB = itemEvent; } var op = syncEngine.GetOpForKey(itemA, itemB, itemStatus); var ops = new [] { op }; if (op != null && (op.ItemOperation != ItemOperation.None || op.StatusOperation != StatusOperation.None)) { logger.LogInformation("Operation to perform:"); logger.LogInformation(op.ToString()); } else { logger.LogDebug("No changes to make"); } try { syncer.Sync(syncJob, ops); syncJob.Save(syncJobFile); } catch (Exception ex) { logger.LogError(ex, "Error performing sync"); } } }
public void Monitor(SyncJob syncJob, string syncJobFile) { var fswA = CreateFsWatcher(syncJob, syncJobFile, syncJob.PathA); var fswB = CreateFsWatcher(syncJob, syncJobFile, syncJob.PathB); }
public void Sync(SyncJob job, IList <SyncOperation <FileStatusLine> > ops) { foreach (var op in ops) { if (op?.Item == null) { logger.LogInformation("Nothing to do"); continue; } string fileA = Path.Join(job.PathA, op.Item.Key); string fileB = Path.Join(job.PathB, op.Item.Key); switch (op.ItemOperation) { case ItemOperation.CopyToA: logger.LogInformation($"Copy B -> A: {op.Item.Key}"); Directory.CreateDirectory(Path.GetDirectoryName(fileA)); Copy(fileB, fileA); break; case ItemOperation.CopyToB: logger.LogInformation($"Copy A -> B: {op.Item.Key}"); Directory.CreateDirectory(Path.GetDirectoryName(fileB)); Copy(fileA, fileB); break; case ItemOperation.DeleteFromA: logger.LogInformation($"Delete From A: {op.Item.Key}"); Delete(fileA); break; case ItemOperation.DeleteFromB: logger.LogInformation($"Delete From B: {op.Item.Key}"); Delete(fileB); break; } var matchedLines = job.StatusLines.Where(sl => sl.Key == op.Item.Key); FileStatusLine statusLine; switch (op.StatusOperation) { case StatusOperation.AddToStatus: statusLine = matchedLines.SingleOrDefault(); if (statusLine != null) { throw new DuplicateNameException($"The key already exists in the sync job status. {op.Item.Key}"); } job.StatusLines.Add(new FileStatusLine { Key = op.Item.Key, LastModified = op.Item.Item.LastModified }); break; case StatusOperation.UpdateStatus: statusLine = matchedLines.Single(); statusLine.LastModified = op.Item.Item.LastModified; break; case StatusOperation.DeleteFromStatus: statusLine = matchedLines.Single(); job.StatusLines.Remove(statusLine); break; } } }
static void RunSyncJob(SyncOptions options) { var syncJob = SyncJob.Load(options.SyncJobFile); // only allow one instance running against any sync file if (syncJob.CurrentPid > 0 && Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).Select(p => p.Id).Contains(syncJob.CurrentPid)) { Console.WriteLine($"Unable to run sync on {options.SyncJobFile}. Sync already running in process {syncJob.CurrentPid}"); return; } syncJob.CurrentPid = Process.GetCurrentProcess().Id; syncJob.Save(options.SyncJobFile); try { var logger = new LoggerFactory() .AddSerilog(new LoggerConfiguration() .MinimumLevel.Is(options.Debug ? LogEventLevel.Debug : LogEventLevel.Information) .WriteTo.Console(restrictedToMinimumLevel: options.Quiet ? LogEventLevel.Warning : LogEventLevel.Information, outputTemplate: "{Message:lj}{NewLine}") .WriteTo.File(syncJob.LogPath, rollingInterval: Serilog.RollingInterval.Day, retainedFileCountLimit: syncJob.LogFileLimit) .CreateLogger()) .CreateLogger("sync_run"); logger.LogInformation(""); logger.LogInformation("------------------------------------------------"); logger.LogInformation($"START sync: {syncJob.PathA} <-> {syncJob.PathB}"); logger.LogInformation("------------------------------------------------"); logger.LogInformation(""); var syncEngine = new SyncEngine <FileStatusLine>(logger, FileMatcher); var fileSyncer = new FileSyncer(logger); var stopwatch = new Stopwatch(); stopwatch.Start(); var sourceFiles = new DirectoryParser(logger).Parse(syncJob.PathA) .Select(fi => CreateSyncItem(fi, syncJob.PathA)) .ToHashSet(); logger.LogInformation($"Parsing PathA ({syncJob.PathA}) took {stopwatch.Elapsed.TotalSeconds} seconds"); stopwatch.Restart(); var destFiles = new DirectoryParser(logger).Parse(syncJob.PathB) .Select(fi => CreateSyncItem(fi, syncJob.PathB)) .ToHashSet(); logger.LogInformation($"Parsing PathB ({syncJob.PathB}) took {stopwatch.Elapsed.TotalSeconds} seconds"); stopwatch.Restart(); var statusLines = syncJob.StatusLines .Select(sl => new SyncItem <FileStatusLine>("", sl.Key, sl)) .ToHashSet(); logger.LogInformation($"Parsing Job Status took {stopwatch.Elapsed.TotalSeconds} seconds"); Console.WriteLine(new string(' ', Console.WindowWidth)); stopwatch.Restart(); var changeset = syncEngine.GetChangeSet(sourceFiles, destFiles, statusLines); logger.LogInformation($"Calculating changeset took {stopwatch.Elapsed.TotalSeconds} seconds"); PrintOperations(); if (!GetUserConfirmation()) { return; } stopwatch.Restart(); fileSyncer.Sync(syncJob, changeset); logger.LogInformation($"File sync took {stopwatch.Elapsed.TotalSeconds} seconds"); syncJob.Save(options.SyncJobFile); if (options.Realtime) { logger.LogInformation(""); logger.LogInformation("Entering realtime file system monitoring..."); var fileMonitor = new RealtimeFileMonitor(logger, syncEngine, fileSyncer); fileMonitor.Monitor(syncJob, options.SyncJobFile); while (true) { System.Threading.Thread.Sleep(1); } } SyncItem <FileStatusLine> CreateSyncItem(FileInfo fileInfo, string basePath) { string key = Path.GetRelativePath(basePath, fileInfo.FullName); var syncLine = new FileStatusLine { Key = key, LastModified = fileInfo.LastWriteTimeUtc }; return(new SyncItem <FileStatusLine>(fileInfo.FullName, key, syncLine)); } SyncItem <FileStatusLine> FileMatcher(SyncItem <FileStatusLine> a, SyncItem <FileStatusLine> b) { logger.LogDebug($"Conflict Resolution: A = {a.Item.LastModified.ToString()}, B = {b.Item.LastModified.ToString()}"); return(a.Item.LastModified > b.Item.LastModified ? a : a.Item.LastModified < b.Item.LastModified ? b : null); } void PrintOperations() { if (changeset.Count() > 0) { Log("Operations to perform:"); int maxKeyLen = changeset.Select(s => s.Item.Key.Length).Max() + 2; foreach (var op in changeset) { if (op.ItemOperation == ItemOperation.None && op.StatusOperation == StatusOperation.None) { continue; } Log(op.ToString(maxKeyLen)); } } else { Log("\tDirectories are in-sync"); } void Log(string message) { logger.LogInformation(message); } } bool GetUserConfirmation() { if (!options.Force) { logger.LogInformation(""); logger.LogInformation("Confirm? (yes/NO): "); string confirm = Console.ReadLine(); if (confirm.ToLower() != "yes") { logger.LogDebug("User cancelled sync"); return(false); } logger.LogDebug("Sync confirmed"); } else { logger.LogDebug("Force option set. Confirmation skipped"); } return(true); } } finally { syncJob.CurrentPid = 0; syncJob.Save(options.SyncJobFile); } }