/* * Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: Emails.Transactional * File: MinioStorage.cs * * MinioStorage.cs is part of Emails.Transactional which is part of the larger * VNLib collection of libraries and utilities. * * Emails.Transactional is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 2 of the License, * or (at your option) any later version. * * Emails.Transactional 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Emails.Transactional. If not, see http://www.gnu.org/licenses/. */ using System.IO; using System.Threading; using System.Threading.Tasks; using VNLib.Utils.IO; using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins; using VNLib.Plugins.Extensions.Loading; using Minio; using Minio.Handlers; using Minio.DataModel; using Minio.DataModel.Args; using Minio.DataModel.Tracing; namespace Emails.Transactional { [ConfigurationName("s3_config")] internal sealed class MinioStorage : ISimpleFilesystem, IAsyncConfigurable { private readonly MinioClient Client; private readonly S3Config Config; public MinioStorage(PluginBase plugin, IConfigScope s3Config) { //Deserialize the config Config = s3Config.Deserialze(); Client = new(); Client.WithEndpoint(Config.ServerAddress) .WithSSL(Config.UseSsl.HasValue && Config.UseSsl.Value) .WithTimeout(10 * 1000); //Accept optional region if (!string.IsNullOrWhiteSpace(Config.Region)) { Client.WithRegion(Config.Region); } //Setup debug trace if (plugin.IsDebug()) { Client.SetTraceOn(new ReqLogger(plugin.Log)); } } public string GetExternalFilePath(string filePath) { string path = Path.Combine(Config.BaseBucket!, filePath); //force forward only slashes return path.Replace('\\', '/'); } /// public async Task ConfigureServiceAsync(PluginBase plugin) { using ISecretResult? secret = await plugin.GetSecretAsync("s3_secret"); //Build client Client.WithCredentials(Config.ClientId, secret.Result.ToString()) .Build(); } /// public Task DeleteFileAsync(string filePath, CancellationToken cancellation) { string fullPath = GetExternalFilePath(filePath); RemoveObjectArgs args = new(); args.WithBucket(Config.BaseBucket) .WithObject(fullPath); //Remove the object return Client.RemoveObjectAsync(args, cancellation); } /// public Task WriteFileAsync(string filePath, Stream data, string ct, CancellationToken cancellation) { string fullPath = GetExternalFilePath(filePath); PutObjectArgs args = new(); args.WithBucket(Config.BaseBucket) .WithContentType(ct) .WithObject(fullPath) .WithObjectSize(data.Length) .WithStreamData(data); //Upload the object return Client.PutObjectAsync(args, cancellation); } /// public async Task ReadFileAsync(string filePath, Stream output, CancellationToken cancellation) { string fullPath = GetExternalFilePath(filePath); //Get the item GetObjectArgs args = new(); args.WithBucket(Config.BaseBucket) .WithObject(fullPath) .WithCallbackStream(async (stream, cancellation) => { //Read the objec to memory await stream.CopyToAsync(output, 4096, MemoryUtil.Shared, cancellation); }); try { //Get the post content file ObjectStat stat = await Client.GetObjectAsync(args, cancellation); } catch (Minio.Exceptions.ObjectNotFoundException) { //File not found return -1; } return output.Position; } internal class ReqLogger : IRequestLogger { private readonly ILogProvider logProvider; public ReqLogger(ILogProvider log) { logProvider = log; } public void LogRequest(RequestToLog requestToLog, ResponseToLog responseToLog, double durationMs) { logProvider.Debug("S3 result\n{method} {uri} HTTP {ms}ms\nHTTP {status} {message}\n{content}", requestToLog.Method, requestToLog.Resource, durationMs, responseToLog.StatusCode, responseToLog.ErrorMessage, responseToLog.Content ); } } } }