diff --git a/SFTPSync/SFTPSync.cs b/SFTPSync/SFTPSync.cs index 8b4a19d..a812003 100644 --- a/SFTPSync/SFTPSync.cs +++ b/SFTPSync/SFTPSync.cs @@ -24,7 +24,7 @@ static async Task Main(string[] args) { await remoteSyncWorkers[0].DoneMakingFolders; } - remoteSyncWorkers.Add(new RemoteSync(args[0], args[1], args[2], args[3], args[4], pattern, remoteSyncWorkers.Count == 0, director, null)); + remoteSyncWorkers.Add(new RemoteSync(args[0], args[1], args[2], args[3], args[4], pattern, remoteSyncWorkers.Count == 0, director, null, false, remoteSyncWorkers.Count == 0)); Logger.LogInfo($"Started sync worker {remoteSyncWorkers.Count} for pattern {pattern}"); } diff --git a/SFTPSyncLib/RemoteSync.cs b/SFTPSyncLib/RemoteSync.cs index 6d0389a..f6197d5 100644 --- a/SFTPSyncLib/RemoteSync.cs +++ b/SFTPSyncLib/RemoteSync.cs @@ -13,6 +13,10 @@ public class RemoteSync : IDisposable string _localRootDirectory; string _remoteRootDirectory; readonly List? _excludedFolders; + readonly bool _deleteEnabled; + readonly CancellationTokenSource _shutdownCts = new CancellationTokenSource(); + readonly object _disposeLock = new object(); + Task? _connectTask; SftpClient _sftp; SyncDirector _director; @@ -27,7 +31,7 @@ public class RemoteSync : IDisposable public RemoteSync(string host, string username, string password, string localRootDirectory, string remoteRootDirectory, - string searchPattern, bool createFolders, SyncDirector director, List? excludedFolders) + string searchPattern, bool createFolders, SyncDirector director, List? excludedFolders, bool deleteEnabled, bool handleDirectoryDeletes) { _host = host; _username = username; @@ -37,6 +41,7 @@ public RemoteSync(string host, string username, string password, _remoteRootDirectory = remoteRootDirectory.TrimEnd('/', '\\'); _director = director; _excludedFolders = excludedFolders ?? new List(); + _deleteEnabled = deleteEnabled; _sftp = new SftpClient(host, username, password); //The first instance is responsible for creating ALL of the the directories. @@ -52,11 +57,20 @@ public RemoteSync(string host, string username, string password, // Register callbacks immediately; handler will ignore events until initial sync completes. _director.AddCallback(searchPattern, (args) => Fsw_Changed(null, args)); + if (deleteEnabled && handleDirectoryDeletes) + { + _director.AddDirectoryDeleteCallback((args) => Fsw_DirectoryDeleted(null, args)); + } + if (handleDirectoryDeletes) + { + _director.AddDirectoryCreateCallback((args) => Fsw_DirectoryCreated(null, args)); + _director.AddDirectoryRenameCallback((args) => Fsw_DirectoryRenamed(null, args)); + } } public RemoteSync(string host, string username, string password, string localRootDirectory, string remoteRootDirectory, - string searchPattern, SyncDirector director, List? excludedFolders, Task initialSyncTask) + string searchPattern, SyncDirector director, List? excludedFolders, Task initialSyncTask, bool deleteEnabled, bool handleDirectoryDeletes) { _host = host; _username = username; @@ -66,6 +80,7 @@ public RemoteSync(string host, string username, string password, _remoteRootDirectory = remoteRootDirectory.TrimEnd('/', '\\'); _director = director; _excludedFolders = excludedFolders ?? new List(); + _deleteEnabled = deleteEnabled; _sftp = new SftpClient(host, username, password); DoneMakingFolders = Task.CompletedTask; @@ -74,6 +89,15 @@ public RemoteSync(string host, string username, string password, // Register callbacks immediately; handler will ignore events until initial sync completes. _director.AddCallback(searchPattern, (args) => Fsw_Changed(null, args)); + if (deleteEnabled && handleDirectoryDeletes) + { + _director.AddDirectoryDeleteCallback((args) => Fsw_DirectoryDeleted(null, args)); + } + if (handleDirectoryDeletes) + { + _director.AddDirectoryCreateCallback((args) => Fsw_DirectoryCreated(null, args)); + _director.AddDirectoryRenameCallback((args) => Fsw_DirectoryRenamed(null, args)); + } } public static async Task RunSharedInitialSyncAsync( @@ -96,7 +120,7 @@ public static async Task RunSharedInitialSyncAsync( } catch (Exception ex) { - Logger.LogError($"Failed to create directories. Exception: {ex.Message}"); + Logger.LogError($"Create directories failed. Exception: {ex.Message}"); throw; } @@ -127,7 +151,7 @@ public static async Task RunSharedInitialSyncAsync( } catch (Exception ex) { - Logger.LogError($"Failed to sync {item.LocalPath} ({item.SearchPattern}). Exception: {ex.Message}"); + Logger.LogError($"Sync failed for {item.LocalPath} ({item.SearchPattern}). Exception: {ex.Message}"); } } } @@ -176,7 +200,7 @@ public static void SyncFile(SftpClient sftp, string sourcePath, string destinati retryCount++; if (retryCount >= maxRetries) { - Logger.LogError($"Failed to sync after {maxRetries} retries. Exception: {ex.Message}"); + Logger.LogError($"Sync failed after {maxRetries} retries. Exception: {ex.Message}"); return; } @@ -190,7 +214,7 @@ public static Task> SyncDirectoryAsync(SftpClient sftp, st { if (new DirectoryInfo(sourcePath).EnumerateFiles(searchPattern, SearchOption.TopDirectoryOnly).Any()) { - Logger.LogInfo($"Sync started for {sourcePath}\\{searchPattern}"); + Logger.LogInfo($"Initial sync started for {sourcePath}\\{searchPattern}"); return Task>.Factory.FromAsync(sftp.BeginSynchronizeDirectories, sftp.EndSynchronizeDirectories, sourcePath, @@ -220,7 +244,7 @@ public async Task CreateDirectories(string localPath, string remotePath) } catch (Exception) { - Logger.LogError("Failed to create directories. Check the remote root directory exists."); + Logger.LogError("Failed to create directories. Check the remote target directory exists."); Environment.Exit(-1); } @@ -241,7 +265,9 @@ public async Task InitialSync(string localPath, string remotePath) return; //Get the local directories to sync - var localDirectories = FilteredDirectories(localPath); + var localDirectories = FilteredDirectories(localPath) + .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) + .ToArray(); //Get the remote directories to sync, removing the .DIR suffix if it exists var remoteDirectories = (await ListDirectoryAsync(_sftp, remotePath)).Where(item => item.IsDirectory).ToDictionary(item => @@ -293,19 +319,20 @@ private static string[] FilteredDirectories(string localRootDirectory, string lo string remoteRootDirectory, List? excludedFolders) { - var stack = new Stack<(string LocalPath, string RemotePath)>(); - stack.Push((localRootDirectory, remoteRootDirectory)); + var queue = new Queue<(string LocalPath, string RemotePath)>(); + queue.Enqueue((localRootDirectory, remoteRootDirectory)); - while (stack.Count > 0) + while (queue.Count > 0) { - var current = stack.Pop(); + var current = queue.Dequeue(); yield return current; - foreach (var directory in FilteredDirectories(localRootDirectory, current.LocalPath, excludedFolders)) + foreach (var directory in FilteredDirectories(localRootDirectory, current.LocalPath, excludedFolders) + .OrderBy(path => path, StringComparer.OrdinalIgnoreCase)) { var directoryName = directory.Split(Path.DirectorySeparatorChar).Last(); var remotePath = current.RemotePath + "/" + directoryName; - stack.Push((directory, remotePath)); + queue.Enqueue((directory, remotePath)); } } } @@ -317,7 +344,9 @@ private static async Task CreateDirectoriesInternal( string remotePath, List? excludedFolders) { - var localDirectories = FilteredDirectories(localRootDirectory, localPath, excludedFolders); + var localDirectories = FilteredDirectories(localRootDirectory, localPath, excludedFolders) + .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) + .ToArray(); var remoteDirectories = (await ListDirectoryAsync(sftp, remotePath)).Where(item => item.IsDirectory).ToDictionary(item => { @@ -419,7 +448,9 @@ private bool EnsureConnectedSafe() public Task ConnectAsync() { - return Task.Run(() => EnsureConnectedSafe()); + var task = Task.Run(() => EnsureConnectedSafe()); + _connectTask = task; + return task; } @@ -427,15 +458,28 @@ private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) { try { + if (_shutdownCts.IsCancellationRequested) + return; + if (!DoneInitialSync.IsCompleted) return; + if (_deleteEnabled && arg.ChangeType == WatcherChangeTypes.Deleted) + { + await SyncFileDeleteAsync(arg); + return; + } + if (arg.ChangeType == WatcherChangeTypes.Changed || arg.ChangeType == WatcherChangeTypes.Created || arg.ChangeType == WatcherChangeTypes.Renamed) { + var renamedArgs = arg as RenamedEventArgs; var changedPath = Path.GetDirectoryName(arg.FullPath); var fullRemotePath = GetRemotePathForLocal(changedPath ?? _localRootDirectory); var fullRemoteFilePath = GetRemotePathForLocal(arg.FullPath); + var oldRemoteFilePath = renamedArgs != null + ? GetRemotePathForLocal(renamedArgs.OldFullPath) + : null; await Task.Yield(); bool makeDirectory = true; lock (_activeDirSync) @@ -497,7 +541,7 @@ private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) return; if (DateTime.UtcNow - waitStart > TimeSpan.FromSeconds(30)) { - Logger.LogWarnig($"Timed out waiting for file to be ready: {arg.FullPath}"); + Logger.LogWarnig($"Timeout waiting for file ready: {arg.FullPath}"); return; } await Task.Delay(25); @@ -519,6 +563,31 @@ private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) if (fileConnectionOk) { SyncFile(_sftp, arg.FullPath, fullRemoteFilePath); + + if (_deleteEnabled && oldRemoteFilePath != null + && !string.Equals(oldRemoteFilePath, fullRemoteFilePath, StringComparison.OrdinalIgnoreCase)) + { + if (_sftp.Exists(oldRemoteFilePath)) + { + var attributes = _sftp.GetAttributes(oldRemoteFilePath); + if (!attributes.IsDirectory) + { + _sftp.DeleteFile(oldRemoteFilePath); + } + else + { + Logger.LogWarnig($"Rename cleanup skipped (directory): {oldRemoteFilePath}"); + } + } + else + { + Logger.LogWarnig($"Rename cleanup skipped (not found): {oldRemoteFilePath}"); + } + } + else if (_deleteEnabled && oldRemoteFilePath == null) + { + Logger.LogWarnig("Rename cleanup skipped (missing old path)."); + } } } finally @@ -536,15 +605,201 @@ private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) } catch (Exception ex) { - Logger.LogError($"Unhandled exception in file sync handler: {ex.Message}"); + Logger.LogError($"Unhandled file sync handler exception: {ex.Message}"); + } + } + + private async void Fsw_DirectoryDeleted(object? sender, FileSystemEventArgs arg) + { + try + { + if (_shutdownCts.IsCancellationRequested) + return; + + if (!DoneInitialSync.IsCompleted) + return; + + var remotePath = GetRemotePathForLocal(arg.FullPath); + await _sftpLock.WaitAsync(); + try + { + if (EnsureConnectedSafe()) + { + if (!_sftp.Exists(remotePath) && !_sftp.Exists(remotePath + ".DIR")) + { + Logger.LogWarnig($"Deleting directory skipped (not found): {remotePath}"); + } + else + { + Logger.LogInfo($"Deleting directory {remotePath}"); + DeleteRemotePathRecursive(_sftp, remotePath); + } + } + } + finally + { + _sftpLock.Release(); + } + } + catch (Exception ex) + { + Logger.LogError($"Unhandled directory delete handler exception: {ex.Message}"); + } + } + + private async void Fsw_DirectoryCreated(object? sender, FileSystemEventArgs arg) + { + try + { + if (_shutdownCts.IsCancellationRequested) + return; + + if (!DoneInitialSync.IsCompleted) + return; + + if (IsExcludedDirectoryPath(arg.FullPath)) + return; + + var remotePath = GetRemotePathForLocal(arg.FullPath); + await _sftpLock.WaitAsync(); + try + { + if (EnsureConnectedSafe()) + { + if (!_sftp.Exists(remotePath)) + { + Logger.LogInfo($"Creating directory {remotePath}"); + _sftp.CreateDirectory(remotePath); + } + } + } + finally + { + _sftpLock.Release(); + } + } + catch (Exception ex) + { + Logger.LogError($"Unhandled directory create handler exception: {ex.Message}"); + } + } + + private async void Fsw_DirectoryRenamed(object? sender, RenamedEventArgs arg) + { + try + { + if (_shutdownCts.IsCancellationRequested) + return; + + if (!DoneInitialSync.IsCompleted) + return; + + if (IsExcludedDirectoryPath(arg.OldFullPath) || IsExcludedDirectoryPath(arg.FullPath)) + return; + + var oldRemotePath = GetRemotePathForLocal(arg.OldFullPath); + var newRemotePath = GetRemotePathForLocal(arg.FullPath); + + await _sftpLock.WaitAsync(); + try + { + if (!EnsureConnectedSafe()) + return; + + if (TryRenameRemoteDirectory(_sftp, oldRemotePath, newRemotePath)) + { + Logger.LogInfo($"Renamed directory {oldRemotePath} -> {newRemotePath}"); + return; + } + + Logger.LogWarnig($"Directory rename failed. Attempting fallback: {oldRemotePath} -> {newRemotePath}"); + + MoveRemoteDirectoryRecursive(_sftp, oldRemotePath, newRemotePath); + + Logger.LogInfo($"Fallback directory rename completed: {oldRemotePath} -> {newRemotePath}"); + } + finally + { + _sftpLock.Release(); + } + } + catch (Exception ex) + { + Logger.LogError($"Unhandled directory rename handler exception: {ex.Message}"); + } + } + + private async Task SyncFileDeleteAsync(FileSystemEventArgs arg) + { + if (_shutdownCts.IsCancellationRequested) + return; + + var remotePath = GetRemotePathForLocal(arg.FullPath); + + await _sftpLock.WaitAsync(); + + try + { + if (!EnsureConnectedSafe()) + return; + + if (_sftp.Exists(remotePath)) + { + var attributes = _sftp.GetAttributes(remotePath); + + if (!attributes.IsDirectory) + { + Logger.LogInfo($"Syncing delete {arg.FullPath}"); + _sftp.DeleteFile(remotePath); + } + else + { + Logger.LogWarnig($"Syncing delete skipped (directory): {remotePath}"); + } + } + else + { + Logger.LogWarnig($"Syncing delete {remotePath} not found"); + } + } + finally + { + _sftpLock.Release(); } } public void Dispose() { - if (_sftp != null) + lock (_disposeLock) { + if (_disposed) + return; + + if (!_shutdownCts.IsCancellationRequested) + { + _shutdownCts.Cancel(); + } + + var connectTask = _connectTask; + if (connectTask != null && !connectTask.IsCompleted) + { + _ = connectTask.ContinueWith(_ => DisposeNow(), TaskScheduler.Default); + return; + } + + DisposeNow(); + } + } + + private void DisposeNow() + { + lock (_disposeLock) + { + if (_disposed) + return; + _disposed = true; + _shutdownCts.Dispose(); _sftp.Dispose(); } } @@ -562,5 +817,147 @@ public SyncWorkItem(string localPath, string remotePath, string searchPattern) public string RemotePath { get; } public string SearchPattern { get; } } + + private bool IsExcludedDirectoryPath(string localPath) + { + var fullPath = Path.GetFullPath(localPath).TrimEnd(Path.DirectorySeparatorChar); + if (!fullPath.StartsWith(_localRootDirectory, StringComparison.OrdinalIgnoreCase)) + return false; + + var relativePath = fullPath.Substring(_localRootDirectory.Length); + if (string.IsNullOrWhiteSpace(relativePath)) + return false; + + bool isExcluded = relativePath.EndsWith(".git", StringComparison.OrdinalIgnoreCase) + || relativePath.EndsWith(".vs", StringComparison.OrdinalIgnoreCase) + || relativePath.EndsWith("bin", StringComparison.OrdinalIgnoreCase) + || relativePath.EndsWith("obj", StringComparison.OrdinalIgnoreCase) + || relativePath.Contains("."); + + if (!isExcluded && _excludedFolders != null && _excludedFolders.Count > 0) + { + isExcluded = _excludedFolders.Any(excluded => + { + var excludedFullPath = Path.GetFullPath(excluded).TrimEnd(Path.DirectorySeparatorChar); + return string.Equals(fullPath, excludedFullPath, StringComparison.OrdinalIgnoreCase); + }); + } + + return isExcluded; + } + + private static void DeleteRemotePathRecursive(SftpClient sftp, string remotePath) + { + if (!sftp.Exists(remotePath)) + { + var vmsDirectoryPath = remotePath + ".DIR"; + if (!sftp.Exists(vmsDirectoryPath)) + return; + remotePath = vmsDirectoryPath; + } + + var attributes = sftp.GetAttributes(remotePath); + if (!attributes.IsDirectory) + { + sftp.DeleteFile(remotePath); + return; + } + + foreach (var entry in sftp.ListDirectory(remotePath)) + { + if (entry.Name == "." || entry.Name == "..") + continue; + + if (entry.IsDirectory) + { + DeleteRemotePathRecursive(sftp, entry.FullName); + } + else + { + sftp.DeleteFile(entry.FullName); + } + } + + sftp.DeleteDirectory(remotePath); + } + + private static bool TryRenameRemoteDirectory(SftpClient sftp, string oldRemotePath, string newRemotePath) + { + string? actualOldPath = null; + string? actualNewPath = null; + + if (sftp.Exists(oldRemotePath)) + { + actualOldPath = oldRemotePath; + actualNewPath = newRemotePath; + } + else if (sftp.Exists(oldRemotePath + ".DIR")) + { + actualOldPath = oldRemotePath + ".DIR"; + actualNewPath = newRemotePath + ".DIR"; + } + + if (actualOldPath == null || actualNewPath == null) + return false; + + try + { + sftp.RenameFile(actualOldPath, actualNewPath); + return true; + } + catch (Exception ex) + { + Logger.LogError($"Directory rename failed: {ex.Message}"); + return false; + } + } + + private static void MoveRemoteDirectoryRecursive(SftpClient sftp, string oldRemotePath, string newRemotePath) + { + string actualOldPath; + string actualNewPath = newRemotePath; + + if (sftp.Exists(oldRemotePath)) + { + actualOldPath = oldRemotePath; + } + else if (sftp.Exists(oldRemotePath + ".DIR")) + { + actualOldPath = oldRemotePath + ".DIR"; + actualNewPath = newRemotePath + ".DIR"; + } + else + { + Logger.LogError($"Directory not found for rename: {oldRemotePath}"); + return; + } + + if (!sftp.Exists(actualNewPath)) + { + sftp.CreateDirectory(actualNewPath); + } + + foreach (var entry in sftp.ListDirectory(actualOldPath)) + { + if (entry.Name == "." || entry.Name == "..") + continue; + + var destinationPath = actualNewPath + "/" + entry.Name; + try + { + sftp.RenameFile(entry.FullName, destinationPath); + } + catch (Exception ex) + { + Logger.LogError($"Remote file move failed for {entry.FullName}: {ex.Message}"); + if (entry.IsDirectory) + { + MoveRemoteDirectoryRecursive(sftp, entry.FullName, destinationPath); + } + } + } + + DeleteRemotePathRecursive(sftp, actualOldPath); + } } } diff --git a/SFTPSyncLib/SyncDirector.cs b/SFTPSyncLib/SyncDirector.cs index 813d695..253d679 100644 --- a/SFTPSyncLib/SyncDirector.cs +++ b/SFTPSyncLib/SyncDirector.cs @@ -7,20 +7,37 @@ public class SyncDirector { FileSystemWatcher _fsw; List<(Regex, Action)> callbacks = new List<(Regex, Action)>(); + readonly List> directoryDeleteCallbacks = new List>(); + readonly List> directoryCreateCallbacks = new List>(); + readonly List> directoryRenameCallbacks = new List>(); + readonly HashSet knownDirectories = new HashSet(StringComparer.OrdinalIgnoreCase); + readonly object directoryLock = new object(); + readonly bool deleteEnabled; + bool _disposed; - public SyncDirector(string rootFolder) + public SyncDirector(string rootFolder, bool deleteEnabled = false) { - _fsw = new FileSystemWatcher(rootFolder, "*.*") + this.deleteEnabled = deleteEnabled; + _fsw = new FileSystemWatcher(rootFolder, "*") { IncludeSubdirectories = true, - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName, // Max allowed size; helps reduce missed events during bursts. InternalBufferSize = 64 * 1024 }; + if (deleteEnabled) + { + SeedKnownDirectories(rootFolder); + } + _fsw.Created += Fsw_Created; _fsw.Changed += Fsw_Changed; _fsw.Renamed += Fsw_Renamed; + if (deleteEnabled) + { + _fsw.Deleted += Fsw_Deleted; + } _fsw.Error += Fsw_Error; _fsw.EnableRaisingEvents = true; @@ -34,8 +51,36 @@ public void AddCallback(string match, Action handler) callbacks.Add((new Regex(regexPattern, RegexOptions.IgnoreCase), handler)); } + public void AddDirectoryDeleteCallback(Action handler) + { + directoryDeleteCallbacks.Add(handler); + } + + public void AddDirectoryCreateCallback(Action handler) + { + directoryCreateCallbacks.Add(handler); + } + + public void AddDirectoryRenameCallback(Action handler) + { + directoryRenameCallbacks.Add(handler); + } + private void Fsw_Created(object sender, FileSystemEventArgs e) { + if (deleteEnabled) + { + TrackDirectoryIfPresent(e.FullPath); + } + + if (Directory.Exists(e.FullPath)) + { + foreach (var callback in directoryCreateCallbacks) + { + callback(e); + } + } + var name = Path.GetFileName(e.FullPath); foreach (var (regex, callback) in callbacks) { @@ -60,6 +105,43 @@ private void Fsw_Changed(object sender, FileSystemEventArgs e) private void Fsw_Renamed(object sender, RenamedEventArgs e) { + if (deleteEnabled) + { + HandleRenameForDirectoryTracking(e.OldFullPath, e.FullPath); + } + + if (Directory.Exists(e.FullPath)) + { + foreach (var callback in directoryRenameCallbacks) + { + callback(e); + } + } + + var name = Path.GetFileName(e.FullPath); + foreach (var (regex, callback) in callbacks) + { + if (regex.IsMatch(name)) + { + callback(e); + } + } + } + + private void Fsw_Deleted(object sender, FileSystemEventArgs e) + { + if (!deleteEnabled) + return; + + if (TryRemoveKnownDirectory(e.FullPath)) + { + foreach (var callback in directoryDeleteCallbacks) + { + callback(e); + } + return; + } + var name = Path.GetFileName(e.FullPath); foreach (var (regex, callback) in callbacks) { @@ -74,5 +156,79 @@ private void Fsw_Error(object sender, ErrorEventArgs e) { Logger.LogError($"FileSystemWatcher error: {e.GetException()?.Message}"); } + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _fsw.EnableRaisingEvents = false; + _fsw.Dispose(); + } + + private void SeedKnownDirectories(string rootFolder) + { + try + { + var directories = Directory.EnumerateDirectories(rootFolder, "*", SearchOption.AllDirectories) + .Select(NormalizePath) + .ToList(); + + lock (directoryLock) + { + knownDirectories.Add(NormalizePath(rootFolder)); + foreach (var dir in directories) + { + knownDirectories.Add(dir); + } + } + } + catch (Exception ex) + { + Logger.LogError($"Failed to seed directory tracking. Exception: {ex.Message}"); + } + } + + private void TrackDirectoryIfPresent(string path) + { + if (!Directory.Exists(path)) + return; + + var normalized = NormalizePath(path); + lock (directoryLock) + { + knownDirectories.Add(normalized); + } + } + + private void HandleRenameForDirectoryTracking(string oldPath, string newPath) + { + var oldNormalized = NormalizePath(oldPath); + bool wasDirectory; + lock (directoryLock) + { + wasDirectory = knownDirectories.Remove(oldNormalized); + } + + if (wasDirectory) + { + TrackDirectoryIfPresent(newPath); + } + } + + private bool TryRemoveKnownDirectory(string path) + { + var normalized = NormalizePath(path); + lock (directoryLock) + { + return knownDirectories.Remove(normalized); + } + } + + private static string NormalizePath(string path) + { + return Path.TrimEndingDirectorySeparator(Path.GetFullPath(path)); + } } } diff --git a/SFTPSyncSetup/Product.wxs b/SFTPSyncSetup/Product.wxs index 26196c9..74146a9 100644 --- a/SFTPSyncSetup/Product.wxs +++ b/SFTPSyncSetup/Product.wxs @@ -5,7 +5,7 @@ Id="*" Name="SFTPSync" Language="1033" - Version="1.6" + Version="1.7" Manufacturer="Synergex International Corporation" UpgradeCode="6000f870-b811-4e22-b80b-5b8956317d09"> diff --git a/SFTPSyncSetup/SFTPSyncSetup.wixproj b/SFTPSyncSetup/SFTPSyncSetup.wixproj index 54c01f4..04ec8da 100644 --- a/SFTPSyncSetup/SFTPSyncSetup.wixproj +++ b/SFTPSyncSetup/SFTPSyncSetup.wixproj @@ -6,7 +6,7 @@ 3.10 5074cbcb-641b-4a9c-b3bc-8dd0b78810a6 2.0 - SFTPSync-1.6 + SFTPSync-1.7 Package SFTPSyncSetup diff --git a/SFTPSyncUI/AppSettings.cs b/SFTPSyncUI/AppSettings.cs index f568ed2..66ea6fd 100644 --- a/SFTPSyncUI/AppSettings.cs +++ b/SFTPSyncUI/AppSettings.cs @@ -192,6 +192,25 @@ public bool AccessVerified } } + // Support delted files and folders + + private bool deleteEnabled = false; + public bool DeleteEnabled + { + get => deleteEnabled; + set + { + if (deleteEnabled != value) + { + deleteEnabled = value; + if (!initialLoadSettings) + { + SaveToFile(); + } + } + } + } + //Local path private string localPath = String.Empty; @@ -331,6 +350,5 @@ public List ExcludedDirectories } } } - } } \ No newline at end of file diff --git a/SFTPSyncUI/MainForm.Designer.cs b/SFTPSyncUI/MainForm.Designer.cs index 2eb9b26..3b8b288 100644 --- a/SFTPSyncUI/MainForm.Designer.cs +++ b/SFTPSyncUI/MainForm.Designer.cs @@ -51,15 +51,19 @@ private void InitializeComponent() // listBoxMessages // listBoxMessages.Dock = DockStyle.Fill; + listBoxMessages.DrawMode = DrawMode.OwnerDrawFixed; + listBoxMessages.Font = new Font("Cascadia Mono", 8F, FontStyle.Regular, GraphicsUnit.Point, 0); listBoxMessages.FormattingEnabled = true; listBoxMessages.HorizontalScrollbar = true; - listBoxMessages.Location = new Point(0, 28); - listBoxMessages.Margin = new Padding(10); + listBoxMessages.ItemHeight = 17; + listBoxMessages.Location = new Point(0, 24); + listBoxMessages.Margin = new Padding(9, 8, 9, 8); listBoxMessages.Name = "listBoxMessages"; listBoxMessages.ScrollAlwaysVisible = true; listBoxMessages.SelectionMode = SelectionMode.None; - listBoxMessages.Size = new Size(1182, 725); + listBoxMessages.Size = new Size(1036, 525); listBoxMessages.TabIndex = 14; + listBoxMessages.DrawItem += ListBoxMessages_DrawItem; // // menuStrip1 // @@ -67,7 +71,8 @@ private void InitializeComponent() menuStrip1.Items.AddRange(new ToolStripItem[] { mnuFile, mnuTools, mnuHelp }); menuStrip1.Location = new Point(0, 0); menuStrip1.Name = "menuStrip1"; - menuStrip1.Size = new Size(1182, 28); + menuStrip1.Padding = new Padding(5, 2, 0, 2); + menuStrip1.Size = new Size(1036, 24); menuStrip1.TabIndex = 5; menuStrip1.Text = "menuStrip1"; // @@ -75,14 +80,14 @@ private void InitializeComponent() // mnuFile.DropDownItems.AddRange(new ToolStripItem[] { mnuFileStartSync, mnuFileStopSync, toolStripMenuItem1, mnuFileExit }); mnuFile.Name = "mnuFile"; - mnuFile.Size = new Size(46, 24); + mnuFile.Size = new Size(37, 20); mnuFile.Text = "&File"; // // mnuFileStartSync // mnuFileStartSync.Enabled = false; mnuFileStartSync.Name = "mnuFileStartSync"; - mnuFileStartSync.Size = new Size(224, 26); + mnuFileStartSync.Size = new Size(134, 22); mnuFileStartSync.Text = "&Start sync"; mnuFileStartSync.Click += mnuFileStartSync_Click; // @@ -90,20 +95,20 @@ private void InitializeComponent() // mnuFileStopSync.Enabled = false; mnuFileStopSync.Name = "mnuFileStopSync"; - mnuFileStopSync.Size = new Size(224, 26); + mnuFileStopSync.Size = new Size(134, 22); mnuFileStopSync.Text = "S&top sync"; mnuFileStopSync.Click += mnuFileStopSync_Click; // // toolStripMenuItem1 // toolStripMenuItem1.Name = "toolStripMenuItem1"; - toolStripMenuItem1.Size = new Size(221, 6); + toolStripMenuItem1.Size = new Size(131, 6); // // mnuFileExit // mnuFileExit.Name = "mnuFileExit"; mnuFileExit.ShortcutKeys = Keys.Alt | Keys.F4; - mnuFileExit.Size = new Size(224, 26); + mnuFileExit.Size = new Size(134, 22); mnuFileExit.Text = "E&xit"; mnuFileExit.Click += mnuFileExit_Click; // @@ -111,13 +116,13 @@ private void InitializeComponent() // mnuTools.DropDownItems.AddRange(new ToolStripItem[] { mnuToolsOptions }); mnuTools.Name = "mnuTools"; - mnuTools.Size = new Size(58, 24); + mnuTools.Size = new Size(47, 20); mnuTools.Text = "&Tools"; // // mnuToolsOptions // mnuToolsOptions.Name = "mnuToolsOptions"; - mnuToolsOptions.Size = new Size(224, 26); + mnuToolsOptions.Size = new Size(116, 22); mnuToolsOptions.Text = "&Options"; mnuToolsOptions.Click += mnuToolsOptions_Click; // @@ -125,7 +130,7 @@ private void InitializeComponent() // mnuHelp.DropDownItems.AddRange(new ToolStripItem[] { mnuHelpView, mnuHelpAbout }); mnuHelp.Name = "mnuHelp"; - mnuHelp.Size = new Size(55, 24); + mnuHelp.Size = new Size(44, 20); mnuHelp.Text = "&Help"; // // mnuHelpView @@ -133,14 +138,14 @@ private void InitializeComponent() mnuHelpView.Enabled = false; mnuHelpView.Name = "mnuHelpView"; mnuHelpView.ShortcutKeys = Keys.F1; - mnuHelpView.Size = new Size(184, 26); + mnuHelpView.Size = new Size(146, 22); mnuHelpView.Text = "&View Help"; mnuHelpView.Click += mnuHelpView_Click; // // mnuHelpAbout // mnuHelpAbout.Name = "mnuHelpAbout"; - mnuHelpAbout.Size = new Size(184, 26); + mnuHelpAbout.Size = new Size(146, 22); mnuHelpAbout.Text = "&About"; mnuHelpAbout.Click += mnuHelpAbout_Click; // @@ -148,30 +153,31 @@ private void InitializeComponent() // StatusBar.ImageScalingSize = new Size(20, 20); StatusBar.Items.AddRange(new ToolStripItem[] { Panel1 }); - StatusBar.Location = new Point(0, 729); + StatusBar.Location = new Point(0, 549); StatusBar.Name = "StatusBar"; - StatusBar.Size = new Size(1182, 24); + StatusBar.Padding = new Padding(1, 0, 12, 0); + StatusBar.Size = new Size(1036, 22); StatusBar.TabIndex = 6; + StatusBar.Text = "StatusBar.Text"; // // Panel1 // - Panel1.AutoSize = false; - Panel1.ImageAlign = ContentAlignment.MiddleLeft; Panel1.Name = "Panel1"; - Panel1.Size = new Size(1200, 18); - Panel1.TextAlign = ContentAlignment.MiddleLeft; + Panel1.Size = new Size(76, 17); + Panel1.Text = "Sync inactive"; // // MainForm // - AutoScaleDimensions = new SizeF(8F, 20F); + AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(1182, 753); - Controls.Add(StatusBar); + ClientSize = new Size(1036, 571); Controls.Add(listBoxMessages); Controls.Add(menuStrip1); + Controls.Add(StatusBar); Icon = (Icon)resources.GetObject("$this.Icon"); MainMenuStrip = menuStrip1; - MinimumSize = new Size(1200, 800); + Margin = new Padding(3, 2, 3, 2); + MinimumSize = new Size(1052, 608); Name = "MainForm"; StartPosition = FormStartPosition.CenterScreen; Text = "SFTP Sync"; @@ -185,7 +191,6 @@ private void InitializeComponent() } #endregion - private ToolStripStatusLabel Panel1; private StatusStrip StatusBar; private ListBox listBoxMessages; private MenuStrip menuStrip1; @@ -199,5 +204,6 @@ private void InitializeComponent() private ToolStripSeparator toolStripMenuItem1; private ToolStripMenuItem mnuTools; private ToolStripMenuItem mnuToolsOptions; + private ToolStripStatusLabel Panel1; } } diff --git a/SFTPSyncUI/MainForm.cs b/SFTPSyncUI/MainForm.cs index 1310532..9fd30c7 100644 --- a/SFTPSyncUI/MainForm.cs +++ b/SFTPSyncUI/MainForm.cs @@ -3,6 +3,7 @@ using SFTPSyncLib; using System.Diagnostics; using System.Text.RegularExpressions; +using System.Windows.Forms; namespace SFTPSyncUI { @@ -72,12 +73,9 @@ public MainForm(AppSettings settings) }; //Set the initial window visibility - ShowInTaskbar = _settings.StartInTray; WindowState = _settings.StartInTray ? FormWindowState.Minimized : FormWindowState.Normal; + ShowInTaskbar = WindowState != FormWindowState.Minimized; - // Can we and should we start the sync process now? - if (checkCanStartSync() && _settings.AutoStartSync) - startSync(); } private void mnuFileExit_Click(object sender, EventArgs e) { @@ -225,8 +223,7 @@ private void startSync() if (!syncRunning) { mnuFileStartSync.Enabled = false; - //TODO: Can't currently enable stop sync because it crashes the app - //mnuFileStopSync.Enabled = true; + mnuFileStopSync.Enabled = true; syncRunning = true; SFTPSyncUI.StartSync(AppendLog); } @@ -259,26 +256,72 @@ private void MainForm_FormClosing(object sender, FormClosingEventArgs e) } } + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + ShowInTaskbar = WindowState != FormWindowState.Minimized && Visible; + } + + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + + // Auto-start after handle exists so BeginInvoke works even when minimized. + if (checkCanStartSync() && _settings.AutoStartSync) + startSync(); + } + public void SetStatusBarText(string text) { if (InvokeRequired) { - Invoke(new Action(() => StatusBar.Items[0].Text = text)); + Invoke(new Action(() => Panel1.Text = text)); } else { - StatusBar.Items[0].Text = text; + Panel1.Text = text; } } private void mnuToolsOptions_Click(object sender, EventArgs e) { - var dialog = new SettingsForm(_settings,syncRunning); + var dialog = new SettingsForm(_settings, syncRunning); dialog.ShowDialog(this); if (!syncRunning) checkCanStartSync(); } + + private void ListBoxMessages_DrawItem(object sender, DrawItemEventArgs e) + { + if (e.Index < 0 || e.Font == null) + return; + + string? itemText = listBoxMessages.Items[e.Index].ToString(); + + if (itemText == null) + return; + + Color textColor = Color.Black; + + if (itemText.Contains(" ERR: ")) + textColor = Color.Red; + else if (itemText.Contains(" WRN: ")) + textColor = Color.Orange; + + e.DrawBackground(); + + TextRenderer.DrawText( + e.Graphics, + itemText, + e.Font, + e.Bounds, + textColor, + TextFormatFlags.Left | TextFormatFlags.VerticalCenter + ); + + e.DrawFocusRectangle(); + } } } diff --git a/SFTPSyncUI/SFTPSyncUI.cs b/SFTPSyncUI/SFTPSyncUI.cs index 6124146..06b7dc7 100644 --- a/SFTPSyncUI/SFTPSyncUI.cs +++ b/SFTPSyncUI/SFTPSyncUI.cs @@ -12,7 +12,6 @@ internal static class SFTPSyncUI public static string ExecutableFile = String.Empty; private static AppSettings? settings; - private static MainForm? mainForm; private const string autoRunRegistryKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; @@ -196,36 +195,58 @@ static void RemoveProgramFromStartup() } public static List RemoteSyncWorkers = new List(); + private static SyncDirector? activeDirector; + private static Task? syncTask; + private static CancellationTokenSource? syncCts; + + private static readonly object DirectorLock = new object(); + private static readonly object SyncStateLock = new object(); + private static readonly object RemoteSyncLock = new object(); /// - /// + /// Start syncing files between local and remote /// /// public static async void StartSync(Action loggerAction) { + // Make sure we have settings if (settings == null) return; Logger.LogUpdated += loggerAction; Logger.LogInfo("Starting sync workers..."); - mainForm?.SetStatusBarText("Performing initial sync..."); - var capturedSettings = settings; - var capturedMainForm = mainForm; - var setStatus = (string text) => + var setStatus = new Action(status => { - if (capturedMainForm == null) - return; - capturedMainForm.BeginInvoke((Action)(() => capturedMainForm.SetStatusBarText(text))); - }; + mainForm?.BeginInvoke((Action)(() => mainForm.SetStatusBarText(status))); + }); + + setStatus("Performing initial sync... DO NOT ALTER SYNCED FILES UNTIL COMPLETE"); + + CancellationToken token; + + lock (SyncStateLock) + { + syncCts?.Cancel(); + syncCts?.Dispose(); + syncCts = new CancellationTokenSource(); + token = syncCts.Token; + } try { - await Task.Run(async () => + syncTask = Task.Run(async () => { - var director = new SyncDirector(capturedSettings.LocalPath); + if (token.IsCancellationRequested) + return; - var patterns = capturedSettings.LocalSearchPattern + var director = new SyncDirector(settings.LocalPath, settings.DeleteEnabled); + lock (DirectorLock) + { + activeDirector = director; + } + + var patterns = settings.LocalSearchPattern .Split(';', StringSplitOptions.RemoveEmptyEntries) .Select(pattern => pattern.Trim()) .Where(pattern => pattern.Length > 0) @@ -233,8 +254,11 @@ await Task.Run(async () => if (patterns.Length == 0) { - Logger.LogError("No valid search patterns were configured."); - setStatus("Invalid search patterns"); + if (!token.IsCancellationRequested) + { + Logger.LogError("No valid search patterns were configured."); + setStatus("Invalid search patterns"); + } return; } @@ -243,34 +267,56 @@ await Task.Run(async () => foreach (var pattern in patterns) { + if (token.IsCancellationRequested) + return; + 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}"); + int workerIndex; + RemoteSync worker; + lock (RemoteSyncLock) + { + workerIndex = RemoteSyncWorkers.Count; + worker = new RemoteSync( + settings.RemoteHost, + settings.RemoteUsername, + DPAPIEncryption.Decrypt(settings.RemotePassword), + settings.LocalPath, + settings.RemotePath, + pattern, + director, + settings.ExcludedDirectories, + initialSyncTask, + settings.DeleteEnabled, + workerIndex == 0); + RemoteSyncWorkers.Add(worker); + } + + Logger.LogInfo($"Started sync worker {workerIndex + 1} for pattern {pattern}"); } catch (Exception) { - Logger.LogError($"Failed to start sync worker for pattern {pattern}"); + if (!token.IsCancellationRequested) + Logger.LogError($"Failed to start sync worker for pattern {pattern}"); } } var connectTasks = new List(); - Logger.LogInfo($"Establishing SFTP connections for {RemoteSyncWorkers.Count} workers"); + RemoteSync[] workerSnapshot; + lock (RemoteSyncLock) + { + workerSnapshot = RemoteSyncWorkers.ToArray(); + } - for (int i = 0; i < RemoteSyncWorkers.Count; i++) + if (token.IsCancellationRequested) + return; + + Logger.LogInfo($"Establishing SFTP connections for {workerSnapshot.Length} workers"); + + for (int i = 0; i < workerSnapshot.Length; i++) { - connectTasks.Add(RemoteSyncWorkers[i].ConnectAsync()); + connectTasks.Add(workerSnapshot[i].ConnectAsync()); } await Task.WhenAll(connectTasks); @@ -279,24 +325,33 @@ await Task.Run(async () => try { runInitialSyncTask = RemoteSync.RunSharedInitialSyncAsync( - capturedSettings.RemoteHost, - capturedSettings.RemoteUsername, - DPAPIEncryption.Decrypt(capturedSettings.RemotePassword), - capturedSettings.LocalPath, - capturedSettings.RemotePath, + settings.RemoteHost, + settings.RemoteUsername, + DPAPIEncryption.Decrypt(settings.RemotePassword), + settings.LocalPath, + settings.RemotePath, patterns, - capturedSettings.ExcludedDirectories, + settings.ExcludedDirectories, patterns.Length); } catch (Exception ex) { - Logger.LogError($"Failed to start initial sync. Exception: {ex.Message}"); - setStatus("Initial sync failed"); + if (!token.IsCancellationRequested) + { + Logger.LogError($"Failed to start initial sync. Exception: {ex.Message}"); + setStatus("Initial sync failed"); + } return; } + if (token.IsCancellationRequested) + return; + await runInitialSyncTask.ContinueWith(t => { + if (token.IsCancellationRequested) + return; + if (t.IsFaulted && t.Exception != null) { initialSyncTcs.TrySetException(t.Exception.InnerExceptions); @@ -319,19 +374,30 @@ await runInitialSyncTask.ContinueWith(t => } catch (Exception ex) { - Logger.LogError($"Initial sync failed. Exception: {ex.Message}"); - setStatus("Initial sync failed"); + if (!token.IsCancellationRequested) + { + 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"); + if (!token.IsCancellationRequested) + { + Logger.LogInfo("Initial sync complete, real-time sync active"); + setStatus("Sync active"); + } }); + + await syncTask; } catch (Exception ex) { - Logger.LogError($"Failed to start sync. Exception: {ex.Message}"); - mainForm?.SetStatusBarText("Sync failed"); + if (!token.IsCancellationRequested) + { + Logger.LogError($"Failed to start sync. Exception: {ex.Message}"); + setStatus("Sync failed"); + } return; } } @@ -340,12 +406,32 @@ await runInitialSyncTask.ContinueWith(t => /// /// /// - public static void StopSync(Action loggerAction) + public static async void StopSync(Action loggerAction) { - Logger.LogInfo("Stopping sync..."); - mainForm?.SetStatusBarText("Stopping sync..."); + var setStatus = new Action(status => + { + mainForm?.BeginInvoke((Action)(() => mainForm.SetStatusBarText(status))); + }); + + setStatus("Stopping sync..."); + + lock (SyncStateLock) + { + syncCts?.Cancel(); + syncCts?.Dispose(); + syncCts = null; + } + + Logger.LogUpdated -= loggerAction; - foreach (var remoteSync in RemoteSyncWorkers) + RemoteSync[] workerSnapshot; + lock (RemoteSyncLock) + { + workerSnapshot = RemoteSyncWorkers.ToArray(); + RemoteSyncWorkers.Clear(); + } + + foreach (var remoteSync in workerSnapshot) { try { @@ -354,12 +440,27 @@ public static void StopSync(Action loggerAction) catch { /* Swallow any exceptions */ } } - RemoteSyncWorkers.Clear(); + lock (DirectorLock) + { + activeDirector?.Dispose(); + activeDirector = null; + } - Logger.LogUpdated -= loggerAction; + var task = syncTask; + if (task != null) + { + try + { + await task; + } + catch + { + } + } + + loggerAction($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} INF: Sync stopped"); - Logger.LogInfo("Sync stopped"); - mainForm?.SetStatusBarText("Sync stopped"); + setStatus("Sync inactive"); } } } diff --git a/SFTPSyncUI/SFTPSyncUI.csproj b/SFTPSyncUI/SFTPSyncUI.csproj index f2d3759..ad38383 100644 --- a/SFTPSyncUI/SFTPSyncUI.csproj +++ b/SFTPSyncUI/SFTPSyncUI.csproj @@ -20,10 +20,10 @@ LICENSE.md True Synergex International, Inc. - 1.6 - 1.6 + 1.7 + 1.7 preview - 1.6 + 1.7 diff --git a/SFTPSyncUI/SettingsForm.Designer.cs b/SFTPSyncUI/SettingsForm.Designer.cs index d45c938..9385872 100644 --- a/SFTPSyncUI/SettingsForm.Designer.cs +++ b/SFTPSyncUI/SettingsForm.Designer.cs @@ -53,6 +53,7 @@ private void InitializeComponent() btnAdd = new Button(); btnRemove = new Button(); groupLocal = new GroupBox(); + chkSupportDelete = new CheckBox(); groupRemote = new GroupBox(); groupStartup = new GroupBox(); btnClose = new Button(); @@ -64,9 +65,10 @@ private void InitializeComponent() // checkStartAtLogin // checkStartAtLogin.AutoSize = true; - checkStartAtLogin.Location = new Point(46, 36); + checkStartAtLogin.Location = new Point(40, 27); + checkStartAtLogin.Margin = new Padding(3, 2, 3, 2); checkStartAtLogin.Name = "checkStartAtLogin"; - checkStartAtLogin.Size = new Size(117, 24); + checkStartAtLogin.Size = new Size(93, 19); checkStartAtLogin.TabIndex = 12; checkStartAtLogin.Text = "Start at &login"; checkStartAtLogin.UseVisualStyleBackColor = true; @@ -75,9 +77,10 @@ private void InitializeComponent() // checkStartInTray // checkStartInTray.AutoSize = true; - checkStartInTray.Location = new Point(180, 36); + checkStartInTray.Location = new Point(158, 27); + checkStartInTray.Margin = new Padding(3, 2, 3, 2); checkStartInTray.Name = "checkStartInTray"; - checkStartInTray.Size = new Size(156, 24); + checkStartInTray.Size = new Size(126, 19); checkStartInTray.TabIndex = 13; checkStartInTray.Text = "Start in system &tray"; checkStartInTray.UseVisualStyleBackColor = true; @@ -86,9 +89,10 @@ private void InitializeComponent() // buttonVerifyAccess // buttonVerifyAccess.Enabled = false; - buttonVerifyAccess.Location = new Point(725, 110); + buttonVerifyAccess.Location = new Point(634, 82); + buttonVerifyAccess.Margin = new Padding(3, 2, 3, 2); buttonVerifyAccess.Name = "buttonVerifyAccess"; - buttonVerifyAccess.Size = new Size(104, 29); + buttonVerifyAccess.Size = new Size(91, 22); buttonVerifyAccess.TabIndex = 11; buttonVerifyAccess.Text = "&Verify Access"; buttonVerifyAccess.UseVisualStyleBackColor = true; @@ -97,9 +101,10 @@ private void InitializeComponent() // checkBoxShowPassword // checkBoxShowPassword.AutoSize = true; - checkBoxShowPassword.Location = new Point(695, 70); + checkBoxShowPassword.Location = new Point(608, 52); + checkBoxShowPassword.Margin = new Padding(3, 2, 3, 2); checkBoxShowPassword.Name = "checkBoxShowPassword"; - checkBoxShowPassword.Size = new Size(134, 24); + checkBoxShowPassword.Size = new Size(108, 19); checkBoxShowPassword.TabIndex = 10; checkBoxShowPassword.Text = "Show &password"; checkBoxShowPassword.UseVisualStyleBackColor = true; @@ -108,9 +113,10 @@ private void InitializeComponent() // checkBoxAutoStartSync // checkBoxAutoStartSync.AutoSize = true; - checkBoxAutoStartSync.Location = new Point(356, 36); + checkBoxAutoStartSync.Location = new Point(312, 27); + checkBoxAutoStartSync.Margin = new Padding(3, 2, 3, 2); checkBoxAutoStartSync.Name = "checkBoxAutoStartSync"; - checkBoxAutoStartSync.Size = new Size(128, 24); + checkBoxAutoStartSync.Size = new Size(105, 19); checkBoxAutoStartSync.TabIndex = 14; checkBoxAutoStartSync.Text = "&Auto start sync"; checkBoxAutoStartSync.UseVisualStyleBackColor = true; @@ -118,26 +124,28 @@ private void InitializeComponent() // // textBoxRemotePath // - textBoxRemotePath.Location = new Point(122, 70); + textBoxRemotePath.Location = new Point(107, 52); + textBoxRemotePath.Margin = new Padding(3, 2, 3, 2); textBoxRemotePath.Name = "textBoxRemotePath"; - textBoxRemotePath.Size = new Size(454, 27); + textBoxRemotePath.Size = new Size(398, 23); textBoxRemotePath.TabIndex = 9; textBoxRemotePath.TextChanged += textBoxRemotePath_TextChanged; // // labelRemotePath // labelRemotePath.AutoSize = true; - labelRemotePath.Location = new Point(21, 73); + labelRemotePath.Location = new Point(18, 55); labelRemotePath.Name = "labelRemotePath"; - labelRemotePath.Size = new Size(95, 20); + labelRemotePath.Size = new Size(75, 15); labelRemotePath.TabIndex = 15; labelRemotePath.Text = "Remote path"; // // textBoxRemotePassword // - textBoxRemotePassword.Location = new Point(674, 37); + textBoxRemotePassword.Location = new Point(590, 28); + textBoxRemotePassword.Margin = new Padding(3, 2, 3, 2); textBoxRemotePassword.Name = "textBoxRemotePassword"; - textBoxRemotePassword.Size = new Size(155, 27); + textBoxRemotePassword.Size = new Size(136, 23); textBoxRemotePassword.TabIndex = 8; textBoxRemotePassword.UseSystemPasswordChar = true; textBoxRemotePassword.TextChanged += textBoxRemotePassword_TextChanged; @@ -145,68 +153,72 @@ private void InitializeComponent() // labelRemotePassword // labelRemotePassword.AutoSize = true; - labelRemotePassword.Location = new Point(598, 40); + labelRemotePassword.Location = new Point(523, 30); labelRemotePassword.Name = "labelRemotePassword"; - labelRemotePassword.Size = new Size(70, 20); + labelRemotePassword.Size = new Size(57, 15); labelRemotePassword.TabIndex = 13; labelRemotePassword.Text = "Password"; // // textBoxRemoteUser // - textBoxRemoteUser.Location = new Point(437, 37); + textBoxRemoteUser.Location = new Point(382, 28); + textBoxRemoteUser.Margin = new Padding(3, 2, 3, 2); textBoxRemoteUser.Name = "textBoxRemoteUser"; - textBoxRemoteUser.Size = new Size(139, 27); + textBoxRemoteUser.Size = new Size(122, 23); textBoxRemoteUser.TabIndex = 7; textBoxRemoteUser.TextChanged += textBoxRemoteUser_TextChanged; // // labelRemoteUser // labelRemoteUser.AutoSize = true; - labelRemoteUser.Location = new Point(356, 40); + labelRemoteUser.Location = new Point(312, 30); labelRemoteUser.Name = "labelRemoteUser"; - labelRemoteUser.Size = new Size(75, 20); + labelRemoteUser.Size = new Size(60, 15); labelRemoteUser.TabIndex = 11; labelRemoteUser.Text = "Username"; // // textBoxRemoteHost // - textBoxRemoteHost.Location = new Point(122, 37); + textBoxRemoteHost.Location = new Point(107, 28); + textBoxRemoteHost.Margin = new Padding(3, 2, 3, 2); textBoxRemoteHost.Name = "textBoxRemoteHost"; - textBoxRemoteHost.Size = new Size(214, 27); + textBoxRemoteHost.Size = new Size(188, 23); textBoxRemoteHost.TabIndex = 6; textBoxRemoteHost.TextChanged += textBoxRemoteHost_TextChanged; // // labelRemoteHost // labelRemoteHost.AutoSize = true; - labelRemoteHost.Location = new Point(21, 40); + labelRemoteHost.Location = new Point(18, 30); labelRemoteHost.Name = "labelRemoteHost"; - labelRemoteHost.Size = new Size(93, 20); + labelRemoteHost.Size = new Size(74, 15); labelRemoteHost.TabIndex = 9; labelRemoteHost.Text = "Remote host"; // // textBoxSearchSpec // - textBoxSearchSpec.Location = new Point(122, 65); + textBoxSearchSpec.Location = new Point(107, 49); + textBoxSearchSpec.Margin = new Padding(3, 2, 3, 2); textBoxSearchSpec.Name = "textBoxSearchSpec"; - textBoxSearchSpec.Size = new Size(707, 27); + textBoxSearchSpec.Size = new Size(619, 23); textBoxSearchSpec.TabIndex = 2; textBoxSearchSpec.TextChanged += textBoxSearchSpec_TextChanged; // // labelSearchSpec // labelSearchSpec.AutoSize = true; - labelSearchSpec.Location = new Point(21, 68); + labelSearchSpec.Location = new Point(18, 51); labelSearchSpec.Name = "labelSearchSpec"; - labelSearchSpec.Size = new Size(87, 20); + labelSearchSpec.Size = new Size(69, 15); labelSearchSpec.TabIndex = 7; labelSearchSpec.Text = "Search spec"; // // buttonLocalPath // - buttonLocalPath.Location = new Point(795, 32); + buttonLocalPath.Location = new Point(696, 24); + buttonLocalPath.Margin = new Padding(3, 2, 3, 2); buttonLocalPath.Name = "buttonLocalPath"; - buttonLocalPath.Size = new Size(34, 29); + buttonLocalPath.Size = new Size(30, 22); buttonLocalPath.TabIndex = 1; buttonLocalPath.Text = "..."; buttonLocalPath.UseVisualStyleBackColor = true; @@ -214,44 +226,48 @@ private void InitializeComponent() // // textBoxLocalPath // - textBoxLocalPath.Location = new Point(122, 32); + textBoxLocalPath.Location = new Point(107, 24); + textBoxLocalPath.Margin = new Padding(3, 2, 3, 2); textBoxLocalPath.Name = "textBoxLocalPath"; - textBoxLocalPath.Size = new Size(667, 27); + textBoxLocalPath.Size = new Size(584, 23); textBoxLocalPath.TabIndex = 0; textBoxLocalPath.TextChanged += textBoxLocalPath_TextChanged; // // labelLocalPath // labelLocalPath.AutoSize = true; - labelLocalPath.Location = new Point(21, 36); + labelLocalPath.Location = new Point(18, 27); labelLocalPath.Name = "labelLocalPath"; - labelLocalPath.Size = new Size(78, 20); + labelLocalPath.Size = new Size(62, 15); labelLocalPath.TabIndex = 4; labelLocalPath.Text = "Local path"; // // listBox // listBox.FormattingEnabled = true; - listBox.Location = new Point(122, 98); + listBox.ItemHeight = 15; + listBox.Location = new Point(107, 74); + listBox.Margin = new Padding(3, 2, 3, 2); listBox.Name = "listBox"; - listBox.Size = new Size(707, 204); + listBox.Size = new Size(619, 154); listBox.TabIndex = 3; listBox.SelectedIndexChanged += listBox_SelectedIndexChanged; // // labelExclusions // labelExclusions.AutoSize = true; - labelExclusions.Location = new Point(21, 100); + labelExclusions.Location = new Point(18, 75); labelExclusions.Name = "labelExclusions"; - labelExclusions.Size = new Size(76, 20); + labelExclusions.Size = new Size(61, 15); labelExclusions.TabIndex = 17; labelExclusions.Text = "Exclusions"; // // btnAdd // - btnAdd.Location = new Point(635, 308); + btnAdd.Location = new Point(556, 231); + btnAdd.Margin = new Padding(3, 2, 3, 2); btnAdd.Name = "btnAdd"; - btnAdd.Size = new Size(94, 29); + btnAdd.Size = new Size(82, 22); btnAdd.TabIndex = 4; btnAdd.Text = "&Add"; btnAdd.UseVisualStyleBackColor = true; @@ -259,9 +275,10 @@ private void InitializeComponent() // // btnRemove // - btnRemove.Location = new Point(735, 308); + btnRemove.Location = new Point(643, 231); + btnRemove.Margin = new Padding(3, 2, 3, 2); btnRemove.Name = "btnRemove"; - btnRemove.Size = new Size(94, 29); + btnRemove.Size = new Size(82, 22); btnRemove.TabIndex = 5; btnRemove.Text = "&Remove"; btnRemove.UseVisualStyleBackColor = true; @@ -269,6 +286,7 @@ private void InitializeComponent() // // groupLocal // + groupLocal.Controls.Add(chkSupportDelete); groupLocal.Controls.Add(listBox); groupLocal.Controls.Add(btnRemove); groupLocal.Controls.Add(textBoxSearchSpec); @@ -278,13 +296,26 @@ private void InitializeComponent() groupLocal.Controls.Add(buttonLocalPath); groupLocal.Controls.Add(textBoxLocalPath); groupLocal.Controls.Add(labelLocalPath); - groupLocal.Location = new Point(12, 12); + groupLocal.Location = new Point(10, 9); + groupLocal.Margin = new Padding(3, 2, 3, 2); groupLocal.Name = "groupLocal"; - groupLocal.Size = new Size(857, 353); + groupLocal.Padding = new Padding(3, 2, 3, 2); + groupLocal.Size = new Size(750, 265); groupLocal.TabIndex = 20; groupLocal.TabStop = false; groupLocal.Text = "Local Windows System Settings"; // + // chkSupportDelete + // + chkSupportDelete.AutoSize = true; + chkSupportDelete.Location = new Point(107, 234); + chkSupportDelete.Name = "chkSupportDelete"; + chkSupportDelete.Size = new Size(132, 19); + chkSupportDelete.TabIndex = 18; + chkSupportDelete.Text = "Allow remote &delete"; + chkSupportDelete.UseVisualStyleBackColor = true; + chkSupportDelete.CheckedChanged += chkSupportDelete_CheckedChanged; + // // groupRemote // groupRemote.Controls.Add(textBoxRemotePath); @@ -297,9 +328,11 @@ private void InitializeComponent() groupRemote.Controls.Add(labelRemotePassword); groupRemote.Controls.Add(textBoxRemotePassword); groupRemote.Controls.Add(labelRemotePath); - groupRemote.Location = new Point(12, 371); + groupRemote.Location = new Point(10, 278); + groupRemote.Margin = new Padding(3, 2, 3, 2); groupRemote.Name = "groupRemote"; - groupRemote.Size = new Size(857, 155); + groupRemote.Padding = new Padding(3, 2, 3, 2); + groupRemote.Size = new Size(750, 116); groupRemote.TabIndex = 21; groupRemote.TabStop = false; groupRemote.Text = "Remote OpenVMS System Settings"; @@ -310,18 +343,21 @@ private void InitializeComponent() groupStartup.Controls.Add(checkBoxAutoStartSync); groupStartup.Controls.Add(checkStartAtLogin); groupStartup.Controls.Add(checkStartInTray); - groupStartup.Location = new Point(12, 532); + groupStartup.Location = new Point(10, 399); + groupStartup.Margin = new Padding(3, 2, 3, 2); groupStartup.Name = "groupStartup"; - groupStartup.Size = new Size(857, 84); + groupStartup.Padding = new Padding(3, 2, 3, 2); + groupStartup.Size = new Size(750, 63); groupStartup.TabIndex = 22; groupStartup.TabStop = false; groupStartup.Text = "Application Startup Settings"; // // btnClose // - btnClose.Location = new Point(725, 33); + btnClose.Location = new Point(634, 25); + btnClose.Margin = new Padding(3, 2, 3, 2); btnClose.Name = "btnClose"; - btnClose.Size = new Size(104, 29); + btnClose.Size = new Size(91, 22); btnClose.TabIndex = 15; btnClose.Text = "&Close"; btnClose.UseVisualStyleBackColor = true; @@ -329,14 +365,15 @@ private void InitializeComponent() // // SettingsForm // - AutoScaleDimensions = new SizeF(8F, 20F); + AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(878, 628); + ClientSize = new Size(768, 471); Controls.Add(groupStartup); Controls.Add(groupRemote); Controls.Add(groupLocal); FormBorderStyle = FormBorderStyle.FixedDialog; Icon = (Icon)resources.GetObject("$this.Icon"); + Margin = new Padding(3, 2, 3, 2); MaximizeBox = false; MinimizeBox = false; Name = "SettingsForm"; @@ -380,5 +417,6 @@ private void InitializeComponent() private GroupBox groupRemote; private GroupBox groupStartup; private Button btnClose; + private CheckBox chkSupportDelete; } } diff --git a/SFTPSyncUI/SettingsForm.cs b/SFTPSyncUI/SettingsForm.cs index b03905b..a50300c 100644 --- a/SFTPSyncUI/SettingsForm.cs +++ b/SFTPSyncUI/SettingsForm.cs @@ -2,6 +2,7 @@ using Renci.SshNet; using SFTPSyncLib; using System.Diagnostics; +using System.Security.Policy; using System.Text.RegularExpressions; namespace SFTPSyncUI @@ -16,7 +17,7 @@ public SettingsForm(AppSettings settings, bool syncRunning) { InitializeComponent(); _settings = settings; - _syncRunning = syncRunning; + _syncRunning = syncRunning; //Load current settings. initialUiLoad = true; @@ -35,6 +36,8 @@ public SettingsForm(AppSettings settings, bool syncRunning) textBoxRemotePath.Text = _settings.RemotePath; buttonVerifyAccess.Enabled = !_settings.AccessVerified; + chkSupportDelete.Checked = _settings.DeleteEnabled; + //Application settings checkStartAtLogin.Checked = _settings.StartAtLogin; checkStartInTray.Checked = _settings.StartInTray; @@ -253,6 +256,11 @@ private void checkBoxAutoStartSync_CheckedChanged(object sender, EventArgs e) if (!initialUiLoad) _settings?.AutoStartSync = checkBoxAutoStartSync.Checked; } + private void chkSupportDelete_CheckedChanged(object sender, EventArgs e) + { + if (!initialUiLoad) + _settings?.DeleteEnabled = chkSupportDelete.Checked; + } private void checkBoxShowPassword_CheckedChanged(object sender, EventArgs e) { @@ -304,6 +312,5 @@ private void btnClose_Click(object sender, EventArgs e) { this.Close(); } - } } diff --git a/SFTPSyncUI/appsettings.json b/SFTPSyncUI/appsettings.json index a6a032b..c4c6a14 100644 --- a/SFTPSyncUI/appsettings.json +++ b/SFTPSyncUI/appsettings.json @@ -3,6 +3,7 @@ "StartInTray": false, "AutoStartSync": false, "AccessVerified": false, + "DeleteEnabled": false, "LocalPath": "", "ExcludedPaths": [], "LocalSearchPattern": "*.DBL",