Ftp基础(五):.NetCore中使用Ftp的建议(FluentFTP)

  上一篇说道C#使用FluentFTP来简单的连接使用Ftp,本篇是个人在.NetCore中使用Ftp的建议(可能有点啰嗦):

  1、为Ftp的配置创建基类

  在开发过程中,我们如果要使用Ftp,往往需要这几个信息:  

    Host:Ftp地址
    Port:端口号
    User:用户名
    Password:密码
    WorkingDirectory:工作目录,如果你希望整个Ftp只给一个系统使用,可以不用,直接使用根目录
    Mode:模式:主动模式、被动模式

    除了上述几个信息,我们在开发过程中可能还会有其它配置,比如我们的业务配置,也有可能是Ftp的配置,但是这些配置都和具体场景有关。

  而这些信息,可能来自我们的配置文件,也可能来自数据库配置,还可能来自我们UI的输入,所以,我们应该给Ftp的配置一个基类(也可以是抽象类),比如:

    public class FtpOptions
    {
        /// <summary>
        /// Ftp地址,默认localhost
        /// </summary>
        public string Host { get; set; } = "localhost";
        /// <summary>
        /// Ftp端口号,默认21
        /// </summary>
        public int Port { get; set; } = 21;
        /// <summary>
        /// User
        /// </summary>
        public string User { get; set; }
        /// <summary>
        /// Password
        /// </summary>
        public string Password { get; set; }
        /// <summary>
        /// 根目录
        /// </summary>
        public string WorkingDirectory { get; set; }
        /// <summary>
        /// 是否主动模式
        /// </summary>
        public bool Active { get; set; }
    }

  这样,当我们有一些特定需求的时候,可以继承这个基类,比如我们下载的时候,可能需要限速,那么我们可以创建这样的配置类:

    public class DownloadFtpOptions : FtpOptions
    {
        /// <summary>
        /// 下载限速(字节)
        /// </summary>
        public long Bytes { get; set; }
    }

  再比如,当我们使用Ftp来作为两个系统之间的信息传递的媒介时,比如  

    A系统上传文件到Ftp指定的目录,那么需要的配置是:Ftp基本配置、上传目录
    B系统定时扫描指定的目录来下载文件,那么需要的配置是:Ftp基本配置、扫描目录、扫描时间间隔

  那么我们可以分别为A\B系统创建这么两个配置类:  

    public class UploadFtpOptions : FtpOptions
    {
        /// <summary>
        /// 上传文件的目录
        /// </summary>
        public string Path { get; set; }
    }
    public class ScanFtpOptions : FtpOptions
    {
        /// <summary>
        /// 扫描时间间隔
        /// </summary>
        public int Interval { get; set; }
        /// <summary>
        /// 扫描文件的目录
        /// </summary>
        public string Path { get; set; }
    }

  2、通过一个方法来获取FtpClient或者AsyncFtpClient

  有了配置,接下来我们需要使用,利用FluentFTP来操作Ftp的话,我们需要创建FtpClient或者AsyncFtpClient,这个时候,我们应该通过一个通用的方法来创建,而这个方法,可以是基类的一个公共方法,也可以是基类的一个拓展方法:

  使用基类的公共方法,可以使用virtual修饰,后续子类有需要,可以自定义重写

    public class FtpOptions
    {
        //省略属性...

        /// <summary>
        /// 创建ftp连接
        /// </summary>
        /// <returns></returns>
        public virtual FtpClient GetFtpClient()
        {
            var ftpClient = new FtpClient(Host);
            if (Port > 0)
            {
                ftpClient.Port = Port;
            }
            if (!string.IsNullOrEmpty(User) && !string.IsNullOrEmpty(Password))
            {
                ftpClient.Credentials = new NetworkCredential(User, Password);
            }
            ftpClient.Encoding = Encoding.UTF8;
            ftpClient.Config.DataConnectionType = Active ? FtpDataConnectionType.AutoActive : FtpDataConnectionType.AutoPassive;
            ftpClient.Connect();

            ftpClient.CreateDirectory(WorkingDirectory);
            ftpClient.SetWorkingDirectory(WorkingDirectory);
            return ftpClient;
        }
        /// <summary>
        /// 创建ftp连接(异步)
        /// </summary>
        /// <returns></returns>
        public virtual async Task<AsyncFtpClient> GetAsyncFtpClient()
        {
            var ftpClient = new AsyncFtpClient(Host);
            if (Port > 0)
            {
                ftpClient.Port = Port;
            }
            if (!string.IsNullOrEmpty(User) && !string.IsNullOrEmpty(Password))
            {
                ftpClient.Credentials = new NetworkCredential(User, Password);
            }
            ftpClient.Encoding = Encoding.UTF8;
            ftpClient.Config.DataConnectionType = Active ? FtpDataConnectionType.AutoActive : FtpDataConnectionType.AutoPassive;
            await ftpClient.Connect();

            await ftpClient.CreateDirectory(WorkingDirectory);
            await ftpClient.SetWorkingDirectory(WorkingDirectory);
            return ftpClient;
        }
    }

  使用基类的拓展方法:

    public static class FtpOptionsExtensions
    {
        /// <summary>
        /// 创建ftp连接
        /// </summary>
        /// <param name="klarfDiscoveryOptions"></param>
        /// <returns></returns>
        public static FtpClient GetFtpClient(this FtpOptions ftpOptions)
        {
            var ftpClient = new FtpClient(ftpOptions.Host);
            if (ftpOptions.Port > 0)
            {
                ftpClient.Port = ftpOptions.Port;
            }
            if (!string.IsNullOrEmpty(ftpOptions.User) && !string.IsNullOrEmpty(ftpOptions.Password))
            {
                ftpClient.Credentials = new NetworkCredential(ftpOptions.User, ftpOptions.Password);
            }
            ftpClient.Encoding = Encoding.UTF8;
            ftpClient.Config.DataConnectionType = ftpOptions.Active ? FtpDataConnectionType.AutoActive : FtpDataConnectionType.AutoPassive;
            ftpClient.Connect();

            ftpClient.CreateDirectory(ftpOptions.WorkingDirectory);
            ftpClient.SetWorkingDirectory(ftpOptions.WorkingDirectory);
            return ftpClient;
        }
        /// <summary>
        /// 创建ftp连接(异步)
        /// </summary>
        /// <param name="klarfDiscoveryOptions"></param>
        /// <returns></returns>
        public static async Task<AsyncFtpClient> GetAsyncFtpClient(this FtpOptions ftpOptions)
        {
            var ftpClient = new AsyncFtpClient(ftpOptions.Host);
            if (ftpOptions.Port > 0)
            {
                ftpClient.Port = ftpOptions.Port;
            }
            if (!string.IsNullOrEmpty(ftpOptions.User) && !string.IsNullOrEmpty(ftpOptions.Password))
            {
                ftpClient.Credentials = new NetworkCredential(ftpOptions.User, ftpOptions.Password);
            }
            ftpClient.Encoding = Encoding.UTF8;
            ftpClient.Config.DataConnectionType = ftpOptions.Active ? FtpDataConnectionType.AutoActive : FtpDataConnectionType.AutoPassive;
            await ftpClient.Connect();

            await ftpClient.CreateDirectory(ftpOptions.WorkingDirectory);
            await ftpClient.SetWorkingDirectory(ftpOptions.WorkingDirectory);
            return ftpClient;
        }
    }

  两种方式各有各的优劣,具体开发再选择吧。

  3、面向接口来使用Ftp或者FluentFTP

  在开发过程中,应该有些同学第一反应就是要把FtpClient封装成类似HttpClient的方式去使用吧,先封装个工厂,然后使用它来创建FtpClient,其实个人不推荐这种做法,因为它这么做属于对Ftp形成了强依赖。既然使用了.NetCore,那么我们很多时候应该面向接口开发,特别是针对Ftp这类第三方库时,我们应该尽可能对它形成弱依赖,而不能说不使用它,我们系统就玩不转了吧。

  其实,这就需要我们能对自己系统的业务进行区分,比如,当我们只是想存储文件时,我们可以创建这么一个接口:  

    public interface IFileStorage
    {
        /// <summary>
        /// 文件是否存在
        /// </summary>
        /// <param name="fileName"></param>
        /// <returns></returns>
        Task<bool> FileExistsAsync(string fileName);
        /// <summary>
        /// 删除文件
        /// </summary>
        /// <param name="fileName"></param>
        /// <returns></returns>
        Task DeleteFileAsync(string fileName);
        /// <summary>
        /// 上传文件
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="stream"></param>
        /// <returns></returns>
        Task UploadAsync(string fileName, Stream stream);
        /// <summary>
        /// 下载文件
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="stream"></param>
        /// <returns></returns>
        Task DownloadAsync(string fileName, Stream stream);
    }

  我们Ftp的操作只需要实现这个接口然后把接口注入就可以了,这样我们哪天不想使用Ftp存储文件,只需要使用其它的工具实现这个就可以了。

   :你可能回想为什么没有目录的操作?这个是因为目录不具备这个业务的共性,存的是文件,不是目录,而且哪天如果要缓存MongoGridFS、某些对象存储方案时,可能根本就没有目录的概念,当然我们也可以在这个接口上加上目录的操作,如果不支持目录操作,我们只需要空实现就好了

  再比如,如果我们需要扫描Ftp的某个目录,我们可以创建这么一个接口:  

    public interface IScanSource
    {
        /// <summary>
        /// 返回指定目录下的文件及目录
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        Task<ScanItem[]> List(string path);
        /// <summary>
        /// 下载文件
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="stream"></param>
        /// <returns></returns>
        Task DownloadAsync(string fileName, Stream stream);
    }
    public class ScanItem
    {
        /// <summary>
        /// 文件或者名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 是否是目录
        /// </summary>
        public bool IsDirectory { get; set; }
    }

   通用,我们只需要使用Ftp来实现 这个接口就可以了,后续如果我们扫描的是本地的目录、Cifs/Smb等其它的方案,我们只需要对应的实现就可以了

  

  总结

  Ftp很早就出现了,优劣我们暂且不评论,以前系统用的很多,但是现在不一样了,特别是互联网项目,几乎都不用的,而取代它的方案,现在还是有很多可以选择,比如用的多的还有Cifs/Smb,所以我们在开发时,尽量不要对这种工具形成强依赖,如果确实避不开,我们应该要考虑把它所有操作自己做个封装,封装在一个类中,后续如果要替代,我们也可以很方便平稳的过渡。

 

posted @ 2023-12-30 19:04  没有星星的夏季  阅读(224)  评论(0编辑  收藏  举报