From 23ae18d38c765c0355fe798aa2c4d26d9d696d21 Mon Sep 17 00:00:00 2001 From: joaofx Date: Thu, 18 Jul 2024 00:35:12 +0200 Subject: [PATCH] updated libs and added storage helpers --- .../Corpo.Skeleton.PageTests.csproj | 6 +- .../Corpo.Skeleton.Tests.csproj | 4 +- .../src/Playground/Features/Jobs/JobNew.cs | 16 +- .../Playground.PageTests.csproj | 4 +- .../Playground.Tests/Playground.Tests.csproj | 4 +- src/Miru.Core/Miru.Core.csproj | 4 +- .../Corpo.Skeleton.PageTests.csproj.stub | 6 +- .../Corpo.Skeleton.Tests.csproj.stub | 4 +- src/Miru.Core/Yml.cs | 12 +- src/Miru.Fabrication/Miru.Fabrication.csproj | 2 +- .../Miru.PageTesting.Chrome.csproj | 2 +- src/Miru.PageTesting/Miru.PageTesting.csproj | 4 +- src/Miru.Postgres/Miru.Postgres.csproj | 2 +- src/Miru.SqlServer/Miru.SqlServer.csproj | 4 +- src/Miru.Sqlite/Miru.Sqlite.csproj | 4 +- src/Miru.Storage.Ftp/Miru.Storage.Ftp.csproj | 2 +- src/Miru.Testing/Miru.Testing.csproj | 10 +- .../StorageTestServiceCollectionExtensions.cs | 4 +- src/Miru/Miru.csproj | 30 +-- src/Miru/Queuing/AppQueueRunConsolable.cs | 61 ++++++ src/Miru/Queuing/IAppWarmup.cs | 6 + src/Miru/Queuing/NullAppWarmup.cs | 8 + src/Miru/Storages/Attachment.cs | 61 ++++++ src/Miru/Storages/AttachmentInterceptor.cs | 58 +++++ src/Miru/Storages/MiruPathExtensions.cs | 49 +++++ src/Miru/Storages/Storage2Extensions.cs | 204 ++++++++++++++++++ .../Miru.PageTesting.Tests.csproj | 8 +- .../Miru.Postgres.Tests.csproj | 4 +- tests/Miru.Tests/Miru.Tests.csproj | 6 +- tests/Miru.Tests/Storages/AttachmentTest.cs | 66 ++++++ 30 files changed, 579 insertions(+), 76 deletions(-) create mode 100644 src/Miru/Queuing/AppQueueRunConsolable.cs create mode 100644 src/Miru/Queuing/IAppWarmup.cs create mode 100644 src/Miru/Queuing/NullAppWarmup.cs create mode 100644 src/Miru/Storages/Attachment.cs create mode 100644 src/Miru/Storages/AttachmentInterceptor.cs create mode 100644 src/Miru/Storages/MiruPathExtensions.cs create mode 100644 src/Miru/Storages/Storage2Extensions.cs create mode 100644 tests/Miru.Tests/Storages/AttachmentTest.cs diff --git a/samples/Corpo.Skeleton/tests/Corpo.Skeleton.PageTests/Corpo.Skeleton.PageTests.csproj b/samples/Corpo.Skeleton/tests/Corpo.Skeleton.PageTests/Corpo.Skeleton.PageTests.csproj index e6e3f592..327b1b31 100644 --- a/samples/Corpo.Skeleton/tests/Corpo.Skeleton.PageTests/Corpo.Skeleton.PageTests.csproj +++ b/samples/Corpo.Skeleton/tests/Corpo.Skeleton.PageTests/Corpo.Skeleton.PageTests.csproj @@ -9,10 +9,10 @@ - - + + - + diff --git a/samples/Corpo.Skeleton/tests/Corpo.Skeleton.Tests/Corpo.Skeleton.Tests.csproj b/samples/Corpo.Skeleton/tests/Corpo.Skeleton.Tests/Corpo.Skeleton.Tests.csproj index dd8aa7cb..5f805bbd 100644 --- a/samples/Corpo.Skeleton/tests/Corpo.Skeleton.Tests/Corpo.Skeleton.Tests.csproj +++ b/samples/Corpo.Skeleton/tests/Corpo.Skeleton.Tests/Corpo.Skeleton.Tests.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/samples/Playground/src/Playground/Features/Jobs/JobNew.cs b/samples/Playground/src/Playground/Features/Jobs/JobNew.cs index aa56614f..2033ae50 100644 --- a/samples/Playground/src/Playground/Features/Jobs/JobNew.cs +++ b/samples/Playground/src/Playground/Features/Jobs/JobNew.cs @@ -62,22 +62,14 @@ public class NameNewJob : IRequest public string Name { get; set; } } - public class JobHandler : IRequestHandler + public class JobHandler(ILogger logger, PlaygroundDbContext db) + : IRequestHandler { - private readonly ILogger _logger; - private readonly PlaygroundDbContext _db; - - public JobHandler(ILogger logger, PlaygroundDbContext db) - { - _logger = logger; - _db = db; - } - public async Task Handle(NameNewJob request, CancellationToken cancellationToken) { - _db.Users.ToList(); + db.Users.ToList(); - _logger.LogInformation("From ILogger {Name}", request.Name); + logger.LogInformation("From ILogger {Name}", request.Name); App.Log.Information("Name is {Name}", request.Name); diff --git a/samples/Playground/tests/Playground.PageTests/Playground.PageTests.csproj b/samples/Playground/tests/Playground.PageTests/Playground.PageTests.csproj index 889ab753..bb8d9735 100644 --- a/samples/Playground/tests/Playground.PageTests/Playground.PageTests.csproj +++ b/samples/Playground/tests/Playground.PageTests/Playground.PageTests.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/samples/Playground/tests/Playground.Tests/Playground.Tests.csproj b/samples/Playground/tests/Playground.Tests/Playground.Tests.csproj index cd3e1cdb..d4ff71e4 100644 --- a/samples/Playground/tests/Playground.Tests/Playground.Tests.csproj +++ b/samples/Playground/tests/Playground.Tests/Playground.Tests.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/src/Miru.Core/Miru.Core.csproj b/src/Miru.Core/Miru.Core.csproj index 9bbfc448..8138a44b 100644 --- a/src/Miru.Core/Miru.Core.csproj +++ b/src/Miru.Core/Miru.Core.csproj @@ -7,10 +7,10 @@ - + - + diff --git a/src/Miru.Core/Templates/Corpo.Skeleton.PageTests.csproj.stub b/src/Miru.Core/Templates/Corpo.Skeleton.PageTests.csproj.stub index 2e747b65..5be7660a 100644 --- a/src/Miru.Core/Templates/Corpo.Skeleton.PageTests.csproj.stub +++ b/src/Miru.Core/Templates/Corpo.Skeleton.PageTests.csproj.stub @@ -9,10 +9,10 @@ - - + + - + diff --git a/src/Miru.Core/Templates/Corpo.Skeleton.Tests.csproj.stub b/src/Miru.Core/Templates/Corpo.Skeleton.Tests.csproj.stub index c13233f0..ffb9685a 100644 --- a/src/Miru.Core/Templates/Corpo.Skeleton.Tests.csproj.stub +++ b/src/Miru.Core/Templates/Corpo.Skeleton.Tests.csproj.stub @@ -6,8 +6,8 @@ - - + + diff --git a/src/Miru.Core/Yml.cs b/src/Miru.Core/Yml.cs index e393b5b8..feaa5049 100644 --- a/src/Miru.Core/Yml.cs +++ b/src/Miru.Core/Yml.cs @@ -50,18 +50,16 @@ public static string ToYml(T value) => public static T FromYml(string content) => Deserializer.Value.Deserialize(content); - public class FilterPropertiesInspector : TypeInspectorSkeleton + public class FilterPropertiesInspector(ITypeInspector innerTypeDescriptor) : TypeInspectorSkeleton { - private readonly ITypeInspector _innerTypeDescriptor; + public override string GetEnumName(Type enumType, string name) => + innerTypeDescriptor.GetEnumName(enumType, name); - public FilterPropertiesInspector(ITypeInspector innerTypeDescriptor) - { - _innerTypeDescriptor = innerTypeDescriptor; - } + public override string GetEnumValue(object enumValue) => innerTypeDescriptor.GetEnumValue(enumValue); public override IEnumerable GetProperties(Type type, object container) { - var properties = _innerTypeDescriptor.GetProperties(type, container) + var properties = innerTypeDescriptor.GetProperties(type, container) .Where(p => !p.Name.ContainsNoCase("password")) .Where(p => !(p.Name.ContainsNoCase("body") && type.FullName.Equals("Miru.Mailing.Email"))); diff --git a/src/Miru.Fabrication/Miru.Fabrication.csproj b/src/Miru.Fabrication/Miru.Fabrication.csproj index 27017d7f..37f55954 100644 --- a/src/Miru.Fabrication/Miru.Fabrication.csproj +++ b/src/Miru.Fabrication/Miru.Fabrication.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Miru.PageTesting.Chrome/Miru.PageTesting.Chrome.csproj b/src/Miru.PageTesting.Chrome/Miru.PageTesting.Chrome.csproj index 4b610aba..39860dff 100644 --- a/src/Miru.PageTesting.Chrome/Miru.PageTesting.Chrome.csproj +++ b/src/Miru.PageTesting.Chrome/Miru.PageTesting.Chrome.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Miru.PageTesting/Miru.PageTesting.csproj b/src/Miru.PageTesting/Miru.PageTesting.csproj index a97e6637..93c949ce 100644 --- a/src/Miru.PageTesting/Miru.PageTesting.csproj +++ b/src/Miru.PageTesting/Miru.PageTesting.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/src/Miru.Postgres/Miru.Postgres.csproj b/src/Miru.Postgres/Miru.Postgres.csproj index 170f87ba..6976dccd 100644 --- a/src/Miru.Postgres/Miru.Postgres.csproj +++ b/src/Miru.Postgres/Miru.Postgres.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Miru.SqlServer/Miru.SqlServer.csproj b/src/Miru.SqlServer/Miru.SqlServer.csproj index 1f090929..f11a1d7d 100644 --- a/src/Miru.SqlServer/Miru.SqlServer.csproj +++ b/src/Miru.SqlServer/Miru.SqlServer.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/Miru.Sqlite/Miru.Sqlite.csproj b/src/Miru.Sqlite/Miru.Sqlite.csproj index 10e3143b..a64e471c 100644 --- a/src/Miru.Sqlite/Miru.Sqlite.csproj +++ b/src/Miru.Sqlite/Miru.Sqlite.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/src/Miru.Storage.Ftp/Miru.Storage.Ftp.csproj b/src/Miru.Storage.Ftp/Miru.Storage.Ftp.csproj index 7a776252..ee1ace79 100644 --- a/src/Miru.Storage.Ftp/Miru.Storage.Ftp.csproj +++ b/src/Miru.Storage.Ftp/Miru.Storage.Ftp.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Miru.Testing/Miru.Testing.csproj b/src/Miru.Testing/Miru.Testing.csproj index b7c14fb7..0710c9fb 100644 --- a/src/Miru.Testing/Miru.Testing.csproj +++ b/src/Miru.Testing/Miru.Testing.csproj @@ -15,11 +15,11 @@ - - - - - + + + + + diff --git a/src/Miru.Testing/StorageTestServiceCollectionExtensions.cs b/src/Miru.Testing/StorageTestServiceCollectionExtensions.cs index 18ad51ba..f842182c 100644 --- a/src/Miru.Testing/StorageTestServiceCollectionExtensions.cs +++ b/src/Miru.Testing/StorageTestServiceCollectionExtensions.cs @@ -158,11 +158,11 @@ public static void ShouldNotContain(this MiruPath fileName, params string[] line } } - public static MiruPath MakeFake(this MiruPath path) + public static MiruPath MakeFake(this MiruPath path, int letters = 16) { path.Dir().EnsureDirExist(); - File.WriteAllText(path, new Faker().Lorem.Sentence()); + File.WriteAllText(path, new Faker().Lorem.Letter(letters)); return path; } diff --git a/src/Miru/Miru.csproj b/src/Miru/Miru.csproj index e972610f..9730fabb 100644 --- a/src/Miru/Miru.csproj +++ b/src/Miru/Miru.csproj @@ -11,24 +11,24 @@ - + - - - - - + + + + + - - - + + + @@ -39,18 +39,18 @@ - - + + - - + + - - + + diff --git a/src/Miru/Queuing/AppQueueRunConsolable.cs b/src/Miru/Queuing/AppQueueRunConsolable.cs new file mode 100644 index 00000000..16163922 --- /dev/null +++ b/src/Miru/Queuing/AppQueueRunConsolable.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.CommandLine; +using System.Linq; +using Hangfire; +using Miru.Consolables; + +namespace Miru.Queuing; + +// TODO: add automated tests +public class AppQueueRunConsolable : Consolable +{ + public AppQueueRunConsolable() : base("app.queue.run", "Run queue") + { + Add(new Option(["--queues", "--queue", "-q"]) + { + AllowMultipleArgumentsPerToken = true + }); + } + + public class ConsolableHandler(IAppWarmup appWarmup, JobStorage jobStorage) + : IConsolableHandler + { + public string[] Queues { get; set; } = { "default:20" }; + + public async Task Execute() + { + appWarmup.InitiateServices(); + + // miru app.queue.run --queues default:15 default,exports:5 scheduled:1 + var servers = new List(); + + foreach (var queue in Queues) + { + var split = queue.Split(':'); + var queueNames = split[0].Split(',').ToArray(); + var workers = split[1]; + + var options = new BackgroundJobServerOptions + { + Queues = queueNames, + WorkerCount = workers.ToInt() + }; + + Console2.WhiteLine($"Adding a queue processor for {split[0]} queues with {workers} workers"); + + servers.Add(new BackgroundJobServer(options, jobStorage)); + } + + var tasks = new List(); + + foreach (var server in servers) + tasks.Add(server.WaitForShutdownAsync(default)); + + Console2.GreenLine("Queues are running"); + + Task.WaitAll(tasks.ToArray()); + + await Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Miru/Queuing/IAppWarmup.cs b/src/Miru/Queuing/IAppWarmup.cs new file mode 100644 index 00000000..eab2f2b7 --- /dev/null +++ b/src/Miru/Queuing/IAppWarmup.cs @@ -0,0 +1,6 @@ +namespace Miru.Queuing; + +public interface IAppWarmup +{ + void InitiateServices(); +} \ No newline at end of file diff --git a/src/Miru/Queuing/NullAppWarmup.cs b/src/Miru/Queuing/NullAppWarmup.cs new file mode 100644 index 00000000..763a6fea --- /dev/null +++ b/src/Miru/Queuing/NullAppWarmup.cs @@ -0,0 +1,8 @@ +namespace Miru.Queuing; + +public class NullAppWarmup : IAppWarmup +{ + public void InitiateServices() + { + } +} \ No newline at end of file diff --git a/src/Miru/Storages/Attachment.cs b/src/Miru/Storages/Attachment.cs new file mode 100644 index 00000000..46949f65 --- /dev/null +++ b/src/Miru/Storages/Attachment.cs @@ -0,0 +1,61 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.IO; +using Microsoft.AspNetCore.Http; +using Miru.Behaviors.TimeStamp; +using Miru.Domain; + +namespace Miru.Storages; + +[Table("Attachments")] +public class Attachment : + Entity, + ITimeStamped +{ + public string Key { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + public long ByteSize { get; set; } + public string Checksum { get; set; } + + public string Entity { get; set; } + public string Property { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + + [NotMapped] + public MiruPath FullPath { get; set; } + + public Attachment() + { + } + + public Attachment(IStorage storage, MiruPath filePath) + { + Key = filePath.RelativeTo(storage); + FileName = filePath.FileName(); + ByteSize = filePath.FileSize(); + ContentType = filePath.ContentType(); + Checksum = filePath.GetMd5(); + } + + public Attachment(MiruPath filePath, Stream stream, IFormFile formFile, MiruPath fullPath) + { + // In LocalDisk storage it is the path in the storage + // Key is the value that point where the file is + Key = filePath; + FileName = Path.GetFileName(filePath); + ByteSize = stream.Length; + ContentType = formFile.ContentType; + Checksum = fullPath.GetMd5(); + FullPath = fullPath; + } + + public void UpdateFileMetadata(Attachment attachment) + { + ByteSize = attachment.ByteSize; + ContentType = attachment.ContentType; + Checksum = attachment.Checksum; + UpdatedAt = App.Now(); + } +} \ No newline at end of file diff --git a/src/Miru/Storages/AttachmentInterceptor.cs b/src/Miru/Storages/AttachmentInterceptor.cs new file mode 100644 index 00000000..73e6bb7e --- /dev/null +++ b/src/Miru/Storages/AttachmentInterceptor.cs @@ -0,0 +1,58 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Miru.Storages; + +public class AttachmentInterceptor : SaveChangesInterceptor +{ + public override ValueTask> SavingChangesAsync( + DbContextEventData @event, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + var entries = @event.Context?.ChangeTracker.Entries(); + + var modifiedEntries = entries + .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified); + + foreach (var entry in modifiedEntries) + { + var attachmentIdProperties = entry.Properties + .Where(p => + { + var foreignKeys = p.Metadata.GetContainingForeignKeys(); + + foreach (var foreignKey in foreignKeys) + if (foreignKey.PrincipalEntityType.ClrType == typeof(Attachment)) + return true; + + return false; + }) + .ToList(); + + foreach (var attachmentIdProperty in attachmentIdProperties) + { + var attachmentId = attachmentIdProperty.CurrentValue; + + // search for the Attachment in the entries of current session + var attachmentEntry = entries + .FirstOrDefault(x => x.Properties.Any(p => + x.Metadata.ClrType == typeof(Attachment) && + p.Metadata.Name == "Id" && Convert.ToInt64(p.CurrentValue) == Convert.ToInt64(attachmentId))); + + if (attachmentEntry != null && attachmentEntry.Entity is Attachment attachment) + { + attachment.Entity = entry.Metadata.ClrType.Name; + attachment.Property = attachmentIdProperty + .Metadata + .GetContainingForeignKeys() + .First() + .DependentToPrincipal?.Name; + } + } + } + + return new ValueTask>(result); + } +} \ No newline at end of file diff --git a/src/Miru/Storages/MiruPathExtensions.cs b/src/Miru/Storages/MiruPathExtensions.cs new file mode 100644 index 00000000..b70cc35b --- /dev/null +++ b/src/Miru/Storages/MiruPathExtensions.cs @@ -0,0 +1,49 @@ +using System.IO; + +namespace Miru.Storages; + +public static class MiruPathExtensions +{ + public static MiruPath RelativeTo(this MiruPath fullPath, IStorage storage) => + storage.RelativePath(fullPath); + + public static MiruPath FullPathTo(this MiruPath relativePath, IStorage storage) => + storage.Path / relativePath; + + public static MiruPath AltSeparator(this MiruPath path) => + path.ToString().Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + /// + /// Returns the new renamed path + /// + public static MiruPath Rename(this MiruPath path, string newNameWithExtension) + { + var newPath = path.Dir() / newNameWithExtension; + + File.Move(path, newPath); + + return newPath; + } + + /// + /// Returns the new renamed path + /// + public static MiruPath AddExtension(this MiruPath path, string extensionWithoutDot) + { + var newPath = $"{path}.{extensionWithoutDot}"; + + File.Move(path, newPath); + + return newPath; + } + + public static void CopyTo(this MiruPath path, MiruPath destination) + { + destination.Dir().EnsureDirExist(); + + File.Copy(path, destination); + } + + public static MiruPath RandomFileName(this MiruPath path, string extension) => + path / $"{Path.GetRandomFileName()}.{extension}"; +} \ No newline at end of file diff --git a/src/Miru/Storages/Storage2Extensions.cs b/src/Miru/Storages/Storage2Extensions.cs new file mode 100644 index 00000000..ade60270 --- /dev/null +++ b/src/Miru/Storages/Storage2Extensions.cs @@ -0,0 +1,204 @@ +using System.IO; +using System.Security.Cryptography; +using FluentMigrator.Builders.Alter.Table; +using FluentMigrator.Builders.Create.Table; +using Microsoft.AspNetCore.Http; +using MimeTypes; +using Miru.Databases.Migrations.FluentMigrator; + +namespace Miru.Storages; + +public static class Storage2Extensions +{ + public static async Task WriteAllTextAsync( + this MiruPath path, + string content) + { + path.Dir().EnsureDirExist(); + + await File.WriteAllTextAsync(path, content); + } + + public static async Task GetAsync( + this IStorage sourceStorage, + MiruPath sourcePath, + IStorage destinationStorage, + MiruPath destinationPath) + { + await using var remoteFileStream = await sourceStorage.GetAsync(sourcePath); + await destinationStorage.PutAsync(destinationPath, remoteFileStream); + } + + public static async Task MoveDiskFileAsync( + this IStorage storage, + MiruPath from, + MiruPath to, + bool overwrite = false, + CancellationToken ct = default) + { + to.Dir().EnsureDirExist(); + + if (overwrite) + to.DeleteFileIfExists(); + + File.Move(from, to); + + await Task.CompletedTask; + } + + // public async static Task> ListDiskFilesAsync( + // this IStorage storage, + // MiruPath path, + // string pattern = "*.*", + // CancellationToken ct = default) + // { + // return await Task.FromResult(Directory + // .GetFiles(storage.Path / path, pattern) + // .Select(x => new MiruPath(x)) + // .ToList()); + // } + + public static string ContentType(this MiruPath miruPath) => + MimeTypeMap.GetMimeType(miruPath); + + public static async Task PurgeAsync(this IAppStorage storage, Attachment attachment) + { + var remotePath = attachment.Key; + + var fullRemotePath = storage.Path / remotePath; + + File.Delete(fullRemotePath); + + await Task.CompletedTask; + } + + // public async static Task AttachAsync( + // this IAppStorage storage, + // MiruPath filePath, + // IFormFile formFile, + // TEntity entity, + // Expression> property) + // where TEntity : IEntity + // where TProperty : Attachment + // { + // await using var stream = formFile.OpenReadStream(); + // + // var fullPath = storage.Path / filePath; + // + // await storage.PutAsync(filePath, stream); + // + // var accessor = Baseline.Reflection.ReflectionHelper.GetAccessor(property); + // + // // TODO: use constructor + // var attachment = new Attachment + // { + // // In LocalDisk storage it is the path in the storage + // // Key is the value that point where the file is + // Key = filePath, + // FileName = Path.GetFileName(filePath), + // Entity = typeof(TEntity).Name, + // ByteSize = stream.Length, + // ContentType = formFile.ContentType, + // Checksum = fullPath.GetMd5(), + // Property = accessor.Name + // }; + // + // accessor.SetValue(entity, attachment); + // } + + public static Attachment Attach2(this IAppStorage storage, MiruPath fullPath) => + new Attachment + { + Key = fullPath.RelativeTo(storage), + FileName = fullPath.FileName(), + ByteSize = fullPath.FileSize(), + ContentType = fullPath.ContentType(), + Checksum = fullPath.GetMd5(), + }; + + public static async Task AttachAsync2( + this IAppStorage storage, + MiruPath filePath, + IFormFile formFile) + { + // TODO: should throw exception + if (formFile is null) + return null; + + var fullPath = storage.Path / filePath; + var relativePath = storage.RelativePath(fullPath); + + using (var stream = formFile.OpenReadStream()) + { + await storage.PutAsync(filePath, stream); + + return new Attachment(relativePath, stream, formFile, fullPath); + } + } + + public async static Task AttachBase64Async( + this IAppStorage storage, + MiruPath remotePath, + string base64) + { + var data = Convert.FromBase64String(base64); + + await using var stream = new MemoryStream(data); + + await storage.PutAsync(remotePath, stream); + + return new Attachment + { + Key = remotePath, + FileName = remotePath.FileName(), + ContentType = remotePath.ContentType(), + ByteSize = stream.Length, + Checksum = stream.GetMd5(), + }; + } + + public static MiruPath PathFor(this IStorage storage, MiruPath forPath) => + storage.Path / forPath; + + public static string FileExtensionWithDot(this IFormFile formFile) => + Path.GetExtension(formFile.FileName); + + public static string GetMd5(this MiruPath path) + { + using var md5 = MD5.Create(); + + using var stream = File.OpenRead(path); + + var hash = md5.ComputeHash(stream); + + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + } + + public static string GetMd5(this Stream stream) + { + using var md5 = MD5.Create(); + + var hash = md5.ComputeHash(stream); + + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + } + + public static MiruPath FullPath(this IAppStorage storage, MiruPath relativePath) => + storage.Path / relativePath; + + public static ICreateTableColumnOptionOrForeignKeyCascadeOrWithColumnSyntax AsStorageAttachment( + this ICreateTableColumnAsTypeSyntax column, + string attachmentsTable = "Attachments", + bool deleteOnCascade = true) + { + return column.AsForeignKeyReference(attachmentsTable, deleteOnCascade: deleteOnCascade); + } + + public static IAlterTableColumnOptionOrAddColumnOrAlterColumnSyntax AsStorageAttachment( + this IAlterTableColumnAsTypeSyntax column, + string attachmentsTable = "Attachments", + bool deleteOnCascade = true) + { + return column.AsForeignKeyReference(attachmentsTable, deleteOnCascade: deleteOnCascade); + } +} \ No newline at end of file diff --git a/tests/Miru.PageTesting.Tests/Miru.PageTesting.Tests.csproj b/tests/Miru.PageTesting.Tests/Miru.PageTesting.Tests.csproj index c4f89f86..6efbc38d 100644 --- a/tests/Miru.PageTesting.Tests/Miru.PageTesting.Tests.csproj +++ b/tests/Miru.PageTesting.Tests/Miru.PageTesting.Tests.csproj @@ -12,12 +12,12 @@ Has to be .Sdk.Web to allow unit test asp.net components correctly - - + + - + - + diff --git a/tests/Miru.Postgres.Tests/Miru.Postgres.Tests.csproj b/tests/Miru.Postgres.Tests/Miru.Postgres.Tests.csproj index d8dc4ca0..48a085e7 100644 --- a/tests/Miru.Postgres.Tests/Miru.Postgres.Tests.csproj +++ b/tests/Miru.Postgres.Tests/Miru.Postgres.Tests.csproj @@ -12,8 +12,8 @@ Has to be .Sdk.Web to allow unit test asp.net components correctly - - + + diff --git a/tests/Miru.Tests/Miru.Tests.csproj b/tests/Miru.Tests/Miru.Tests.csproj index 4ad2bc81..8c151e24 100644 --- a/tests/Miru.Tests/Miru.Tests.csproj +++ b/tests/Miru.Tests/Miru.Tests.csproj @@ -12,10 +12,10 @@ Has to be .Sdk.Web to allow unit test asp.net components correctly - - + + - + diff --git a/tests/Miru.Tests/Storages/AttachmentTest.cs b/tests/Miru.Tests/Storages/AttachmentTest.cs new file mode 100644 index 00000000..7c4c004a --- /dev/null +++ b/tests/Miru.Tests/Storages/AttachmentTest.cs @@ -0,0 +1,66 @@ +using Hangfire; +using Hangfire.MemoryStorage; +using Microsoft.Extensions.DependencyInjection; +using Miru.Foundation.Logging; +using Miru.Storages; +using Miru.Tests.Queuing; + +namespace Miru.Tests.Storages; + +public class AttachmentTest : MiruCoreTest +{ + private IAppStorage _storage; + + public override IServiceCollection ConfigureServices(IServiceCollection services) + { + return services.AddStorages(); + } + + [SetUp] + public void Setup() + { + _storage = _.AppStorage(); + } + + [Test] + public void Should_create_an_attachment_from_file_path() + { + // arrange + var file = (_storage.Path / "file.txt").MakeFake(letters: 8); + using var stream = _storage.GetAsync(file); + + // act + var attachment = new Attachment(_storage, file); + + // assert + attachment.Key.ShouldBe("file.txt"); + attachment.FileName.ShouldBe("file.txt"); + attachment.ByteSize.ShouldBe(8); + attachment.ContentType.ShouldBe("text/plain"); + attachment.Checksum.ShouldBe(file.GetMd5()); + } + + [Test] + [Ignore("WIP")] + public void Should_create_an_attachment_from_form_file() + { + // arrange + // var file = (_storage.Path / "file.txt").MakeFake(); + // using var stream = _storage.GetAsync(file); + // + // // act + // var attachment = new Attachment(file, stream,) + // // assert + } + + [Test] + [Ignore("WIP")] + public void Should_update_current_attachment_metadata_with_other_attachment() + { + // arrange + + // act + + // assert + } +} \ No newline at end of file