/* * Copyright (c) 2023 Vaughn Nugent * * Library: CMNext * Package: Content.Publishing.Blog.Admin * File: FtpStorageManager.cs * * CMNext is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * CMNext is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ using System; using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using FluentFTP; using FluentFTP.Exceptions; using VNLib.Net.Http; using VNLib.Utils.Logging; using VNLib.Utils.Resources; using VNLib.Plugins; using VNLib.Plugins.Extensions.Loading; namespace Content.Publishing.Blog.Admin.Storage { [ConfigurationName("ftp_config")] internal class FtpStorageManager : StorageBase, IDisposable { private readonly AsyncFtpClient _client; private readonly string _username; private readonly string? _baasePath; protected override string? BasePath => _baasePath; public FtpStorageManager(PluginBase plugin, IConfigScope config) { string? url = config["url"].GetString(); _username = config["username"].GetString() ?? throw new KeyNotFoundException("Missing required username in config"); _baasePath = config["base_path"].GetString(); Uri uri = new (url!); //Init new client _client = new( uri.Host, uri.Port, //Logger in debug mode logger:plugin.IsDebug() ? new FtpDebugLogger(plugin.Log) : null ); } public override async Task ConfigureServiceAsync(PluginBase plugin) { using ISecretResult password = await plugin.GetSecretAsync("ftp_password"); //Init client credentials _client.Credentials = new NetworkCredential(_username, password?.Result.ToString()); _client.Config.EncryptionMode = FtpEncryptionMode.Auto; _client.Config.ValidateAnyCertificate = true; plugin.Log.Information("Connecting to ftp server"); await _client.AutoConnect(CancellationToken.None); plugin.Log.Information("Successfully connected to ftp server"); } /// public override Task DeleteFileAsync(string filePath, CancellationToken cancellation) { return _client.DeleteFile(GetExternalFilePath(filePath), cancellation); } /// public override async Task ReadFileAsync(string filePath, Stream output, CancellationToken cancellation) { try { //Read the file await _client.DownloadStream(output, GetExternalFilePath(filePath), token: cancellation); return output.Position; } catch (FtpMissingObjectException) { //File not found return -1; } } /// public override async Task SetFileAsync(string filePath, Stream data, ContentType ct, CancellationToken cancellation) { //Upload the file to the server FtpStatus status = await _client.UploadStream(data, GetExternalFilePath(filePath), FtpRemoteExists.Overwrite, true, token: cancellation); if (status == FtpStatus.Failed) { throw new ResourceUpdateFailedException($"Failed to update the remote resource {filePath}"); } } /// public override string GetExternalFilePath(string filePath) { return string.IsNullOrWhiteSpace(_baasePath) ? filePath : $"{_baasePath}/{filePath}"; } public void Dispose() { _client?.Dispose(); } sealed record class FtpDebugLogger(ILogProvider Log) : IFtpLogger { void IFtpLogger.Log(FtpLogEntry entry) { Log.Debug("FTP [{lvl}] -> {cnt}", entry.Severity.ToString(), entry.Message); } } } }