aboutsummaryrefslogtreecommitdiff
path: root/back-end/src/Storage/FtpStorageManager.cs
blob: d775d544a6618bcf2e2a96408f27bc5d30dca65b (plain)
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/*
* Copyright (c) 2024 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 FluentFTP;
using FluentFTP.Exceptions;

using VNLib.Utils.Logging;
using VNLib.Utils.Resources;
using VNLib.Plugins;
using VNLib.Plugins.Extensions.Loading;

namespace Content.Publishing.Blog.Admin.Storage
{

    [ConfigurationName("storage")]
    internal class FtpStorageManager : StorageBase, IDisposable
    {
        private readonly AsyncFtpClient _client;
        private readonly S3Config _storageConf;

        protected override string? BasePath => _storageConf.BaseBucket;

        public FtpStorageManager(PluginBase plugin, IConfigScope config)
        {
            _storageConf = config.Deserialze<S3Config>();

            Uri uri = new (_storageConf.ServerAddress!);

            //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.Secrets().GetSecretAsync("storage_secret");

            //Init client credentials
            _client.Credentials = new NetworkCredential(_storageConf.ClientId, password?.Result.ToString());

            //If the user forces ssl, then assume it's an implicit connection and force certificate checking
            if(_storageConf.UseSsl == true)
            {
                _client.Config.EncryptionMode = FtpEncryptionMode.Implicit;
            }
            else
            {
                _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");
        }


        ///<inheritdoc/>
        public override Task DeleteFileAsync(string filePath, CancellationToken cancellation)
        {
            return _client.DeleteFile(GetExternalFilePath(filePath), cancellation);
        }

        ///<inheritdoc/>
        public override async Task<long> 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;
            }
        }

        ///<inheritdoc/>
        public override async Task WriteFileAsync(string filePath, Stream data, string 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}");
            }
        }

        ///<inheritdoc/>
        public override string GetExternalFilePath(string filePath)
        {
            return string.IsNullOrWhiteSpace(BasePath) ? filePath : $"{BasePath}/{filePath}";
        }

        public void Dispose()
        {
            _client?.Dispose();
        }

        sealed class FtpDebugLogger(ILogProvider Log) : IFtpLogger
        {
            void IFtpLogger.Log(FtpLogEntry entry)
            {
                Log.Debug("FTP [{lvl}] -> {cnt}", entry.Severity.ToString(), entry.Message);
            }
        }

    }
}