SSH.NET SFTP 递归获取目录和文件列表

20

我正在使用Renci.SshNet库通过SFTP递归获取文件和目录列表。我能够连接到SFTP站点,但我不知道如何在C#中递归获取文件和目录的列表。我没有找到任何有用的示例。

是否有人尝试过这个?如果有,能否发布一些样例代码关于如何递归获取这些文件和文件夹。

谢谢,
Prav

6个回答

35

由于 ChangeDirectoryListDirectory 之间的交互不符合预期,因此该库存在一些怪异之处,使得这个递归列表变得棘手。

下面的代码并不会列出 /home 目录中的文件,而是列出了根目录 / 的文件:

sftp.ChangeDirectory("home");
sftp.ListDirectory("").Select (s => s.FullName);

以下代码无法工作,并返回SftpPathNotFoundException异常:

sftp.ChangeDirectory("home");
sftp.ListDirectory("home").Select (s => s.FullName);

以下是列出 /home 目录中文件的正确方法

sftp.ChangeDirectory("/");
sftp.ListDirectory("home").Select (s => s.FullName);

如果你问我,这相当疯狂。使用ChangeDirectory方法设置默认目录对ListDirectory方法没有影响,除非在该方法的参数中指定一个文件夹。似乎应该为此编写一个错误报告。

因此,当您编写递归函数时,您需要先设置默认目录,然后在遍历文件夹时在ListDirectory调用中更改目录。列表返回一个SftpFiles的可枚举项。这些可单独检查是否IsDirectory == true。请注意,列表也会返回...条目(它们是目录)。如果要避免无限循环,您将要跳过这些条目。 :-)

编辑2/23/2018

我正在回顾我的一些旧答案,并为上面的答案道歉并提供以下可行代码。请注意,此示例不需要ChangeDirectory,因为它使用Fullname作为ListDirectory的参数:

void Main()
{
    using (var client = new Renci.SshNet.SftpClient("sftp.host.com", "user", "password"))
    {
        var files = new List<String>();
        client.Connect();
        ListDirectory(client, ".", ref files);
        client.Disconnect();
        files.Dump();
    }
}

void ListDirectory(SftpClient client, String dirName, ref List<String> files)
{
    foreach (var entry in client.ListDirectory(dirName))
    {

        if (entry.IsDirectory)
        {
            ListDirectory(client, entry.FullName, ref files);
        }
        else
        {
            files.Add(entry.FullName);
        }
    }
}

8
sftp.ListDirectory("")无法工作,但sftp.ListDirectory(".")可以 - 记住,'.'表示'当前目录'。 但是,它似乎不支持'~'快捷方式来代表主文件夹。 - Gargravarr
谢谢解释,是否有一个完整的例子可以实际上获取从给定根文件夹开始的所有目录和子目录的列表或树形图?我想知道为什么ssh.net在ListDirectory方法本身中没有提供bool递归。这些天是否有任何更好的解决方案可用于.net?例如,是否有一个更好的免费库,已经实现了这些基本任务?但是,一个工作示例将非常有帮助,谢谢! - Erik
1
对我来说非常有效。但是我将它从List<string>更改为sftp文件类型,因此不再使用files.Add(entry.FullName),而只需要使用files.Add(entry),您将获得每个文件的所有信息。我之所以使用这个是因为我的FTP非常慢,这可以进行一些优化,我认为在下载时可能不必再次获取文件信息。 - Kasper Olesen

6

试试这个:

var filePaths = client.ListDirectory(client.WorkingDirectory);

2
这是一个完整的类。它是.NET Core 2.1 Http触发器函数应用程序(v2)。
我想要摆脱以“.”开头的任何目录,因为我的sftp服务器有.cache文件夹和带有密钥的.ssh文件夹。同时,也不想处理像“。”或“..”这样的文件夹名称。
我最终将做的是将SftpFile投影到我使用的类型中,并将其返回给调用者(在这种情况下,它将是逻辑应用程序)。然后,我将将该对象传递到存储过程中,并使用OPENJSON来构建我的监视表。这基本上是创建我的SFTP处理队列的第一步,该队列将文件从我的SFTP文件夹移动到我的数据湖(暂时是blob,直到我找到更好的东西)。
我使用.WorkingDirectory的原因是我创建了一个用户,其主目录为“/home”。这使我可以遍历所有用户文件夹。我的应用程序不需要具有特定文件夹作为起点,只需要用户“root”即可。
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Renci.SshNet;
using Renci.SshNet.Sftp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SFTPFileMonitor
{
    public class GetListOfFiles
    {
        [FunctionName("GetListOfFiles")]
        public async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            List<SftpFile> zFiles;
            int fileCount;
            decimal totalSizeGB;
            long totalSizeBytes;

            using (SftpClient sftpClient = new SftpClient("hostname", "username", "password"))
            {
                sftpClient.Connect();
                zFiles = await GetFiles(sftpClient, sftpClient.WorkingDirectory, new List<SftpFile>());
                fileCount = zFiles.Count;
                totalSizeBytes = zFiles.Sum(l => l.Length);
                totalSizeGB = BytesToGB(totalSizeBytes);
            }

            return new OkObjectResult(new { fileCount, totalSizeBytes, totalSizeGB, zFiles });
        }
        private async Task<List<SftpFile>> GetFiles(SftpClient sftpClient, string directory, List<SftpFile> files)
        {
            foreach (SftpFile sftpFile in sftpClient.ListDirectory(directory))
            {
                if (sftpFile.Name.StartsWith('.')) { continue; }

                if (sftpFile.IsDirectory)
                {
                    await GetFiles(sftpClient, sftpFile.FullName, files);
                }
                else
                {
                    files.Add(sftpFile);
                }
            }
            return files;
        }
        private decimal BytesToGB(long bytes)
        {
            return Convert.ToDecimal(bytes) / 1024 / 1024 / 1024;
        }
    }
}

