13

I am currently using Renci SSH.NET to upload files and folders to a Unix Server using SFTP, and creating directories using

sftp.CreateDirectory("//server/test/test2");

works perfectly, as long as the folder "test" already exists. If it doesn't, the CreateDirectory method fails, and this happens everytime when you try to create directories containing multiple levels.

Is there an elegant way to recursively generate all the directories in a string? I was assuming that the CreateDirectory method does that automatically.

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Erik
  • 2,316
  • 9
  • 36
  • 58
  • Maybe uploading a zip/7z file and decompress on the server is a better solution if there are too many not-created folders. – zwcloud Dec 04 '22 at 07:35
  • @zwcloud You can do that. But you need a shell or other access to the server for that. You cannot decompress an archive over SFTP. – Martin Prikryl Dec 04 '22 at 15:53

7 Answers7

24

There's no other way.

Just iterate directory levels, testing each level using SftpClient.GetAttributes and create the levels that do not exist.

static public void CreateDirectoryRecursively(this SftpClient client, string path)
{
    string current = "";

    if (path[0] == '/')
    {
        path = path.Substring(1);
    }

    while (!string.IsNullOrEmpty(path))
    {
        int p = path.IndexOf('/');
        current += '/';
        if (p >= 0)
        {
            current += path.Substring(0, p);
            path = path.Substring(p + 1);
        }
        else
        {
            current += path;
            path = "";
        }

        try
        {
            SftpFileAttributes attrs = client.GetAttributes(current);
            if (!attrs.IsDirectory)
            {
                throw new Exception("not directory");
            }
        }
        catch (SftpPathNotFoundException)
        {
            client.CreateDirectory(current);
        }
    }
}
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
12

A little improvement on the code provided by Martin Prikryl

Don't use Exceptions as a flow control mechanism. The better alternative here is to check if the current path exists first.

if (client.Exists(current))
{
    SftpFileAttributes attrs = client.GetAttributes(current);
    if (!attrs.IsDirectory)
    {
        throw new Exception("not directory");
    }
}
else
{
    client.CreateDirectory(current);
}

instead of the try catch construct

try
{
    SftpFileAttributes attrs = client.GetAttributes(current);
    if (!attrs.IsDirectory)
    {
        throw new Exception("not directory");
    }
}
catch (SftpPathNotFoundException)
{
    client.CreateDirectory(current);
}
Guido
  • 121
  • 1
  • 5
  • 5
    Also worth saying that your code is less efficient as it requires two round trips to the server for each directory level. Note how the `.Exists` is implemented. Internally it does exactly what my original code do. It calls `.GetAttributes` and returns `true` if it does not throw. And it if throws, it catches the `SftpPathNotFoundException` and returns `false`. So you only seemingly avoided using exceptions for control flow. That's why I chose a variant with a single call and the `try` ... `catch` construct. If a latency to the server is big, you will tell a difference. +1 anyway :) – Martin Prikryl Sep 01 '16 at 07:24
  • 4
    + Do not base your answer on an exiting one. Your answer has has to stand on its own. So please include a complete code, instead of saying "use this instead of that in that code". Of course, acknowledging source of the code is expected. – Martin Prikryl Sep 01 '16 at 07:27
4

Hi I found my answer quite straight forwared. Since I found this old post, I thought others might also stumble upon it. The accepted answer is not that good, so here is my take. It does not use any counting gimmicks, so I think it's a little more easy to understand.

public void CreateAllDirectories(SftpClient client, string path)
    {
        // Consistent forward slashes
        path = path.Replace(@"\", "/");
        foreach (string dir in path.Split('/'))
        {
            // Ignoring leading/ending/multiple slashes
            if (!string.IsNullOrWhiteSpace(dir))
            {
                if(!client.Exists(dir))
                {
                    client.CreateDirectory(dir);
                }
                client.ChangeDirectory(dir);
            }
        }
        // Going back to default directory
        client.ChangeDirectory("/");
    }
Ojot
  • 61
  • 4
  • My (accepted) answer is longer, because 1) it does not have the side effect of changing the working directory 2) it is more efficient, as it does not change working directory (you will tell the difference, if your connection has big latency). 3) Your answer will not report an error, if there's an existing *file* that matches the name of the *directory* you want to create. – Martin Prikryl Sep 06 '19 at 08:07
  • I get issues on my SFTP server on couchdrop, a forward slash is encoded to %2F – Tom McDonough Apr 29 '20 at 09:11
  • i think it's better to back forward to the original destination instead of "/" put at beginning var orginalPath = sftpClient.WorkingDirectory; at end sftpClient.ChangeDirectory(orginalPath); – Abdullah Tahan Aug 22 '22 at 23:39
