diff --git a/README.md b/README.md
index 0cc04fb9b..4d17e4186 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,8 @@ This project was inspired by **Sharp.SSH** library which was ported from java an
* Supports DES-EDE3-CBC, DES-EDE3-CFB, DES-CBC, AES-128-CBC, AES-192-CBC and AES-256-CBC algorithms for private key encryption
* Supports two-factor or higher authentication
* Supports SOCKS4, SOCKS5 and HTTP Proxy
+* Sample of sftp client exe
+* sftp client can now be configured to always use absolute paths on the server (i.e. changedirectory will keep track of the absolute path and all file actions will provide absolute paths to the server). This gives support for buggy server implementations where changedirectory will use the grown permissions
## Key Exchange Method
diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Connect_SftpSessionConnectFailure.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Connect_SftpSessionConnectFailure.cs
index afbc247b3..e65d72256 100644
--- a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Connect_SftpSessionConnectFailure.cs
+++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Connect_SftpSessionConnectFailure.cs
@@ -64,7 +64,7 @@ private void SetupMocks()
.Setup(p => p.CreateSftpResponseFactory())
.Returns(_sftpResponseFactoryMock.Object);
_serviceFactoryMock.InSequence(sequence)
- .Setup(p => p.CreateSftpSession(_sessionMock.Object, -1, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object))
+ .Setup(p => p.CreateSftpSession(_sessionMock.Object, -1, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object, false))
.Returns(_sftpSessionMock.Object);
_sftpSessionMock.InSequence(sequence)
.Setup(p => p.Connect())
diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Connected.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Connected.cs
index ba7d080fb..8f93575a9 100644
--- a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Connected.cs
+++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Connected.cs
@@ -49,7 +49,7 @@ protected void Arrange()
.Setup(p => p.CreateSftpResponseFactory())
.Returns(_sftpResponseFactoryMock.Object);
_serviceFactoryMock.InSequence(sequence)
- .Setup(p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object))
+ .Setup(p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object, false))
.Returns(_sftpSessionMock.Object);
_sftpSessionMock.InSequence(sequence).Setup(p => p.Connect());
_sessionMock.InSequence(sequence).Setup(p => p.OnDisconnecting());
@@ -74,7 +74,7 @@ public void CreateSftpMessageFactoryOnServiceFactoryShouldBeInvokedOnce()
public void CreateSftpSessionOnServiceFactoryShouldBeInvokedOnce()
{
_serviceFactoryMock.Verify(
- p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object),
+ p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object, false),
Times.Once);
}
diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disconnected.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disconnected.cs
index 516b8aebf..a4a65d2ee 100644
--- a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disconnected.cs
+++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disconnected.cs
@@ -45,7 +45,7 @@ protected void Arrange()
.Setup(p => p.CreateSftpResponseFactory())
.Returns(_sftpResponseFactoryMock.Object);
_serviceFactoryMock.InSequence(sequence)
- .Setup(p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object))
+ .Setup(p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object, false))
.Returns(_sftpSessionMock.Object);
_sftpSessionMock.InSequence(sequence).Setup(p => p.Connect());
_sessionMock.InSequence(sequence).Setup(p => p.OnDisconnecting());
@@ -71,7 +71,7 @@ public void CreateSftpMessageFactoryOnServiceFactoryShouldBeInvokedOnce()
public void CreateSftpSessionOnServiceFactoryShouldBeInvokedOnce()
{
_serviceFactoryMock.Verify(
- p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object),
+ p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object, false),
Times.Once);
}
diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disposed.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disposed.cs
index 73b07ad46..8fb6a210b 100644
--- a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disposed.cs
+++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Dispose_Disposed.cs
@@ -49,7 +49,7 @@ protected void Arrange()
.Setup(p => p.CreateSftpResponseFactory())
.Returns(_sftpResponseFactoryMock.Object);
_serviceFactoryMock.InSequence(sequence)
- .Setup(p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object))
+ .Setup(p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object, false))
.Returns(_sftpSessionMock.Object);
_sftpSessionMock.InSequence(sequence).Setup(p => p.Connect());
_sessionMock.InSequence(sequence).Setup(p => p.OnDisconnecting());
@@ -75,7 +75,7 @@ public void CreateSftpMessageFactoryOnServiceFactoryShouldBeInvokedOnce()
public void CreateSftpSessionOnServiceFactoryShouldBeInvokedOnce()
{
_serviceFactoryMock.Verify(
- p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object),
+ p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object, false),
Times.Once);
}
diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Finalize_Connected.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Finalize_Connected.cs
index 1c1328d75..507c08a37 100644
--- a/src/Renci.SshNet.Tests/Classes/SftpClientTest_Finalize_Connected.cs
+++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest_Finalize_Connected.cs
@@ -40,7 +40,7 @@ protected void Arrange()
_sessionMock.Setup(p => p.Connect());
_serviceFactoryMock.Setup(p => p.CreateSftpResponseFactory())
.Returns(_sftpResponseFactoryMock.Object);
- _serviceFactoryMock.Setup(p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object))
+ _serviceFactoryMock.Setup(p => p.CreateSftpSession(_sessionMock.Object, _operationTimeout, _connectionInfo.Encoding, _sftpResponseFactoryMock.Object, false))
.Returns(_sftpSessionMock.Object);
_sftpSessionMock.Setup(p => p.Connect());
diff --git a/src/Renci.SshNet.VS2017.sln b/src/Renci.SshNet.VS2017.sln
index 00469f612..00db49bf3 100644
--- a/src/Renci.SshNet.VS2017.sln
+++ b/src/Renci.SshNet.VS2017.sln
@@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Renci.SshNet.NET35", "Renci
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Renci.SshNet.NETCore", "Renci.SshNet.NETCore\Renci.SshNet.NETCore.csproj", "{8E8229EB-6780-4A8A-B470-E2023FA55AB5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Renci.sftp", "renci.sftp\Renci.sftp.csproj", "{C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -103,6 +105,26 @@ Global
{8E8229EB-6780-4A8A-B470-E2023FA55AB5}.Release|x64.Build.0 = Release|Any CPU
{8E8229EB-6780-4A8A-B470-E2023FA55AB5}.Release|x86.ActiveCfg = Release|Any CPU
{8E8229EB-6780-4A8A-B470-E2023FA55AB5}.Release|x86.Build.0 = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|ARM.Build.0 = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|x64.Build.0 = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Debug|x86.Build.0 = Debug|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|ARM.ActiveCfg = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|ARM.Build.0 = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|x64.ActiveCfg = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|x64.Build.0 = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|x86.ActiveCfg = Release|Any CPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -111,6 +133,9 @@ Global
{94EE3919-19FA-4D9B-8DA9-249050B15232} = {2D6CAE62-D053-476F-9BDD-2B1F27FA9C5D}
{A6C3FFD3-16A5-44D3-8C1F-3613D6DD17D1} = {2D6CAE62-D053-476F-9BDD-2B1F27FA9C5D}
EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {BE1E1CE2-57C9-4766-858E-1DE90D2F51FD}
+ EndGlobalSection
GlobalSection(TestCaseManagementSettings) = postSolution
CategoryFile = Renci.SshNet1.vsmdi
EndGlobalSection
diff --git a/src/Renci.SshNet/IServiceFactory.cs b/src/Renci.SshNet/IServiceFactory.cs
index 8941af093..406e1abc3 100644
--- a/src/Renci.SshNet/IServiceFactory.cs
+++ b/src/Renci.SshNet/IServiceFactory.cs
@@ -32,10 +32,11 @@ internal partial interface IServiceFactory
/// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
/// The encoding.
/// The factory to use for creating SFTP messages.
+ /// If true, the sftp client will always pass absolute paths to serve, and will locally mimmick 'changedir' operations
///
/// An .
///
- ISftpSession CreateSftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpMessageFactory);
+ ISftpSession CreateSftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpMessageFactory, bool changeDirIsLocal);
///
/// Create a new .
diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs
index 9d7e8310b..09721eefe 100644
--- a/src/Renci.SshNet/ServiceFactory.cs
+++ b/src/Renci.SshNet/ServiceFactory.cs
@@ -53,12 +53,13 @@ public ISession CreateSession(ConnectionInfo connectionInfo)
/// The number of milliseconds to wait for an operation to complete, or -1 to wait indefinitely.
/// The encoding.
/// The factory to use for creating SFTP messages.
+ /// If true, the sftp client will always pass absolute paths to serve, and will locally mimmick 'changedir' operations
///
/// An .
///
- public ISftpSession CreateSftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpMessageFactory)
+ public ISftpSession CreateSftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpMessageFactory, bool changeDirIsLocal)
{
- return new SftpSession(session, operationTimeout, encoding, sftpMessageFactory);
+ return new SftpSession(session, operationTimeout, encoding, sftpMessageFactory, changeDirIsLocal);
}
///
diff --git a/src/Renci.SshNet/Sftp/SftpSession.cs b/src/Renci.SshNet/Sftp/SftpSession.cs
index 283ff6179..4be01b34b 100644
--- a/src/Renci.SshNet/Sftp/SftpSession.cs
+++ b/src/Renci.SshNet/Sftp/SftpSession.cs
@@ -21,6 +21,13 @@ internal class SftpSession : SubsystemSession, ISftpSession
private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(false);
private IDictionary _supportedExtensions;
+ private string localWorkingDirectory;
+
+ ///
+ /// If true, the sftp client will always pass absolute paths to serve, and will locally mimmick 'changedir' operations
+ ///
+ private bool changeDirIsLocal;
+
///
/// Gets the character encoding to use.
///
@@ -55,11 +62,20 @@ public uint NextRequestId
}
}
- public SftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpResponseFactory)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// If true, the sftp client will always pass absolute paths to serve, and will locally mimmick 'changedir' operations
+ public SftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpResponseFactory, bool changeDirIsLocal=false)
: base(session, "sftp", operationTimeout)
{
Encoding = encoding;
_sftpResponseFactory = sftpResponseFactory;
+ this.changeDirIsLocal = changeDirIsLocal;
}
///
@@ -68,11 +84,78 @@ public SftpSession(ISession session, int operationTimeout, Encoding encoding, IS
/// The new working directory.
public void ChangeDirectory(string path)
{
- var fullPath = GetCanonicalPath(path);
- var handle = RequestOpenDir(fullPath);
+ if (!this.changeDirIsLocal)
+ {
+ var fullPath = GetCanonicalPath(path);
+ var handle = RequestOpenDir(fullPath);
+ RequestClose(handle);
+ WorkingDirectory = fullPath;
+ }
+ else
+ {
+ string fullPath = GetRelativePathIfNeeded(path);
+
+ var handle = RequestOpenDir(path);
+ RequestClose(handle);
+ this.localWorkingDirectory = fullPath;
+ WorkingDirectory = GetCanonicalPath(fullPath);
+ }
+ }
+
+ ///
+ /// returns the path relative to the original local path based on the given path
+ ///
+ ///
+ ///
+ private string GetRelativePathIfNeeded(string path)
+ {
+ if (!path.StartsWith("/") && this.changeDirIsLocal)
+ {
+ path = Compact(localWorkingDirectory + "/" + path);
+ }
- RequestClose(handle);
- WorkingDirectory = fullPath;
+ return path;
+ }
+
+ ///
+ /// Compacts an absolute path, removing the .. on the path
+ ///
+ /// the original uncompacted path
+ /// the compacted path
+ private string Compact(string path)
+ {
+ string[] pieces = path.Split('/');
+ for (int i = 0; i < pieces.Length; i++)
+ {
+ if (pieces[i] == ".")
+ {
+ pieces[i] = null;
+ continue;
+ }
+
+ if (pieces[i] == "..")
+ {
+ if (i > 0)
+ {
+ pieces[i - 1] = null;
+ }
+ pieces[i] = null;
+ continue;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < pieces.Length; i++)
+ {
+ if (String.IsNullOrEmpty(pieces[i]))
+ {
+ continue;
+ }
+ sb.Append("/");
+ sb.Append(pieces[i]);
+ }
+
+ return sb.ToString();
}
internal void SendMessage(SftpMessage sftpMessage)
@@ -90,11 +173,21 @@ internal void SendMessage(SftpMessage sftpMessage)
///
public string GetCanonicalPath(string path)
{
- var fullPath = GetFullRemotePath(path);
+ return GetCanonicalPathForRemotePath(GetRelativePathIfNeeded(path));
+ }
+ ///
+ /// Resolves a given path into an absolute path on the server.
+ ///
+ /// The path to resolve relative to the server.
+ ///
+ /// The absolute path.
+ ///
+ private string GetCanonicalPathForRemotePath(string remotePath)
+ {
var canonizedPath = string.Empty;
- var realPathFiles = RequestRealPath(fullPath, true);
+ var realPathFiles = RequestRealPath(remotePath, true);
if (realPathFiles != null)
{
@@ -105,13 +198,13 @@ public string GetCanonicalPath(string path)
return canonizedPath;
// Check for special cases
- if (fullPath.EndsWith("/.", StringComparison.OrdinalIgnoreCase) ||
- fullPath.EndsWith("/..", StringComparison.OrdinalIgnoreCase) ||
- fullPath.Equals("/", StringComparison.OrdinalIgnoreCase) ||
- fullPath.IndexOf('/') < 0)
- return fullPath;
+ if (remotePath.EndsWith("/.", StringComparison.OrdinalIgnoreCase) ||
+ remotePath.EndsWith("/..", StringComparison.OrdinalIgnoreCase) ||
+ remotePath.Equals("/", StringComparison.OrdinalIgnoreCase) ||
+ remotePath.IndexOf('/') < 0)
+ return remotePath;
- var pathParts = fullPath.Split('/');
+ var pathParts = remotePath.Split('/');
var partialFullPath = string.Join("/", pathParts, 0, pathParts.Length - 1);
@@ -127,7 +220,7 @@ public string GetCanonicalPath(string path)
if (string.IsNullOrEmpty(canonizedPath))
{
- return fullPath;
+ return remotePath;
}
var slash = string.Empty;
@@ -172,6 +265,7 @@ protected override void OnChannelOpen()
// Resolve current directory
WorkingDirectory = RequestRealPath(".")[0].Key;
+ localWorkingDirectory = ".";
}
protected override void OnDataReceived(byte[] data)
@@ -360,7 +454,7 @@ public byte[] RequestOpen(string path, Flags flags, bool nullOnError = false)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpOpenRequest(ProtocolVersion, NextRequestId, path, Encoding, flags,
+ var request = new SftpOpenRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding, flags,
response =>
{
handle = response.Handle;
@@ -399,7 +493,7 @@ public SftpOpenAsyncResult BeginOpen(string path, Flags flags, AsyncCallback cal
{
var asyncResult = new SftpOpenAsyncResult(callback, state);
- var request = new SftpOpenRequest(ProtocolVersion, NextRequestId, path, Encoding, flags,
+ var request = new SftpOpenRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding, flags,
response =>
{
asyncResult.SetAsCompleted(response.Handle, false);
@@ -693,7 +787,7 @@ public SftpFileAttributes RequestLStat(string path)
SftpFileAttributes attributes = null;
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpLStatRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpLStatRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
attributes = response.Attributes;
@@ -731,7 +825,7 @@ public SFtpStatAsyncResult BeginLStat(string path, AsyncCallback callback, objec
{
var asyncResult = new SFtpStatAsyncResult(callback, state);
- var request = new SftpLStatRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpLStatRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
asyncResult.SetAsCompleted(response.Attributes, false);
@@ -822,7 +916,7 @@ public void RequestSetStat(string path, SftpFileAttributes attributes)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpSetStatRequest(ProtocolVersion, NextRequestId, path, Encoding, attributes,
+ var request = new SftpSetStatRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding, attributes,
response =>
{
exception = GetSftpException(response);
@@ -883,7 +977,7 @@ public byte[] RequestOpenDir(string path, bool nullOnError = false)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpOpenDirRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpOpenDirRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
handle = response.Handle;
@@ -959,7 +1053,7 @@ public void RequestRemove(string path)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpRemoveRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpRemoveRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
exception = GetSftpException(response);
@@ -987,7 +1081,7 @@ public void RequestMkDir(string path)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpMkDirRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpMkDirRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
exception = GetSftpException(response);
@@ -1015,7 +1109,7 @@ public void RequestRmDir(string path)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpRmDirRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpRmDirRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
exception = GetSftpException(response);
@@ -1143,7 +1237,7 @@ public SftpFileAttributes RequestStat(string path, bool nullOnError = false)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpStatRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpStatRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
attributes = response.Attributes;
@@ -1181,7 +1275,7 @@ public SFtpStatAsyncResult BeginStat(string path, AsyncCallback callback, object
{
var asyncResult = new SFtpStatAsyncResult(callback, state);
- var request = new SftpStatRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpStatRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
asyncResult.SetAsCompleted(response.Attributes, false);
@@ -1237,7 +1331,7 @@ public void RequestRename(string oldPath, string newPath)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpRenameRequest(ProtocolVersion, NextRequestId, oldPath, newPath, Encoding,
+ var request = new SftpRenameRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(oldPath), GetRelativePathIfNeeded(newPath), Encoding,
response =>
{
exception = GetSftpException(response);
@@ -1274,7 +1368,7 @@ internal KeyValuePair[] RequestReadLink(string path,
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpReadLinkRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new SftpReadLinkRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
result = response.Files;
@@ -1315,7 +1409,7 @@ public void RequestSymLink(string linkpath, string targetpath)
using (var wait = new AutoResetEvent(false))
{
- var request = new SftpSymLinkRequest(ProtocolVersion, NextRequestId, linkpath, targetpath, Encoding,
+ var request = new SftpSymLinkRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(linkpath), GetRelativePathIfNeeded(targetpath), Encoding,
response =>
{
exception = GetSftpException(response);
@@ -1353,7 +1447,7 @@ public void RequestPosixRename(string oldPath, string newPath)
using (var wait = new AutoResetEvent(false))
{
- var request = new PosixRenameRequest(ProtocolVersion, NextRequestId, oldPath, newPath, Encoding,
+ var request = new PosixRenameRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(oldPath), GetRelativePathIfNeeded(newPath), Encoding,
response =>
{
exception = GetSftpException(response);
@@ -1393,7 +1487,7 @@ public SftpFileSytemInformation RequestStatVfs(string path, bool nullOnError = f
using (var wait = new AutoResetEvent(false))
{
- var request = new StatVfsRequest(ProtocolVersion, NextRequestId, path, Encoding,
+ var request = new StatVfsRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(path), Encoding,
response =>
{
information = response.GetReply().Information;
@@ -1485,7 +1579,7 @@ internal void HardLink(string oldPath, string newPath)
using (var wait = new AutoResetEvent(false))
{
- var request = new HardLinkRequest(ProtocolVersion, NextRequestId, oldPath, newPath,
+ var request = new HardLinkRequest(ProtocolVersion, NextRequestId, GetRelativePathIfNeeded(oldPath), GetRelativePathIfNeeded(newPath),
response =>
{
exception = GetSftpException(response);
diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs
index 5a0993009..7d916056c 100644
--- a/src/Renci.SshNet/SftpClient.cs
+++ b/src/Renci.SshNet/SftpClient.cs
@@ -35,6 +35,12 @@ public class SftpClient : BaseClient
/// Holds the size of the buffer.
///
private uint _bufferSize;
+
+ ///
+ /// Gets or sets a value indicating if ChangeDir must be kept local to the client.
+ /// By default, this is false.
+ ///
+ public bool ChangeDirIsLocal { get; set; }
///
/// Gets or sets the operation timeout.
@@ -2207,7 +2213,8 @@ private ISftpSession CreateAndConnectToSftpSession()
var sftpSession = ServiceFactory.CreateSftpSession(Session,
_operationTimeout,
ConnectionInfo.Encoding,
- ServiceFactory.CreateSftpResponseFactory());
+ ServiceFactory.CreateSftpResponseFactory(),
+ this.ChangeDirIsLocal);
try
{
sftpSession.Connect();
diff --git a/src/Renci.sftp/Program.cs b/src/Renci.sftp/Program.cs
new file mode 100644
index 000000000..89bdfe082
--- /dev/null
+++ b/src/Renci.sftp/Program.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Renci.sftp
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ if (args.Length == 0)
+ {
+ Usage();
+ return;
+ }
+
+ int port = 22;
+ string destination = "127.0.0.1";
+ bool localChangeDir = false;
+
+ for (int i = 0; i < args.Length; i++)
+ {
+ if (args[i] == "-P")
+ {
+ if (i + 1 < args.Length)
+ {
+ port = int.Parse(args[i + 1]);
+ i++;
+ }
+
+ continue;
+ }
+
+ if (args[i] == "-LCHD")
+ {
+ localChangeDir = true;
+ continue;
+ }
+
+ destination = args[i];
+ }
+
+ Console.Write("user: ");
+ string username = Console.ReadLine();
+ Console.Write("pwd: ");
+ string password = ReadPassword();
+ Console.WriteLine("connecting...");
+ using (var client = new SshNet.SftpClient(destination, port, username, password))
+ {
+ client.ChangeDirIsLocal = localChangeDir;
+ client.Connect();
+
+ while (true)
+ {
+ Console.WriteLine();
+ string current = client.WorkingDirectory;
+ Console.Write(current);
+ Console.Write(">>");Console.Out.Flush();
+ string line = Console.ReadLine();
+ try
+ {
+ if (!Process(client, line.Split(' ')))
+ {
+ break;
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+ }
+ }
+ }
+
+ private static bool Process(Renci.SshNet.SftpClient client, string[] line)
+ {
+ switch (line[0])
+ {
+ case "cd":
+ if (line.Length == 1)
+ {
+ Console.WriteLine(client.WorkingDirectory);
+ }
+ else
+ {
+ client.ChangeDirectory(line[1]);
+ }
+ break;
+ case "pwd":
+ Console.WriteLine(Environment.CurrentDirectory);
+ break;
+ case "rm":
+ client.DeleteFile(line[1]);
+ break;
+ case "put":
+ using (var l = File.OpenRead(line[1]))
+ {
+ string dest = (line.Length > 2 ? line[2] : line[1]);
+ using (var f = client.OpenWrite(dest))
+ {
+ byte[] buffer = new byte[1024 * 16];
+ int r;
+ while ((r = l.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ f.Write(buffer, 0, r);
+ }
+ f.Flush();
+ }
+ }
+ break;
+ case "cat":
+ Console.WriteLine(client.ReadAllText(line[1]));
+ break;
+ case "get":
+ using (var l = client.OpenRead(line[1]))
+ {
+ string dest = (line.Length > 2 ? line[2] : line[1]);
+ using (var f = File.OpenWrite(dest))
+ {
+ byte[] buffer = new byte[1024 * 16];
+ int r;
+ while ((r = l.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ f.Write(buffer, 0, r);
+ }
+ f.Flush();
+ }
+ }
+ break;
+ case "dir":
+ {
+ string path = (line.Length > 1 ? line[1] : ".");
+ foreach (var el in Directory.GetFileSystemEntries(path))
+ {
+ Console.WriteLine(el);
+ }
+ break;
+ }
+ case "ls":
+ {
+ string path = (line.Length > 1 ? line[1] : ".");
+
+ foreach (var el in client.ListDirectory(path))
+ {
+ Console.WriteLine(el.FullName);
+ }
+ break;
+ }
+ case "exit":
+ case "quit":
+ return false;
+ default:
+ throw new ArgumentException("unsupported command " + line[0]);
+ }
+
+ return true;
+ }
+
+ private static string ReadPassword()
+ {
+ string pass = String.Empty;
+ do
+ {
+ var key = Console.ReadKey(true);
+
+ if (key.Key == ConsoleKey.Enter)
+ {
+ Console.WriteLine();
+ break;
+ }
+
+ if (key.Key == ConsoleKey.Backspace && pass.Length > 0)
+ {
+ pass = pass.Substring(0, (pass.Length - 1));
+ Console.Write("\b \b");
+ }
+ else
+ {
+ pass = pass + key.KeyChar;
+ }
+ } while (true);
+
+ return pass;
+ }
+
+ private static void Usage()
+ {
+ Console.WriteLine(@"usage: Renci.sftp [-P port] destination");
+ }
+ }
+}
diff --git a/src/Renci.sftp/Properties/AssemblyInfo.cs b/src/Renci.sftp/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..8377b8ddb
--- /dev/null
+++ b/src/Renci.sftp/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Renci.sftp")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Renci.sftp")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("c0b7112c-064e-4a9a-a22c-7dcbd77fc48b")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/src/Renci.sftp/Renci.sftp.csproj b/src/Renci.sftp/Renci.sftp.csproj
new file mode 100644
index 000000000..8c4a671b8
--- /dev/null
+++ b/src/Renci.sftp/Renci.sftp.csproj
@@ -0,0 +1,53 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {C0B7112C-064E-4A9A-A22C-7DCBD77FC48B}
+ Exe
+ Renci.sftp
+ Renci.sftp
+ v3.5
+ 512
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {dd1c552f-7f48-4269-abb3-2e4c89b7e43a}
+ Renci.SshNet.NET35
+
+
+
+
\ No newline at end of file