Browse Source

Added ExistsAsync and GetAsync to SftpClient (#1501)

* Added ExistsAsync to SftpClient

* Added GetAsync to SftpClient

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Ryan Esteves 1 year ago
parent
commit
4c5d0c075e

+ 11 - 0
src/Renci.SshNet/Sftp/ISftpSession.cs

@@ -116,6 +116,17 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         SftpFileAttributes RequestLStat(string path);
 
+        /// <summary>
+        ///  Asynchronously performs SSH_FXP_LSTAT request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+        /// <returns>
+        /// A task the represents the asynchronous <c>SSH_FXP_LSTAT</c> request. The value of its
+        /// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
+        /// </returns>
+        Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken);
+
         /// <summary>
         /// Performs SSH_FXP_LSTAT request.
         /// </summary>

+ 32 - 0
src/Renci.SshNet/Sftp/SftpSession.cs

@@ -1031,6 +1031,38 @@ namespace Renci.SshNet.Sftp
             return attributes;
         }
 
+        /// <summary>
+        ///  Asynchronously performs SSH_FXP_LSTAT request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+        /// <returns>
+        /// A task the represents the asynchronous <c>SSH_FXP_LSTAT</c> request. The value of its
+        /// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
+        /// </returns>
+        public async Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var tcs = new TaskCompletionSource<SftpFileAttributes>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+#if NET || NETSTANDARD2_1_OR_GREATER
+            await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
+#else
+            using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
+#endif // NET || NETSTANDARD2_1_OR_GREATER
+            {
+                SendRequest(new SftpLStatRequest(ProtocolVersion,
+                                                 NextRequestId,
+                                                 path,
+                                                 _encoding,
+                                                 response => tcs.TrySetResult(response.Attributes),
+                                                 response => tcs.TrySetException(GetSftpException(response))));
+
+                return await tcs.Task.ConfigureAwait(false);
+            }
+        }
+
         /// <summary>
         /// Performs SSH_FXP_LSTAT request.
         /// </summary>

+ 90 - 0
src/Renci.SshNet/SftpClient.cs

@@ -689,6 +689,38 @@ namespace Renci.SshNet
             return new SftpFile(_sftpSession, fullPath, attributes);
         }
 
+        /// <summary>
+        /// Gets reference to remote file or directory.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>
+        /// A <see cref="Task{ISftpFile}"/> that represents the get operation.
+        /// The task result contains the reference to <see cref="ISftpFile"/> file object.
+        /// </returns>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public async Task<ISftpFile> GetAsync(string path, CancellationToken cancellationToken)
+        {
+            CheckDisposed();
+            ThrowHelper.ThrowIfNull(path);
+
+            if (_sftpSession is null)
+            {
+                throw new SshConnectionException("Client not connected.");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+
+            var attributes = await _sftpSession.RequestLStatAsync(fullPath, cancellationToken).ConfigureAwait(false);
+
+            return new SftpFile(_sftpSession, fullPath, attributes);
+        }
+
         /// <summary>
         /// Checks whether file or directory exists.
         /// </summary>
@@ -743,6 +775,64 @@ namespace Renci.SshNet
             }
         }
 
+        /// <summary>
+        /// Checks whether file or directory exists.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>
+        /// A <see cref="Task{T}"/> that represents the exists operation.
+        /// The task result contains <see langword="true"/> if directory or file exists; otherwise <see langword="false"/>.
+        /// </returns>
+        /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public async Task<bool> ExistsAsync(string path, CancellationToken cancellationToken = default)
+        {
+            CheckDisposed();
+            ThrowHelper.ThrowIfNullOrWhiteSpace(path);
+
+            if (_sftpSession is null)
+            {
+                throw new SshConnectionException("Client not connected.");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+
+            /*
+             * Using SSH_FXP_REALPATH is not an alternative as the SFTP specification has not always
+             * been clear on how the server should respond when the specified path is not present on
+             * the server:
+             *
+             * SSH 1 to 4:
+             * No mention of how the server should respond if the path is not present on the server.
+             *
+             * SSH 5:
+             * The server SHOULD fail the request if the path is not present on the server.
+             *
+             * SSH 6:
+             * Draft 06: The server SHOULD fail the request if the path is not present on the server.
+             * Draft 07 to 13: The server MUST NOT fail the request if the path does not exist.
+             *
+             * Note that SSH 6 (draft 06 and forward) allows for more control options, but we
+             * currently only support up to v3.
+             */
+
+            try
+            {
+                _ = await _sftpSession.RequestLStatAsync(fullPath, cancellationToken).ConfigureAwait(false);
+                return true;
+            }
+            catch (SftpPathNotFoundException)
+            {
+                return false;
+            }
+        }
+
         /// <summary>
         /// Downloads remote file specified by the path into the stream.
         /// </summary>

+ 79 - 0
test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs

@@ -87,6 +87,85 @@ namespace Renci.SshNet.IntegrationTests.OldIntegrationTests
                 Assert.IsFalse(file.IsDirectory);
             }
         }