3

FWIW, here's my rather simple take on it. The one requirement is that the server destination path is seperated by forward-slashes, as is the norm. I check for this before calling the function.

    private void CreateServerDirectoryIfItDoesntExist(string serverDestinationPath, SftpClient sftpClient)
    {
        if (serverDestinationPath[0] == '/')
            serverDestinationPath = serverDestinationPath.Substring(1);

        string[] directories = serverDestinationPath.Split('/');
        for (int i = 0; i < directories.Length; i++)
        {
            string dirName = string.Join("/", directories, 0, i + 1);
            if (!sftpClient.Exists(dirName))
                sftpClient.CreateDirectory(dirName);
        }
    }

HTH

Morten Nørgaard
  • 2,609
  • 2
  • 24
  • 24
  • i think this's the best solution where it doesn't change the directory and easier to read and maintain , but you are re-doing the whole string.Join on every iteration, it is not very efficient. You should keep a variable current that just starts with "", and then in the loop you can do current = string.Join("/", current, directories[i]); While doing that, the for can be changed to a foreach. – Abdullah Tahan Aug 23 '22 at 13:34
1

I put together all the answers here and came up with the following method.

This method doesn't change working directory, doesn't do the redundant checks that use both GetAttributes() and Exists() (these are internally same codes according to Martin Prikryl), doesn't repeat string.Join from the beginning over and over, is easier to read, and works well with relative paths too.

private void CreateDirectoryRecursively(string targetPath, SftpClient client)
{
    string currentPath = "";
    if (targetPath[0] == '.')
    {
        currentPath = ".";
        targetPath = targetPath[1..];
    }
    foreach (string segment in targetPath.Split('/'))
    {
        // Ignoring leading/ending/multiple slashes
        if (!string.IsNullOrWhiteSpace(segment))
        {
            currentPath += $"/{segment}";
            if (!client.Exists(currentPath))
                client.CreateDirectory(currentPath);
        }
    }
}
Jake
  • 11
  • 2
0

A little modification on the accepted answer to use spans.

It's probably utterly pointless in this case, since the overhead of the sftp client is far greater than copying strings, but it can be useful in other similiar scenarios:

        public static void EnsureDirectory(this SftpClient client, string path)
        {
            if (path.Length is 0)
                return;

            var curIndex = 0;
            var todo = path.AsSpan();
            if (todo[0] == '/' || todo[0] == '\\')
            {
                todo = todo.Slice(1);
                curIndex++;
            }

            while (todo.Length > 0)
            {
                var endOfNextIndex = todo.IndexOf('/');
                if (endOfNextIndex < 0)
                    endOfNextIndex = todo.IndexOf('\\');

                string current;
                if (endOfNextIndex >= 0)
                {
                    curIndex += endOfNextIndex + 1;
                    current = path.Substring(0, curIndex);
                    todo = path.AsSpan().Slice(curIndex);
                }
                else
                {
                    current = path;
                    todo = ReadOnlySpan<char>.Empty;
                }

                try
                {
                    client.CreateDirectory(current);
                }
                catch (SshException ex) when (ex.Message == "Already exists.") { }
            }
        }
Yair Halberstadt
  • 5,733
  • 28
  • 60
-1

my approach is more sufficient and easier to read and maintain

public static void CreateDirectoryRecursively(this ISftpClient sftpClient, string path)
        {
            // Consistent forward slashes
            var separators = new char[] { Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar };
            string[] directories = path.Split(separators);

            string currentDirectory = "";

            for (int i = 1; i < directories.Length; i++)
            {
                currentDirectory = string.Join("/", currentDirectory, directories[i]);
                if (!sftpClient.Exists(currentDirectory))
                {
                    sftpClient.CreateDirectory(currentDirectory);
                }
            }
        }
Abdullah Tahan
  • 1,963
  • 17
  • 28
  • ... and terribly inefficient too. At least one request to the server for each directory level, even if the complete target path exists. – Martin Prikryl Aug 23 '22 at 16:32