diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.Elsa.Activities.BlobStoring/LINGYUN/Abp/Elsa/Activities/BlobStoring/ElsaBlobContainer.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.Elsa.Activities.BlobStoring/LINGYUN/Abp/Elsa/Activities/BlobStoring/ElsaBlobContainer.cs index b37a25c74..1a7e8cc87 100644 --- a/aspnet-core/modules/elsa/LINGYUN.Abp.Elsa.Activities.BlobStoring/LINGYUN/Abp/Elsa/Activities/BlobStoring/ElsaBlobContainer.cs +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.Elsa.Activities.BlobStoring/LINGYUN/Abp/Elsa/Activities/BlobStoring/ElsaBlobContainer.cs @@ -2,7 +2,7 @@ namespace LINGYUN.Abp.Elsa.Activities.BlobStoring; -[BlobContainerName("elsa")] +[BlobContainerName("workflow")] public class ElsaBlobContainer { } diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN.Abp.ElsaNext.BlobStoring.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN.Abp.ElsaNext.BlobStoring.csproj new file mode 100644 index 000000000..60f8ffd5b --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN.Abp.ElsaNext.BlobStoring.csproj @@ -0,0 +1,29 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.BlobStoring + LINGYUN.Abp.ElsaNext.BlobStoring + false + false + false + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/AbpElsaNextBlobStoringModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/AbpElsaNextBlobStoringModule.cs new file mode 100644 index 000000000..251a6f7b7 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/AbpElsaNextBlobStoringModule.cs @@ -0,0 +1,38 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.BlobStoring.Extensions; +using LINGYUN.Abp.ElsaNext.Localization; +using Volo.Abp.BlobStoring; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring; + +[DependsOn( + typeof(AbpBlobStoringModule), + typeof(AbpElsaNextModule))] +public class AbpElsaNextBlobStoringModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(elsa => + { + elsa.UseAbpBlobStoring(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/BlobActivity.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/BlobActivity.cs new file mode 100644 index 000000000..3b52c5650 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/BlobActivity.cs @@ -0,0 +1,39 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using System.Threading.Tasks; +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Activities; + +public abstract class BlobActivity : CodeActivity +{ + [Input(Description = "Path of the blob.")] + public Input Path { get; set; } = default!; + + protected virtual IBlobContainer GetBlobContainer(ActivityExecutionContext context) + { + return context.GetRequiredService>(); + } + + protected async virtual ValueTask OnErrorCompletedAsync(ActivityCompletedContext context) + { + await context.TargetContext.CompleteActivityAsync(); + } +} + +public abstract class BlobActivity : CodeActivity +{ + [Input(Description = "Path of the blob.")] + public Input Path { get; set; } = default!; + + protected virtual IBlobContainer GetBlobContainer(ActivityExecutionContext context) + { + return context.GetRequiredService>(); + } + + protected async virtual ValueTask OnErrorCompletedAsync(ActivityCompletedContext context) + { + await context.TargetContext.CompleteActivityAsync(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/DeleteBlob.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/DeleteBlob.cs new file mode 100644 index 000000000..2cf9dfb71 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/DeleteBlob.cs @@ -0,0 +1,20 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Activities; + +[Activity("Elsa", "BlobStoring", "Delete a blob.", Kind = ActivityKind.Task)] +public class DeleteBlob : BlobActivity +{ + protected async override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var path = Path.Get(context); + var blobContainer = GetBlobContainer(context); + var result = await blobContainer.DeleteAsync(path, cancellationToken); + + Result.Set(context, result); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/ReadBlob.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/ReadBlob.cs new file mode 100644 index 000000000..f81a22de0 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/ReadBlob.cs @@ -0,0 +1,40 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Activities; + +[Activity("Elsa", "BlobStoring", "Open a blob.", Kind = ActivityKind.Task)] +public class ReadBlob : BlobActivity +{ + [Port] public IActivity? Error { get; set; } + + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var path = Path.Get(context); + var blobContainer = GetBlobContainer(context); + + var logger = context.GetRequiredService>(); + try + { + var data = await blobContainer.GetAsync(path, cancellationToken); + + Result.Set(context, data); + await context.CompleteActivityAsync(); + } + catch (Exception e) + { + logger.LogWarning(e, "Error while open a blob."); + context.AddExecutionLogEntry("Error", e.Message, payload: new + { + e.StackTrace + }); + await context.ScheduleActivityAsync(Error, OnErrorCompletedAsync); + } + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/SaveBlob.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/SaveBlob.cs new file mode 100644 index 000000000..74f9c8eb7 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/SaveBlob.cs @@ -0,0 +1,99 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System; +using System.Collections; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Activities; + +[Activity("Elsa", "BlobStoring", "Save a blob.", Kind = ActivityKind.Task)] +public class SaveBlob : BlobActivity +{ + [Input(Description = "Blob exists whether overwrite.")] + public Input Overwrite { get; set; } = null!; + + [Input(Description = "The file data to save. This can be a stream, binary data, a string, a form file or a collection of files.")] + public Input Data { get; set; } = null!; + + [Port] public IActivity? Error { get; set; } + + protected async override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var data = await ResolveAsStreamAsync(Data.Get(context), cancellationToken); + var overwrite = Overwrite.GetOrDefault(context); + var path = Path.Get(context); + var blobContainer = GetBlobContainer(context); + + var logger = context.GetRequiredService>(); + try + { + await blobContainer.SaveAsync(path, data, overwrite, cancellationToken); + await context.CompleteActivityAsync(); + } + catch (Exception e) + { + logger.LogWarning(e, "Error while save a blob."); + context.AddExecutionLogEntry("Error", e.Message, payload: new + { + e.StackTrace + }); + await context.ScheduleActivityAsync(Error, OnErrorCompletedAsync); + } + } + + private async Task ResolveAsStreamAsync(object data, CancellationToken cancellationToken) + { + if (data is Stream stream) + return stream; + + if (data is byte[] bytes) + return new MemoryStream(bytes); + + if (data is IFormFile formFile) + return formFile.OpenReadStream(); + + if (data is string stringData) + return new MemoryStream(Encoding.UTF8.GetBytes(stringData)); + + if (data is IEnumerable enumerable) + { + var files = enumerable.Cast().ToList(); + return files.Count == 1 ? await ResolveAsStreamAsync(files[0], cancellationToken) : await CreateZipArchiveAsync(files, cancellationToken); + } + + throw new NotSupportedException($"The provided data type is not supported: {data.GetType().Name}"); + } + + private async Task CreateZipArchiveAsync(IEnumerable files, CancellationToken cancellationToken = default) + { + var currentFileIndex = 0; + var zipStream = new MemoryStream(); + var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, true); + + foreach (var file in files) + { + var entryName = $"file-{currentFileIndex}.bin"; + var entry = zipArchive.CreateEntry(entryName); + var fileStream = await ResolveAsStreamAsync(file, cancellationToken); + await using var entryStream = entry.Open(); + await fileStream.CopyToAsync(entryStream, cancellationToken); + await entryStream.FlushAsync(cancellationToken); + entryStream.Close(); + currentFileIndex++; + } + + zipStream.Seek(0, SeekOrigin.Begin); + return zipStream; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/ElsaBlobContainer.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/ElsaBlobContainer.cs new file mode 100644 index 000000000..dfa9f6c5c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/ElsaBlobContainer.cs @@ -0,0 +1,8 @@ +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring; + +[BlobContainerName("workflow")] +public class ElsaBlobContainer +{ +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Extensions/ModuleExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..837c60b0b --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Extensions/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.BlobStoring.Features; +using System; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Extensions; + +public static class ModuleExtensions +{ + public static IModule UseAbpBlobStoring(this IModule configuration, Action? configure = null) + { + configuration.Configure(configure); + return configuration; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Features/BlobStoringFeature.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Features/BlobStoringFeature.cs new file mode 100644 index 000000000..b14e0f3e4 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Features/BlobStoringFeature.cs @@ -0,0 +1,17 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Features; + +public class BlobStoringFeature : FeatureBase +{ + public BlobStoringFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.AddActivitiesFrom(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/en.json new file mode 100644 index 000000000..988f1cc78 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/en.json @@ -0,0 +1,12 @@ +{ + "culture": "en", + "texts": { + "Path of the blob.": "Path of the blob.", + "Blob exists whether overwrite.": "Blob exists whether overwrite.", + "The file data to save. This can be a stream, binary data, a string, a form file or a collection of files.": "The file data to save. This can be a stream, binary data, a string, a form file or a collection of files.", + "BlobStoring": "Blob Storing", + "Delete Blob": "Delete a blob.", + "Read Blob.": "Read a blob.", + "Save Blob.": "Save a blob." + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..353220eb9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "culture": "zh-Hans", + "texts": { + "Path of the blob.": "文件存储路径。", + "Blob exists whether overwrite.": "覆盖已有文件。", + "The file data to save. This can be a stream, binary data, a string, a form file or a collection of files.": "要保存的文件数据。可以是流、二进制数据、字符串、表单文件或一组文件。", + "BlobStoring": "对象存储", + "Delete Blob": "删除文件", + "Read Blob": "打开文件", + "Save Blob": "保存文件" + } +}