只是我个人的想法,因为 BytesToGB 函数只在 RunAsync(...) 函数中使用,所以可以将其转换为本地函数... - Adrian Hum
1
@AdrianHum 或者是在助手类中的扩展方法。 - jbusciglio acuity
我实际上将其作为扩展方法,调用Win32长字符串函数,该函数返回像Windows一样的文件大小...到三个数字...1.23GB 12.3GB 0.81kb等等...并且它与Windows保持一致。 - Adrian Hum

1
我使用递归实现了这个功能。创建了一个名为TransportResponse的类,如下所示。
 public class TransportResponse
{
    public string directoryName { get; set; }
    public string fileName { get; set; }
    public DateTime fileTimeStamp { get; set; }
    public MemoryStream fileStream { get; set; }
    public List<TransportResponse> lstTransportResponse { get; set; }
}

我创建了一个TransportResponse类的列表。如果directoryName不为空,则它将包含相同类别的列表,其中将文件作为MemoryStream放在该目录中(根据您的用例,可以更改此内容)。
List<TransportResponse> lstResponse = new List<TransportResponse>();
using (var client = new SftpClient(connectionInfo))
  {
          try
          {
                    Console.WriteLine("Connecting to " + connectionInfo.Host + " ...");
                    client.Connect();
                    Console.WriteLine("Connected to " + connectionInfo.Host + " ...");
           }
           catch (Exception ex)
           {
                    Console.WriteLine("Could not connect to "+ connectionInfo.Host +" server. Exception Details: " + ex.Message);
           }
           if (client.IsConnected)
           {
                    var files = client.ListDirectory(transport.SourceFolder);
                    lstResponse = downloadFilesInDirectory(files, client);
                    client.Disconnect();
            }
            else
            {
                    Console.WriteLine("Could not download files from "+ transport.TransportIdentifier +" because client was not connected.");
             }
   }



private static List<TransportResponse> downloadFilesInDirectory(IEnumerable<SftpFile> files, SftpClient client)
    {
        List<TransportResponse> lstResponse = new List<TransportResponse>();
        foreach (var file in files)
        {
            if (!file.IsDirectory)
            {
                if (file.Name != "." && file.Name != "..")
                {
                    if (!TransportDAL.checkFileExists(file.Name, file.LastWriteTime))
                    {
                        using (MemoryStream fs = new MemoryStream())
                        {
                            try
                            {
                                Console.WriteLine("Reading " + file.Name + "...");
                                client.DownloadFile(file.FullName, fs);
                                fs.Seek(0, SeekOrigin.Begin);
                                lstResponse.Add(new TransportResponse { fileName = file.Name, fileTimeStamp = file.LastWriteTime, fileStream = new MemoryStream(fs.GetBuffer()) });
                            }
                            catch(Exception ex)
                            {
                                Console.WriteLine("Error reading File. Exception Details: " + ex.Message);
                            }
                        }
                    }
                    else
                    {
                        Console.WriteLine("File was downloaded previously");
                    }
                }
            }
            else
            {
                if (file.Name != "." && file.Name != "..")
                {
                    lstResponse.Add(new TransportResponse { directoryName = file.Name,lstTransportResponse = downloadFilesInDirectory(client.ListDirectory(file.Name), client) });
                }                
            }
        }

        return lstResponse;
    }

希望这有所帮助。谢谢。

0

@Carlos Bos

这个库有一些怪癖,使得递归列表变得棘手,因为ChangeDirectory和ListDirectory之间的交互并不像你期望的那样工作。

正确的

当ChangeDirectory()参数是“.”时,它可以正常工作。

但是如果你执行

SftpClient sftp ...;
sftp.ChangeDirectory("some_folder");
//get file list
List<SftpFile> fileList = sftp.ListDirectory("some_folder").ToList();

然后会出现断言,因为ListDirectory()调用期望的是"some_folder/some_folder"

我使用的解决方法是在远程上传/重命名到"some_folder"之前保存和恢复当前目录,并且在操作之前需要列出该文件夹(例如查看文件是否已存在)

string working_directory = sftp.WorkingDirectory;
sftp.ChangeDirectory("some_folder");
sftp.RenameFile("name", "new_name");
sftp.ChangeDirectory(working_directory);

要检查文件是否存在,这个调用就足够了

sftp.Exists(path)

或者如果您想添加其他条件,例如区分大小写与否

 public FileExistence checkFileExists(string folder, string fileName)
    {
      //get file list
      List<SftpFile> fileList = sftp.ListDirectory(folder).ToList();

      if (fileList == null)
      {
        return FileExistence.UNCONFIRMED;
      }

      foreach (SftpFile f in fileList)
      {
        Console.WriteLine(f.ToString());
        //a not case sensitive comparison is made
        if (f.IsRegularFile && f.Name.ToLower() == fileName.ToLower())
        {
          return FileExistence.EXISTS;
        }
      }

      //if not found in traversal , it does not exist
      return FileExistence.DOES_NOT_EXIST;
    }

FileExistence是什么

public enum FileExistence
    {
      EXISTS,
      DOES_NOT_EXIST,
      UNCONFIRMED
    };

0

扩展方法:

    public static IEnumerable<SftpFile> ListDirectoryRecursive(this SftpClient client, String dirName)
    {
        foreach (var entry in client.ListDirectory(dirName))
        {
            if (Regex.IsMatch(entry.Name, "^\\.+$")) continue;
            yield return entry;
            if (entry.IsDirectory)
                foreach (var innerEntry in ListDirectoryRecursive(client, entry.FullName))
                    yield return innerEntry;
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接