diff --git a/Activities/FTP/UiPath.FTP.Activities/Properties/UiPath.FTP.Activities.Designer.cs b/Activities/FTP/UiPath.FTP.Activities/Properties/UiPath.FTP.Activities.Designer.cs index b6a878f02..2de148989 100644 --- a/Activities/FTP/UiPath.FTP.Activities/Properties/UiPath.FTP.Activities.Designer.cs +++ b/Activities/FTP/UiPath.FTP.Activities/Properties/UiPath.FTP.Activities.Designer.cs @@ -1465,7 +1465,7 @@ public static string NewPathDescription { } /// - /// Looks up a localized string similar to No valid authentication method found: You need to supply either Private Key file (and optionally passphare) or Password. + /// Looks up a localized string similar to No valid authentication method found: You need to supply either Private Key file (and optionally passphrase) or Password. /// public static string NoValidAuthenticationMethod { get { diff --git a/Activities/FTP/UiPath.FTP.Activities/Properties/UiPath.FTP.Activities.resx b/Activities/FTP/UiPath.FTP.Activities/Properties/UiPath.FTP.Activities.resx index 11624d979..e57e9acdd 100644 --- a/Activities/FTP/UiPath.FTP.Activities/Properties/UiPath.FTP.Activities.resx +++ b/Activities/FTP/UiPath.FTP.Activities/Properties/UiPath.FTP.Activities.resx @@ -220,7 +220,7 @@ Path to the Private key in PKCS #1 PEM format - No valid authentication method found: You need to supply either Private Key file (and optionally passphare) or Password + No valid authentication method found: You need to supply either Private Key file (and optionally passphrase) or Password New remote path diff --git a/Activities/FTP/UiPath.FTP.Activities/WithFtpSession.cs b/Activities/FTP/UiPath.FTP.Activities/WithFtpSession.cs index debbc51c0..f80bc5505 100644 --- a/Activities/FTP/UiPath.FTP.Activities/WithFtpSession.cs +++ b/Activities/FTP/UiPath.FTP.Activities/WithFtpSession.cs @@ -250,7 +250,8 @@ protected override async Task> ExecuteAsync(Native throw new ArgumentNullException(Resources.EmptyUsernameException); } - if (string.IsNullOrWhiteSpace(ftpConfiguration.Password) && string.IsNullOrWhiteSpace(ftpConfiguration.ClientCertificatePath)) + if (string.IsNullOrWhiteSpace(ftpConfiguration.Password) && string.IsNullOrWhiteSpace(ftpConfiguration.ClientCertificatePath) + && !UseSftp) { throw new ArgumentNullException(Resources.NoValidAuthenticationMethod); } diff --git a/Activities/FTP/UiPath.FTP.Tests/ActivitiesTests/DirectoryExistsTests.cs b/Activities/FTP/UiPath.FTP.Tests/ActivitiesTests/DirectoryExistsTests.cs new file mode 100644 index 000000000..223c12124 --- /dev/null +++ b/Activities/FTP/UiPath.FTP.Tests/ActivitiesTests/DirectoryExistsTests.cs @@ -0,0 +1,98 @@ +using Moq; +using System; +using System.Activities; +using System.Activities.Statements; +using System.Threading; +using System.Threading.Tasks; +using UiPath.FTP.Activities; +using Xunit; + +namespace UiPath.FTP.Tests.ActivitiesTests +{ + public class DirectoryExistsTests + { + [Fact] + public void DirectoryExists_SetsExistsTrue_WhenSessionReturnsTrue() + { + var session = CreateSessionMock(exists: true); + bool result = RunActivity(session, "/remote/dir"); + Assert.True(result); + } + + [Fact] + public void DirectoryExists_SetsExistsFalse_WhenSessionReturnsFalse() + { + var session = CreateSessionMock(exists: false); + bool result = RunActivity(session, "/remote/dir"); + Assert.False(result); + } + + [Fact] + public void DirectoryExists_CallsDirectoryExistsAsync_WithCorrectPath() + { + var session = CreateSessionMock(exists: false); + RunActivity(session, "/expected/path"); + session.Verify( + s => s.DirectoryExistsAsync("/expected/path", It.IsAny()), + Times.Once); + } + + [Fact] + public void DirectoryExists_PropagatesException_WhenSessionThrows() + { + var session = new Mock(); + session + .Setup(s => s.DirectoryExistsAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new InvalidOperationException("session error")); + + var ex = Assert.ThrowsAny(() => RunActivity(session, "/remote/dir")); + Assert.Contains(TestsHelper.Flatten(ex), e => e is InvalidOperationException); + } + + // --- Helpers --- + + private static Mock CreateSessionMock(bool exists) + { + var session = new Mock(); + session + .Setup(s => s.DirectoryExistsAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(exists)); + return session; + } + + private static bool RunActivity(Mock session, string remotePath) + { + var result = new bool[1]; + var resultVariable = new Variable("exists"); + var activity = new WithFtpSession(session.Object) + { + Host = new InArgument("NonUsed"), + Username = new InArgument("NonUsed"), + Password = new InArgument("NonUsed"), + }; + + if (activity.Body.Handler is Sequence seq) + { + seq.Variables.Add(resultVariable); + seq.Activities.Add(new DirectoryExists + { + RemotePath = new InArgument(remotePath), + Exists = new OutArgument(resultVariable) + }); + seq.Activities.Add(new InvokeMethod + { + TargetType = typeof(TestsHelper), + MethodName = nameof(TestsHelper.CopyBool), + Parameters = + { + new InArgument(ctx => resultVariable.Get(ctx)), + new InArgument(ctx => result) + } + }); + } + + WorkflowInvoker.Invoke(activity, TimeSpan.FromSeconds(5)); + return result[0]; + } + } +} diff --git a/Activities/FTP/UiPath.FTP.Tests/ActivitiesTests/FileExistsTests.cs b/Activities/FTP/UiPath.FTP.Tests/ActivitiesTests/FileExistsTests.cs new file mode 100644 index 000000000..b31caefed --- /dev/null +++ b/Activities/FTP/UiPath.FTP.Tests/ActivitiesTests/FileExistsTests.cs @@ -0,0 +1,98 @@ +using Moq; +using System; +using System.Activities; +using System.Activities.Statements; +using System.Threading; +using System.Threading.Tasks; +using UiPath.FTP.Activities; +using Xunit; + +namespace UiPath.FTP.Tests.ActivitiesTests +{ + public class FileExistsTests + { + [Fact] + public void FileExists_SetsExistsTrue_WhenSessionReturnsTrue() + { + var session = CreateSessionMock(exists: true); + bool result = RunActivity(session, "/remote/file.txt"); + Assert.True(result); + } + + [Fact] + public void FileExists_SetsExistsFalse_WhenSessionReturnsFalse() + { + var session = CreateSessionMock(exists: false); + bool result = RunActivity(session, "/remote/file.txt"); + Assert.False(result); + } + + [Fact] + public void FileExists_CallsFileExistsAsync_WithCorrectPath() + { + var session = CreateSessionMock(exists: false); + RunActivity(session, "/expected/file.txt"); + session.Verify( + s => s.FileExistsAsync("/expected/file.txt", It.IsAny()), + Times.Once); + } + + [Fact] + public void FileExists_PropagatesException_WhenSessionThrows() + { + var session = new Mock(); + session + .Setup(s => s.FileExistsAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new InvalidOperationException("session error")); + + var ex = Assert.ThrowsAny(() => RunActivity(session, "/remote/file.txt")); + Assert.Contains(TestsHelper.Flatten(ex), e => e is InvalidOperationException); + } + + // --- Helpers --- + + private static Mock CreateSessionMock(bool exists) + { + var session = new Mock(); + session + .Setup(s => s.FileExistsAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(exists)); + return session; + } + + private static bool RunActivity(Mock session, string remotePath) + { + var result = new bool[1]; + var resultVariable = new Variable("exists"); + var activity = new WithFtpSession(session.Object) + { + Host = new InArgument("NonUsed"), + Username = new InArgument("NonUsed"), + Password = new InArgument("NonUsed"), + }; + + if (activity.Body.Handler is Sequence seq) + { + seq.Variables.Add(resultVariable); + seq.Activities.Add(new FileExists + { + RemotePath = new InArgument(remotePath), + Exists = new OutArgument(resultVariable) + }); + seq.Activities.Add(new InvokeMethod + { + TargetType = typeof(TestsHelper), + MethodName = nameof(TestsHelper.CopyBool), + Parameters = + { + new InArgument(ctx => resultVariable.Get(ctx)), + new InArgument(ctx => result) + } + }); + } + + WorkflowInvoker.Invoke(activity, TimeSpan.FromSeconds(5)); + return result[0]; + } + } +} diff --git a/Activities/FTP/UiPath.FTP.Tests/SessionsTests/SftpSessionTests.cs b/Activities/FTP/UiPath.FTP.Tests/SessionsTests/SftpSessionTests.cs index 0588557f2..948999c90 100644 --- a/Activities/FTP/UiPath.FTP.Tests/SessionsTests/SftpSessionTests.cs +++ b/Activities/FTP/UiPath.FTP.Tests/SessionsTests/SftpSessionTests.cs @@ -1,5 +1,11 @@ using Renci.SshNet; +using Renci.SshNet.Common; using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace UiPath.FTP.Tests @@ -44,12 +50,186 @@ public void SFTP_DefaultTimeoutWhenNotSet() Assert.Equal(defaultSftpClient.OperationTimeout, sftpClient.OperationTimeout); } - private static FtpConfiguration CreateTestConfig(int? timeout = null) + [Fact] + public void SFTP_KeyboardInteractiveMethod_IsAlwaysRegistered() + { + var config = CreateTestConfig(); + using var session = new SftpSession(config); + + var authMethods = session.Client.ConnectionInfo.AuthenticationMethods; + + Assert.Contains(authMethods, m => m is KeyboardInteractiveAuthenticationMethod); + } + + [Fact] + public void SFTP_KeyboardInteractiveMethod_RespondsWithPasswordForNonEchoPrompt() + { + const string password = "s3cr3t"; // NOSONAR - dummy test credentials + var config = CreateTestConfig(password: password); + using var session = new SftpSession(config); + + var kbiMethod = session.Client.ConnectionInfo.AuthenticationMethods + .OfType() + .Single(); + + var prompt = new AuthenticationPrompt(0, false, "Password: "); + RaiseAuthenticationPrompt(kbiMethod, new[] { prompt }); + + Assert.Equal(password, prompt.Response); + } + + [Fact] + public void SFTP_KeyboardInteractiveMethod_LeavesEchoPromptEmpty() + { + var config = CreateTestConfig(password: "s3cr3t"); // NOSONAR - dummy test credentials + using var session = new SftpSession(config); + + var kbiMethod = session.Client.ConnectionInfo.AuthenticationMethods + .OfType() + .Single(); + + var prompt = new AuthenticationPrompt(0, true, "Username: "); + RaiseAuthenticationPrompt(kbiMethod, new[] { prompt }); + + Assert.Null(prompt.Response); + } + + [Fact] + public void SFTP_KeyboardInteractiveMethod_RespondsWithEmptyStringWhenPasswordIsNull() + { + var config = new FtpConfiguration("localhost") + { + Username = "testUser", + Password = null + }; + using var session = new SftpSession(config); + + var kbiMethod = session.Client.ConnectionInfo.AuthenticationMethods + .OfType() + .Single(); + + var prompt = new AuthenticationPrompt(0, false, "Password: "); + RaiseAuthenticationPrompt(kbiMethod, new[] { prompt }); + + Assert.Equal(string.Empty, prompt.Response); + } + + // --- SafeExists / SshException swallowing --- + + [Fact] + public async Task SFTP_DirectoryExistsAsync_ReturnsFalse_WhenClientExistsThrowsBareSshException() + { + using var session = new SftpSessionWithFakeExists( + CreateTestConfig(), + () => throw new SshException("SSH_FX_FAILURE")); + + bool result = await ((IFtpSession)session).DirectoryExistsAsync("/some/path", CancellationToken.None); + + Assert.False(result); + } + + [Fact] + public async Task SFTP_FileExistsAsync_ReturnsFalse_WhenClientExistsThrowsBareSshException() + { + using var session = new SftpSessionWithFakeExists( + CreateTestConfig(), + () => throw new SshException("SSH_FX_FAILURE")); + + bool result = await ((IFtpSession)session).FileExistsAsync("/some/file.txt", CancellationToken.None); + + Assert.False(result); + } + + [Fact] + public async Task SFTP_DirectoryExistsAsync_Rethrows_WhenClientExistsThrowsSshConnectionException() + { + using var session = new SftpSessionWithFakeExists( + CreateTestConfig(), + () => throw new SshConnectionException("dropped")); + + await Assert.ThrowsAsync( + () => ((IFtpSession)session).DirectoryExistsAsync("/some/path", CancellationToken.None)); + } + + [Fact] + public async Task SFTP_FileExistsAsync_Rethrows_WhenClientExistsThrowsSshConnectionException() + { + using var session = new SftpSessionWithFakeExists( + CreateTestConfig(), + () => throw new SshConnectionException("dropped")); + + await Assert.ThrowsAsync( + () => ((IFtpSession)session).FileExistsAsync("/some/file.txt", CancellationToken.None)); + } + + [Fact] + public async Task SFTP_DirectoryExistsAsync_ReturnsFalse_WhenClientExistsReturnsFalse() + { + using var session = new SftpSessionWithFakeExists(CreateTestConfig(), () => false); + + bool result = await ((IFtpSession)session).DirectoryExistsAsync("/some/path", CancellationToken.None); + + Assert.False(result); + } + + [Fact] + public async Task SFTP_FileExistsAsync_ReturnsFalse_WhenClientExistsReturnsFalse() + { + using var session = new SftpSessionWithFakeExists(CreateTestConfig(), () => false); + + bool result = await ((IFtpSession)session).FileExistsAsync("/some/file.txt", CancellationToken.None); + + Assert.False(result); + } + + // --- Helpers --- + + /// + /// Subclass of that overrides + /// so tests can inject arbitrary behaviour without needing to override the sealed + /// SftpClient.Exists method. + /// + private sealed class SftpSessionWithFakeExists : SftpSession + { + private readonly Func _existsBehaviour; + + public SftpSessionWithFakeExists(FtpConfiguration config, Func existsBehaviour) + : base(config) + { + _existsBehaviour = existsBehaviour; + } + + protected internal override bool ClientExists(string path) => _existsBehaviour(); + } + + /// + /// Fires the event + /// by invoking its backing delegate directly. SSH.NET does not expose a raise method, so + /// reflection is used here purely as a test seam. + /// + private static void RaiseAuthenticationPrompt( + KeyboardInteractiveAuthenticationMethod method, + IEnumerable prompts) + { + var eventArgs = new AuthenticationPromptEventArgs("testUser", string.Empty, string.Empty, prompts.ToList().AsReadOnly()); + + var field = typeof(KeyboardInteractiveAuthenticationMethod) + .GetField("AuthenticationPrompt", BindingFlags.Instance | BindingFlags.NonPublic) + ?? typeof(KeyboardInteractiveAuthenticationMethod) + .GetField("_authenticationPrompt", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(field); // SSH.NET renamed the backing field; update the lookup above. + + var handler = field.GetValue(method) as EventHandler; + Assert.NotNull(handler); // SftpSession did not subscribe to AuthenticationPrompt. + handler.Invoke(method, eventArgs); + } + + private static FtpConfiguration CreateTestConfig(int? timeout = null, string password = "notUsed") { return new FtpConfiguration("localhost") { Username = "testUser", - Password = "notUsed", // NOSONAR - dummy test credentials, never connect to any server + Password = password, // NOSONAR - dummy test credentials, never connect to any server Timeout = timeout }; } diff --git a/Activities/FTP/UiPath.FTP.Tests/TestsHelper.cs b/Activities/FTP/UiPath.FTP.Tests/TestsHelper.cs index bfe8645e5..e0bf65854 100644 --- a/Activities/FTP/UiPath.FTP.Tests/TestsHelper.cs +++ b/Activities/FTP/UiPath.FTP.Tests/TestsHelper.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace UiPath.FTP.Tests { @@ -11,5 +12,30 @@ public static void CopyObjects(IEnumerable lstObjects, IList Flatten(Exception ex) + { + while (ex != null) + { + yield return ex; + if (ex is AggregateException agg) + { + foreach (var inner in agg.InnerExceptions) + foreach (var e in Flatten(inner)) + yield return e; + yield break; + } + ex = ex.InnerException; + } + } } } diff --git a/Activities/FTP/UiPath.FTP/Properties/UiPath.FTP.Designer.cs b/Activities/FTP/UiPath.FTP/Properties/UiPath.FTP.Designer.cs index e3306ce3f..87e3b3f82 100644 --- a/Activities/FTP/UiPath.FTP/Properties/UiPath.FTP.Designer.cs +++ b/Activities/FTP/UiPath.FTP/Properties/UiPath.FTP.Designer.cs @@ -79,7 +79,7 @@ internal static string FileExistsException { } /// - /// Looks up a localized string similar to No valid authentication method found: You need to supply either Private Key file (and optionally passphare) or Password. + /// Looks up a localized string similar to No valid authentication method found: You need to supply either Private Key file (and optionally passphrase) or Password. /// internal static string NoValidAuthenticationMethod { get { diff --git a/Activities/FTP/UiPath.FTP/Properties/UiPath.FTP.resx b/Activities/FTP/UiPath.FTP/Properties/UiPath.FTP.resx index 22a32d344..ff93957fa 100644 --- a/Activities/FTP/UiPath.FTP/Properties/UiPath.FTP.resx +++ b/Activities/FTP/UiPath.FTP/Properties/UiPath.FTP.resx @@ -130,7 +130,7 @@ Unsupported object type encountered. - No valid authentication method found: You need to supply either Private Key file (and optionally passphare) or Password + No valid authentication method found: You need to supply either Private Key file (and optionally passphrase) or Password The destination directory already exists diff --git a/Activities/FTP/UiPath.FTP/SftpSession.cs b/Activities/FTP/UiPath.FTP/SftpSession.cs index 6084bfc0a..52ec28e34 100644 --- a/Activities/FTP/UiPath.FTP/SftpSession.cs +++ b/Activities/FTP/UiPath.FTP/SftpSession.cs @@ -1,4 +1,5 @@ using Renci.SshNet; +using Renci.SshNet.Common; using Renci.SshNet.Sftp; using System; using System.Collections.Generic; @@ -45,6 +46,24 @@ public SftpSession(FtpConfiguration ftpConfiguration) authMethods.Add(new PrivateKeyAuthenticationMethod(ftpConfiguration.Username, keyFiles)); } + // Always register keyboard-interactive so that servers which advertise only + // keyboard-interactive (OpenSSH with ChallengeResponseAuthentication/PAM) can + // authenticate using the configured password. The handler responds with the + // password for every non-echo prompt; echo prompts are left empty because the + // SSH username is already supplied at the transport layer. + var kbiMethod = new KeyboardInteractiveAuthenticationMethod(ftpConfiguration.Username); + kbiMethod.AuthenticationPrompt += (sender, e) => + { + foreach (var prompt in e.Prompts) + { + if (!prompt.IsEchoed) + { + prompt.Response = ftpConfiguration.Password ?? string.Empty; + } + } + }; + authMethods.Add(kbiMethod); + //Throw an error if we ended up with no authentication method if (authMethods.Count == 0) { @@ -297,7 +316,39 @@ private async Task> GetMissingDirectoriesAsync(string remote return missingDirectories; } + /// + /// Calls on the underlying client. + /// Extracted as a protected internal virtual method so that tests can subclass + /// and override this single call without needing to mock the + /// sealed SftpClient.Exists method directly. + /// + protected internal virtual bool ClientExists(string path) => _sftpClient.Exists(path); + + /// + /// Wraps and returns false for bare + /// (SSH_FX_FAILURE) thrown by some SFTP server + /// implementations when a path does not exist, while letting typed subclasses + /// (e.g. , ) + /// propagate so that real connection/timeout failures are not silently swallowed. + /// + private bool SafeExists(string path) + { + try + { + return ClientExists(path); + } + catch (SshException ex) when (ex.GetType() == typeof(SshException)) + { + Trace.TraceWarning( + "SftpSession.SafeExists: bare SshException treated as 'not found' for path '{0}': {1}", + path, + ex.Message); + return false; + } + } + #region IFtpSession members + bool IFtpSession.IsConnected() { return _sftpClient.IsConnected; @@ -389,7 +440,7 @@ bool IFtpSession.DirectoryExists(string path) throw new ArgumentNullException(nameof(path)); } - return _sftpClient.Exists(path) && ((IFtpSession)this).GetObjectType(path) == UiPath.FTP.FtpObjectType.Directory; + return SafeExists(path) && ((IFtpSession)this).GetObjectType(path) == UiPath.FTP.FtpObjectType.Directory; } async Task IFtpSession.DirectoryExistsAsync(string path, CancellationToken cancellationToken) @@ -399,7 +450,7 @@ async Task IFtpSession.DirectoryExistsAsync(string path, CancellationToken throw new ArgumentNullException(nameof(path)); } - if (_sftpClient.Exists(path)) + if (SafeExists(path)) { return await ((IFtpSession)this).GetObjectTypeAsync(path, cancellationToken) == UiPath.FTP.FtpObjectType.Directory; } @@ -532,7 +583,7 @@ bool IFtpSession.FileExists(string path) throw new ArgumentNullException(nameof(path)); } - return _sftpClient.Exists(path) && ((IFtpSession)this).GetObjectType(path) == UiPath.FTP.FtpObjectType.File; + return SafeExists(path) && ((IFtpSession)this).GetObjectType(path) == UiPath.FTP.FtpObjectType.File; } async Task IFtpSession.FileExistsAsync(string path, CancellationToken cancellationToken) @@ -542,7 +593,7 @@ async Task IFtpSession.FileExistsAsync(string path, CancellationToken canc throw new ArgumentNullException(nameof(path)); } - if (_sftpClient.Exists(path)) + if (SafeExists(path)) { return await ((IFtpSession)this).GetObjectTypeAsync(path, cancellationToken) == UiPath.FTP.FtpObjectType.File; } @@ -559,7 +610,7 @@ UiPath.FTP.FtpObjectType IFtpSession.GetObjectType(string path) throw new ArgumentNullException(nameof(path)); } - if (!_sftpClient.Exists(path)) + if (!SafeExists(path)) { throw new ArgumentException(string.Format(Resources.PathNotFoundException, path), nameof(path)); } @@ -574,7 +625,7 @@ UiPath.FTP.FtpObjectType IFtpSession.GetObjectType(string path) throw new ArgumentNullException(nameof(path)); } - if (!_sftpClient.Exists(path)) + if (!SafeExists(path)) { throw new ArgumentException(string.Format(Resources.PathNotFoundException, path), nameof(path)); } @@ -613,25 +664,25 @@ void IFtpSession.Move(string remotePath, string newPath, bool overwrite) throw new ArgumentNullException(nameof(newPath)); } - if (!_sftpClient.Exists(remotePath)) + if (!SafeExists(remotePath)) { throw new IOException(string.Format(Resources.PathNotFoundException, remotePath)); } - if (_sftpClient.Exists(newPath) && _sftpClient.Get(newPath).IsRegularFile && !overwrite) + if (SafeExists(newPath) && _sftpClient.Get(newPath).IsRegularFile && !overwrite) { throw new IOException(Resources.FileExistsException); } var file = _sftpClient.Get(remotePath); - if(_sftpClient.Exists(newPath) && file.IsRegularFile) + if(SafeExists(newPath) && file.IsRegularFile) { var movePath = _sftpClient.Get(newPath); if (movePath.IsDirectory) { var newFP = string.Format("{0}/{1}", movePath.FullName, file.Name); - if (_sftpClient.Exists(newFP) && _sftpClient.Get(newFP).IsRegularFile) + if (SafeExists(newFP) && _sftpClient.Get(newFP).IsRegularFile) { if (overwrite) _sftpClient.DeleteFile(newFP); @@ -668,7 +719,7 @@ void IFtpSession.Upload(string localPath, string remotePath, bool overwrite, boo foreach (Tuple pair in listing) { string directoryPath = FtpConfiguration.GetDirectoryPath(pair.Item2); - if (!_sftpClient.Exists(directoryPath)) + if (!SafeExists(directoryPath)) { _sftpClient.CreateDirectory(directoryPath); } @@ -683,7 +734,7 @@ void IFtpSession.Upload(string localPath, string remotePath, bool overwrite, boo { if (File.Exists(localPath)) { - if (_sftpClient.Exists(remotePath) && !overwrite) + if (SafeExists(remotePath) && !overwrite) { throw new IOException(Resources.FileExistsException); } @@ -720,7 +771,7 @@ async Task IFtpSession.UploadAsync(string localPath, string remotePath, bool ove cancellationToken.ThrowIfCancellationRequested(); string directoryPath = FtpConfiguration.GetDirectoryPath(pair.Item2); - if (!_sftpClient.Exists(directoryPath)) + if (!SafeExists(directoryPath)) { _sftpClient.CreateDirectory(directoryPath); } @@ -735,7 +786,7 @@ async Task IFtpSession.UploadAsync(string localPath, string remotePath, bool ove { if (File.Exists(localPath)) { - if (_sftpClient.Exists(remotePath) && !overwrite) + if (SafeExists(remotePath) && !overwrite) { throw new IOException(Resources.FileExistsException); }