From a9c7addecd296ea33216dbdc47fa9affefa2758b Mon Sep 17 00:00:00 2001 From: olegkap_cp Date: Wed, 29 Dec 2010 21:56:21 +0000 Subject: [PATCH] Reorganize SFTP functionality and add some methods to replicate sftp utility as close as possible Add SFTP Tests Other minor fixes --- .../Renci.SshClient.Tests.csproj | 7 +- .../SftpClientTests/CreateDirectoryTest.cs | 104 +++ .../SftpClientTests/DeleteDirectoryTest.cs | 72 +++ .../{SftpClientTest.cs => DeleteFileTest.cs} | 4 +- .../SftpClientTests/ListDirectoryTest.cs | 123 ++++ .../SftpClientTests/RenameFileTest.cs | 64 ++ .../SftpClientTests/UploadDownloadFileTest.cs | 338 ++++++++++ Renci.SshClient/Renci.SshClient/BaseClient.cs | 10 + .../Common/SshFileNotFoundException.cs | 24 + .../Common/SshPermissionDeniedException.cs | 24 + .../Renci.SshClient/Renci.SshClient.csproj | 5 + .../Renci.SshClient/Sftp/FileStatusCommand.cs | 35 + .../Sftp/Messages/NameMessage.cs | 4 +- .../Sftp/Messages/SymLinkMessage.cs | 4 - .../Sftp/SetFileStatusCommand.cs | 38 ++ .../Renci.SshClient/Sftp/SftpCommand.cs | 29 +- .../Renci.SshClient/Sftp/SftpFile.cs | 183 ++++-- .../Renci.SshClient/Sftp/SftpSession.cs | 7 +- .../Sftp/SymbolicLinkCommand.cs | 36 ++ Renci.SshClient/Renci.SshClient/SftpClient.cs | 609 +++++++++--------- Renci.SshClient/Renci.SshClient/SshClient.cs | 12 +- Renci.SshClient/Renci.SshClient/SshCommand.cs | 13 - 22 files changed, 1356 insertions(+), 389 deletions(-) create mode 100644 Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/CreateDirectoryTest.cs create mode 100644 Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/DeleteDirectoryTest.cs rename Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/{SftpClientTest.cs => DeleteFileTest.cs} (55%) create mode 100644 Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/ListDirectoryTest.cs create mode 100644 Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/RenameFileTest.cs create mode 100644 Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/UploadDownloadFileTest.cs create mode 100644 Renci.SshClient/Renci.SshClient/Common/SshFileNotFoundException.cs create mode 100644 Renci.SshClient/Renci.SshClient/Common/SshPermissionDeniedException.cs create mode 100644 Renci.SshClient/Renci.SshClient/Sftp/FileStatusCommand.cs create mode 100644 Renci.SshClient/Renci.SshClient/Sftp/SetFileStatusCommand.cs create mode 100644 Renci.SshClient/Renci.SshClient/Sftp/SymbolicLinkCommand.cs diff --git a/Renci.SshClient/Renci.SshClient.Tests/Renci.SshClient.Tests.csproj b/Renci.SshClient/Renci.SshClient.Tests/Renci.SshClient.Tests.csproj index f2ac460a..08e96f77 100644 --- a/Renci.SshClient/Renci.SshClient.Tests/Renci.SshClient.Tests.csproj +++ b/Renci.SshClient/Renci.SshClient.Tests/Renci.SshClient.Tests.csproj @@ -60,6 +60,12 @@ + + + + + + @@ -67,7 +73,6 @@ True Resources.resx - diff --git a/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/CreateDirectoryTest.cs b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/CreateDirectoryTest.cs new file mode 100644 index 00000000..ba97c477 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/CreateDirectoryTest.cs @@ -0,0 +1,104 @@ +using System; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshClient.Common; +using Renci.SshClient.Tests.Properties; + +namespace Renci.SshClient.Tests.SftpClientTests +{ + /// + /// Summary description for CreateDirectoryTest + /// + [TestClass] + public class CreateDirectoryTest + { + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshConnectionException))] + public void Test_Sftp_CreateDirectory_Without_Connecting() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.CreateDirectory("test"); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_CreateDirectory_In_Current_Location() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + sftp.CreateDirectory("test"); + + sftp.DeleteDirectory("test"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshPermissionDeniedException))] + public void Test_Sftp_CreateDirectory_In_Forbidden_Directory() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + sftp.CreateDirectory("/sbin/test"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshPermissionDeniedException))] + public void Test_Sftp_CreateDirectory_Invalid_Path() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + sftp.CreateDirectory("/abcdefg/abcefg"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_CreateDirectory_Already_Exists() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + sftp.CreateDirectory("test"); + + var exceptionThrown = false; + try + { + sftp.CreateDirectory("test"); + } + catch (SshException) + { + exceptionThrown = true; + } + + Assert.IsTrue(exceptionThrown); + + sftp.DeleteDirectory("test"); + + sftp.Disconnect(); + } + } + + + } +} diff --git a/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/DeleteDirectoryTest.cs b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/DeleteDirectoryTest.cs new file mode 100644 index 00000000..31a9d408 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/DeleteDirectoryTest.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshClient.Common; +using Renci.SshClient.Tests.Properties; + +namespace Renci.SshClient.Tests.SftpClientTests +{ + [TestClass] + public class DeleteDirectoryTest + { + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshConnectionException))] + public void Test_Sftp_DeleteDirectory_Without_Connecting() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.DeleteDirectory("test"); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshFileNotFoundException))] + public void Test_Sftp_DeleteDirectory_Which_Doesnt_Exists() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + sftp.DeleteDirectory("abcdef"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshPermissionDeniedException))] + public void Test_Sftp_DeleteDirectory_Which_No_Permissions() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + sftp.DeleteDirectory("/usr"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_DeleteDirectory() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + sftp.CreateDirectory("abcdef"); + + sftp.DeleteDirectory("abcdef"); + + sftp.Disconnect(); + } + } + + } +} diff --git a/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/SftpClientTest.cs b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/DeleteFileTest.cs similarity index 55% rename from Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/SftpClientTest.cs rename to Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/DeleteFileTest.cs index 8a3a87f9..e7a8282d 100644 --- a/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/SftpClientTest.cs +++ b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/DeleteFileTest.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Text; -namespace Renci.SshClient.Tests +namespace Renci.SshClient.Tests.SftpClientTests { - class SftpClientTest + class DeleteFileTest { } } diff --git a/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/ListDirectoryTest.cs b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/ListDirectoryTest.cs new file mode 100644 index 00000000..db56c123 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/ListDirectoryTest.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshClient.Common; +using Renci.SshClient.Tests.Properties; +using System.Diagnostics; + +namespace Renci.SshClient.Tests.SftpClientTests +{ + [TestClass] + public class ListDirectoryTest + { + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshConnectionException))] + public void Test_Sftp_ListDirectory_Without_Connecting() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + var files = sftp.ListDirectory("."); + foreach (var file in files) + { + Debug.WriteLine(file.AbsolutePath); + } + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshPermissionDeniedException))] + public void Test_Sftp_ListDirectory_Permission_Denied() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var files = sftp.ListDirectory("/etc/audit"); + foreach (var file in files) + { + Debug.WriteLine(file.AbsolutePath); + } + + sftp.Disconnect(); + } + } + + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshFileNotFoundException))] + public void Test_Sftp_ListDirectory_Not_Exists() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var files = sftp.ListDirectory("/asdfgh"); + foreach (var file in files) + { + Debug.WriteLine(file.AbsolutePath); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_ListDirectory_Current() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var files = sftp.ListDirectory("."); + + Assert.IsTrue(files.Count() > 0); + + foreach (var file in files) + { + Debug.WriteLine(file.AbsolutePath); + } + + sftp.Disconnect(); + } + } + + + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_ListDirectory_HugeDirectory() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + // Create 30000 directory items + for (int i = 0; i < 30000; i++) + { + sftp.CreateDirectory(string.Format("test_{0}", i)); + } + + var files = sftp.ListDirectory("."); + + // Ensure that directory has at least 30000 items + Assert.IsTrue(files.Count() > 30000); + + // Delete 10000 directory items + for (int i = 0; i < 30000; i++) + { + sftp.DeleteDirectory(string.Format("test_{0}", i)); + } + + sftp.Disconnect(); + } + } + + + } +} diff --git a/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/RenameFileTest.cs b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/RenameFileTest.cs new file mode 100644 index 00000000..e74189a1 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/RenameFileTest.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshClient.Tests.Properties; +using System.IO; + +namespace Renci.SshClient.Tests.SftpClientTests +{ + [TestClass] + public class RenameFileTest + { + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Rename_File() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + string uploadedFileName = Path.GetTempFileName(); + string remoteFileName1 = Path.GetRandomFileName(); + string remoteFileName2 = Path.GetRandomFileName(); + + this.CreateTestFile(uploadedFileName, 1); + + using (var file = File.OpenRead(uploadedFileName)) + { + sftp.UploadFile(file, remoteFileName1); + } + + sftp.RenameFile(remoteFileName1, remoteFileName2); + + sftp.DeleteFile(remoteFileName2); + + File.Delete(uploadedFileName); + + sftp.Disconnect(); + } + } + + /// + /// Creates the test file. + /// + /// Name of the file. + /// Size in megabytes. + private void CreateTestFile(string fileName, int size) + { + using (var testFile = File.Create(fileName)) + { + + var random = new Random(); + for (int i = 0; i < 1024 * size; i++) + { + var buffer = new byte[1024]; + random.NextBytes(buffer); + testFile.Write(buffer, 0, buffer.Length); + } + } + } + + } +} diff --git a/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/UploadDownloadFileTest.cs b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/UploadDownloadFileTest.cs new file mode 100644 index 00000000..6b362903 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient.Tests/SftpClientTests/UploadDownloadFileTest.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshClient.Common; +using Renci.SshClient.Tests.Properties; +using System.IO; +using System.Security.Cryptography; +using Renci.SshClient.Sftp; +using System.Threading; + +namespace Renci.SshClient.Tests.SftpClientTests +{ + [TestClass] + public class UploadDownloadFileTest + { + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Upload_And_Download_1MB_File() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + string uploadedFileName = Path.GetTempFileName(); + string remoteFileName = Path.GetRandomFileName(); + + this.CreateTestFile(uploadedFileName, 1); + + // Calculate has value + var uploadedHash = CalculateMD5(uploadedFileName); + + using (var file = File.OpenRead(uploadedFileName)) + { + sftp.UploadFile(file, remoteFileName); + } + + string downloadedFileName = Path.GetTempFileName(); + + using (var file = File.OpenWrite(downloadedFileName)) + { + sftp.DownloadFile(remoteFileName, file); + } + + var downloadedHash = CalculateMD5(downloadedFileName); + + sftp.DeleteFile(remoteFileName); + + File.Delete(uploadedFileName); + File.Delete(downloadedFileName); + + sftp.Disconnect(); + + Assert.AreEqual(uploadedHash, downloadedHash); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Upload_Forbidden() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + string uploadedFileName = Path.GetTempFileName(); + string remoteFileName = "/usr/"; + + this.CreateTestFile(uploadedFileName, 1); + var exceptionOccured = false; + + try + { + + using (var file = File.OpenRead(uploadedFileName)) + { + sftp.UploadFile(file, remoteFileName); + } + } + catch (SshPermissionDeniedException) + { + exceptionOccured = true; + } + + sftp.DeleteFile(remoteFileName); + + File.Delete(uploadedFileName); + + sftp.Disconnect(); + + Assert.IsTrue(exceptionOccured); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Download_Forbidden() + { + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + string remoteFileName = "/root/install.log"; + + var exceptionOccured = false; + + try + { + using (var ms = new MemoryStream()) + { + sftp.UploadFile(ms, remoteFileName); + } + } + catch (SshPermissionDeniedException) + { + exceptionOccured = true; + } + + sftp.Disconnect(); + + Assert.IsTrue(exceptionOccured); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Multiple_Async_Upload_And_Download_10Files_5MB_Each() + { + var maxFiles = 10; + var maxSize = 5; + + using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + sftp.Connect(); + + var testInfoList = new Dictionary(); + + for (int i = 0; i < maxFiles; i++) + { + var testInfo = new TestInfo(); + testInfo.UploadedFileName = Path.GetTempFileName(); + testInfo.DownloadedFileName = Path.GetTempFileName(); + testInfo.RemoteFileName = Path.GetRandomFileName(); + + this.CreateTestFile(testInfo.UploadedFileName, maxSize); + + // Calculate hash value + testInfo.UploadedHash = CalculateMD5(testInfo.UploadedFileName); + + testInfoList.Add(testInfo.RemoteFileName, testInfo); + } + + var uploadWaitHandles = new List(); + + // Start file uploads + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + testInfo.UploadedFile = File.OpenRead(testInfo.UploadedFileName); + testInfo.UploadResult = sftp.BeginUploadFile(testInfo.UploadedFile, remoteFile, null, null); + + + uploadWaitHandles.Add(testInfo.UploadResult.AsyncWaitHandle); + } + + // Wait for upload to finish + bool uploadCompleted = false; + while (!uploadCompleted) + { + // Assume upload completed + uploadCompleted = true; + + foreach (var testInfo in testInfoList.Values) + { + SftpAsyncResult sftpResult = testInfo.UploadResult as SftpAsyncResult; + + testInfo.UploadedBytes = sftpResult.UploadedBytes; + + if (!testInfo.UploadResult.IsCompleted) + { + uploadCompleted = false; + } + } + Thread.Sleep(500); + } + + // End file uploads + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + + sftp.EndUploadFile(testInfo.UploadResult); + testInfo.UploadedFile.Dispose(); + } + + // Start file downloads + + var downloadWaitHandles = new List(); + + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + testInfo.DownloadedFile = File.OpenWrite(testInfo.DownloadedFileName); + testInfo.DownloadResult = sftp.BeginDownloadFile(remoteFile, testInfo.DownloadedFile, null, null); + + downloadWaitHandles.Add(testInfo.DownloadResult.AsyncWaitHandle); + } + + // Wait for download to finish + bool downloadCompleted = false; + while (!downloadCompleted) + { + // Assume download completed + downloadCompleted = true; + + foreach (var testInfo in testInfoList.Values) + { + SftpAsyncResult sftpResult = testInfo.DownloadResult as SftpAsyncResult; + + testInfo.DownloadedBytes = sftpResult.DownloadedBytes; + + if (!testInfo.DownloadResult.IsCompleted) + { + downloadCompleted = false; + } + } + Thread.Sleep(500); + } + + var hashMatches = true; + var uploadDownloadSizeOk = true; + // End file downloads + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + + sftp.EndDownloadFile(testInfo.DownloadResult); + + testInfo.DownloadedFile.Dispose(); + + testInfo.DownloadedHash = CalculateMD5(testInfo.DownloadedFileName); + + if (!(testInfo.UploadedBytes > 0 && testInfo.DownloadedBytes > 0 && testInfo.DownloadedBytes == testInfo.UploadedBytes)) + { + uploadDownloadSizeOk = false; + } + + if (!testInfo.DownloadedHash.Equals(testInfo.UploadedHash)) + { + hashMatches = false; + } + } + + // Clean up after test + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + + sftp.DeleteFile(remoteFile); + + File.Delete(testInfo.UploadedFileName); + File.Delete(testInfo.DownloadedFileName); + } + + sftp.Disconnect(); + + Assert.IsTrue(hashMatches, "Hash does not match"); + Assert.IsTrue(uploadDownloadSizeOk, "Uploaded and downloaded bytes does not match"); + } + } + + /// + /// Creates the test file. + /// + /// Name of the file. + /// Size in megabytes. + private void CreateTestFile(string fileName, int size) + { + using (var testFile = File.Create(fileName)) + { + + var random = new Random(); + for (int i = 0; i < 1024 * size; i++) + { + var buffer = new byte[1024]; + random.NextBytes(buffer); + testFile.Write(buffer, 0, buffer.Length); + } + } + } + + protected static string CalculateMD5(string fileName) + { + using (FileStream file = new FileStream(fileName, FileMode.Open)) + { + MD5 md5 = new MD5CryptoServiceProvider(); + byte[] retVal = md5.ComputeHash(file); + file.Close(); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < retVal.Length; i++) + { + sb.Append(retVal[i].ToString("x2")); + } + return sb.ToString(); + } + } + + /// + /// Helper class to help with upload and download testing + /// + private class TestInfo + { + public string RemoteFileName { get; set; } + + public string UploadedFileName { get; set; } + + public string DownloadedFileName { get; set; } + + public ulong UploadedBytes { get; set; } + + public ulong DownloadedBytes { get; set; } + + public FileStream UploadedFile { get; set; } + + public FileStream DownloadedFile { get; set; } + + public string UploadedHash { get; set; } + + public string DownloadedHash { get; set; } + + public IAsyncResult UploadResult { get; set; } + + public IAsyncResult DownloadResult { get; set; } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/BaseClient.cs b/Renci.SshClient/Renci.SshClient/BaseClient.cs index 79a3d2a7..7737a10d 100644 --- a/Renci.SshClient/Renci.SshClient/BaseClient.cs +++ b/Renci.SshClient/Renci.SshClient/BaseClient.cs @@ -159,6 +159,16 @@ namespace Renci.SshClient { } + + /// + /// Ensures that client is connected. + /// + /// When client not connected. + protected void EnsureConnection() + { + if (!this.Session.IsConnected) + throw new SshConnectionException("Client not connected."); + } #region IDisposable Members diff --git a/Renci.SshClient/Renci.SshClient/Common/SshFileNotFoundException.cs b/Renci.SshClient/Renci.SshClient/Common/SshFileNotFoundException.cs new file mode 100644 index 00000000..3cad5a3e --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Common/SshFileNotFoundException.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Renci.SshClient.Common +{ + /// + /// The exception that is thrown when file or directory is not found. + /// + [Serializable] + public class SshFileNotFoundException : SshException + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + public SshFileNotFoundException(string message) + : base(message) + { + + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Common/SshPermissionDeniedException.cs b/Renci.SshClient/Renci.SshClient/Common/SshPermissionDeniedException.cs new file mode 100644 index 00000000..cc22970b --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Common/SshPermissionDeniedException.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Renci.SshClient.Common +{ + /// + /// The exception that is thrown when operation permission is denied. + /// + [Serializable] + public class SshPermissionDeniedException : SshException + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + public SshPermissionDeniedException(string message) + : base(message) + { + + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Renci.SshClient.csproj b/Renci.SshClient/Renci.SshClient/Renci.SshClient.csproj index 2f3ecc9b..9ac4560f 100644 --- a/Renci.SshClient/Renci.SshClient/Renci.SshClient.csproj +++ b/Renci.SshClient/Renci.SshClient/Renci.SshClient.csproj @@ -75,7 +75,9 @@ + + @@ -96,6 +98,9 @@ + + + diff --git a/Renci.SshClient/Renci.SshClient/Sftp/FileStatusCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/FileStatusCommand.cs new file mode 100644 index 00000000..5366ba8b --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/FileStatusCommand.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Renci.SshClient.Sftp.Messages; + +namespace Renci.SshClient.Sftp +{ + internal class FileStatusCommand : SftpCommand + { + private string _path; + + public SftpFile SftpFile { get; private set; } + + public FileStatusCommand(SftpSession sftpSession, string path) + : base(sftpSession) + { + this._path = path; + } + + protected override void OnExecute() + { + this.SendStatMessage(this._path); + } + + protected override void OnAttributes(Attributes attributes) + { + base.OnAttributes(attributes); + + this.SftpFile = new SftpFile(this._path, attributes); + + this.CompleteExecution(); + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/Messages/NameMessage.cs b/Renci.SshClient/Renci.SshClient/Sftp/Messages/NameMessage.cs index dcbf2d01..7afa1aa2 100644 --- a/Renci.SshClient/Renci.SshClient/Sftp/Messages/NameMessage.cs +++ b/Renci.SshClient/Renci.SshClient/Sftp/Messages/NameMessage.cs @@ -27,10 +27,10 @@ namespace Renci.SshClient.Sftp.Messages for (int i = 0; i < this.Count; i++) { var fileName = this.ReadString(); - var fullName = this.ReadString(); + var fullName = this.ReadString(); // This field value has meaningless information var attributes = this.ReadAttributes(); - files.Add(new SftpFile(fileName, fullName, attributes)); + files.Add(new SftpFile(fileName, attributes)); } this.Files = files; } diff --git a/Renci.SshClient/Renci.SshClient/Sftp/Messages/SymLinkMessage.cs b/Renci.SshClient/Renci.SshClient/Sftp/Messages/SymLinkMessage.cs index 81e4d649..898a7a64 100644 --- a/Renci.SshClient/Renci.SshClient/Sftp/Messages/SymLinkMessage.cs +++ b/Renci.SshClient/Renci.SshClient/Sftp/Messages/SymLinkMessage.cs @@ -13,14 +13,11 @@ namespace Renci.SshClient.Sftp.Messages public string ExistingPath { get; set; } - public bool IsSymLink { get; set; } - protected override void LoadData() { base.LoadData(); this.NewLinkPath = this.ReadString(); this.ExistingPath = this.ReadString(); - this.IsSymLink = this.ReadBoolean(); } protected override void SaveData() @@ -28,7 +25,6 @@ namespace Renci.SshClient.Sftp.Messages base.SaveData(); this.Write(this.NewLinkPath, Encoding.UTF8); this.Write(this.ExistingPath, Encoding.UTF8); - this.Write(this.IsSymLink); } } } diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SetFileStatusCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/SetFileStatusCommand.cs new file mode 100644 index 00000000..fd3ac840 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/SetFileStatusCommand.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Renci.SshClient.Sftp.Messages; + +namespace Renci.SshClient.Sftp +{ + internal class SetFileStatusCommand : SftpCommand + { + private string _path; + + private Attributes _attributes; + + public SftpFile SftpFile { get; private set; } + + public SetFileStatusCommand(SftpSession sftpSession, string path, Attributes attributes) + : base(sftpSession) + { + this._path = path; + + this._attributes = attributes; + } + + protected override void OnExecute() + { + this.SendSetStatMessage(this._path, this._attributes); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SftpCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/SftpCommand.cs index 183cb79f..118a6f62 100644 --- a/Renci.SshClient/Renci.SshClient/Sftp/SftpCommand.cs +++ b/Renci.SshClient/Renci.SshClient/Sftp/SftpCommand.cs @@ -249,13 +249,12 @@ namespace Renci.SshClient.Sftp }); } - protected void SendSymLinkMessage(string existingPath, string newLinkPath, bool isSymLink) + protected void SendSymLinkMessage(string linkPath, string path) { this.SendMessage(new SymLinkMessage { - ExistingPath = existingPath, - NewLinkPath = newLinkPath, - IsSymLink = isSymLink, + NewLinkPath = linkPath, + ExistingPath = path, }); } @@ -276,14 +275,20 @@ namespace Renci.SshClient.Sftp { this.OnStatus(e.Message.StatusCode, e.Message.ErrorMessage, e.Message.Language); - if (e.Message.StatusCode == StatusCodes.NoSuchFile || - e.Message.StatusCode == StatusCodes.PermissionDenied || - e.Message.StatusCode == StatusCodes.Failure || - e.Message.StatusCode == StatusCodes.BadMessage || - e.Message.StatusCode == StatusCodes.NoConnection || - e.Message.StatusCode == StatusCodes.ConnectionLost || - e.Message.StatusCode == StatusCodes.OperationUnsupported - ) + if (e.Message.StatusCode == StatusCodes.PermissionDenied) + { + throw new SshPermissionDeniedException(e.Message.ErrorMessage); + } + else if (e.Message.StatusCode == StatusCodes.NoSuchFile) + { + throw new SshFileNotFoundException(e.Message.ErrorMessage); + } + else if (e.Message.StatusCode == StatusCodes.Failure || + e.Message.StatusCode == StatusCodes.BadMessage || + e.Message.StatusCode == StatusCodes.NoConnection || + e.Message.StatusCode == StatusCodes.ConnectionLost || + e.Message.StatusCode == StatusCodes.OperationUnsupported + ) { // Throw an exception if it was not handled by the command throw new SshException(e.Message.ErrorMessage); diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SftpFile.cs b/Renci.SshClient/Renci.SshClient/Sftp/SftpFile.cs index dd3b567d..d4d67e19 100644 --- a/Renci.SshClient/Renci.SshClient/Sftp/SftpFile.cs +++ b/Renci.SshClient/Renci.SshClient/Sftp/SftpFile.cs @@ -12,62 +12,27 @@ namespace Renci.SshClient.Sftp /// /// Initializes a new instance of the class. /// - /// Name of the file. - /// The full name. + /// Name of the file. /// The attributes. - public SftpFile(string fileName, string fullName, Attributes attributes) + public SftpFile(string absolutePath, Attributes attributes) { - this.Name = fileName; + this.AbsolutePath = absolutePath; - if (attributes.Size.HasValue) - this.Size = (int)attributes.Size.Value; - else - this.Size = -1; - - if (attributes.UserId.HasValue) - this.UserId = (int)attributes.UserId.Value; - else - this.UserId = -1; - - if (attributes.GroupId.HasValue) - this.GroupId = (int)attributes.GroupId.Value; - else - this.GroupId = -1; - - if (attributes.Permissions.HasValue) - this.Permissions = (int)attributes.Permissions.Value; - else - this.Permissions = -1; - - if (attributes.AccessTime.HasValue) - this.AccessedTime = attributes.AccessTime.Value; - else - this.AccessedTime = DateTime.MinValue; - - if (attributes.ModifyTime.HasValue) - this.ModifiedTime = attributes.ModifyTime.Value; - else - this.ModifiedTime = DateTime.MinValue; - - if (attributes.Extensions != null) - this.Extensions = new Dictionary(attributes.Extensions); + this.Attributes = attributes; } + /// + /// Gets file status information attributes. + /// + public Attributes Attributes { get; private set; } + /// /// Gets or sets file name. /// /// /// File name. /// - public string Name { get; set; } - - /// - /// Gets or sets file filename. - /// - /// - /// File filename. - /// - public string Filename { get; set; } + public string AbsolutePath { get; set; } /// /// Gets or sets file accessed time. @@ -75,7 +40,20 @@ namespace Renci.SshClient.Sftp /// /// File accessed time. /// - public DateTime AccessedTime { get; set; } + public DateTime AccessedTime + { + get + { + if (this.Attributes.AccessTime.HasValue) + return this.Attributes.AccessTime.Value; + else + return DateTime.MinValue; + } + set + { + this.Attributes.AccessTime = value; + } + } /// /// Gets or sets file modified time. @@ -83,7 +61,20 @@ namespace Renci.SshClient.Sftp /// /// File modified time. /// - public DateTime ModifiedTime { get; set; } + public DateTime ModifiedTime + { + get + { + if (this.Attributes.ModifyTime.HasValue) + return this.Attributes.ModifyTime.Value; + else + return DateTime.MinValue; + } + set + { + this.Attributes.ModifyTime = value; + } + } /// /// Gets or sets file size. @@ -91,7 +82,27 @@ namespace Renci.SshClient.Sftp /// /// File size. /// - public long Size { get; set; } + public long Size + { + get + { + if (this.Attributes.Size.HasValue) + return (long)this.Attributes.Size.Value; + else + return -1; + } + set + { + if (value > -1) + { + this.Attributes.Size = new Nullable((ulong)value); + } + else + { + this.Attributes.Size = null; + } + } + } /// /// Gets or sets file user id. @@ -99,7 +110,27 @@ namespace Renci.SshClient.Sftp /// /// File user id. /// - public int UserId { get; set; } + public int UserId + { + get + { + if (this.Attributes.UserId.HasValue) + return (int)this.Attributes.UserId.Value; + else + return -1; + } + set + { + if (value > -1) + { + this.Attributes.UserId = new Nullable((uint)value); + } + else + { + this.Attributes.UserId = null; + } + } + } /// /// Gets or sets file group id. @@ -107,7 +138,27 @@ namespace Renci.SshClient.Sftp /// /// File group id. /// - public int GroupId { get; set; } + public int GroupId + { + get + { + if (this.Attributes.GroupId.HasValue) + return (int)this.Attributes.GroupId.Value; + else + return -1; + } + set + { + if (value > -1) + { + this.Attributes.GroupId = new Nullable((uint)value); + } + else + { + this.Attributes.GroupId = null; + } + } + } /// /// Gets or sets file permissions. @@ -115,7 +166,27 @@ namespace Renci.SshClient.Sftp /// /// File permissions. /// - public int Permissions { get; set; } + public int Permissions + { + get + { + if (this.Attributes.Permissions.HasValue) + return (int)this.Attributes.Permissions.Value; + else + return -1; + } + set + { + if (value > -1) + { + this.Attributes.Permissions = new Nullable((uint)value); + } + else + { + this.Attributes.Permissions = null; + } + } + } /// /// Gets or sets file extensions attributes. @@ -123,7 +194,13 @@ namespace Renci.SshClient.Sftp /// /// File extensions. /// - public IDictionary Extensions { get; set; } + public IDictionary Extensions + { + get + { + return this.Attributes.Extensions; + } + } /// /// Returns a that represents this instance. @@ -133,7 +210,7 @@ namespace Renci.SshClient.Sftp /// public override string ToString() { - return string.Format("Name {0}, Size {1}, User ID {2}, Group ID {3}, Permissions {4:X}, Accessed {5}, Modified {6}", this.Name, this.Size, this.UserId, this.GroupId, this.Permissions, this.AccessedTime, this.ModifiedTime); + return string.Format("Name {0}, Size {1}, User ID {2}, Group ID {3}, Permissions {4:X}, Accessed {5}, Modified {6}", this.AbsolutePath, this.Size, this.UserId, this.GroupId, this.Permissions, this.AccessedTime, this.ModifiedTime); } } } diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SftpSession.cs b/Renci.SshClient/Renci.SshClient/Sftp/SftpSession.cs index 6e949155..84e83173 100644 --- a/Renci.SshClient/Renci.SshClient/Sftp/SftpSession.cs +++ b/Renci.SshClient/Renci.SshClient/Sftp/SftpSession.cs @@ -25,10 +25,12 @@ namespace Renci.SshClient.Sftp private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(false); - public int _operationTimeout; + private int _operationTimeout; public event EventHandler ErrorOccured; + public int ProtocolVersion { get; private set; } + #region SFTP messages internal event EventHandler> StatusMessageReceived; @@ -67,6 +69,8 @@ namespace Renci.SshClient.Sftp }); this.WaitHandle(this._sftpVersionConfirmed, this._operationTimeout); + + this.ProtocolVersion = 3; } public void Disconnect() @@ -295,6 +299,5 @@ namespace Renci.SshClient.Sftp } #endregion - } } diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SymbolicLinkCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/SymbolicLinkCommand.cs new file mode 100644 index 00000000..36557e82 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/SymbolicLinkCommand.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Renci.SshClient.Sftp.Messages; + +namespace Renci.SshClient.Sftp +{ + internal class SymbolicLinkCommand : SftpCommand + { + private string _linkPath; + + private string _path; + + public SymbolicLinkCommand(SftpSession sftpSession, string linkPath, string path) + : base(sftpSession) + { + this._linkPath = linkPath; + + this._path = path; + } + + protected override void OnExecute() + { + this.SendSymLinkMessage(this._linkPath, this._path); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/SftpClient.cs b/Renci.SshClient/Renci.SshClient/SftpClient.cs index 0ed780a3..0523c08c 100644 --- a/Renci.SshClient/Renci.SshClient/SftpClient.cs +++ b/Renci.SshClient/Renci.SshClient/SftpClient.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using System.IO; using Renci.SshClient.Sftp; @@ -28,6 +29,16 @@ namespace Renci.SshClient /// The size of the buffer. public uint BufferSize { get; set; } + /// + /// Gets remote working directory. + /// + public string WorkingDirectory { get; private set; } + + /// + /// Gets sftp protocol version. + /// + public int ProtocolVersion { get; private set; } + #region Constructors /// @@ -89,18 +100,232 @@ namespace Renci.SshClient #endregion - #region List Directory + /// + /// Changes remote directory to path. + /// + /// New directory path. + public void ChangeDirectory(string path) + { + // TODO: Check if change directory should be improved based on "6.10.1 Best Practice for Dealing with Paths" paragraph. + + // Ensure that connection is established. + this.EnsureConnection(); + + this.WorkingDirectory = this.ValidatePath(this.ResolvePath(path)); + } /// - /// Begins the list directory operation. + /// Changes group of file(s)to specified group id. + /// + /// File(s) path, may match multiple files. + /// Numeric GID. + public void ChangeGroup(string path, ushort groupId) + { + // TODO: Need to be tested + + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ResolvePath(path); + + var cmd = new FileStatusCommand(this._sftpSession, fullPath); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + + cmd.SftpFile.GroupId = groupId; + + var setCmd = new SetFileStatusCommand(this._sftpSession, fullPath, cmd.SftpFile.Attributes); + + setCmd.CommandTimeout = this.OperationTimeout; + + setCmd.Execute(); + } + + /// + /// Changes permissions of file(s) to specified mode. + /// + /// File(s) path, may match multiple files. + /// The mode. + public void ChangePermissions(string path, int mode) + { + // TODO: Need to be tested + + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ResolvePath(path); + + var cmd = new FileStatusCommand(this._sftpSession, fullPath); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + + cmd.SftpFile.Permissions = mode; + + var setCmd = new SetFileStatusCommand(this._sftpSession, fullPath, cmd.SftpFile.Attributes); + + setCmd.CommandTimeout = this.OperationTimeout; + + setCmd.Execute(); + } + + /// + /// Changes the owner of file(s) to specified owner. + /// + /// File(s) path, may match multiple files. + /// Numeric UID. + public void ChangeOwner(string path, ushort owner) + { + // TODO: Need to be tested + + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ResolvePath(path); + + var cmd = new FileStatusCommand(this._sftpSession, fullPath); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + + cmd.SftpFile.UserId = owner; + + var setCmd = new SetFileStatusCommand(this._sftpSession, fullPath, cmd.SftpFile.Attributes); + + setCmd.CommandTimeout = this.OperationTimeout; + + setCmd.Execute(); + } + + /// + /// Creates remote directory specified by path. + /// + /// Directory path to create. + /// + /// + public void CreateDirectory(string path) + { + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ResolvePath(path); + + var cmd = new CreateDirectoryCommand(this._sftpSession, fullPath); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + } + + /// + /// Deletes remote directory specified by path. + /// + /// Directory to be deleted path. + public void DeleteDirectory(string path) + { + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ValidatePath(this.ResolvePath(path)); + + var cmd = new RemoveDirectoryCommand(this._sftpSession, fullPath); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + } + + /// + /// Deletes remote file specified by path. + /// + /// File to be deleted path. + public void DeleteFile(string path) + { + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ValidatePath(this.ResolvePath(path)); + + var cmd = new RemoveFileCommand(this._sftpSession, fullPath); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + } + + /// + /// Renames remote file from old path to new path. + /// + /// Path to the old file location. + /// Path to the new file location. + public void RenameFile(string oldPath, string newPath) + { + // Ensure that connection is established. + this.EnsureConnection(); + + var oldFullPath = this.ValidatePath(this.ResolvePath(oldPath)); + + var newFullPath = this.ResolvePath(newPath); + + var cmd = new RenameFileCommand(this._sftpSession, oldFullPath, newFullPath); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + } + + /// + /// Creates a symbolic link from old path to new path. + /// + /// The old path. + /// The new path. + public void SymbolicLink(string linkPath, string path) + { + // TODO: Need to be tested, currently does not work + + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ValidatePath(this.ResolvePath(path)); + + var linkFullPath = this.ResolvePath(linkPath); + + var cmd = new SymbolicLinkCommand(this._sftpSession, linkFullPath, fullPath); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + } + + /// + /// Retrieves list of files in remote directory. /// /// The path. - /// The async callback. - /// The state. + /// List of directory entries + public IEnumerable ListDirectory(string path) + { + return this.EndListDirectory(this.BeginListDirectory(path, null, null)); + } + + /// + /// Begins an asynchronous operation of retrieving list of files in remote directory. + /// + /// The path. + /// The method to be called when the asynchronous write operation is completed. + /// A user-provided object that distinguishes this particular asynchronous write request from other requests. /// An that references the asynchronous operation. public IAsyncResult BeginListDirectory(string path, AsyncCallback asyncCallback, object state) { - var cmd = new ListDirectoryCommand(this._sftpSession, path); + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ValidatePath(this.ResolvePath(path)); + + var cmd = new ListDirectoryCommand(this._sftpSession, fullPath); cmd.CommandTimeout = this.OperationTimeout; @@ -108,9 +333,9 @@ namespace Renci.SshClient } /// - /// Ends the list directory operation. + /// Ends an asynchronous operation of retrieving list of files in remote directory. /// - /// An that references the asynchronous operation. + /// The pending asynchronous SFTP request. /// List of files public IEnumerable EndListDirectory(IAsyncResult asyncResult) { @@ -129,30 +354,31 @@ namespace Renci.SshClient } /// - /// Lists the directory. + /// Downloads remote file specified by the path into the stream. /// - /// The path. - /// List of files - public IEnumerable ListDirectory(string path) + /// File to download. + /// Stream to write the file into. + public void DownloadFile(string path, Stream output) { - return this.EndListDirectory(this.BeginListDirectory(path, null, null)); + this.EndDownloadFile(this.BeginDownloadFile(path, output, null, null)); } - #endregion - - #region Rename file - /// - /// Begins the rename file. + /// Begins an asynchronous file downloading into the stream. /// - /// The old filename. - /// New name of the file. - /// The async callback. - /// The state. + /// The path. + /// The output. + /// The method to be called when the asynchronous write operation is completed. + /// A user-provided object that distinguishes this particular asynchronous write request from other requests. /// An that references the asynchronous operation. - public IAsyncResult BeginRenameFile(string oldFilename, string newFileName, AsyncCallback asyncCallback, object state) + public IAsyncResult BeginDownloadFile(string path, Stream output, AsyncCallback asyncCallback, object state) { - var cmd = new RenameFileCommand(this._sftpSession, oldFilename, newFileName); + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ValidatePath(this.ResolvePath(path)); + + var cmd = new DownloadFileCommand(this._sftpSession, this.BufferSize, fullPath, output); cmd.CommandTimeout = this.OperationTimeout; @@ -160,10 +386,10 @@ namespace Renci.SshClient } /// - /// Ends the rename file. + /// Ends an asynchronous file downloading into the stream. /// - /// An that references the asynchronous operation. - public void EndRenameFile(IAsyncResult asyncResult) + /// The pending asynchronous SFTP request. + public void EndDownloadFile(IAsyncResult asyncResult) { var sftpAsyncResult = asyncResult as SftpAsyncResult; @@ -172,35 +398,37 @@ namespace Renci.SshClient throw new InvalidOperationException("Not valid IAsyncResult object."); } - var cmd = sftpAsyncResult.GetCommand(); + var cmd = sftpAsyncResult.GetCommand(); cmd.EndExecute(sftpAsyncResult); } /// - /// Renames the file. + /// Uploads stream into remote file.. /// - /// The old filename. - /// New name of the file. - public void RenameFile(string oldFilename, string newFileName) + /// Data input stream. + /// Remote file path. + public void UploadFile(Stream input, string path) { - this.EndRenameFile(this.BeginRenameFile(oldFilename, newFileName, null, null)); + this.EndUploadFile(this.BeginUploadFile(input, path, null, null)); } - #endregion - - #region Remove Directory - /// - /// Begins the remove directory. + /// Begins an asynchronous uploading the steam into remote file. /// - /// The path. - /// The async callback. - /// The state. + /// Data input stream. + /// Remote file path. + /// The method to be called when the asynchronous write operation is completed. + /// A user-provided object that distinguishes this particular asynchronous write request from other requests. /// An that references the asynchronous operation. - public IAsyncResult BeginRemoveDirectory(string path, AsyncCallback asyncCallback, object state) + public IAsyncResult BeginUploadFile(Stream input, string path, AsyncCallback asyncCallback, object state) { - var cmd = new RemoveDirectoryCommand(this._sftpSession, path); + // Ensure that connection is established. + this.EnsureConnection(); + + var fullPath = this.ResolvePath(path); + + var cmd = new UploadFileCommand(this._sftpSession, this.BufferSize, fullPath, input); cmd.CommandTimeout = this.OperationTimeout; @@ -208,151 +436,9 @@ namespace Renci.SshClient } /// - /// Ends the remove directory. + /// Ends an asynchronous uploading the steam into remote file. /// - /// An that references the asynchronous operation. - public void EndRemoveDirectory(IAsyncResult asyncResult) - { - var sftpAsyncResult = asyncResult as SftpAsyncResult; - - if (sftpAsyncResult == null) - { - throw new InvalidOperationException("Not valid IAsyncResult object."); - } - - var cmd = sftpAsyncResult.GetCommand(); - - cmd.EndExecute(sftpAsyncResult); - } - - /// - /// Removes the directory. - /// - /// The path. - public void RemoveDirectory(string path) - { - this.EndRemoveDirectory(this.BeginRemoveDirectory(path, null, null)); - } - - #endregion - - #region Create Directory - - /// - /// Begins the create directory. - /// - /// The path. - /// The async callback. - /// The state. - /// An that references the asynchronous operation. - public IAsyncResult BeginCreateDirectory(string path, AsyncCallback asyncCallback, object state) - { - var cmd = new CreateDirectoryCommand(this._sftpSession, path); - - cmd.CommandTimeout = this.OperationTimeout; - - return cmd.BeginExecute(asyncCallback, state); - } - - /// - /// Ends the create directory. - /// - /// An that references the asynchronous operation. - public void EndCreateDirectory(IAsyncResult asyncResult) - { - var sftpAsyncResult = asyncResult as SftpAsyncResult; - - if (sftpAsyncResult == null) - { - throw new InvalidOperationException("Not valid IAsyncResult object."); - } - - var cmd = sftpAsyncResult.GetCommand(); - - cmd.EndExecute(sftpAsyncResult); - } - - /// - /// Creates the directory. - /// - /// The path. - public void CreateDirectory(string path) - { - this.EndCreateDirectory(this.BeginCreateDirectory(path, null, null)); - } - - #endregion - - #region Remove File - - /// - /// Begins the remove file. - /// - /// The filename. - /// The async callback. - /// The state. - /// An that references the asynchronous operation. - public IAsyncResult BeginRemoveFile(string filename, AsyncCallback asyncCallback, object state) - { - var cmd = new RemoveFileCommand(this._sftpSession, filename); - - cmd.CommandTimeout = this.OperationTimeout; - - return cmd.BeginExecute(asyncCallback, state); - } - - /// - /// Ends the remove file. - /// - /// An that references the asynchronous operation. - public void EndRemoveFile(IAsyncResult asyncResult) - { - var sftpAsyncResult = asyncResult as SftpAsyncResult; - - if (sftpAsyncResult == null) - { - throw new InvalidOperationException("Not valid IAsyncResult object."); - } - - var cmd = sftpAsyncResult.GetCommand(); - - cmd.EndExecute(sftpAsyncResult); - } - - /// - /// Removes the file. - /// - /// The filename. - public void RemoveFile(string filename) - { - this.EndRemoveFile(this.BeginRemoveFile(filename, null, null)); - } - - #endregion - - #region Upload File - - /// - /// Begins the upload file. - /// - /// The filename. - /// The input. - /// The async callback. - /// The state. - /// An that references the asynchronous operation. - public IAsyncResult BeginUploadFile(string filename, Stream input, AsyncCallback asyncCallback, object state) - { - var cmd = new UploadFileCommand(this._sftpSession, this.BufferSize, filename, input); - - cmd.CommandTimeout = this.OperationTimeout; - - return cmd.BeginExecute(asyncCallback, state); - } - - /// - /// Ends the upload file. - /// - /// An that references the asynchronous operation. + /// The pending asynchronous SFTP request. public void EndUploadFile(IAsyncResult asyncResult) { var sftpAsyncResult = asyncResult as SftpAsyncResult; @@ -367,118 +453,6 @@ namespace Renci.SshClient cmd.EndExecute(sftpAsyncResult); } - /// - /// Uploads the file. - /// - /// The filename. - /// An that references the asynchronous operation. - public void UploadFile(string filename, Stream input) - { - this.EndUploadFile(this.BeginUploadFile(filename, input, null, null)); - } - - #endregion - - #region Download File - - /// - /// Begins the download. - /// - /// The filename. - /// The output. - /// The async callback. - /// The state. - /// An that references the asynchronous operation. - public IAsyncResult BeginDownload(string filename, Stream output, AsyncCallback asyncCallback, object state) - { - var cmd = new DownloadFileCommand(this._sftpSession, this.BufferSize, filename, output); - - cmd.CommandTimeout = this.OperationTimeout; - - return cmd.BeginExecute(asyncCallback, state); - } - - /// - /// Ends the download. - /// - /// An that references the asynchronous operation. - public void EndDownload(IAsyncResult asyncResult) - { - var sftpAsyncResult = asyncResult as SftpAsyncResult; - - if (sftpAsyncResult == null) - { - throw new InvalidOperationException("Not valid IAsyncResult object."); - } - - var cmd = sftpAsyncResult.GetCommand(); - - cmd.EndExecute(sftpAsyncResult); - } - - /// - /// Downloads the specified filename. - /// - /// The filename. - /// The output. - public void Download(string filename, Stream output) - { - this.EndDownload(this.BeginDownload(filename, output, null, null)); - } - - #endregion - - #region Get Real Path - - /// - /// Begins the get real path. - /// - /// The path. - /// The async callback. - /// The state. - /// An that references the asynchronous operation. - public IAsyncResult BeginGetRealPath(string path, AsyncCallback asyncCallback, object state) - { - var cmd = new RealPathCommand(this._sftpSession, path); - - cmd.CommandTimeout = this.OperationTimeout; - - return cmd.BeginExecute(asyncCallback, state); - } - - /// - /// Ends the get real path. - /// - /// An that references the asynchronous operation. - /// - public IEnumerable EndGetRealPath(IAsyncResult asyncResult) - { - var sftpAsyncResult = asyncResult as SftpAsyncResult; - - if (sftpAsyncResult == null) - { - throw new InvalidOperationException("Not valid IAsyncResult object."); - } - - var cmd = sftpAsyncResult.GetCommand(); - - cmd.EndExecute(sftpAsyncResult); - - return cmd.Files; - } - - /// - /// Gets the real path. - /// - /// The path. - /// - public IEnumerable GetRealPath(string path) - { - return this.EndGetRealPath(this.BeginGetRealPath(path, null, null)); - } - - #endregion - /// /// Called when client is connected to the server. /// @@ -489,6 +463,12 @@ namespace Renci.SshClient this._sftpSession = new SftpSession(this.Session, this.OperationTimeout); this._sftpSession.Connect(); + + // Resolve current directory + this.WorkingDirectory = this.ValidatePath("."); + + // Resolve current running version + this.ProtocolVersion = this._sftpSession.ProtocolVersion; } /// @@ -501,5 +481,38 @@ namespace Renci.SshClient this._sftpSession.Disconnect(); } + /// + /// Resolves the path client side without server validation. + /// + /// Path to resolve. + /// Resolved path + private string ResolvePath(string path) + { + // If path starts with "/" then its "absolute" + if (!path.StartsWith("/")) + { + return string.Format("{0}/{1}", this.WorkingDirectory, path); + } + else + return path; + } + + /// + /// Resolves path into absolute path on the server. + /// + /// PAth to resolve.. + /// Absolute path + private string ValidatePath(string path) + { + var cmd = new RealPathCommand(this._sftpSession, path); + + cmd.CommandTimeout = this.OperationTimeout; + + cmd.Execute(); + + var file = cmd.Files.FirstOrDefault(); + + return file.AbsolutePath; + } } } \ No newline at end of file diff --git a/Renci.SshClient/Renci.SshClient/SshClient.cs b/Renci.SshClient/Renci.SshClient/SshClient.cs index 5ed0bef2..5a320810 100644 --- a/Renci.SshClient/Renci.SshClient/SshClient.cs +++ b/Renci.SshClient/Renci.SshClient/SshClient.cs @@ -112,6 +112,9 @@ namespace Renci.SshClient /// public T AddForwardedPort(string boundHost, uint boundPort, string connectedHost, uint connectedPort) where T : ForwardedPort, new() { + // Ensure that connection is established. + this.EnsureConnection(); + T port = new T(); port.Session = this.Session; @@ -157,7 +160,7 @@ namespace Renci.SshClient /// object. public SshCommand CreateCommand(string commandText) { - return new SshCommand(this.Session, commandText); + return this.CreateCommand(commandText, Encoding.ASCII); } /// @@ -168,10 +171,12 @@ namespace Renci.SshClient /// object which uses specified encoding. public SshCommand CreateCommand(string commandText, Encoding encoding) { + // Ensure that connection is established. + this.EnsureConnection(); + return new SshCommand(this.Session, commandText, encoding); } - /// /// Creates and executes the command. /// @@ -199,6 +204,9 @@ namespace Renci.SshClient /// public Shell CreateShell(Stream input, TextWriter output, TextWriter extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, string terminalMode) { + // Ensure that connection is established. + this.EnsureConnection(); + return new Shell(this.Session, input, output, extendedOutput, terminalName, columns, rows, width, height, terminalMode); } } diff --git a/Renci.SshClient/Renci.SshClient/SshCommand.cs b/Renci.SshClient/Renci.SshClient/SshCommand.cs index c5fc1494..f21f6d6e 100644 --- a/Renci.SshClient/Renci.SshClient/SshCommand.cs +++ b/Renci.SshClient/Renci.SshClient/SshCommand.cs @@ -86,16 +86,6 @@ namespace Renci.SshClient } } - /// - /// Initializes a new instance of the class. - /// - /// The session. - /// The command text. - internal SshCommand(Session session, string commandText) - : this(session, commandText, Encoding.ASCII) - { - } - /// /// Initializes a new instance of the class. /// @@ -123,9 +113,6 @@ namespace Renci.SshClient /// Operation has timed out. public IAsyncResult BeginExecute(AsyncCallback callback, object state) { - if (!this._session.IsConnected) - throw new SshConnectionException("Not connected."); - // Prevent from executing BeginExecute before calling EndExecute if (this._asyncResult != null) {