+        [TestMethod]
+        [TestCategory("Sftp")]
+        public async Task Test_Get_Root_DirectoryAsync()
+        {
+            using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+            {
+                sftp.Connect();
+                var directory = await sftp.GetAsync("/", default).ConfigureAwait(false);
+
+                Assert.AreEqual("/", directory.FullName);
+                Assert.IsTrue(directory.IsDirectory);
+                Assert.IsFalse(directory.IsRegularFile);
+            }
+        }
+
+        [TestMethod]
+        [TestCategory("Sftp")]
+        [ExpectedException(typeof(SftpPathNotFoundException))]
+        public async Task Test_Get_Invalid_DirectoryAsync()
+        {
+            using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+            {
+                sftp.Connect();
+
+                await sftp.GetAsync("/xyz", default).ConfigureAwait(false);
+            }
+        }
+
+        [TestMethod]
+        [TestCategory("Sftp")]
+        public async Task Test_Get_FileAsync()
+        {
+            using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+            {
+                sftp.Connect();
+
+                sftp.UploadFile(new MemoryStream(), "abc.txt");
+
+                var file = await sftp.GetAsync("abc.txt", default).ConfigureAwait(false);
+
+                Assert.AreEqual("/home/sshnet/abc.txt", file.FullName);
+                Assert.IsTrue(file.IsRegularFile);
+                Assert.IsFalse(file.IsDirectory);
+            }
+        }
+
+        [TestMethod]
+        [TestCategory("Sftp")]
+        [Description("Test passing null to Get.")]
+        [ExpectedException(typeof(ArgumentNullException))]
+        public async Task Test_Get_File_NullAsync()
+        {
+            using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+            {
+                sftp.Connect();
+
+                var file = await sftp.GetAsync(null, default).ConfigureAwait(false);
+
+                sftp.Disconnect();
+            }
+        }
+
+        [TestMethod]
+        [TestCategory("Sftp")]
+        public async Task Test_Get_International_FileAsync()
+        {
+            using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
+            {
+                sftp.Connect();
+
+                sftp.UploadFile(new MemoryStream(), "test-üöä-");
+
+                var file = await sftp.GetAsync("test-üöä-", default).ConfigureAwait(false);
+
+                Assert.AreEqual("/home/sshnet/test-üöä-", file.FullName);
+                Assert.IsTrue(file.IsRegularFile);
+                Assert.IsFalse(file.IsDirectory);
+            }
+        }
 
         [TestMethod]
         [TestCategory("Sftp")]

+ 2 - 2
test/Renci.SshNet.IntegrationTests/SftpClientTests.cs

@@ -61,12 +61,12 @@ namespace Renci.SshNet.IntegrationTests
 
             // Create new directory and check if it exists
             _sftpClient.CreateDirectory(testDirectory);
-            Assert.IsTrue(_sftpClient.Exists(testDirectory));
+            Assert.IsTrue(await _sftpClient.ExistsAsync(testDirectory));
 
             // Upload file and check if it exists
             using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent));
             _sftpClient.UploadFile(fileStream, testFilePath);
-            Assert.IsTrue(_sftpClient.Exists(testFilePath));
+            Assert.IsTrue(await _sftpClient.ExistsAsync(testFilePath));
 
             // Check if ListDirectory works
             var expectedFiles = new List<(string FullName, bool IsRegularFile, bool IsDirectory)>()

+ 132 - 0
test/Renci.SshNet.IntegrationTests/SftpTests.cs

@@ -3770,6 +3770,138 @@ namespace Renci.SshNet.IntegrationTests
             #endregion Teardown
         }
 
+        [TestMethod]
+        public async Task Sftp_ExistsAsync()
+        {
+            const string remoteHome = "/home/sshnet";
+
+            #region Setup
+
+            using (var client = new SshClient(_connectionInfoFactory.Create()))
+            {
+                client.Connect();
+
+                #region Clean-up
+
+                using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}")
+                )
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                #endregion Clean-up
+
+                #region Setup
+
+                using (var command = client.CreateCommand($"touch {remoteHome + "/file.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"mkdir {remoteHome + "/directory.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"ln -s {remoteHome + "/file.exists"} {remoteHome + "/symlink.to.file.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"ln -s {remoteHome + "/directory.exists"} {remoteHome + "/symlink.to.directory.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                #endregion Setup
+            }
+
+            #endregion Setup
+
+            #region Assert
+
+            using (var client = new SftpClient(_connectionInfoFactory.Create()))
+            {
+                await client.ConnectAsync(default).ConfigureAwait(false);
+
+                Assert.IsFalse(await client.ExistsAsync(remoteHome + "/DoesNotExist"));
+                Assert.IsTrue(await client.ExistsAsync(remoteHome + "/file.exists"));
+                Assert.IsTrue(await client.ExistsAsync(remoteHome + "/symlink.to.file.exists"));
+                Assert.IsTrue(await client.ExistsAsync(remoteHome + "/directory.exists"));
+                Assert.IsTrue(await client.ExistsAsync(remoteHome + "/symlink.to.directory.exists"));
+            }
+
+            #endregion Assert
+
+            #region Teardown
+
+            using (var client = new SshClient(_connectionInfoFactory.Create()))
+            {
+                client.Connect();
+
+                using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+
+                using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}"))
+                {
+                    await command.ExecuteAsync();
+                    Assert.AreEqual(0, command.ExitStatus, command.Error);
+                }
+            }
+
+            #endregion Teardown
+        }
+
         [TestMethod]
         public void Sftp_ListDirectory()
         {