Я создал простую рабочую службу, которая использует FluentFTP для синхронизации файлов или каталогов от одного ftp-клиента к другому или просто к локальной машине, в зависимости от того, как настроен appsettings.json. В целом я очень доволен кодом, но в некоторых моментах я считаю, что код повторяется или выглядит немного неаккуратно. Это первый микросервис, который я закончил, мне бы хотелось поделиться некоторыми мыслями о том, как я могу его улучшить.
public class FtpWorker : IFtpWorker
{
private readonly ILogger<FtpWorker> _logger;
private TransferSettingsOptions _options;
private string _localDirectory;
public FtpWorker(ILogger<FtpWorker> logger)
{
_logger = logger;
}
public async Task RunAsync(TransferSettingsOptions options)
{
_options = options;
_localDirectory = _options.LocalPath;
if (!_options.LocalPathIsFile)
{
Directory.CreateDirectory(_options.LocalPath);
}
switch (DetermineRunMode())
{
case RunMode.DownloadDir:
await RunDownloadDirAsync();
break;
case RunMode.DownloadFile:
await RunDownloadFileAsync();
break;
case RunMode.UploadDir:
await RunUploadDirAsync();
break;
case RunMode.UploadFile:
await RunUploadFileAsync();
break;
case RunMode.SyncDirs:
await RunSyncDirsAsync();
break;
case RunMode.SyncFile:
await RunSyncFileAsync();
break;
default:
break;
};
}
private async Task RunDownloadDirAsync()
{
if (_options.Source is not null && !string.IsNullOrWhiteSpace(_options.Source.Server))
{
try
{
await DownloadDirectoryFromSourceAsync();
}
catch (Exception ex)
{
_logger.LogError("Exception in RunDownloadDirAsync: {Message}", ex.Message);
}
}
}
private async Task RunDownloadFileAsync()
{
if (_options.Source is not null && !string.IsNullOrWhiteSpace(_options.Source.Server))
{
try
{
await DownloadFileFromSourceAsync();
}
catch (Exception ex)
{
_logger.LogError("Exception in RunDownloadDirAsync: {Message}", ex.Message);
}
}
}
private async Task RunUploadDirAsync()
{
throw new NotImplementedException();
}
private async Task RunUploadFileAsync()
{
if (_options.Destination != null && !string.IsNullOrWhiteSpace(_options.Destination.Server))
{
try
{
await UploadFileToDestinationAsync();
}
catch (Exception ex)
{
_logger.LogError("Exception in RunUploadFileAsync: {Message}", ex.Message);
}
}
else
{
_logger.LogError("Destination or DestinationServer empty in RunUploadFile");
}
}
private async Task RunSyncDirsAsync()
{
if (_options.Source != null || !string.IsNullOrWhiteSpace(_options.Source.Server))
{
try
{
await DownloadDirectoryFromSourceAsync();
}
catch (Exception ex)
{
_logger.LogError("Exception in DownloadFromSource: {Message}", ex.Message);
}
}
else
{
_logger.LogDebug("No source configured.");
}
foreach (var opt in _options.ChangeExtensions)
{
ChangeFileExtensions(opt);
}
if (_options.Destination != null || !string.IsNullOrWhiteSpace(_options.Destination.Server))
{
try
{
await UploadDirectoryToDestinationAsync();
}
catch (Exception ex)
{
_logger.LogError("Exception in UploadToDestination: {Message}", ex.Message);
}
}
else
{
_logger.LogDebug("No destination configured.");
}
}
private Task RunSyncFileAsync()
{
throw new NotImplementedException();
}
private async Task<List<FtpResult>> DownloadDirectoryFromSourceAsync()
{
var token = new CancellationToken();
using (var ftp = new FtpClient(_options.Source.Server, _options.Source.Port, _options.Source.User, _options.Source.Password))
{
ftp.OnLogEvent += Log;
await ftp.ConnectAsync(token);
var rules = new List<FtpRule>
{
new FtpFileExtensionRule(true, _options.Source.FileTypesToDownload)
};
var results = await ftp.DownloadDirectoryAsync(_options.LocalPath, _options.Source.RemotePath, FtpFolderSyncMode.Update,
FtpLocalExists.Skip, FtpVerify.None, rules);
if (_options.Source.DeleteOnceDownloaded)
{
foreach (var download in results)
{
if (download.IsSuccess && download.Type == FtpFileSystemObjectType.File)
{
await ftp.DeleteFileAsync(download.RemotePath);
}
}
}
foreach (var download in results)
{
if (download.IsFailed)
{
_logger.LogWarning("Download of {Name} failed: {Exception}", download.Name, download.Exception);
}
}
return results;
}
}
private async Task<FtpStatus> DownloadFileFromSourceAsync()
{
var token = new CancellationToken();
using (var ftp = new FtpClient(_options.Source.Server, _options.Source.Port, _options.Source.User, _options.Source.Password))
{
ftp.OnLogEvent += Log;
await ftp.ConnectAsync(token);
var overwriteExisting = _options.Source.OverwriteExisting ? FtpLocalExists.Overwrite : FtpLocalExists.Skip;
string localPath = _options.Destination.RemotePath;
if (!_options.LocalPathIsFile)
{
var fileName = Path.GetFileName(_options.Source.RemotePath);
localPath = $"{_options.LocalPath}/{fileName}";
}
var result = await ftp.DownloadFileAsync(localPath, _options.Source.RemotePath, overwriteExisting);
if (_options.Source.DeleteOnceDownloaded)
{
if (result.IsSuccess())
{
try
{
await ftp.DeleteFileAsync(_options.Source.RemotePath, token);
}
catch (Exception ex)
{
_logger.LogWarning("Error deleting {RemotePath}: {Message}", _options.Source.RemotePath, ex.Message);
}
}
}
return result;
}
}
private void ChangeFileExtensions(ChangeExtensionsOptions options)
{
foreach (var file in Directory.GetFiles(_localDirectory, $"*.{options.Source}"))
{
var newFileName = @$"{_localDirectory}{Path.GetFileNameWithoutExtension(file)}.{options.Target}";
try
{
File.Move(file, newFileName, true);
}
catch (Exception ex)
{
_logger.LogWarning("Moving file {file} failed: {Message}", file, ex.Message);
}
}
}
private async Task<FtpStatus> UploadFileToDestinationAsync()
{
var token = new CancellationToken();
using (var ftp = new FtpClient(_options.Destination.Server, _options.Destination.Port, _options.Destination.User, _options.Destination.Password))
{
ftp.OnLogEvent += Log;
await ftp.ConnectAsync(token);
var overwriteExisting = _options.Destination.OverwriteExisting ? FtpRemoteExists.Overwrite : FtpRemoteExists.Skip;
string remotePath = _options.Destination.RemotePath;
if (!_options.Destination.RemotePathIsFile)
{
var fileName = Path.GetFileName(_options.LocalPath);
remotePath = $"{_options.Destination.RemotePath}/{fileName}";
}
var result = await ftp.UploadFileAsync(_options.LocalPath, remotePath, overwriteExisting);
if (_options.Destination.DeleteOnceUploaded)
{
if (result.IsSuccess())
{
try
{
if (_options.LocalPathIsFile)
{
File.Delete(_options.LocalPath);
}
}
catch (Exception ex)
{
_logger.LogWarning("Error deleting {LocalPath}: {Message}", _options.LocalPath, ex.Message);
}
}
}
return result;
};
}
private async Task<List<FtpResult>> UploadDirectoryToDestinationAsync()
{
var token = new CancellationToken();
using (var ftp = new FtpClient(_options.Destination.Server, _options.Destination.Port, _options.Destination.User, _options.Destination.Password))
{
ftp.OnLogEvent += Log;
await ftp.ConnectAsync(token);
var results = await ftp.UploadDirectoryAsync(_options.LocalPath, _options.Destination.RemotePath, FtpFolderSyncMode.Update,
FtpRemoteExists.Skip, FtpVerify.None);
if (_options.Destination.DeleteOnceUploaded)
{
foreach (var upload in results)
{
if (upload.IsSuccess)
{
try
{
File.Delete(upload.LocalPath);
_logger.LogInformation("File deleted: {LocalPath}", upload.LocalPath);
}
catch (Exception ex)
{
_logger.LogWarning("Error deleting file {LocalPath}: {Message}", upload.LocalPath, ex.Message);
}
}
}
}
foreach (var upload in results)
{
if (upload.IsFailed)
{
_logger.LogWarning("Upload of {LocalPath} failed: {Exception}", upload.LocalPath, upload.Exception);
}
}
return results;
}
}
private RunMode DetermineRunMode()
{
if (_options.LocalPathIsFile)
{
_logger.LogDebug("Local Path: {LocalPath} is file, RunMode determined as UploadFile", _options.LocalPath);
return RunMode.UploadFile;
}
else if (_options.Source is not null && _options.Destination is not null)
{
if (_options.Source.RemotePathIsFile)
{
_logger.LogDebug("Source & Destination defined, Source.RemotePath: {RemotePath} is file, RunMode determined as SyncFile", _options.Source.RemotePath);
return RunMode.SyncFile;
}
else
{
_logger.LogDebug("Source & Destination defined, Source.RemotePath: {RemotePath} is directory, RunMode determined as SyncDirs", _options.Source.RemotePath);
return RunMode.SyncDirs;
}
}
else if (_options.Source is null && _options.Destination is not null)
{
if (_options.Destination.RemotePathIsFile)
{
_logger.LogDebug("Only Destination defined, Destination.RemotePath: {RemotePath} is file, RunMode determined as UploadFile", _options.Destination.RemotePath);
return RunMode.UploadFile;
}
else
{
_logger.LogDebug("Only Destination defined, Destination.RemotePath: {RemotePath} is directory, RunMode determined as UploadDir", _options.Destination.RemotePath);
return RunMode.UploadDir;
}
}
else
{
if (_options.Source.RemotePathIsFile)
{
_logger.LogDebug("Only Source defined, Source.RemotePath: {RemotePath} is file, RunMode determined as DownloadFile", _options.Source.RemotePath);
return RunMode.DownloadFile;
}
else
{
_logger.LogDebug("Only Source defined, Source.RemotePath: {RemotePath} is directory, RunMode determined as DownloadDir", _options.Source.RemotePath);
return RunMode.DownloadDir;
}
}
}
Мне любопытно, есть ли более простой способ справиться с операторами using, поскольку я, кажется, каждый раз передаю параметры одинаково, есть ли лучший способ?
1 ответ
Быстрые замечания:
Я замечаю, что вы всегда используете
ex.Message. Однако что, если у вас естьInnerException? Я бы порекомендовал подход так (Я скопировал код метода ниже; обратите внимание, что вы можете адаптироватьstring.Joinна свой вкус, конечно). Кроме того, я также рекомендую регистрировать всю трассировку стека, если вы столкнетесь с исключением, когда сообщение не говорит вам достаточно.public static string Execute(Exception exc) { var messages = new List<string>(); do { messages.Add(exc.Message); exc = exc.InnerException; } while (exc != null); return string.Join(" - ", messages); }В нескольких местах вы используете
new FtpClient(_options.Source.Server, _options.Source.Port, _options.Source.User, _options.Source.Password). Переместите это в метод и вызовите этот метод. То же самое сnew FtpClient(_options.Destination.Server, _options.Destination.Port, _options.Destination.User, _options.Destination.Password).На самом деле, если
_options.Sourceи_options.Destinationотносятся к тому же типу (чего я ожидал, но вы не опубликовали этот класс), я бы порекомендовал метод, который принимает этот класс в качестве параметра и возвращаетFtpClient.DetermineRunMode()для меня это слишком шумно, однообразно и неизящно. Я бы предпочел подход, в котором вы определяли бы различные факторы (например, оба лиSourceиDestinationопределены) и в конце компилировать сообщение, напримерvar sourceIsNotNull = _options.Source is not null; var destinationIsNotNull = _options.Destination is not null; var message = (sourceIsNotNull && destinationIsNotNull) ? "Source & Destination defined" : sourceIsNotNull ? "Only Source defined" : "Only Destination defined";Возможно, вы могли бы иметь метод для каждого «фактора», например, который определен, что
RunModeесть лиRemotePathэто файл или папка, …, возможно, даже переместите все это в отдельный класс (называемыйRunModeRetrieverили подобное).(Кроме того, учитывая, что вы делаете
return, Я не думаю, что все этиelses даже необходимы.)Вы слишком много проверяете в самих методах. В
RunDownloadDirAsyncты уже знаешь это_options.Source is not null(потому что это провереноDetermineRunModeиRunDownloadDirAsyncвызывается из-за результата вызова этого метода), поэтому нет необходимости проявлять особую осторожность: это только добавляет шума к вашей логике. Вы даже не последовательны: вы регистрируете_logger.LogError("Destination or DestinationServer empty in RunUploadFile");, но нет эквивалента дляstring.IsNullOrWhiteSpace(_options.Source.Server).Вы должны переместить все эти проверки перед выполнением
DetermineRunMode, возможно, даже переосмыслить эту логику в классе «проверки данных», который рассматриваетTransferSettingsOptions, проверяет наличие всех необходимых данных, возвращает «сбой», если отсутствуют данные в сочетании с отчетом о том, что отсутствует, и возвращает «успех», когда все в порядке, и, возможно, также определяетRunModeделая все эти проверки.У каждого метода есть
try...catch. Почему бы не поставитьtry...catchвокругswitch (DetermineRunMode())вместо? Просто не забудьте включитьRunModeкогда вы регистрируете любые возможныеException.

Спасибо, метод расширения для исключений очень полезен, я даже не подумал об этом, и я знал, что есть более простой способ настроить операторы using, еще раз спасибо. Что касается ваших комментариев к
DetermineRunMode(), Я не уверен, что понимаю, как предложенные вами изменения улучшат ситуацию, разве добавление кучи встроенных условных выражений не только еще больше запутывает ситуацию?— Гэри МакГрегор-Манци
Ах, на самом деле я вижу, избыточные вызовы, вы просто помещаете логические значения в переменные, чтобы им не нужно было идти
if source is nullтогда в следующий разif source is not null, понятно!— Гэри МакГрегор-Манци
@ GaryMacGregor-Manzi Это не только избыточность вызовов, но и постоянное переключение функций: вы проверяете данные, затем определяете RunMode, затем вы вызываете определенный метод, основанный на RunMode, который затем выполняет дополнительную проверку данных. Сгруппируйте функциональность: сначала проверьте данные, потому что нет смысла выполнять остальные действия, если они неполные. Затем определите RunMode, затем выполните соответствующий метод.
— BCdotWEB