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,所以我们在开发时,尽量不要对这种工具形成强依赖,如果确实避不开,我们应该要考虑把它所有操作自己做个封装,封装在一个类中,后续如果要替代,我们也可以很方便平稳的过渡。