SFTP CreateDirectoryAsync is not idempotent
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 4.4k
- Forks
- 993
- Avg merge
- 9d 21h
- Merged PRs (30d)
- 1
Description
My specifications:
OS: Ubuntu Desktop and Windows 11
Framework: .NET 9
Package version: 2025.1.0
This is my current code:
using SftpClient client = new SftpClient(host, port, username, password);
await client.ConnectAsync(cancellationToken);
try
{
await client.CreateDirectoryAsync(someDir, cancellationToken);
}
catch (SftpException ex)
{
// Only some logging code here.
}
When this directory already exists, all I get as an exception message is "failure".
Renci.SshNet.Common.SftpException: failure
at Renci.SshNet.SubsystemSession.<WaitOnHandleAsync> g__DoWaitAsync | 38_0[T](TaskCompletionSource`1 tcs, Int32 millisecondsTimeout, CancellationToken cancellationToken)
at Renci.SshNet.SftpClient.CreateDirectoryAsync(String path, CancellationToken cancellationToken)
Problems:
- I can't know for sure if the exception is about the directory already existing, or whether something else is wrong. Type SftpException with message "failure" is just not descriptive enough.
- CreateDirectoryAsync is not idempotent. Throwing an exception seems very counter-intuitive for a method like this.
- The SftpException is not described in the method documentation:
/// <summary>
/// Asynchronously requests to create a remote directory specified by path.
/// </summary>
/// <param name="path">Directory path to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous create directory operation.</returns>
/// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
/// <exception cref="SftpPermissionDeniedException">Permission to create the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
/// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
/// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
If I would want to do this properly, I need quite a lot of code. This is the extension method I wrote:
internal static async ValueTask EnsureDirectoryExists(this SftpClient client, string directory, CancellationToken cancellationToken)
{
bool directoryExists = await client.ExistsAsync(directory, cancellationToken);
if (directoryExists) return;
try
{
await client.CreateDirectoryAsync(directory, cancellationToken);
}
catch (SftpException ex)
{
try
{
bool directoryAlreadyExisted= await client.ExistsAsync(directory, cancellationToken);
if (directoryAlreadyExisted)
{
// The directory already existed, so we can ignore the exception.
return;
}
else
{
// The directory does not exist.
throw;
}
}
catch (SftpException)
{
// Failed checking if the directory exists.
// Throwing the original exception as that illustrates what really is the problem here.
throw ex;
}
}
}
There are 2 ways this could be fixed:
- Add an EnsureDirectoryExists method. I'm sure this can be done much closer to the actual SFTP protocol than the extension method I created.
- Add an SftpDirectoryAlreadyExistsException that inherits from SftpException.
I'm very curious, is there a reason why something like EnsureDirectoryExists was never added to SSH.NET?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at SftpClient.CreateDirectoryAsync and its existing XML documentation, then compare its behavior with ExistsAsync and the reported SFTP response when the directory already exists. Determine whether the requested idempotent behavior or a more specific exception fits the protocol; done means callers can distinguish an existing directory from other failures.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- networking
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100