using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Globalization; using System.Net; using System.Text; using System.Threading; using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Sftp; using System.Threading.Tasks; #if FEATURE_ASYNC_ENUMERABLE using System.Runtime.CompilerServices; #endif namespace Renci.SshNet { /// /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. /// public class SftpClient : BaseClient, ISftpClient { private static readonly Encoding Utf8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); /// /// Holds the instance that is used to communicate to the /// SFTP server. /// private ISftpSession _sftpSession; /// /// Holds the operation timeout. /// private int _operationTimeout; /// /// Holds the size of the buffer. /// private uint _bufferSize; /// /// Gets or sets the operation timeout. /// /// /// The timeout to wait until an operation completes. The default value is negative /// one (-1) milliseconds, which indicates an infinite timeout period. /// /// The method was called after the client was disposed. /// represents a value that is less than -1 or greater than milliseconds. public TimeSpan OperationTimeout { get { CheckDisposed(); return TimeSpan.FromMilliseconds(_operationTimeout); } set { CheckDisposed(); var timeoutInMilliseconds = value.TotalMilliseconds; if (timeoutInMilliseconds is < -1d or > int.MaxValue) { throw new ArgumentOutOfRangeException(nameof(value), "The timeout must represent a value between -1 and Int32.MaxValue, inclusive."); } _operationTimeout = (int) timeoutInMilliseconds; } } /// /// Gets or sets the maximum size of the buffer in bytes. /// /// /// The size of the buffer. The default buffer size is 32768 bytes (32 KB). /// /// /// /// For write operations, this limits the size of the payload for /// individual SSH_FXP_WRITE messages. The actual size is always /// capped at the maximum packet size supported by the peer /// (minus the size of protocol fields). /// /// /// For read operations, this controls the size of the payload which /// is requested from the peer in a SSH_FXP_READ message. The peer /// will send the requested number of bytes in a SSH_FXP_DATA message, /// possibly split over multiple SSH_MSG_CHANNEL_DATA messages. /// /// /// To optimize the size of the SSH packets sent by the peer, /// the actual requested size will take into account the size of the /// SSH_FXP_DATA protocol fields. /// /// /// The size of the each individual SSH_FXP_DATA message is limited to the /// local maximum packet size of the channel, which is set to 64 KB /// for SSH.NET. However, the peer can limit this even further. /// /// /// The method was called after the client was disposed. public uint BufferSize { get { CheckDisposed(); return _bufferSize; } set { CheckDisposed(); _bufferSize = value; } } /// /// Gets remote working directory. /// /// Client is not connected. /// The method was called after the client was disposed. public string WorkingDirectory { get { CheckDisposed(); if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } return _sftpSession.WorkingDirectory; } } /// /// Gets sftp protocol version. /// /// Client is not connected. /// The method was called after the client was disposed. public int ProtocolVersion { get { CheckDisposed(); if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } return (int) _sftpSession.ProtocolVersion; } } /// /// Gets the current SFTP session. /// /// /// The current SFTP session. /// internal ISftpSession SftpSession { get { return _sftpSession; } } #region Constructors /// /// Initializes a new instance of the class. /// /// The connection info. /// is null. public SftpClient(ConnectionInfo connectionInfo) : this(connectionInfo, ownsConnectionInfo: false) { } /// /// Initializes a new instance of the class. /// /// Connection host. /// Connection port. /// Authentication username. /// Authentication password. /// is null. /// is invalid. -or- is null or contains only whitespace characters. /// is not within and . [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")] public SftpClient(string host, int port, string username, string password) : this(new PasswordConnectionInfo(host, port, username, password), ownsConnectionInfo: true) { } /// /// Initializes a new instance of the class. /// /// Connection host. /// Authentication username. /// Authentication password. /// is null. /// is invalid. -or- is null contains only whitespace characters. public SftpClient(string host, string username, string password) : this(host, ConnectionInfo.DefaultPort, username, password) { } /// /// Initializes a new instance of the class. /// /// Connection host. /// Connection port. /// Authentication username. /// Authentication private key file(s) . /// is null. /// is invalid. -or- is nunullll or contains only whitespace characters. /// is not within and . [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")] public SftpClient(string host, int port, string username, params IPrivateKeySource[] keyFiles) : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true) { } /// /// Initializes a new instance of the class. /// /// Connection host. /// Authentication username. /// Authentication private key file(s) . /// is null. /// is invalid. -or- is null or contains only whitespace characters. public SftpClient(string host, string username, params IPrivateKeySource[] keyFiles) : this(host, ConnectionInfo.DefaultPort, username, keyFiles) { } /// /// Initializes a new instance of the class. /// /// The connection info. /// Specified whether this instance owns the connection info. /// is null. /// /// If is true, the connection info will be disposed when this /// instance is disposed. /// private SftpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo) : this(connectionInfo, ownsConnectionInfo, new ServiceFactory()) { } /// /// Initializes a new instance of the class. /// /// The connection info. /// Specified whether this instance owns the connection info. /// The factory to use for creating new services. /// is null. /// is null. /// /// If is true, the connection info will be disposed when this /// instance is disposed. /// internal SftpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory) : base(connectionInfo, ownsConnectionInfo, serviceFactory) { _operationTimeout = SshNet.Session.Infinite; _bufferSize = 1024 * 32; } #endregion Constructors /// /// Changes remote directory to path. /// /// New directory path. /// is null. /// Client is not connected. /// Permission to change directory denied by remote host. -or- A SSH command was denied by the server. /// was not found on the remote host. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public void ChangeDirectory(string path) { CheckDisposed(); if (path is null) { throw new ArgumentNullException(nameof(path)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } _sftpSession.ChangeDirectory(path); } /// /// Changes permissions of file(s) to specified mode. /// /// File(s) path, may match multiple files. /// The mode. /// is null. /// Client is not connected. /// Permission to change permission on the path(s) was denied by the remote host. -or- A SSH command was denied by the server. /// was not found on the remote host. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public void ChangePermissions(string path, short mode) { var file = Get(path); file.SetPermissions(mode); } /// /// Creates remote directory specified by path. /// /// Directory path to create. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to create the directory was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public void CreateDirectory(string path) { CheckDisposed(); if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException(path); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); _sftpSession.RequestMkDir(fullPath); } /// /// Deletes remote directory specified by path. /// /// Directory to be deleted path. /// is null or contains only whitespace characters. /// Client is not connected. /// was not found on the remote host. /// Permission to delete the directory was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public void DeleteDirectory(string path) { CheckDisposed(); if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); _sftpSession.RequestRmDir(fullPath); } /// /// Deletes remote file specified by path. /// /// File to be deleted path. /// is null or contains only whitespace characters. /// Client is not connected. /// was not found on the remote host. /// Permission to delete the file was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public void DeleteFile(string path) { CheckDisposed(); if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); _sftpSession.RequestRemove(fullPath); } /// /// Asynchronously deletes remote file specified by path. /// /// File to be deleted path. /// The to observe. /// A that represents the asynchronous delete operation. /// is null or contains only whitespace characters. /// Client is not connected. /// was not found on the remote host. /// Permission to delete the file was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public async Task DeleteFileAsync(string path, CancellationToken cancellationToken) { CheckDisposed(); if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } cancellationToken.ThrowIfCancellationRequested(); var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); await _sftpSession.RequestRemoveAsync(fullPath, cancellationToken).ConfigureAwait(false); } /// /// Renames remote file from old path to new path. /// /// Path to the old file location. /// Path to the new file location. /// is null. -or- or is null. /// Client is not connected. /// Permission to rename the file was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public void RenameFile(string oldPath, string newPath) { RenameFile(oldPath, newPath, isPosix: false); } /// /// Asynchronously renames remote file from old path to new path. /// /// Path to the old file location. /// Path to the new file location. /// The to observe. /// A that represents the asynchronous rename operation. /// is null. -or- or is null. /// Client is not connected. /// Permission to rename the file was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public async Task RenameFileAsync(string oldPath, string newPath, CancellationToken cancellationToken) { CheckDisposed(); if (oldPath is null) { throw new ArgumentNullException(nameof(oldPath)); } if (newPath is null) { throw new ArgumentNullException(nameof(newPath)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } cancellationToken.ThrowIfCancellationRequested(); var oldFullPath = await _sftpSession.GetCanonicalPathAsync(oldPath, cancellationToken).ConfigureAwait(false); var newFullPath = await _sftpSession.GetCanonicalPathAsync(newPath, cancellationToken).ConfigureAwait(false); await _sftpSession.RequestRenameAsync(oldFullPath, newFullPath, cancellationToken).ConfigureAwait(false); } /// /// Renames remote file from old path to new path. /// /// Path to the old file location. /// Path to the new file location. /// if set to true then perform a posix rename. /// is null. -or- or is null. /// Client is not connected. /// Permission to rename the file was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public void RenameFile(string oldPath, string newPath, bool isPosix) { CheckDisposed(); if (oldPath is null) { throw new ArgumentNullException(nameof(oldPath)); } if (newPath is null) { throw new ArgumentNullException(nameof(newPath)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var oldFullPath = _sftpSession.GetCanonicalPath(oldPath); var newFullPath = _sftpSession.GetCanonicalPath(newPath); if (isPosix) { _sftpSession.RequestPosixRename(oldFullPath, newFullPath); } else { _sftpSession.RequestRename(oldFullPath, newFullPath); } } /// /// Creates a symbolic link from old path to new path. /// /// The old path. /// The new path. /// is null. -or- is null or contains only whitespace characters. /// Client is not connected. /// Permission to create the symbolic link was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public void SymbolicLink(string path, string linkPath) { CheckDisposed(); if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } if (string.IsNullOrWhiteSpace(linkPath)) { throw new ArgumentException("linkPath"); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); var linkFullPath = _sftpSession.GetCanonicalPath(linkPath); _sftpSession.RequestSymLink(fullPath, linkFullPath); } /// /// Retrieves list of files in remote directory. /// /// The path. /// The list callback. /// /// A list of files. /// /// is null. /// Client is not connected. /// Permission to list the contents of the directory was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public IEnumerable ListDirectory(string path, Action listCallback = null) { CheckDisposed(); return InternalListDirectory(path, listCallback); } #if FEATURE_ASYNC_ENUMERABLE /// /// Asynchronously enumerates the files in remote directory. /// /// The path. /// The to observe. /// /// An of that represents the asynchronous enumeration operation. /// The enumeration contains an async stream of for the files in the directory specified by . /// /// is null. /// Client is not connected. /// Permission to list the contents of the directory was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public async IAsyncEnumerable ListDirectoryAsync(string path, [EnumeratorCancellation] CancellationToken cancellationToken) { CheckDisposed(); if (path is null) { throw new ArgumentNullException(nameof(path)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } cancellationToken.ThrowIfCancellationRequested(); var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); var handle = await _sftpSession.RequestOpenDirAsync(fullPath, cancellationToken).ConfigureAwait(false); try { var basePath = (fullPath[fullPath.Length - 1] == '/') ? fullPath : fullPath + '/'; while (true) { var files = await _sftpSession.RequestReadDirAsync(handle, cancellationToken).ConfigureAwait(false); if (files is null) { break; } foreach (var file in files) { yield return new SftpFile(_sftpSession, basePath + file.Key, file.Value); } } } finally { await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false); } } #endif //FEATURE_ASYNC_ENUMERABLE /// /// 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. /// The list callback. /// /// An that references the asynchronous operation. /// /// The method was called after the client was disposed. public IAsyncResult BeginListDirectory(string path, AsyncCallback asyncCallback, object state, Action listCallback = null) { CheckDisposed(); var asyncResult = new SftpListDirectoryAsyncResult(asyncCallback, state); ThreadAbstraction.ExecuteThread(() => { try { var result = InternalListDirectory(path, count => { asyncResult.Update(count); listCallback?.Invoke(count); }); asyncResult.SetAsCompleted(result, completedSynchronously: false); } catch (Exception exp) { asyncResult.SetAsCompleted(exp, completedSynchronously: false); } }); return asyncResult; } /// /// Ends an asynchronous operation of retrieving list of files in remote directory. /// /// The pending asynchronous SFTP request. /// /// A list of files. /// /// The object did not come from the corresponding async method on this type.-or- was called multiple times with the same . public IEnumerable EndListDirectory(IAsyncResult asyncResult) { if (asyncResult is not SftpListDirectoryAsyncResult ar || ar.EndInvokeCalled) { throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult."); } // Wait for operation to complete, then return result or throw exception return ar.EndInvoke(); } /// /// Gets reference to remote file or directory. /// /// The path. /// /// A reference to file object. /// /// Client is not connected. /// was not found on the remote host. /// is null. /// The method was called after the client was disposed. public ISftpFile Get(string path) { CheckDisposed(); if (path is null) { throw new ArgumentNullException(nameof(path)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); var attributes = _sftpSession.RequestLStat(fullPath); return new SftpFile(_sftpSession, fullPath, attributes); } /// /// Checks whether file or directory exists; /// /// The path. /// /// true if directory or file exists; otherwise false. /// /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to perform the operation was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. public bool Exists(string path) { CheckDisposed(); if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); // using SSH_FXP_REALPATH is not an alternative as the SFTP specification has not always // been clear on how the server should respond when the specified path is not present on // the server: // // SSH 1 to 4: // No mention of how the server should respond if the path is not present on the server. // // SSH 5: // The server SHOULD fail the request if the path is not present on the server. // // SSH 6: // Draft 06: The server SHOULD fail the request if the path is not present on the server. // Draft 07 to 13: The server MUST NOT fail the request if the path does not exist. // // Note that SSH 6 (draft 06 and forward) allows for more control options, but we // currently only support up to v3. try { _ = _sftpSession.RequestLStat(fullPath); return true; } catch (SftpPathNotFoundException) { return false; } } /// /// Downloads remote file specified by the path into the stream. /// /// File to download. /// Stream to write the file into. /// The download callback. /// is null. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to perform the operation was denied by the remote host. -or- A SSH command was denied by the server. /// was not found on the remote host./// /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// public void DownloadFile(string path, Stream output, Action downloadCallback = null) { CheckDisposed(); InternalDownloadFile(path, output, asyncResult: null, downloadCallback); } /// /// Begins an asynchronous file downloading into the stream. /// /// The path. /// The output. /// /// An that references the asynchronous operation. /// /// is null. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to perform the operation was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// public IAsyncResult BeginDownloadFile(string path, Stream output) { return BeginDownloadFile(path, output, asyncCallback: null, state: null); } /// /// Begins an asynchronous file downloading into the stream. /// /// The path. /// The output. /// The method to be called when the asynchronous write operation is completed. /// /// An that references the asynchronous operation. /// /// is null. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to perform the operation was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// public IAsyncResult BeginDownloadFile(string path, Stream output, AsyncCallback asyncCallback) { return BeginDownloadFile(path, output, asyncCallback, state: null); } /// /// Begins an asynchronous file downloading into the stream. /// /// 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. /// The download callback. /// /// An that references the asynchronous operation. /// /// is null. /// is null or contains only whitespace characters. /// The method was called after the client was disposed. /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// public IAsyncResult BeginDownloadFile(string path, Stream output, AsyncCallback asyncCallback, object state, Action downloadCallback = null) { CheckDisposed(); if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } if (output is null) { throw new ArgumentNullException(nameof(output)); } var asyncResult = new SftpDownloadAsyncResult(asyncCallback, state); ThreadAbstraction.ExecuteThread(() => { try { InternalDownloadFile(path, output, asyncResult, offset => { asyncResult.Update(offset); downloadCallback?.Invoke(offset); }); asyncResult.SetAsCompleted(exception: null, completedSynchronously: false); } catch (Exception exp) { asyncResult.SetAsCompleted(exp, completedSynchronously: false); } }); return asyncResult; } /// /// Ends an asynchronous file downloading into the stream. /// /// The pending asynchronous SFTP request. /// The object did not come from the corresponding async method on this type.-or- was called multiple times with the same . /// Client is not connected. /// Permission to perform the operation was denied by the remote host. -or- A SSH command was denied by the server. /// The path was not found on the remote host. /// A SSH error where is the message from the remote host. public void EndDownloadFile(IAsyncResult asyncResult) { if (asyncResult is not SftpDownloadAsyncResult ar || ar.EndInvokeCalled) { throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult."); } // Wait for operation to complete, then return result or throw exception ar.EndInvoke(); } /// /// Uploads stream into remote file. /// /// Data input stream. /// Remote file path. /// The upload callback. /// is null. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to upload the file was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// public void UploadFile(Stream input, string path, Action uploadCallback = null) { UploadFile(input, path, canOverride: true, uploadCallback); } /// /// Uploads stream into remote file. /// /// Data input stream. /// Remote file path. /// if set to true then existing file will be overwritten. /// The upload callback. /// is null. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to upload the file was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// public void UploadFile(Stream input, string path, bool canOverride, Action uploadCallback = null) { CheckDisposed(); var flags = Flags.Write | Flags.Truncate; if (canOverride) { flags |= Flags.CreateNewOrOpen; } else { flags |= Flags.CreateNew; } InternalUploadFile(input, path, flags, asyncResult: null, uploadCallback); } /// /// Begins an asynchronous uploading the stream into remote file. /// /// Data input stream. /// Remote file path. /// /// An that references the asynchronous operation. /// /// is null. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to list the contents of the directory was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. /// /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// /// /// If the remote file already exists, it is overwritten and truncated. /// /// public IAsyncResult BeginUploadFile(Stream input, string path) { return BeginUploadFile(input, path, canOverride: true, asyncCallback: null, state: null); } /// /// Begins an asynchronous uploading the stream into remote file. /// /// Data input stream. /// Remote file path. /// The method to be called when the asynchronous write operation is completed. /// /// An that references the asynchronous operation. /// /// is null. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to list the contents of the directory was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. /// /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// /// /// If the remote file already exists, it is overwritten and truncated. /// /// public IAsyncResult BeginUploadFile(Stream input, string path, AsyncCallback asyncCallback) { return BeginUploadFile(input, path, canOverride: true, asyncCallback, state: null); } /// /// Begins an asynchronous uploading the stream into remote file. /// /// 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. /// The upload callback. /// /// An that references the asynchronous operation. /// /// is null. /// is null or contains only whitespace characters. /// Client is not connected. /// Permission to list the contents of the directory was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. /// The method was called after the client was disposed. /// /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// /// /// If the remote file already exists, it is overwritten and truncated. /// /// public IAsyncResult BeginUploadFile(Stream input, string path, AsyncCallback asyncCallback, object state, Action uploadCallback = null) { return BeginUploadFile(input, path, canOverride: true, asyncCallback, state, uploadCallback); } /// /// Begins an asynchronous uploading the stream into remote file. /// /// Data input stream. /// Remote file path. /// Specified whether an existing file can be overwritten. /// 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. /// The upload callback. /// /// An that references the asynchronous operation. /// /// is null. /// is null or contains only whitespace characters. /// The method was called after the client was disposed. /// /// /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. /// /// /// When refers to an existing file, set to true to overwrite and truncate that file. /// If is false, the upload will fail and will throw an /// . /// /// public IAsyncResult BeginUploadFile(Stream input, string path, bool canOverride, AsyncCallback asyncCallback, object state, Action uploadCallback = null) { CheckDisposed(); if (input is null) { throw new ArgumentNullException(nameof(input)); } if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } var flags = Flags.Write | Flags.Truncate; if (canOverride) { flags |= Flags.CreateNewOrOpen; } else { flags |= Flags.CreateNew; } var asyncResult = new SftpUploadAsyncResult(asyncCallback, state); ThreadAbstraction.ExecuteThread(() => { try { InternalUploadFile(input, path, flags, asyncResult, offset => { asyncResult.Update(offset); uploadCallback?.Invoke(offset); }); asyncResult.SetAsCompleted(exception: null, completedSynchronously: false); } catch (Exception exp) { asyncResult.SetAsCompleted(exception: exp, completedSynchronously: false); } }); return asyncResult; } /// /// Ends an asynchronous uploading the stream into remote file. /// /// The pending asynchronous SFTP request. /// The object did not come from the corresponding async method on this type.-or- was called multiple times with the same . /// Client is not connected. /// The directory of the file was not found on the remote host. /// Permission to upload the file was denied by the remote host. -or- A SSH command was denied by the server. /// A SSH error where is the message from the remote host. public void EndUploadFile(IAsyncResult asyncResult) { if (asyncResult is not SftpUploadAsyncResult ar || ar.EndInvokeCalled) { throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult."); } // Wait for operation to complete, then return result or throw exception ar.EndInvoke(); } /// /// Gets status using statvfs@openssh.com request. /// /// The path. /// /// A instance that contains file status information. /// /// Client is not connected. /// is null. /// The method was called after the client was disposed. public SftpFileSytemInformation GetStatus(string path) { CheckDisposed(); if (path is null) { throw new ArgumentNullException(nameof(path)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); return _sftpSession.RequestStatVfs(fullPath); } /// /// Asynchronously gets status using statvfs@openssh.com request. /// /// The path. /// The to observe. /// /// A that represents the status operation. /// The task result contains the instance that contains file status information. /// /// Client is not connected. /// is null. /// The method was called after the client was disposed. public async Task GetStatusAsync(string path, CancellationToken cancellationToken) { CheckDisposed(); if (path is null) { throw new ArgumentNullException(nameof(path)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } cancellationToken.ThrowIfCancellationRequested(); var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); return await _sftpSession.RequestStatVfsAsync(fullPath, cancellationToken).ConfigureAwait(false); } #region File Methods /// /// Appends lines to a file, creating the file if it does not already exist. /// /// The file to append the lines to. The file is created if it does not already exist. /// The lines to append to the file. /// isnull -or- is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM) /// public void AppendAllLines(string path, IEnumerable contents) { CheckDisposed(); if (contents is null) { throw new ArgumentNullException(nameof(contents)); } using (var stream = AppendText(path)) { foreach (var line in contents) { stream.WriteLine(line); } } } /// /// Appends lines to a file by using a specified encoding, creating the file if it does not already exist. /// /// The file to append the lines to. The file is created if it does not already exist. /// The lines to append to the file. /// The character encoding to use. /// is null. -or- is null. -or- is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. public void AppendAllLines(string path, IEnumerable contents, Encoding encoding) { CheckDisposed(); if (contents is null) { throw new ArgumentNullException(nameof(contents)); } using (var stream = AppendText(path, encoding)) { foreach (var line in contents) { stream.WriteLine(line); } } } /// /// Appends the specified string to the file, creating the file if it does not already exist. /// /// The file to append the specified string to. /// The string to append to the file. /// is null. -or- is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM). /// public void AppendAllText(string path, string contents) { using (var stream = AppendText(path)) { stream.Write(contents); } } /// /// Appends the specified string to the file, creating the file if it does not already exist. /// /// The file to append the specified string to. /// The string to append to the file. /// The character encoding to use. /// is null. -or- is null. -or- is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. public void AppendAllText(string path, string contents, Encoding encoding) { using (var stream = AppendText(path, encoding)) { stream.Write(contents); } } /// /// Creates a that appends UTF-8 encoded text to the specified file, /// creating the file if it does not already exist. /// /// The path to the file to append to. /// /// A that appends text to a file using UTF-8 encoding without a /// Byte-Order Mark (BOM). /// /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. public StreamWriter AppendText(string path) { return AppendText(path, Utf8NoBOM); } /// /// Creates a that appends text to a file using the specified /// encoding, creating the file if it does not already exist. /// /// The path to the file to append to. /// The character encoding to use. /// /// A that appends text to a file using the specified encoding. /// /// is null. -or- is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. public StreamWriter AppendText(string path, Encoding encoding) { CheckDisposed(); if (encoding is null) { throw new ArgumentNullException(nameof(encoding)); } return new StreamWriter(new SftpFileStream(_sftpSession, path, FileMode.Append, FileAccess.Write, (int) _bufferSize), encoding); } /// /// Creates or overwrites a file in the specified path. /// /// The path and name of the file to create. /// /// A that provides read/write access to the file specified in path. /// /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// If the target file already exists, it is first truncated to zero bytes. /// public SftpFileStream Create(string path) { CheckDisposed(); return new SftpFileStream(_sftpSession, path, FileMode.Create, FileAccess.ReadWrite, (int) _bufferSize); } /// /// Creates or overwrites the specified file. /// /// The path and name of the file to create. /// The maximum number of bytes buffered for reads and writes to the file. /// /// A that provides read/write access to the file specified in path. /// /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// If the target file already exists, it is first truncated to zero bytes. /// public SftpFileStream Create(string path, int bufferSize) { CheckDisposed(); return new SftpFileStream(_sftpSession, path, FileMode.Create, FileAccess.ReadWrite, bufferSize); } /// /// Creates or opens a file for writing UTF-8 encoded text. /// /// The file to be opened for writing. /// /// A that writes text to a file using UTF-8 encoding without /// a Byte-Order Mark (BOM). /// /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public StreamWriter CreateText(string path) { return CreateText(path, Utf8NoBOM); } /// /// Creates or opens a file for writing text using the specified encoding. /// /// The file to be opened for writing. /// The character encoding to use. /// /// A that writes to a file using the specified encoding. /// /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public StreamWriter CreateText(string path, Encoding encoding) { CheckDisposed(); return new StreamWriter(OpenWrite(path), encoding); } /// /// Deletes the specified file or directory. /// /// The name of the file or directory to be deleted. Wildcard characters are not supported. /// is null. /// Client is not connected. /// was not found on the remote host. /// The method was called after the client was disposed. public void Delete(string path) { var file = Get(path); file.Delete(); } /// /// Returns the date and time the specified file or directory was last accessed. /// /// The file or directory for which to obtain access date and time information. /// /// A structure set to the date and time that the specified file or directory was last accessed. /// This value is expressed in local time. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public DateTime GetLastAccessTime(string path) { var file = Get(path); return file.LastAccessTime; } /// /// Returns the date and time, in coordinated universal time (UTC), that the specified file or directory was last accessed. /// /// The file or directory for which to obtain access date and time information. /// /// A structure set to the date and time that the specified file or directory was last accessed. /// This value is expressed in UTC time. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public DateTime GetLastAccessTimeUtc(string path) { var lastAccessTime = GetLastAccessTime(path); return lastAccessTime.ToUniversalTime(); } /// /// Returns the date and time the specified file or directory was last written to. /// /// The file or directory for which to obtain write date and time information. /// /// A structure set to the date and time that the specified file or directory was last written to. /// This value is expressed in local time. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public DateTime GetLastWriteTime(string path) { var file = Get(path); return file.LastWriteTime; } /// /// Returns the date and time, in coordinated universal time (UTC), that the specified file or directory was last written to. /// /// The file or directory for which to obtain write date and time information. /// /// A structure set to the date and time that the specified file or directory was last written to. /// This value is expressed in UTC time. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public DateTime GetLastWriteTimeUtc(string path) { var lastWriteTime = GetLastWriteTime(path); return lastWriteTime.ToUniversalTime(); } /// /// Opens a on the specified path with read/write access. /// /// The file to open. /// A value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten. /// /// An unshared that provides access to the specified file, with the specified mode and read/write access. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public SftpFileStream Open(string path, FileMode mode) { return Open(path, mode, FileAccess.ReadWrite); } /// /// Opens a on the specified path, with the specified mode and access. /// /// The file to open. /// A value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten. /// A value that specifies the operations that can be performed on the file. /// /// An unshared that provides access to the specified file, with the specified mode and access. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public SftpFileStream Open(string path, FileMode mode, FileAccess access) { CheckDisposed(); return new SftpFileStream(_sftpSession, path, mode, access, (int) _bufferSize); } /// /// Asynchronously opens a on the specified path, with the specified mode and access. /// /// The file to open. /// A value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten. /// A value that specifies the operations that can be performed on the file. /// The to observe. /// /// A that represents the asynchronous open operation. /// The task result contains the that provides access to the specified file, with the specified mode and access. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public Task OpenAsync(string path, FileMode mode, FileAccess access, CancellationToken cancellationToken) { CheckDisposed(); if (path is null) { throw new ArgumentNullException(nameof(path)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } cancellationToken.ThrowIfCancellationRequested(); return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int)_bufferSize, cancellationToken); } /// /// Opens an existing file for reading. /// /// The file to be opened for reading. /// /// A read-only on the specified path. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public SftpFileStream OpenRead(string path) { return Open(path, FileMode.Open, FileAccess.Read); } /// /// Opens an existing UTF-8 encoded text file for reading. /// /// The file to be opened for reading. /// /// A on the specified path. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public StreamReader OpenText(string path) { return new StreamReader(OpenRead(path), Encoding.UTF8); } /// /// Opens a file for writing. /// /// The file to be opened for writing. /// /// An unshared object on the specified path with access. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. /// /// If the file does not exist, it is created. /// public SftpFileStream OpenWrite(string path) { CheckDisposed(); return new SftpFileStream(_sftpSession, path, FileMode.OpenOrCreate, FileAccess.Write, (int) _bufferSize); } /// /// Opens a binary file, reads the contents of the file into a byte array, and closes the file. /// /// The file to open for reading. /// /// A byte array containing the contents of the file. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public byte[] ReadAllBytes(string path) { using (var stream = OpenRead(path)) { var buffer = new byte[stream.Length]; _ = stream.Read(buffer, 0, buffer.Length); return buffer; } } /// /// Opens a text file, reads all lines of the file using UTF-8 encoding, and closes the file. /// /// The file to open for reading. /// /// A string array containing all lines of the file. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public string[] ReadAllLines(string path) { return ReadAllLines(path, Encoding.UTF8); } /// /// Opens a file, reads all lines of the file with the specified encoding, and closes the file. /// /// The file to open for reading. /// The encoding applied to the contents of the file. /// /// A string array containing all lines of the file. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public string[] ReadAllLines(string path, Encoding encoding) { // we use the default buffer size for StreamReader - which is 1024 bytes - and the configured buffer size // for the SftpFileStream; may want to revisit this later var lines = new List(); using (var stream = new StreamReader(OpenRead(path), encoding)) { while (!stream.EndOfStream) { lines.Add(stream.ReadLine()); } } return lines.ToArray(); } /// /// Opens a text file, reads all lines of the file with the UTF-8 encoding, and closes the file. /// /// The file to open for reading. /// /// A string containing all lines of the file. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public string ReadAllText(string path) { return ReadAllText(path, Encoding.UTF8); } /// /// Opens a file, reads all lines of the file with the specified encoding, and closes the file. /// /// The file to open for reading. /// The encoding applied to the contents of the file. /// /// A string containing all lines of the file. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public string ReadAllText(string path, Encoding encoding) { // we use the default buffer size for StreamReader - which is 1024 bytes - and the configured buffer size // for the SftpFileStream; may want to revisit this later using (var stream = new StreamReader(OpenRead(path), encoding)) { return stream.ReadToEnd(); } } /// /// Reads the lines of a file with the UTF-8 encoding. /// /// The file to read. /// /// The lines of the file. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public IEnumerable ReadLines(string path) { return ReadAllLines(path); } /// /// Read the lines of a file that has a specified encoding. /// /// The file to read. /// The encoding that is applied to the contents of the file. /// /// The lines of the file. /// /// is null. /// Client is not connected. /// The method was called after the client was disposed. public IEnumerable ReadLines(string path, Encoding encoding) { return ReadAllLines(path, encoding); } /// /// Sets the date and time the specified file was last accessed. /// /// The file for which to set the access date and time information. /// A containing the value to set for the last access date and time of path. This value is expressed in local time. [Obsolete("Note: This method currently throws NotImplementedException because it has not yet been implemented.")] public void SetLastAccessTime(string path, DateTime lastAccessTime) { throw new NotImplementedException(); } /// /// Sets the date and time, in coordinated universal time (UTC), that the specified file was last accessed. /// /// The file for which to set the access date and time information. /// A containing the value to set for the last access date and time of path. This value is expressed in UTC time. [Obsolete("Note: This method currently throws NotImplementedException because it has not yet been implemented.")] public void SetLastAccessTimeUtc(string path, DateTime lastAccessTimeUtc) { throw new NotImplementedException(); } /// /// Sets the date and time that the specified file was last written to. /// /// The file for which to set the date and time information. /// A containing the value to set for the last write date and time of path. This value is expressed in local time. [Obsolete("Note: This method currently throws NotImplementedException because it has not yet been implemented.")] public void SetLastWriteTime(string path, DateTime lastWriteTime) { throw new NotImplementedException(); } /// /// Sets the date and time, in coordinated universal time (UTC), that the specified file was last written to. /// /// The file for which to set the date and time information. /// A containing the value to set for the last write date and time of path. This value is expressed in UTC time. [Obsolete("Note: This method currently throws NotImplementedException because it has not yet been implemented.")] public void SetLastWriteTimeUtc(string path, DateTime lastWriteTimeUtc) { throw new NotImplementedException(); } /// /// Writes the specified byte array to the specified file, and closes the file. /// /// The file to write to. /// The bytes to write to the file. /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public void WriteAllBytes(string path, byte[] bytes) { using (var stream = OpenWrite(path)) { stream.Write(bytes, 0, bytes.Length); } } /// /// Writes a collection of strings to the file using the UTF-8 encoding, and closes the file. /// /// The file to write to. /// The lines to write to the file. /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM). /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public void WriteAllLines(string path, IEnumerable contents) { WriteAllLines(path, contents, Utf8NoBOM); } /// /// Write the specified string array to the file using the UTF-8 encoding, and closes the file. /// /// The file to write to. /// The string array to write to the file. /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM). /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public void WriteAllLines(string path, string[] contents) { WriteAllLines(path, contents, Utf8NoBOM); } /// /// Writes a collection of strings to the file using the specified encoding, and closes the file. /// /// The file to write to. /// The lines to write to the file. /// The character encoding to use. /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public void WriteAllLines(string path, IEnumerable contents, Encoding encoding) { using (var stream = CreateText(path, encoding)) { foreach (var line in contents) { stream.WriteLine(line); } } } /// /// Writes the specified string array to the file by using the specified encoding, and closes the file. /// /// The file to write to. /// The string array to write to the file. /// An object that represents the character encoding applied to the string array. /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public void WriteAllLines(string path, string[] contents, Encoding encoding) { using (var stream = CreateText(path, encoding)) { foreach (var line in contents) { stream.WriteLine(line); } } } /// /// Writes the specified string to the file using the UTF-8 encoding, and closes the file. /// /// The file to write to. /// The string to write to the file. /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM). /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public void WriteAllText(string path, string contents) { using (var stream = CreateText(path)) { stream.Write(contents); } } /// /// Writes the specified string to the file using the specified encoding, and closes the file. /// /// The file to write to. /// The string to write to the file. /// The encoding to apply to the string. /// is null. /// Client is not connected. /// The specified path is invalid, or its directory was not found on the remote host. /// The method was called after the client was disposed. /// /// /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes. /// /// /// If the target file does not exist, it is created. /// /// public void WriteAllText(string path, string contents, Encoding encoding) { using (var stream = CreateText(path, encoding)) { stream.Write(contents); } } /// /// Gets the of the file on the path. /// /// The path to the file. /// /// The of the file on the path. /// /// is null. /// Client is not connected. /// was not found on the remote host. /// The method was called after the client was disposed. public SftpFileAttributes GetAttributes(string path) { CheckDisposed(); if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); return _sftpSession.RequestLStat(fullPath); } /// /// Sets the specified of the file on the specified path. /// /// The path to the file. /// The desired . /// is null. /// Client is not connected. /// The method was called after the client was disposed. public void SetAttributes(string path, SftpFileAttributes fileAttributes) { CheckDisposed(); if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); _sftpSession.RequestSetStat(fullPath, fileAttributes); } // Please don't forget this when you implement these methods: is null. //public FileSecurity GetAccessControl(string path); //public FileSecurity GetAccessControl(string path, AccessControlSections includeSections); //public DateTime GetCreationTime(string path); //public DateTime GetCreationTimeUtc(string path); //public void SetAccessControl(string path, FileSecurity fileSecurity); //public void SetCreationTime(string path, DateTime creationTime); //public void SetCreationTimeUtc(string path, DateTime creationTimeUtc); #endregion // File Methods #region SynchronizeDirectories /// /// Synchronizes the directories. /// /// The source path. /// The destination path. /// The search pattern. /// /// A list of uploaded files. /// /// is null. /// is null or contains only whitespace. /// was not found on the remote host. /// If a problem occurs while copying the file public IEnumerable SynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern) { if (sourcePath is null) { throw new ArgumentNullException(nameof(sourcePath)); } if (string.IsNullOrWhiteSpace(destinationPath)) { throw new ArgumentException("destinationPath"); } return InternalSynchronizeDirectories(sourcePath, destinationPath, searchPattern, asynchResult: null); } /// /// Begins the synchronize directories. /// /// The source path. /// The destination path. /// The search pattern. /// The async callback. /// The state. /// /// An that represents the asynchronous directory synchronization. /// /// is null. /// is null or contains only whitespace. /// If a problem occurs while copying the file public IAsyncResult BeginSynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern, AsyncCallback asyncCallback, object state) { if (sourcePath is null) { throw new ArgumentNullException(nameof(sourcePath)); } if (string.IsNullOrWhiteSpace(destinationPath)) { throw new ArgumentException("destDir"); } var asyncResult = new SftpSynchronizeDirectoriesAsyncResult(asyncCallback, state); ThreadAbstraction.ExecuteThread(() => { try { var result = InternalSynchronizeDirectories(sourcePath, destinationPath, searchPattern, asyncResult); asyncResult.SetAsCompleted(result, completedSynchronously: false); } catch (Exception exp) { asyncResult.SetAsCompleted(exp, completedSynchronously: false); } }); return asyncResult; } /// /// Ends the synchronize directories. /// /// The async result. /// /// A list of uploaded files. /// /// The object did not come from the corresponding async method on this type.-or- was called multiple times with the same . /// The destination path was not found on the remote host. public IEnumerable EndSynchronizeDirectories(IAsyncResult asyncResult) { if (asyncResult is not SftpSynchronizeDirectoriesAsyncResult ar || ar.EndInvokeCalled) { throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult."); } // Wait for operation to complete, then return result or throw exception return ar.EndInvoke(); } private IEnumerable InternalSynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern, SftpSynchronizeDirectoriesAsyncResult asynchResult) { if (!Directory.Exists(sourcePath)) { throw new FileNotFoundException(string.Format("Source directory not found: {0}", sourcePath)); } var uploadedFiles = new List(); var sourceDirectory = new DirectoryInfo(sourcePath); using (var sourceFiles = sourceDirectory.EnumerateFiles(searchPattern).GetEnumerator()) { if (!sourceFiles.MoveNext()) { return uploadedFiles; } #region Existing Files at The Destination var destFiles = InternalListDirectory(destinationPath, listCallback: null); var destDict = new Dictionary(); foreach (var destFile in destFiles) { if (destFile.IsDirectory) { continue; } destDict.Add(destFile.Name, destFile); } #endregion #region Upload the difference const Flags uploadFlag = Flags.Write | Flags.Truncate | Flags.CreateNewOrOpen; do { var localFile = sourceFiles.Current; if (localFile is null) { continue; } var isDifferent = true; if (destDict.TryGetValue(localFile.Name, out var remoteFile)) { // TODO: Use md5 to detect a difference //ltang: File exists at the destination => Using filesize to detect the difference isDifferent = localFile.Length != remoteFile.Length; } if (isDifferent) { var remoteFileName = string.Format(CultureInfo.InvariantCulture, @"{0}/{1}", destinationPath, localFile.Name); try { using (var file = File.OpenRead(localFile.FullName)) { InternalUploadFile(file, remoteFileName, uploadFlag, asyncResult: null, uploadCallback: null); } uploadedFiles.Add(localFile); asynchResult?.Update(uploadedFiles.Count); } catch (Exception ex) { throw new SshException($"Failed to upload {localFile.FullName} to {remoteFileName}", ex); } } } while (sourceFiles.MoveNext()); } #endregion return uploadedFiles; } #endregion /// /// Internals the list directory. /// /// The path. /// The list callback. /// /// A list of files in the specfied directory. /// /// is null. /// Client not connected. private IEnumerable InternalListDirectory(string path, Action listCallback) { if (path is null) { throw new ArgumentNullException(nameof(path)); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); var handle = _sftpSession.RequestOpenDir(fullPath); var basePath = fullPath; if (!basePath.EndsWith("/")) { basePath = string.Format("{0}/", fullPath); } var result = new List(); var files = _sftpSession.RequestReadDir(handle); while (files is not null) { foreach (var f in files) { result.Add(new SftpFile(_sftpSession, string.Format(CultureInfo.InvariantCulture, "{0}{1}", basePath, f.Key), f.Value)); } // Call callback to report number of files read if (listCallback is not null) { // Execute callback on different thread ThreadAbstraction.ExecuteThread(() => listCallback(result.Count)); } files = _sftpSession.RequestReadDir(handle); } _sftpSession.RequestClose(handle); return result; } /// /// Internals the download file. /// /// The path. /// The output. /// An that references the asynchronous request. /// The download callback. /// is null. /// is null or contains whitespace. /// Client not connected. private void InternalDownloadFile(string path, Stream output, SftpDownloadAsyncResult asyncResult, Action downloadCallback) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); using (var fileReader = ServiceFactory.CreateSftpFileReader(fullPath, _sftpSession, _bufferSize)) { var totalBytesRead = 0UL; while (true) { // Cancel download if (asyncResult is not null && asyncResult.IsDownloadCanceled) { break; } var data = fileReader.Read(); if (data.Length == 0) { break; } output.Write(data, 0, data.Length); totalBytesRead += (ulong) data.Length; if (downloadCallback is not null) { // copy offset to ensure it's not modified between now and execution of callback var downloadOffset = totalBytesRead; // Execute callback on different thread ThreadAbstraction.ExecuteThread(() => { downloadCallback(downloadOffset); }); } } } } /// /// Internals the upload file. /// /// The input. /// The path. /// The flags. /// An that references the asynchronous request. /// The upload callback. /// is null. /// is null or contains whitespace. /// Client not connected. private void InternalUploadFile(Stream input, string path, Flags flags, SftpUploadAsyncResult asyncResult, Action uploadCallback) { if (input is null) { throw new ArgumentNullException(nameof(input)); } if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("path"); } if (_sftpSession is null) { throw new SshConnectionException("Client not connected."); } var fullPath = _sftpSession.GetCanonicalPath(path); var handle = _sftpSession.RequestOpen(fullPath, flags); ulong offset = 0; // create buffer of optimal length var buffer = new byte[_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle)]; var bytesRead = input.Read(buffer, 0, buffer.Length); var expectedResponses = 0; var responseReceivedWaitHandle = new AutoResetEvent(initialState: false); do { // Cancel upload if (asyncResult is not null && asyncResult.IsUploadCanceled) { break; } if (bytesRead > 0) { var writtenBytes = offset + (ulong) bytesRead; _sftpSession.RequestWrite(handle, offset, buffer, offset: 0, bytesRead, wait: null, s => { if (s.StatusCode == StatusCodes.Ok) { _ = Interlocked.Decrement(ref expectedResponses); _ = responseReceivedWaitHandle.Set(); // Call callback to report number of bytes written if (uploadCallback is not null) { // Execute callback on different thread ThreadAbstraction.ExecuteThread(() => uploadCallback(writtenBytes)); } } }); _ = Interlocked.Increment(ref expectedResponses); offset += (ulong) bytesRead; bytesRead = input.Read(buffer, 0, buffer.Length); } else if (expectedResponses > 0) { // Wait for expectedResponses to change _sftpSession.WaitOnHandle(responseReceivedWaitHandle, _operationTimeout); } } while (expectedResponses > 0 || bytesRead > 0); _sftpSession.RequestClose(handle); } /// /// Called when client is connected to the server. /// protected override void OnConnected() { base.OnConnected(); _sftpSession = CreateAndConnectToSftpSession(); } /// /// Called when client is disconnecting from the server. /// protected override void OnDisconnecting() { base.OnDisconnecting(); // disconnect, dispose and dereference the SFTP session since we create a new SFTP session // on each connect var sftpSession = _sftpSession; if (sftpSession is not null) { _sftpSession = null; sftpSession.Dispose(); } } /// /// Releases unmanaged and - optionally - managed resources /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { var sftpSession = _sftpSession; if (sftpSession is not null) { _sftpSession = null; sftpSession.Dispose(); } } } private ISftpSession CreateAndConnectToSftpSession() { var sftpSession = ServiceFactory.CreateSftpSession(Session, _operationTimeout, ConnectionInfo.Encoding, ServiceFactory.CreateSftpResponseFactory()); try { sftpSession.Connect(); return sftpSession; } catch { sftpSession.Dispose(); throw; } } } }