0%

[DotnetCore]FTP-下載上傳

前情提要

筆者所處環境為金融業,所在的科別屬寫週邊的應用系統,開發的每個系統多多少少都需要與別的系統串接,常見的有核心外匯通路科等等。如[DotnetCore]Socket程式實作那篇所提,因為通常核心系統環境比較特殊,相對應的串接方式,偏Socket或者交換檔案這類的傳統型居多。這篇主要以FTP交換檔案為主介紹其作法,筆者會再寫兩篇相關的文章,請拭目以待。

內容

安裝FluentFTP套件

1
dotnet add package FluentFTP

定義相關Model

FTP相關Config

筆者這邊把ftp相關的設定定義成一個ConfigModel,以強型別的方式存取設定值

1
2
3
4
5
6
7
8
9
10
public class FTPConfigBaseModel
{
public string Server { get; set; }
public string Account { get; set; }
public string Password { get; set; }
// Server端的路徑
public string RemotePath { get; set; }
// 本機端的路徑
public string LocalPath { get; set; }
}

FTP上傳統一結構

筆者這邊因為大部分上傳檔案,用意在於跟別的系統交換檔案,檔案內容由資料庫內容運算而得,筆者所在環境又屬於那種崇尚寫Store Procedure,因此不要讓需求太發散,索性定一個統一的結構,是回傳ftp上傳使用的資料則統一使用該資料結構

1
2
3
4
5
6
7
8
9
public class FileUploadResponseModel
{
// 寫入檔案名稱
public string FileName { get; set; }
// 因為每個系統需要的編碼定義都不一樣,因此訂出這個屬性由SP決定
public string Encoding { get; set; }
// 實際寫入內容,筆者這邊設定是一筆資料列
public string DataStr { get; set; }
}

撰寫Service

宣告IFtpService

筆者這邊使用dotnet core預設的Dependency Injection,就注到底了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface IFtpService
{
/// <summary>
/// 依照檔名清單下載檔案
/// </summary>
/// <param name="downloadFtpInfo"></param>
/// <param name="fileList"></param>
void DownloadData(FTPConfigBaseModel downloadFtpInfo, List<string> fileList);
/// <summary>
/// 依照SP回傳的結果上傳檔案
/// </summary>
/// <param name="uploadFtpInfo"></param>
/// <param name="fileUploadResponseModels"></param>
void UploadData(FTPConfigBaseModel uploadFtpInfo, List<FileUploadResponseModel> fileUploadResponseModels);
/// <summary>
/// 檢查ftp上是否有檔案存在
/// </summary>
/// <param name="uploadFtpInfo"></param>
/// <param name="filename"></param>
/// <returns></returns>
bool IsExist(FTPConfigBaseModel uploadFtpInfo, string filename);
}

實作FTPService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public class FtpService : IFtpService
{
private readonly ILoggerManager _logger;
private readonly IFilePathService _filePathService;
private readonly IConfiguration _config;

public FtpService(IFilePathService filePathService
, IConfiguration config, ILoggerManager logger)
{
_filePathService = filePathService;
_config = config;
_logger = logger;
}
public void DownloadData(FTPConfigBaseModel downloadFtpInfo, List<string> fileList)
{
var localDir = _filePathService.GetDirectoryPath(downloadFtpInfo.LocalPath);
var remoteDir = downloadFtpInfo.RemotePath;
FtpClient client = new FtpClient(downloadFtpInfo.Server);
var password = Encoding.UTF8.GetString(Convert.FromBase64String(downloadFtpInfo.Password));
var securePwd = PasswordExtension.SecureStringToString(
PasswordExtension.GetPasswordSecurity(password));
try
{
client.Credentials = new NetworkCredential(downloadFtpInfo.Account, securePwd);
client.Connect();
foreach (var item in fileList)
{
var localPath = Path.Combine(localDir, item);
var remotePath = Path.Combine(remoteDir, item);
if (client.FileExists(remotePath))
{
client.DownloadFile(localPath, remotePath);
}
}
}
catch (Exception exception)
{
throw exception;
}
finally
{
client.Disconnect();
}
}

public void UploadData(FTPConfigBaseModel uploadFtpInfo, List<FileUploadResponseModel> fileUploadResponseModels)
{
// Step0: Data Initial
var localDir = _filePathService.GetDirectoryPath(uploadFtpInfo.LocalPath);
var remoteDir = uploadFtpInfo.RemotePath;
// Step1: Data Arrangement
var fileGroupList = fileUploadResponseModels.GroupBy(x => x.FileName).ToList();
var fileList = new List<string>();
foreach (var item in fileGroupList)
{
var fileContent = item.Select(x => x.DataStr)
.Aggregate((x, y) => $"{x}\r\n{y}");
var encoding = item.FirstOrDefault()?.Encoding;
File.WriteAllText(Path.Combine(localDir, item.Key)
, fileContent, Encoding.GetEncoding(encoding));
fileList.Add(item.Key);
}
// Step2: Upload Data
FtpClient client = new FtpClient(uploadFtpInfo.Server);
var password = Encoding.UTF8.GetString(Convert.FromBase64String(uploadFtpInfo.Password));
var securePwd = PasswordExtension.SecureStringToString(
PasswordExtension.GetPasswordSecurity(password));
try
{
client.Credentials = new NetworkCredential(uploadFtpInfo.Account, securePwd);
client.Connect();
foreach (var item in fileList)
{
var localPath = Path.Combine(localDir, item);
var remotePath = Path.Combine(remoteDir, item);
client.UploadFile(localPath, remotePath, FtpRemoteExists.Overwrite);
}
}
catch (Exception exception)
{
throw exception;
}
finally
{
client.Disconnect();
}
}

public bool IsExist(FTPConfigBaseModel uploadFtpInfo, string filename)
{
FtpClient client = new FtpClient(uploadFtpInfo.Server);
var password = Encoding.UTF8.GetString(Convert.FromBase64String(uploadFtpInfo.Password));
var securePwd = PasswordExtension.SecureStringToString(
PasswordExtension.GetPasswordSecurity(password));
try
{
client.Credentials = new NetworkCredential(uploadFtpInfo.Account, securePwd);
client.Connect();
var remotePath = Path.Combine(uploadFtpInfo.RemotePath, filename);
return client.FileExists(remotePath);
}
catch (Exception exception)
{
throw exception;
}
finally
{
client.Disconnect();
}
}
}

其中IFilePathServicePasswordExtension,筆者會再寫一篇,說來話長,筆者這邊環境需要產出源掃報告才可允許上線,源掃軟體使用Fortify,只要跟System.IO.Path時需要再額外加入判斷才算安全,只要是密碼類型的需要透過SecureString處理過才算安全,因此筆者這邊就索性抽出IFilePathServicePasswordExtension,大家都靠這幾個共用的Service做存取伺服器內部檔案及處理密碼字串。

Client端使用

Startup.cs中宣告

1
services.AddTransient<IFtpService, FtpService>();

終端Service中引用

筆者這邊就不另外示範了,主要是將IFtpService從建構值注入就可以使用了,再則可以透過IConfiguration(若將ftp相關資訊設定於appsettings中)的方式讀取出FTP相關設定,製作成FtpConfigBaseModel的格式,傳進IFtpService中即可呼叫下載、上傳與判斷是否存在。

結論

筆者之前工作都很少透過程式碼存取Ftp,既然這裡有存取需求,實作並記錄一下,成就感十足阿,FluentFTP簡單易用,果然名字上有Fluent都特別好用。