FtpEasyTransfer — рабочая служба .NET5 для простой FTP-синхронизации

Я создал простую рабочую службу, которая использует 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 ответ
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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *