diff --git a/SFTPSyncLib/Logger.cs b/SFTPSyncLib/Logger.cs index a674394..e3e423c 100644 --- a/SFTPSyncLib/Logger.cs +++ b/SFTPSyncLib/Logger.cs @@ -40,11 +40,11 @@ private static void Log(string message) switch (_mode) { case LoggerMode.Console: - Console.WriteLine($"{DateTime.Now.ToString()} {message}"); + Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} {message}"); break; case LoggerMode.RaiseEvent: - LogUpdated?.Invoke($"{DateTime.Now.ToString()} {message}"); + LogUpdated?.Invoke($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} {message}"); break; } } diff --git a/SFTPSyncLib/RemoteSync.cs b/SFTPSyncLib/RemoteSync.cs index 71e015b..6d0389a 100644 --- a/SFTPSyncLib/RemoteSync.cs +++ b/SFTPSyncLib/RemoteSync.cs @@ -33,25 +33,25 @@ public RemoteSync(string host, string username, string password, _username = username; _password = password; _searchPattern = searchPattern; - _localRootDirectory = localRootDirectory; - _remoteRootDirectory = remoteRootDirectory; + _localRootDirectory = Path.TrimEndingDirectorySeparator(Path.GetFullPath(localRootDirectory)); + _remoteRootDirectory = remoteRootDirectory.TrimEnd('/', '\\'); _director = director; _excludedFolders = excludedFolders ?? new List(); _sftp = new SftpClient(host, username, password); - _sftp.Connect(); - //Our first instance is responsible for creating all of the the directories - //Subsequent instances will not be created until this is done - DoneMakingFolders = createFolders ? CreateDirectories(_localRootDirectory, _remoteRootDirectory) : Task.CompletedTask; + //The first instance is responsible for creating ALL of the the directories. + //Subsequent instances will not be created until this one completes. + + DoneMakingFolders = createFolders + ? CreateDirectories(_localRootDirectory, _remoteRootDirectory) + : Task.CompletedTask; //Now perform the initial sync for the pattern this instance is responsible for + DoneInitialSync = InitialSync(_localRootDirectory, _remoteRootDirectory); - //Once the initial sync is done, we can start watching the file system for changes - DoneInitialSync.ContinueWith((tmp) => - { - _director.AddCallback(searchPattern, (args) => Fsw_Changed(null, args)); - }); + // Register callbacks immediately; handler will ignore events until initial sync completes. + _director.AddCallback(searchPattern, (args) => Fsw_Changed(null, args)); } public RemoteSync(string host, string username, string password, @@ -62,31 +62,24 @@ public RemoteSync(string host, string username, string password, _username = username; _password = password; _searchPattern = searchPattern; - _localRootDirectory = localRootDirectory; - _remoteRootDirectory = remoteRootDirectory; + _localRootDirectory = Path.TrimEndingDirectorySeparator(Path.GetFullPath(localRootDirectory)); + _remoteRootDirectory = remoteRootDirectory.TrimEnd('/', '\\'); _director = director; _excludedFolders = excludedFolders ?? new List(); _sftp = new SftpClient(host, username, password); - _sftp.Connect(); DoneMakingFolders = Task.CompletedTask; + DoneInitialSync = initialSyncTask; - DoneInitialSync.ContinueWith((tmp) => - { - _director.AddCallback(searchPattern, (args) => Fsw_Changed(null, args)); - }); + // Register callbacks immediately; handler will ignore events until initial sync completes. + _director.AddCallback(searchPattern, (args) => Fsw_Changed(null, args)); } public static async Task RunSharedInitialSyncAsync( - string host, - string username, - string password, - string localRootDirectory, - string remoteRootDirectory, - string[] searchPatterns, - List? excludedFolders, - int workerCount) + string host, string username, string password, + string localRootDirectory, string remoteRootDirectory, + string[] searchPatterns, List? excludedFolders, int workerCount) { if (workerCount <= 0 || searchPatterns.Length == 0) { @@ -108,6 +101,7 @@ public static async Task RunSharedInitialSyncAsync( } var workQueue = new ConcurrentQueue(); + foreach (var pair in EnumerateLocalDirectories(localRootDirectory, remoteRootDirectory, excludedFolders)) { foreach (var pattern in searchPatterns) @@ -117,6 +111,7 @@ public static async Task RunSharedInitialSyncAsync( } var workers = new List(); + for (int i = 0; i < workerCount; i++) { workers.Add(Task.Run(async () => @@ -150,7 +145,7 @@ public static async Task RunSharedInitialSyncAsync( /// Path to the destination file public static void SyncFile(SftpClient sftp, string sourcePath, string destinationPath) { - Logger.LogInfo($"Syncing {sourcePath} -> {destinationPath}"); + Logger.LogInfo($"Syncing {sourcePath}"); int retryCount = 0; const int maxRetries = 5; @@ -195,7 +190,7 @@ public static Task> SyncDirectoryAsync(SftpClient sftp, st { if (new DirectoryInfo(sourcePath).EnumerateFiles(searchPattern, SearchOption.TopDirectoryOnly).Any()) { - Logger.LogInfo($"Sync started for {sourcePath}\\{searchPattern} -> {destinationPath}"); + Logger.LogInfo($"Sync started for {sourcePath}\\{searchPattern}"); return Task>.Factory.FromAsync(sftp.BeginSynchronizeDirectories, sftp.EndSynchronizeDirectories, sourcePath, @@ -214,7 +209,7 @@ private string[] FilteredDirectories(string localPath) public async Task CreateDirectories(string localPath, string remotePath) { - Logger.LogInfo($"Creating directory {localPath} -> {remotePath}"); + Logger.LogInfo($"Creating directory {remotePath}"); try { @@ -337,6 +332,7 @@ private static async Task CreateDirectoriesInternal( var directoryName = item.Split(Path.DirectorySeparatorChar).Last(); if (!remoteDirectories.ContainsKey(directoryName)) { + Logger.LogInfo($"Creating remote directory {remotePath}{directoryName}"); sftp.CreateDirectory(remotePath + "/" + directoryName); } await CreateDirectoriesInternal(sftp, localRootDirectory, localPath + "\\" + directoryName, remotePath + "/" + directoryName, excludedFolders); @@ -383,6 +379,19 @@ public static bool IsFileReady(String sFilename) } } + private string GetRemotePathForLocal(string localPath) + { + var relativePath = Path.GetRelativePath(_localRootDirectory, localPath); + if (relativePath == "." || string.IsNullOrEmpty(relativePath)) + return _remoteRootDirectory; + + relativePath = relativePath.Replace('\\', '/').TrimStart('/'); + if (relativePath.Length == 0) + return _remoteRootDirectory; + + return _remoteRootDirectory + "/" + relativePath; + } + private void EnsureConnected() { if (_disposed) @@ -408,17 +417,25 @@ private bool EnsureConnectedSafe() } } + public Task ConnectAsync() + { + return Task.Run(() => EnsureConnectedSafe()); + } + private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) { try { + if (!DoneInitialSync.IsCompleted) + return; + if (arg.ChangeType == WatcherChangeTypes.Changed || arg.ChangeType == WatcherChangeTypes.Created || arg.ChangeType == WatcherChangeTypes.Renamed) { var changedPath = Path.GetDirectoryName(arg.FullPath); - var relativePath = _localRootDirectory == changedPath ? "" : changedPath?.Substring(_localRootDirectory.Length).Replace('\\', '/'); - var fullRemotePath = _remoteRootDirectory + relativePath; + var fullRemotePath = GetRemotePathForLocal(changedPath ?? _localRootDirectory); + var fullRemoteFilePath = GetRemotePathForLocal(arg.FullPath); await Task.Yield(); bool makeDirectory = true; lock (_activeDirSync) @@ -439,7 +456,7 @@ private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) if (connectionOk) { //check if we're a new directory - if (makeDirectory && Directory.Exists(arg.FullPath) && !_sftp.Exists(fullRemotePath)) + if (makeDirectory && changedPath != null && Directory.Exists(changedPath) && !_sftp.Exists(fullRemotePath)) { _sftp.CreateDirectory(fullRemotePath); } @@ -501,7 +518,7 @@ private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) fileConnectionOk = EnsureConnectedSafe(); if (fileConnectionOk) { - SyncFile(_sftp, arg.FullPath, fullRemotePath + "/" + Path.GetFileName(arg.FullPath)); + SyncFile(_sftp, arg.FullPath, fullRemoteFilePath); } } finally diff --git a/SFTPSyncLib/SyncDirector.cs b/SFTPSyncLib/SyncDirector.cs index 4d3a25f..813d695 100644 --- a/SFTPSyncLib/SyncDirector.cs +++ b/SFTPSyncLib/SyncDirector.cs @@ -18,47 +18,52 @@ public SyncDirector(string rootFolder) InternalBufferSize = 64 * 1024 }; - _fsw.Changed += Fsw_Changed; _fsw.Created += Fsw_Created; + _fsw.Changed += Fsw_Changed; _fsw.Renamed += Fsw_Renamed; _fsw.Error += Fsw_Error; _fsw.EnableRaisingEvents = true; } - private void Fsw_Renamed(object sender, RenamedEventArgs e) + public void AddCallback(string match, Action handler) + { + string regexPattern = "^" + Regex.Escape(match) + .Replace("\\*", ".*") + .Replace("\\?", ".") + "$"; + callbacks.Add((new Regex(regexPattern, RegexOptions.IgnoreCase), handler)); + } + + private void Fsw_Created(object sender, FileSystemEventArgs e) { + var name = Path.GetFileName(e.FullPath); foreach (var (regex, callback) in callbacks) { - if (regex.IsMatch(e.FullPath)) + if (regex.IsMatch(name)) { callback(e); } } } - private void Fsw_Created(object sender, FileSystemEventArgs e) + private void Fsw_Changed(object sender, FileSystemEventArgs e) { + var name = Path.GetFileName(e.FullPath); foreach (var (regex, callback) in callbacks) { - if (regex.IsMatch(e.FullPath)) + if (regex.IsMatch(name)) { callback(e); } } } - public void AddCallback(string match, Action handler) - { - string regexPattern = "^" + Regex.Escape(match).Replace("\\*", ".*") + "$"; - callbacks.Add((new Regex(regexPattern), handler)); - } - - private void Fsw_Changed(object sender, FileSystemEventArgs e) + private void Fsw_Renamed(object sender, RenamedEventArgs e) { + var name = Path.GetFileName(e.FullPath); foreach (var (regex, callback) in callbacks) { - if (regex.IsMatch(e.FullPath)) + if (regex.IsMatch(name)) { callback(e); } diff --git a/SFTPSyncSetup/Product.wxs b/SFTPSyncSetup/Product.wxs index 927e67c..26196c9 100644 --- a/SFTPSyncSetup/Product.wxs +++ b/SFTPSyncSetup/Product.wxs @@ -5,7 +5,7 @@ Id="*" Name="SFTPSync" Language="1033" - Version="1.5" + Version="1.6" Manufacturer="Synergex International Corporation" UpgradeCode="6000f870-b811-4e22-b80b-5b8956317d09"> diff --git a/SFTPSyncSetup/SFTPSyncSetup.wixproj b/SFTPSyncSetup/SFTPSyncSetup.wixproj index 33fabd6..54c01f4 100644 --- a/SFTPSyncSetup/SFTPSyncSetup.wixproj +++ b/SFTPSyncSetup/SFTPSyncSetup.wixproj @@ -6,7 +6,7 @@ 3.10 5074cbcb-641b-4a9c-b3bc-8dd0b78810a6 2.0 - SFTPSync-1.5 + SFTPSync-1.6 Package SFTPSyncSetup diff --git a/SFTPSyncUI/MainForm.cs b/SFTPSyncUI/MainForm.cs index 010a841..1310532 100644 --- a/SFTPSyncUI/MainForm.cs +++ b/SFTPSyncUI/MainForm.cs @@ -159,15 +159,15 @@ private void AppendLog(string message) private void AddMessage(string message) { - listBoxMessages.Items.Add(message); + listBoxMessages.Items.Insert(0, message); - // Optional: auto-scroll to bottom - listBoxMessages.TopIndex = listBoxMessages.Items.Count - 1; + // Keep newest at top + listBoxMessages.TopIndex = 0; // Optional: trim excess from UI if _log did a dequeue if (listBoxMessages.Items.Count > 1000) { - listBoxMessages.Items.RemoveAt(0); + listBoxMessages.Items.RemoveAt(listBoxMessages.Items.Count - 1); } } diff --git a/SFTPSyncUI/SFTPSyncUI.cs b/SFTPSyncUI/SFTPSyncUI.cs index 296e91f..6124146 100644 --- a/SFTPSyncUI/SFTPSyncUI.cs +++ b/SFTPSyncUI/SFTPSyncUI.cs @@ -210,76 +210,130 @@ public static async void StartSync(Action loggerAction) Logger.LogInfo("Starting sync workers..."); mainForm?.SetStatusBarText("Performing initial sync..."); - var director = new SyncDirector(settings.LocalPath); - - var patterns = settings.LocalSearchPattern - .Split(';', StringSplitOptions.RemoveEmptyEntries) - .Select(pattern => pattern.Trim()) - .Where(pattern => pattern.Length > 0) - .ToArray(); - - if (patterns.Length == 0) + var capturedSettings = settings; + var capturedMainForm = mainForm; + var setStatus = (string text) => { - Logger.LogError("No valid search patterns were configured."); - return; - } + if (capturedMainForm == null) + return; + capturedMainForm.BeginInvoke((Action)(() => capturedMainForm.SetStatusBarText(text))); + }; - Task initialSyncTask; try { - initialSyncTask = RemoteSync.RunSharedInitialSyncAsync( - settings.RemoteHost, - settings.RemoteUsername, - DPAPIEncryption.Decrypt(settings.RemotePassword), - settings.LocalPath, - settings.RemotePath, - patterns, - settings.ExcludedDirectories, - patterns.Length); - } - catch (Exception ex) - { - Logger.LogError($"Failed to start initial sync. Exception: {ex.Message}"); - return; - } - - foreach (var pattern in patterns) - { - try - { - RemoteSyncWorkers.Add(new RemoteSync( - settings.RemoteHost, - settings.RemoteUsername, - DPAPIEncryption.Decrypt(settings.RemotePassword), - settings.LocalPath, - settings.RemotePath, - pattern, - director, - settings.ExcludedDirectories, - initialSyncTask)); - - Logger.LogInfo($"Started sync worker {RemoteSyncWorkers.Count} for pattern {pattern}"); - } - catch (Exception) + await Task.Run(async () => { - Logger.LogError($"Failed to start sync worker for pattern {pattern}"); - } - } + var director = new SyncDirector(capturedSettings.LocalPath); - //Wait for all sync workers to finish initial sync then tell the user - try - { - await initialSyncTask; + var patterns = capturedSettings.LocalSearchPattern + .Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(pattern => pattern.Trim()) + .Where(pattern => pattern.Length > 0) + .ToArray(); + + if (patterns.Length == 0) + { + Logger.LogError("No valid search patterns were configured."); + setStatus("Invalid search patterns"); + return; + } + + var initialSyncTcs = new TaskCompletionSource(); + var initialSyncTask = initialSyncTcs.Task; + + foreach (var pattern in patterns) + { + try + { + RemoteSyncWorkers.Add(new RemoteSync( + capturedSettings.RemoteHost, + capturedSettings.RemoteUsername, + DPAPIEncryption.Decrypt(capturedSettings.RemotePassword), + capturedSettings.LocalPath, + capturedSettings.RemotePath, + pattern, + director, + capturedSettings.ExcludedDirectories, + initialSyncTask)); + + Logger.LogInfo($"Started sync worker {RemoteSyncWorkers.Count} for pattern {pattern}"); + } + catch (Exception) + { + Logger.LogError($"Failed to start sync worker for pattern {pattern}"); + } + } + + var connectTasks = new List(); + + Logger.LogInfo($"Establishing SFTP connections for {RemoteSyncWorkers.Count} workers"); + + for (int i = 0; i < RemoteSyncWorkers.Count; i++) + { + connectTasks.Add(RemoteSyncWorkers[i].ConnectAsync()); + } + await Task.WhenAll(connectTasks); + + Task runInitialSyncTask; + + try + { + runInitialSyncTask = RemoteSync.RunSharedInitialSyncAsync( + capturedSettings.RemoteHost, + capturedSettings.RemoteUsername, + DPAPIEncryption.Decrypt(capturedSettings.RemotePassword), + capturedSettings.LocalPath, + capturedSettings.RemotePath, + patterns, + capturedSettings.ExcludedDirectories, + patterns.Length); + } + catch (Exception ex) + { + Logger.LogError($"Failed to start initial sync. Exception: {ex.Message}"); + setStatus("Initial sync failed"); + return; + } + + await runInitialSyncTask.ContinueWith(t => + { + if (t.IsFaulted && t.Exception != null) + { + initialSyncTcs.TrySetException(t.Exception.InnerExceptions); + } + else if (t.IsCanceled) + { + initialSyncTcs.TrySetCanceled(); + } + else + { + initialSyncTcs.TrySetResult(null); + } + }, TaskScheduler.Default); + + //Wait for all sync workers to finish initial sync then tell the user + + try + { + await runInitialSyncTask; + } + catch (Exception ex) + { + Logger.LogError($"Initial sync failed. Exception: {ex.Message}"); + setStatus("Initial sync failed"); + return; + } + + Logger.LogInfo("Initial sync complete, real-time sync active"); + setStatus("Real time sync active"); + }); } catch (Exception ex) { - Logger.LogError($"Initial sync failed. Exception: {ex.Message}"); - mainForm?.SetStatusBarText("Initial sync failed"); + Logger.LogError($"Failed to start sync. Exception: {ex.Message}"); + mainForm?.SetStatusBarText("Sync failed"); return; } - - Logger.LogInfo("Initial sync complete, real-time sync active"); - mainForm?.SetStatusBarText("Real time sync active"); } /// diff --git a/SFTPSyncUI/SFTPSyncUI.csproj b/SFTPSyncUI/SFTPSyncUI.csproj index a864dd6..f2d3759 100644 --- a/SFTPSyncUI/SFTPSyncUI.csproj +++ b/SFTPSyncUI/SFTPSyncUI.csproj @@ -20,10 +20,10 @@ LICENSE.md True Synergex International, Inc. - 1.5 - 1.5 + 1.6 + 1.6 preview - 1.5 + 1.6