From 2ab4a34b37760c22df44e05e1c05855aa6d806eb Mon Sep 17 00:00:00 2001 From: Victor Irzak Date: Tue, 28 Jul 2026 07:41:26 -0400 Subject: [PATCH] Generate ten synchronous SftpClient methods from their async versions Ten of the synchronous methods on SftpClient are mechanical translations of the asynchronous ones beside them: the same argument checks, the same session calls with Async dropped, the same order. Each has been maintained by hand alongside its twin, and every fix to one has to be made to the other. Zomp.SyncMethodGenerator produces the synchronous version from the async source at compile time, into the same partial class, so the public API is unchanged. Verified method by method: each generated body is identical to the hand-written one it replaces, once the fully qualified names are reduced. The async methods now carry where they previously repeated the interface documentation, which is what the twins already converted use, and the generated methods inherit the same documentation ISftpClient declares. Published documentation is unchanged. Left alone: RenameFile, whose async version does not have the isPosix overload the synchronous one routes through; and DownloadFile, UploadFile and ListDirectory, whose two versions genuinely differ. The generator is a build-time only dependency, so nothing is added to the package or to the runtime graph. --- Directory.Packages.props | 1 + src/Renci.SshNet/Renci.SshNet.csproj | 1 + src/Renci.SshNet/SftpClient.cs | 370 ++------------------------- 3 files changed, 20 insertions(+), 352 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d455a8a3b..d40252fbd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,5 +23,6 @@ + diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj index 9b7af1590..f28d7b3db 100644 --- a/src/Renci.SshNet/Renci.SshNet.csproj +++ b/src/Renci.SshNet/Renci.SshNet.csproj @@ -47,6 +47,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs index 947139f84..b834c6bde 100644 --- a/src/Renci.SshNet/SftpClient.cs +++ b/src/Renci.SshNet/SftpClient.cs @@ -23,7 +23,7 @@ namespace Renci.SshNet /// /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. /// - public class SftpClient : BaseClient, ISftpClient + public partial class SftpClient : BaseClient, ISftpClient { private static readonly Encoding Utf8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); @@ -284,41 +284,8 @@ internal SftpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, ISer #endregion Constructors - /// - /// Changes remote directory to path. - /// - /// New directory path. - /// is . - /// 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(); - ArgumentNullException.ThrowIfNull(path); - - if (_sftpSession is null) - { - throw new SshConnectionException("Client not connected."); - } - - _sftpSession.ChangeDirectory(path); - } - - /// - /// 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. - /// is . - /// 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. + /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public Task ChangeDirectoryAsync(string path, CancellationToken cancellationToken = default) { CheckDisposed(); @@ -351,41 +318,8 @@ public void ChangePermissions(string path, short mode) file.SetPermissions(mode); } - /// - /// Creates remote directory specified by path. - /// - /// Directory path to create. - /// is 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(); - ArgumentException.ThrowIfNullOrWhiteSpace(path); - - if (_sftpSession is null) - { - throw new SshConnectionException("Client not connected."); - } - - var fullPath = _sftpSession.GetCanonicalPath(path); - - _sftpSession.RequestMkDir(fullPath); - } - - /// - /// Asynchronously requests to create a remote directory specified by path. - /// - /// Directory path to create. - /// The to observe. - /// A that represents the asynchronous create directory operation. - /// is 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. + /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async Task CreateDirectoryAsync(string path, CancellationToken cancellationToken = default) { CheckDisposed(); @@ -401,32 +335,8 @@ public async Task CreateDirectoryAsync(string path, CancellationToken cancellati await _sftpSession.RequestMkDirAsync(fullPath, cancellationToken).ConfigureAwait(false); } - /// - /// Deletes remote directory specified by path. - /// - /// Directory to be deleted path. - /// is 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(); - ArgumentException.ThrowIfNullOrWhiteSpace(path); - - if (_sftpSession is null) - { - throw new SshConnectionException("Client not connected."); - } - - var fullPath = _sftpSession.GetCanonicalPath(path); - - _sftpSession.RequestRmDir(fullPath); - } - /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async Task DeleteDirectoryAsync(string path, CancellationToken cancellationToken = default) { CheckDisposed(); @@ -444,32 +354,8 @@ public async Task DeleteDirectoryAsync(string path, CancellationToken cancellati await _sftpSession.RequestRmDirAsync(fullPath, cancellationToken).ConfigureAwait(false); } - /// - /// Deletes remote file specified by path. - /// - /// File to be deleted path. - /// is 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(); - ArgumentException.ThrowIfNullOrWhiteSpace(path); - - if (_sftpSession is null) - { - throw new SshConnectionException("Client not connected."); - } - - var fullPath = _sftpSession.GetCanonicalPath(path); - - _sftpSession.RequestRemove(fullPath); - } - /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async Task DeleteFileAsync(string path, CancellationToken cancellationToken) { CheckDisposed(); @@ -725,47 +611,8 @@ public IEnumerable EndListDirectory(IAsyncResult asyncResult) 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 . - /// The method was called after the client was disposed. - public ISftpFile Get(string path) - { - CheckDisposed(); - ArgumentNullException.ThrowIfNull(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); - } - - /// - /// Gets reference to remote file or directory. - /// - /// The path. - /// The to observe. - /// - /// A that represents the get operation. - /// The task result contains the reference to file object. - /// - /// Client is not connected. - /// was not found on the remote host. - /// is . - /// The method was called after the client was disposed. + /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async Task GetAsync(string path, CancellationToken cancellationToken) { CheckDisposed(); @@ -785,74 +632,8 @@ public async Task GetAsync(string path, CancellationToken cancellatio return new SftpFile(_sftpSession, fullPath, attributes); } - /// - /// Checks whether file or directory exists. - /// - /// The path. - /// - /// if directory or file exists; otherwise . - /// - /// is 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(); - ArgumentException.ThrowIfNullOrWhiteSpace(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; - } - } - - /// - /// Checks whether file or directory exists. - /// - /// The path. - /// The to observe. - /// - /// A that represents the exists operation. - /// The task result contains if directory or file exists; otherwise . - /// - /// is 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. + /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async Task ExistsAsync(string path, CancellationToken cancellationToken = default) { CheckDisposed(); @@ -1335,43 +1116,8 @@ public void EndUploadFile(IAsyncResult asyncResult) ar.EndInvoke(); } - /// - /// Gets status using statvfs@openssh.com request. - /// - /// The path. - /// - /// A instance that contains file status information. - /// - /// Client is not connected. - /// is . - /// The method was called after the client was disposed. - public SftpFileSystemInformation GetStatus(string path) - { - CheckDisposed(); - ArgumentNullException.ThrowIfNull(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 . - /// The method was called after the client was disposed. + /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async Task GetStatusAsync(string path, CancellationToken cancellationToken) { CheckDisposed(); @@ -1572,21 +1318,8 @@ public StreamWriter CreateText(string path, Encoding encoding) return new StreamWriter(Open(path, FileMode.Create, FileAccess.Write), encoding); } - /// - /// Deletes the specified file or directory. - /// - /// The name of the file or directory to be deleted. Wildcard characters are not supported. - /// is . - /// 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(); - } - /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async Task DeleteAsync(string path, CancellationToken cancellationToken = default) { var file = await GetAsync(path, cancellationToken).ConfigureAwait(false); @@ -1677,39 +1410,8 @@ 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 . - /// Client is not connected. - /// The method was called after the client was disposed. - public SftpFileStream Open(string path, FileMode mode, FileAccess access) - { - CheckDisposed(); - - return SftpFileStream.Open(_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 . - /// Client is not connected. - /// The method was called after the client was disposed. + /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public Task OpenAsync(string path, FileMode mode, FileAccess access, CancellationToken cancellationToken) { CheckDisposed(); @@ -2023,44 +1725,8 @@ public void WriteAllText(string path, string contents, Encoding encoding) } } - /// - /// Gets the of the file on the path. - /// - /// The path to the file. - /// - /// The of the file on the path. - /// - /// is . - /// 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); - } - - /// - /// Gets the of the file on the path. - /// - /// The path to the file. - /// The to observe. - /// - /// A that represents the attribute retrieval operation. - /// The task result contains the of the file on the path. - /// - /// is . - /// Client is not connected. - /// was not found on the remote host. - /// The method was called after the client was disposed. + /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async Task GetAttributesAsync(string path, CancellationToken cancellationToken) { CheckDisposed();