using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Renci.SshNet.Common;
using Renci.SshNet.Sftp.Requests;
using Renci.SshNet.Sftp.Responses;
namespace Renci.SshNet.Sftp
{
///
/// Represents an SFTP session.
///
internal sealed class SftpSession : SubsystemSession, ISftpSession
{
internal const int MaximumSupportedVersion = 3;
private const int MinimumSupportedVersion = 0;
private readonly Dictionary _requests = new Dictionary();
private readonly ISftpResponseFactory _sftpResponseFactory;
private readonly List _data = new List(32 * 1024);
private readonly Encoding _encoding;
private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(initialState: false);
private IDictionary _supportedExtensions;
///
/// Gets the remote working directory.
///
///
/// The remote working directory.
///
public string WorkingDirectory { get; private set; }
///
/// Gets the SFTP protocol version.
///
///
/// The SFTP protocol version.
///
public uint ProtocolVersion { get; private set; }
private long _requestId;
///
/// Gets the next request id for sftp session.
///
public uint NextRequestId
{
get
{
return (uint)Interlocked.Increment(ref _requestId);
}
}
///
/// Initializes a new instance of the class.
///
/// The SSH session.
/// The operation timeout.
/// The character encoding to use.
/// The factory to create SFTP responses.
public SftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpResponseFactory)
: base(session, "sftp", operationTimeout)
{
_encoding = encoding;
_sftpResponseFactory = sftpResponseFactory;
}
///
/// Changes the current working directory to the specified path.
///
/// The new working directory.
public void ChangeDirectory(string path)
{
var fullPath = GetCanonicalPath(path);
var handle = RequestOpenDir(fullPath);
RequestClose(handle);
WorkingDirectory = fullPath;
}
///
/// Asynchronously requests to change the current working directory to the specified path.
///
/// The new working directory.
/// The token to monitor for cancellation requests.
/// A that tracks the asynchronous change working directory request.
public async Task ChangeDirectoryAsync(string path, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var fullPath = await GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
var handle = await RequestOpenDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
await RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false);
WorkingDirectory = fullPath;
}
internal void SendMessage(SftpMessage sftpMessage)
{
var data = sftpMessage.GetBytes();
SendData(data);
}
///
/// Resolves a given path into an absolute path on the server.
///
/// The path to resolve.
///
/// The absolute path.
///
public string GetCanonicalPath(string path)
{
var fullPath = GetFullRemotePath(path);
var canonizedPath = string.Empty;
var realPathFiles = RequestRealPath(fullPath, nullOnError: true);
if (realPathFiles != null)
{
canonizedPath = realPathFiles[0].Key;
}
if (!string.IsNullOrEmpty(canonizedPath))
{
return canonizedPath;
}
// Check for special cases
if (fullPath.EndsWith("/.", StringComparison.OrdinalIgnoreCase) ||
fullPath.EndsWith("/..", StringComparison.OrdinalIgnoreCase) ||
fullPath.Equals("/", StringComparison.OrdinalIgnoreCase) ||
#if NET || NETSTANDARD2_1
fullPath.IndexOf('/', StringComparison.OrdinalIgnoreCase) < 0)
#else
fullPath.IndexOf('/') < 0)
#endif
{
return fullPath;
}
var pathParts = fullPath.Split('/');
#if NET || NETSTANDARD2_1
var partialFullPath = string.Join('/', pathParts, 0, pathParts.Length - 1);
#else
var partialFullPath = string.Join("/", pathParts, 0, pathParts.Length - 1);
#endif
if (string.IsNullOrEmpty(partialFullPath))
{
partialFullPath = "/";
}
realPathFiles = RequestRealPath(partialFullPath, nullOnError: true);
if (realPathFiles != null)
{
canonizedPath = realPathFiles[0].Key;
}
if (string.IsNullOrEmpty(canonizedPath))
{
return fullPath;
}
var slash = string.Empty;
if (canonizedPath[canonizedPath.Length - 1] != '/')
{
slash = "/";
}
return string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", canonizedPath, slash, pathParts[pathParts.Length - 1]);
}
///
/// Asynchronously resolves a given path into an absolute path on the server.
///
/// The path to resolve.
/// The token to monitor for cancellation requests.
///
/// A task representing the absolute path.
///
public async Task GetCanonicalPathAsync(string path, CancellationToken cancellationToken)
{
var fullPath = GetFullRemotePath(path);
var canonizedPath = string.Empty;
var realPathFiles = await RequestRealPathAsync(fullPath, nullOnError: true, cancellationToken).ConfigureAwait(false);
if (realPathFiles != null)
{
canonizedPath = realPathFiles[0].Key;
}
if (!string.IsNullOrEmpty(canonizedPath))
{
return canonizedPath;
}
// Check for special cases
if (fullPath.EndsWith("/.", StringComparison.Ordinal) ||
fullPath.EndsWith("/..", StringComparison.Ordinal) ||
fullPath.Equals("/", StringComparison.Ordinal) ||
#if NET || NETSTANDARD2_1
fullPath.IndexOf('/', StringComparison.Ordinal) < 0)
#else
fullPath.IndexOf('/') < 0)
#endif
{
return fullPath;
}
var pathParts = fullPath.Split('/');
#if NET || NETSTANDARD2_1
var partialFullPath = string.Join('/', pathParts);
#else
var partialFullPath = string.Join("/", pathParts);
#endif
if (string.IsNullOrEmpty(partialFullPath))
{
partialFullPath = "/";
}
realPathFiles = await RequestRealPathAsync(partialFullPath, nullOnError: true, cancellationToken).ConfigureAwait(false);
if (realPathFiles != null)
{
canonizedPath = realPathFiles[0].Key;
}
if (string.IsNullOrEmpty(canonizedPath))
{
return fullPath;
}
var slash = string.Empty;
if (canonizedPath[canonizedPath.Length - 1] != '/')
{
slash = "/";
}
return canonizedPath + slash + pathParts[pathParts.Length - 1];
}
///
/// Creates an for reading the content of the file represented by a given .
///
/// The handle of the file to read.
/// The SFTP session.
/// The maximum number of bytes to read with each chunk.
/// The maximum number of pending reads.
/// The size of the file or when the size could not be determined.
///
/// An for reading the content of the file represented by the
/// specified .
///
public ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
{
return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
}
internal string GetFullRemotePath(string path)
{
var fullPath = path;
if (!string.IsNullOrEmpty(path) && path[0] != '/' && WorkingDirectory != null)
{
if (WorkingDirectory[WorkingDirectory.Length - 1] == '/')
{
fullPath = WorkingDirectory + path;
}
else
{
fullPath = WorkingDirectory + '/' + path;
}
}
return fullPath;
}
protected override void OnChannelOpen()
{
SendMessage(new SftpInitRequest(MaximumSupportedVersion));
WaitOnHandle(_sftpVersionConfirmed, OperationTimeout);
if (ProtocolVersion is > MaximumSupportedVersion or < MinimumSupportedVersion)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Server SFTP version {0} is not supported.", ProtocolVersion));
}
// Resolve current directory
WorkingDirectory = RequestRealPath(".")[0].Key;
}
protected override void OnDataReceived(byte[] data)
{
const int packetLengthByteCount = 4;
const int sftpMessageTypeByteCount = 1;
const int minimumChannelDataLength = packetLengthByteCount + sftpMessageTypeByteCount;
var offset = 0;
var count = data.Length;
// improve performance and reduce GC pressure by not buffering channel data if the received
// chunk contains the complete packet data.
//
// for this, the buffer should be empty and the chunk should contain at least the packet length
// and the type of the SFTP message
if (_data.Count == 0)
{
while (count >= minimumChannelDataLength)
{
// extract packet length
var packetDataLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 |
data[offset + 3];
var packetTotalLength = packetDataLength + packetLengthByteCount;
// check if complete packet data (or more) is available
if (count >= packetTotalLength)
{
// load and process SFTP message
if (!TryLoadSftpMessage(data, offset + packetLengthByteCount, packetDataLength))
{
return;
}
// remove processed bytes from the number of bytes to process as the channel
// data we received may contain (part of) another message
count -= packetTotalLength;
// move offset beyond bytes we just processed
offset += packetTotalLength;
}
else
{
// we don't have a complete message
break;
}
}
// check if there is channel data left to process or buffer
if (count == 0)
{
return;
}
// check if we processed part of the channel data we received
if (offset > 0)
{
// add (remaining) channel data to internal data holder
var remainingChannelData = new byte[count];
Buffer.BlockCopy(data, offset, remainingChannelData, 0, count);
_data.AddRange(remainingChannelData);
}
else
{
// add (remaining) channel data to internal data holder
_data.AddRange(data);
}
// skip further processing as we'll need a new chunk to complete the message
return;
}
// add (remaining) channel data to internal data holder
_data.AddRange(data);
while (_data.Count >= minimumChannelDataLength)
{
// extract packet length
var packetDataLength = _data[0] << 24 | _data[1] << 16 | _data[2] << 8 | _data[3];
var packetTotalLength = packetDataLength + packetLengthByteCount;
// check if complete packet data is available
if (_data.Count < packetTotalLength)
{
// wait for complete message to arrive first
break;
}
// create buffer to hold packet data
var packetData = new byte[packetDataLength];
// copy packet data and bytes for length to array
_data.CopyTo(packetLengthByteCount, packetData, 0, packetDataLength);
// remove loaded data and bytes for length from _data holder
if (_data.Count == packetTotalLength)
{
// the only buffered data is the data we're processing
_data.Clear();
}
else
{
// remove only the data we're processing
_data.RemoveRange(0, packetTotalLength);
}
// load and process SFTP message
if (!TryLoadSftpMessage(packetData, 0, packetDataLength))
{
break;
}
}
}
private bool TryLoadSftpMessage(byte[] packetData, int offset, int count)
{
// Create SFTP message
var response = _sftpResponseFactory.Create(ProtocolVersion, packetData[offset], _encoding);
// Load message data into it
response.Load(packetData, offset + 1, count - 1);
try
{
if (response is SftpVersionResponse versionResponse)
{
ProtocolVersion = versionResponse.Version;
_supportedExtensions = versionResponse.Extentions;
_ = _sftpVersionConfirmed.Set();
}
else
{
HandleResponse(response as SftpResponse);
}
return true;
}
catch (Exception exp)
{
RaiseError(exp);
return false;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
var sftpVersionConfirmed = _sftpVersionConfirmed;
if (sftpVersionConfirmed != null)
{
_sftpVersionConfirmed = null;
sftpVersionConfirmed.Dispose();
}
}
}
private void SendRequest(SftpRequest request)
{
lock (_requests)
{
_requests.Add(request.RequestId, request);
}
SendMessage(request);
}
///
/// Performs SSH_FXP_OPEN request.
///
/// The path.
/// The flags.
/// If set to returns instead of throwing an exception.
/// File handle.
public byte[] RequestOpen(string path, Flags flags, bool nullOnError = false)
{
byte[] handle = null;
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpOpenRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
flags,
response =>
{
handle = response.Handle;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (!nullOnError && exception is not null)
{
throw exception;
}
return handle;
}
///
/// Asynchronously performs a SSH_FXP_OPEN request.
///
/// The path.
/// The flags.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_OPEN request. The value of its
/// contains the file handle of the specified path.
///
public Task RequestOpenAsync(string path, Flags flags, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpOpenRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
flags,
response => tcs.TrySetResult(response.Handle),
response => tcs.TrySetException(GetSftpException(response))));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_OPEN request.
///
/// The path.
/// The flags.
/// The delegate that is executed when completes.
/// An object that contains any additional user-defined data.
///
/// A that represents the asynchronous call.
///
public SftpOpenAsyncResult BeginOpen(string path, Flags flags, AsyncCallback callback, object state)
{
var asyncResult = new SftpOpenAsyncResult(callback, state);
var request = new SftpOpenRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
flags,
response =>
{
asyncResult.SetAsCompleted(response.Handle, completedSynchronously: false);
},
response =>
{
asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
});
SendRequest(request);
return asyncResult;
}
///
/// Handles the end of an asynchronous open.
///
/// An that represents an asynchronous call.
///
/// A array representing a file handle.
///
///
/// If all available data has been read, the method completes
/// immediately and returns zero bytes.
///
/// is .
public byte[] EndOpen(SftpOpenAsyncResult asyncResult)
{
ThrowHelper.ThrowIfNull(asyncResult);
if (asyncResult.EndInvokeCalled)
{
throw new InvalidOperationException("EndOpen has already been called.");
}
if (asyncResult.IsCompleted)
{
return asyncResult.EndInvoke();
}
using (var waitHandle = asyncResult.AsyncWaitHandle)
{
WaitOnHandle(waitHandle, OperationTimeout);
return asyncResult.EndInvoke();
}
}
///
/// Performs SSH_FXP_CLOSE request.
///
/// The handle.
public void RequestClose(byte[] handle)
{
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpCloseRequest(ProtocolVersion,
NextRequestId,
handle,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Performs a SSH_FXP_CLOSE request.
///
/// The handle.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_CLOSE request.
///
public Task RequestCloseAsync(byte[] handle, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpCloseRequest(ProtocolVersion,
NextRequestId,
handle,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_CLOSE request.
///
/// The handle.
/// The delegate that is executed when completes.
/// An object that contains any additional user-defined data.
///
/// A that represents the asynchronous call.
///
public SftpCloseAsyncResult BeginClose(byte[] handle, AsyncCallback callback, object state)
{
var asyncResult = new SftpCloseAsyncResult(callback, state);
var request = new SftpCloseRequest(ProtocolVersion,
NextRequestId,
handle,
response =>
{
asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
});
SendRequest(request);
return asyncResult;
}
///
/// Handles the end of an asynchronous close.
///
/// An that represents an asynchronous call.
/// is .
public void EndClose(SftpCloseAsyncResult asyncResult)
{
ThrowHelper.ThrowIfNull(asyncResult);
if (asyncResult.EndInvokeCalled)
{
throw new InvalidOperationException("EndClose has already been called.");
}
if (asyncResult.IsCompleted)
{
asyncResult.EndInvoke();
}
else
{
using (var waitHandle = asyncResult.AsyncWaitHandle)
{
WaitOnHandle(waitHandle, OperationTimeout);
asyncResult.EndInvoke();
}
}
}
///
/// Begins an asynchronous read using a SSH_FXP_READ request.
///
/// The handle to the file to read from.
/// The offset in the file to start reading from.
/// The number of bytes to read.
/// The delegate that is executed when completes.
/// An object that contains any additional user-defined data.
///
/// A that represents the asynchronous call.
///
public SftpReadAsyncResult BeginRead(byte[] handle, ulong offset, uint length, AsyncCallback callback, object state)
{
var asyncResult = new SftpReadAsyncResult(callback, state);
var request = new SftpReadRequest(ProtocolVersion,
NextRequestId,
handle,
offset,
length,
response =>
{
asyncResult.SetAsCompleted(response.Data, completedSynchronously: false);
},
response =>
{
if (response.StatusCode != StatusCodes.Eof)
{
asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
}
else
{
asyncResult.SetAsCompleted(Array.Empty(), completedSynchronously: false);
}
});
SendRequest(request);
return asyncResult;
}
///
/// Handles the end of an asynchronous read.
///
/// An that represents an asynchronous call.
///
/// A array representing the data read.
///
///
/// If all available data has been read, the method completes
/// immediately and returns zero bytes.
///
/// is .
public byte[] EndRead(SftpReadAsyncResult asyncResult)
{
ThrowHelper.ThrowIfNull(asyncResult);
if (asyncResult.EndInvokeCalled)
{
throw new InvalidOperationException("EndRead has already been called.");
}
if (asyncResult.IsCompleted)
{
return asyncResult.EndInvoke();
}
using (var waitHandle = asyncResult.AsyncWaitHandle)
{
WaitOnHandle(waitHandle, OperationTimeout);
return asyncResult.EndInvoke();
}
}
///
/// Performs SSH_FXP_READ request.
///
/// The handle.
/// The offset.
/// The length.
///
/// The data that was read, or an empty array when the end of the file was reached.
///
public byte[] RequestRead(byte[] handle, ulong offset, uint length)
{
SshException exception = null;
byte[] data = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpReadRequest(ProtocolVersion,
NextRequestId,
handle,
offset,
length,
response =>
{
data = response.Data;
wait.SetIgnoringObjectDisposed();
},
response =>
{
if (response.StatusCode != StatusCodes.Eof)
{
exception = GetSftpException(response);
}
else
{
data = Array.Empty();
}
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
return data;
}
///
/// Asynchronously performs a SSH_FXP_READ request.
///
/// The handle to the file to read from.
/// The offset in the file to start reading from.
/// The number of bytes to read.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_READ request. The value of
/// its contains the data read from the file, or an empty
/// array when the end of the file is reached.
///
public Task RequestReadAsync(byte[] handle, ulong offset, uint length, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpReadRequest(ProtocolVersion,
NextRequestId,
handle,
offset,
length,
response => tcs.TrySetResult(response.Data),
response =>
{
if (response.StatusCode == StatusCodes.Eof)
{
_ = tcs.TrySetResult(Array.Empty());
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_WRITE request.
///
/// The handle.
/// The the zero-based offset (in bytes) relative to the beginning of the file that the write must start at.
/// The buffer holding the data to write.
/// the zero-based offset in at which to begin taking bytes to write.
/// The length (in bytes) of the data to write.
/// The wait event handle if needed.
/// The callback to invoke when the write has completed.
public void RequestWrite(byte[] handle,
ulong serverOffset,
byte[] data,
int offset,
int length,
AutoResetEvent wait,
Action writeCompleted = null)
{
Debug.Assert((wait is null) != (writeCompleted is null), "Should have one parameter or the other.");
SshException exception = null;
var request = new SftpWriteRequest(ProtocolVersion,
NextRequestId,
handle,
serverOffset,
data,
offset,
length,
response =>
{
if (writeCompleted is not null)
{
writeCompleted.Invoke(response);
}
else
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
}
});
SendRequest(request);
if (wait is not null)
{
WaitOnHandle(wait, OperationTimeout);
if (exception is not null)
{
throw exception;
}
}
}
///
/// Asynchronouly performs a SSH_FXP_WRITE request.
///
/// The handle.
/// The the zero-based offset (in bytes) relative to the beginning of the file that the write must start at.
/// The buffer holding the data to write.
/// the zero-based offset in at which to begin taking bytes to write.
/// The length (in bytes) of the data to write.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_WRITE request.
///
public Task RequestWriteAsync(byte[] handle, ulong serverOffset, byte[] data, int offset, int length, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpWriteRequest(ProtocolVersion,
NextRequestId,
handle,
serverOffset,
data,
offset,
length,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_LSTAT request.
///
/// The path.
///
/// File attributes.
///
public SftpFileAttributes RequestLStat(string path)
{
SshException exception = null;
SftpFileAttributes attributes = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpLStatRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
attributes = response.Attributes;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
return attributes;
}
///
/// Asynchronously performs SSH_FXP_LSTAT request.
///
/// The path.
/// The token to monitor for cancellation requests.
///
/// A task the represents the asynchronous SSH_FXP_LSTAT request. The value of its
/// contains the file attributes of the specified path.
///
public Task RequestLStatAsync(string path, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpLStatRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Attributes),
response => tcs.TrySetException(GetSftpException(response))));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_LSTAT request.
///
/// The path.
/// The delegate that is executed when completes.
/// An object that contains any additional user-defined data.
///
/// A that represents the asynchronous call.
///
public SFtpStatAsyncResult BeginLStat(string path, AsyncCallback callback, object state)
{
var asyncResult = new SFtpStatAsyncResult(callback, state);
var request = new SftpLStatRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
asyncResult.SetAsCompleted(response.Attributes, completedSynchronously: false);
},
response =>
{
asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
});
SendRequest(request);
return asyncResult;
}
///
/// Handles the end of an asynchronous SSH_FXP_LSTAT request.
///
/// An that represents an asynchronous call.
///
/// The file attributes.
///
/// is .
public SftpFileAttributes EndLStat(SFtpStatAsyncResult asyncResult)
{
ThrowHelper.ThrowIfNull(asyncResult);
if (asyncResult.EndInvokeCalled)
{
throw new InvalidOperationException("EndLStat has already been called.");
}
if (asyncResult.IsCompleted)
{
return asyncResult.EndInvoke();
}
using (var waitHandle = asyncResult.AsyncWaitHandle)
{
WaitOnHandle(waitHandle, OperationTimeout);
return asyncResult.EndInvoke();
}
}
///
/// Performs SSH_FXP_FSTAT request.
///
/// The handle.
/// If set to , returns instead of throwing an exception.
///
/// File attributes.
///
public SftpFileAttributes RequestFStat(byte[] handle, bool nullOnError)
{
SshException exception = null;
SftpFileAttributes attributes = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpFStatRequest(ProtocolVersion,
NextRequestId,
handle,
response =>
{
attributes = response.Attributes;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (!nullOnError && exception is not null)
{
throw exception;
}
return attributes;
}
///
/// Asynchronously performs a SSH_FXP_FSTAT request.
///
/// The handle.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_FSTAT request. The value of its
/// contains the file attributes of the specified handle.
///
public Task RequestFStatAsync(byte[] handle, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpFStatRequest(ProtocolVersion,
NextRequestId,
handle,
response => tcs.TrySetResult(response.Attributes),
response => tcs.TrySetException(GetSftpException(response))));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_SETSTAT request.
///
/// The path.
/// The attributes.
public void RequestSetStat(string path, SftpFileAttributes attributes)
{
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpSetStatRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
attributes,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Performs SSH_FXP_FSETSTAT request.
///
/// The handle.
/// The attributes.
public void RequestFSetStat(byte[] handle, SftpFileAttributes attributes)
{
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpFSetStatRequest(ProtocolVersion,
NextRequestId,
handle,
attributes,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Performs SSH_FXP_OPENDIR request.
///
/// The path.
/// If set to , returns instead of throwing an exception.
/// File handle.
public byte[] RequestOpenDir(string path, bool nullOnError = false)
{
SshException exception = null;
byte[] handle = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpOpenDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
handle = response.Handle;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (!nullOnError && exception is not null)
{
throw exception;
}
return handle;
}
///
/// Asynchronously performs a SSH_FXP_OPENDIR request.
///
/// The path.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_OPENDIR request. The value of its
/// contains the handle of the specified path.
///
public Task RequestOpenDirAsync(string path, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpOpenDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Handle),
response => tcs.TrySetException(GetSftpException(response))));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_READDIR request.
///
/// The handle of the directory to read.
///
/// A where the key is the name of a file in
/// the directory and the value is the of the file.
///
public KeyValuePair[] RequestReadDir(byte[] handle)
{
SshException exception = null;
KeyValuePair[] result = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpReadDirRequest(ProtocolVersion,
NextRequestId,
handle,
response =>
{
result = response.Files;
wait.SetIgnoringObjectDisposed();
},
response =>
{
if (response.StatusCode != StatusCodes.Eof)
{
exception = GetSftpException(response);
}
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
return result;
}
///
/// Performs a SSH_FXP_READDIR request.
///
/// The handle of the directory to read.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_READDIR request. The value of its
/// contains a where the
/// key is the name of a file in the directory and the value is the
/// of the file.
///
public Task[]> RequestReadDirAsync(byte[] handle, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled[]>(cancellationToken);
}
var tcs = new TaskCompletionSource[]>(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpReadDirRequest(ProtocolVersion,
NextRequestId,
handle,
response => tcs.TrySetResult(response.Files),
response =>
{
if (response.StatusCode == StatusCodes.Eof)
{
_ = tcs.TrySetResult(null);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_REMOVE request.
///
/// The path.
public void RequestRemove(string path)
{
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpRemoveRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Asynchronously performs a SSH_FXP_REMOVE request.
///
/// The path.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_REMOVE request.
///
public Task RequestRemoveAsync(string path, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpRemoveRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_MKDIR request.
///
/// The path.
public void RequestMkDir(string path)
{
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpMkDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Asynchronously performs SSH_FXP_MKDIR request.
///
/// The path.
/// The to observe.
/// A that represents the asynchronous SSH_FXP_MKDIR operation.
public Task RequestMkDirAsync(string path, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpMkDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_RMDIR request.
///
/// The path.
public void RequestRmDir(string path)
{
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpRmDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
public Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpRmDirRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
var exception = GetSftpException(response);
if (exception is not null)
{
_ = tcs.TrySetException(exception);
}
else
{
_ = tcs.TrySetResult(true);
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_REALPATH request.
///
/// The path.
/// if set to returns null instead of throwing an exception.
///
/// The absolute path.
///
internal KeyValuePair[] RequestRealPath(string path, bool nullOnError = false)
{
SshException exception = null;
KeyValuePair[] result = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpRealPathRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
result = response.Files;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (!nullOnError && exception is not null)
{
throw exception;
}
return result;
}
internal Task[]> RequestRealPathAsync(string path, bool nullOnError, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled[]>(cancellationToken);
}
var tcs = new TaskCompletionSource[]>(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpRealPathRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.Files),
response =>
{
if (nullOnError)
{
_ = tcs.TrySetResult(null);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_REALPATH request.
///
/// The path.
/// The delegate that is executed when completes.
/// An object that contains any additional user-defined data.
///
/// A that represents the asynchronous call.
///
public SftpRealPathAsyncResult BeginRealPath(string path, AsyncCallback callback, object state)
{
var asyncResult = new SftpRealPathAsyncResult(callback, state);
var request = new SftpRealPathRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => asyncResult.SetAsCompleted(response.Files[0].Key, completedSynchronously: false),
response => asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false));
SendRequest(request);
return asyncResult;
}
///
/// Handles the end of an asynchronous SSH_FXP_REALPATH request.
///
/// An that represents an asynchronous call.
///
/// The absolute path.
///
/// is .
public string EndRealPath(SftpRealPathAsyncResult asyncResult)
{
ThrowHelper.ThrowIfNull(asyncResult);
if (asyncResult.EndInvokeCalled)
{
throw new InvalidOperationException("EndRealPath has already been called.");
}
if (asyncResult.IsCompleted)
{
return asyncResult.EndInvoke();
}
using (var waitHandle = asyncResult.AsyncWaitHandle)
{
WaitOnHandle(waitHandle, OperationTimeout);
return asyncResult.EndInvoke();
}
}
///
/// Performs SSH_FXP_STAT request.
///
/// The path.
/// if set to returns null instead of throwing an exception.
///
/// File attributes.
///
public SftpFileAttributes RequestStat(string path, bool nullOnError = false)
{
SshException exception = null;
SftpFileAttributes attributes = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpStatRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
attributes = response.Attributes;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (!nullOnError && exception is not null)
{
throw exception;
}
return attributes;
}
///
/// Performs SSH_FXP_STAT request.
///
/// The path.
/// The delegate that is executed when completes.
/// An object that contains any additional user-defined data.
///
/// A that represents the asynchronous call.
///
public SFtpStatAsyncResult BeginStat(string path, AsyncCallback callback, object state)
{
var asyncResult = new SFtpStatAsyncResult(callback, state);
var request = new SftpStatRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => asyncResult.SetAsCompleted(response.Attributes, completedSynchronously: false),
response => asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false));
SendRequest(request);
return asyncResult;
}
///
/// Handles the end of an asynchronous stat.
///
/// An that represents an asynchronous call.
///
/// The file attributes.
///
/// is .
public SftpFileAttributes EndStat(SFtpStatAsyncResult asyncResult)
{
ThrowHelper.ThrowIfNull(asyncResult);
if (asyncResult.EndInvokeCalled)
{
throw new InvalidOperationException("EndStat has already been called.");
}
if (asyncResult.IsCompleted)
{
return asyncResult.EndInvoke();
}
using (var waitHandle = asyncResult.AsyncWaitHandle)
{
WaitOnHandle(waitHandle, OperationTimeout);
return asyncResult.EndInvoke();
}
}
///
/// Performs SSH_FXP_RENAME request.
///
/// The old path.
/// The new path.
public void RequestRename(string oldPath, string newPath)
{
if (ProtocolVersion < 2)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_RENAME operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpRenameRequest(ProtocolVersion,
NextRequestId,
oldPath,
newPath,
_encoding,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Asynchronously performs a SSH_FXP_RENAME request.
///
/// The old path.
/// The new path.
/// The token to monitor for cancellation requests.
///
/// A task that represents the asynchronous SSH_FXP_RENAME request.
///
public Task RequestRenameAsync(string oldPath, string newPath, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new SftpRenameRequest(ProtocolVersion,
NextRequestId,
oldPath,
newPath,
_encoding,
response =>
{
if (response.StatusCode == StatusCodes.Ok)
{
_ = tcs.TrySetResult(true);
}
else
{
_ = tcs.TrySetException(GetSftpException(response));
}
}));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs SSH_FXP_READLINK request.
///
/// The path.
/// if set to returns instead of throwing an exception.
///
/// An array of where the key is the name of
/// a file and the value is the of the file.
///
internal KeyValuePair[] RequestReadLink(string path, bool nullOnError = false)
{
if (ProtocolVersion < 3)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_READLINK operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
SshException exception = null;
KeyValuePair[] result = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpReadLinkRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
result = response.Files;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (!nullOnError && exception is not null)
{
throw exception;
}
return result;
}
///
/// Performs SSH_FXP_SYMLINK request.
///
/// The linkpath.
/// The targetpath.
public void RequestSymLink(string linkpath, string targetpath)
{
if (ProtocolVersion < 3)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_SYMLINK operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new SftpSymLinkRequest(ProtocolVersion,
NextRequestId,
linkpath,
targetpath,
_encoding,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Performs posix-rename@openssh.com extended request.
///
/// The old path.
/// The new path.
public void RequestPosixRename(string oldPath, string newPath)
{
if (ProtocolVersion < 3)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new PosixRenameRequest(ProtocolVersion,
NextRequestId,
oldPath,
newPath,
_encoding,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
if (!_supportedExtensions.ContainsKey(request.Name))
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name));
}
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Performs statvfs@openssh.com extended request.
///
/// The path.
/// if set to [null on error].
///
/// A for the specified path.
///
public SftpFileSystemInformation RequestStatVfs(string path, bool nullOnError = false)
{
if (ProtocolVersion < 3)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
SshException exception = null;
SftpFileSystemInformation information = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new StatVfsRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response =>
{
information = response.GetReply().Information;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
if (!_supportedExtensions.ContainsKey(request.Name))
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name));
}
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (!nullOnError && exception is not null)
{
throw exception;
}
return information;
}
///
/// Asynchronously performs a statvfs@openssh.com extended request.
///
/// The path.
/// The token to monitor for cancellation requests.
///
/// A task that represents the statvfs@openssh.com extended request. The value of its
/// contains the file system information for the specified
/// path.
///
public Task RequestStatVfsAsync(string path, CancellationToken cancellationToken)
{
if (ProtocolVersion < 3)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
SendRequest(new StatVfsRequest(ProtocolVersion,
NextRequestId,
path,
_encoding,
response => tcs.TrySetResult(response.GetReply().Information),
response => tcs.TrySetException(GetSftpException(response))));
return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken);
}
///
/// Performs fstatvfs@openssh.com extended request.
///
/// The file handle.
/// if set to [null on error].
///
/// A for the specified path.
///
/// This operation is not supported for the current SFTP protocol version.
internal SftpFileSystemInformation RequestFStatVfs(byte[] handle, bool nullOnError = false)
{
if (ProtocolVersion < 3)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
SshException exception = null;
SftpFileSystemInformation information = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new FStatVfsRequest(ProtocolVersion,
NextRequestId,
handle,
response =>
{
information = response.GetReply().Information;
wait.SetIgnoringObjectDisposed();
},
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
if (!_supportedExtensions.ContainsKey(request.Name))
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name));
}
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (!nullOnError && exception is not null)
{
throw exception;
}
return information;
}
///
/// Performs hardlink@openssh.com extended request.
///
/// The old path.
/// The new path.
internal void HardLink(string oldPath, string newPath)
{
if (ProtocolVersion < 3)
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
}
SshException exception = null;
using (var wait = new AutoResetEvent(initialState: false))
{
var request = new HardLinkRequest(ProtocolVersion,
NextRequestId,
oldPath,
newPath,
response =>
{
exception = GetSftpException(response);
wait.SetIgnoringObjectDisposed();
});
if (!_supportedExtensions.ContainsKey(request.Name))
{
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name));
}
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception is not null)
{
throw exception;
}
}
///
/// Calculates the optimal size of the buffer to read data from the channel.
///
/// The buffer size configured on the client.
///
/// The optimal size of the buffer to read data from the channel.
///
public uint CalculateOptimalReadLength(uint bufferSize)
{
// a SSH_FXP_DATA message has 13 bytes of protocol fields:
// bytes 1 to 4: packet length
// byte 5: message type
// bytes 6 to 9: response id
// bytes 10 to 13: length of payload
//
// WinSCP uses a payload length of 32755 bytes
//
// most ssh servers limit the size of the payload of a SSH_MSG_CHANNEL_DATA
// response to 16 KB; if we requested 16 KB of data, then the SSH_FXP_DATA
// payload of the SSH_MSG_CHANNEL_DATA message would be too big (16 KB + 13 bytes), and
// as a result, the ssh server would split this into two responses:
// one containing 16384 bytes (13 bytes header, and 16371 bytes file data)
// and one with the remaining 13 bytes of file data
const uint lengthOfNonDataProtocolFields = 13u;
var maximumPacketSize = Channel.LocalPacketSize;
return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields;
}
///
/// Calculates the optimal size of the buffer to write data on the channel.
///
/// The buffer size configured on the client.
/// The file handle.
///
/// The optimal size of the buffer to write data on the channel.
///
///
/// Currently, we do not take the remote window size into account.
///
public uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle)
{
// 1-4: package length of SSH_FXP_WRITE message
// 5: message type
// 6-9: request id
// 10-13: handle length
//
// 14-21: offset
// 22-25: data length
/*
* Putty uses data length of 4096 bytes
* WinSCP uses data length of 32739 bytes (total 32768 bytes; 32739 + 25 + 4 bytes for handle)
*/
var lengthOfNonDataProtocolFields = 25u + (uint)handle.Length;
var maximumPacketSize = Channel.RemotePacketSize;
return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields;
}
internal static SshException GetSftpException(SftpStatusResponse response)
{
#pragma warning disable IDE0010 // Add missing cases
switch (response.StatusCode)
{
case StatusCodes.Ok:
return null;
case StatusCodes.PermissionDenied:
return new SftpPermissionDeniedException(response.ErrorMessage);
case StatusCodes.NoSuchFile:
return new SftpPathNotFoundException(response.ErrorMessage);
default:
return new SshException(response.ErrorMessage);
}
#pragma warning restore IDE0010 // Add missing cases
}
private void HandleResponse(SftpResponse response)
{
SftpRequest request;
lock (_requests)
{
_ = _requests.TryGetValue(response.ResponseId, out request);
if (request is not null)
{
_ = _requests.Remove(response.ResponseId);
}
}
if (request is null)
{
throw new InvalidOperationException("Invalid response.");
}
request.Complete(response);
}
}
}