parent
e5ea85474b
commit
3aec7924b1
10
Elsa.sln
10
Elsa.sln
|
|
@ -277,6 +277,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFramework
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj", "{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.FileStorage", "src\modules\Elsa.FileStorage\Elsa.FileStorage.csproj", "{732BF088-6AD7-4C4D-9A48-8074253596D4}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "storage", "storage", "{B818988E-639C-4E6E-85C1-B231BCAD9DAB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -695,6 +699,10 @@ Global
|
|||
{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{732BF088-6AD7-4C4D-9A48-8074253596D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{732BF088-6AD7-4C4D-9A48-8074253596D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{732BF088-6AD7-4C4D-9A48-8074253596D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{732BF088-6AD7-4C4D-9A48-8074253596D4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -822,6 +830,8 @@ Global
|
|||
{05F3B23E-CF28-467B-AD3C-595EA4ED6B96} = {AB797AF0-C12C-46DE-A157-7E25625C6200}
|
||||
{74ACD9D0-8B4C-42FA-A582-E93BF0075023} = {AB797AF0-C12C-46DE-A157-7E25625C6200}
|
||||
{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D} = {AB797AF0-C12C-46DE-A157-7E25625C6200}
|
||||
{B818988E-639C-4E6E-85C1-B231BCAD9DAB} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{732BF088-6AD7-4C4D-9A48-8074253596D4} = {B818988E-639C-4E6E-85C1-B231BCAD9DAB}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.SqlServer\Elsa.EntityFrameworkCore.SqlServer.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Quartz.EntityFrameworkCore.Sqlite\Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.FileStorage\Elsa.FileStorage.csproj" />
|
||||
<ProjectReference Include="..\Elsa\Elsa.csproj"/>
|
||||
<ProjectReference Include="..\..\modules\Elsa.Dapper.Migrations\Elsa.Dapper.Migrations.csproj"/>
|
||||
<ProjectReference Include="..\..\modules\Elsa.Dapper\Elsa.Dapper.csproj"/>
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" Version="1.8.2"/>
|
||||
<PackageReference Include="FluentStorage.Azure.Blobs" Version="5.2.2" />
|
||||
<PackageReference Include="Proto.Persistence.Sqlite" Version="1.4.0"/>
|
||||
<PackageReference Include="Proto.Persistence.SqlServer" Version="1.4.0"/>
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Encodings.Web;
|
||||
using Elastic.Clients.Elasticsearch.IndexManagement;
|
||||
using Elsa.Alterations.Extensions;
|
||||
using Elsa.Alterations.MassTransit.Extensions;
|
||||
using Elsa.Dapper.Extensions;
|
||||
|
|
@ -17,6 +18,7 @@ using Elsa.MongoDb.Modules.Identity;
|
|||
using Elsa.MongoDb.Modules.Management;
|
||||
using Elsa.MongoDb.Modules.Runtime;
|
||||
using Elsa.WorkflowServer.Web.WorkflowContexts;
|
||||
using FluentStorage;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Proto.Persistence.Sqlite;
|
||||
|
|
@ -65,6 +67,8 @@ services
|
|||
.AddActivitiesFrom<Program>()
|
||||
.AddWorkflowsFrom<Program>()
|
||||
.UseFluentStorageProvider()
|
||||
.UseFileStorage()
|
||||
// .UseFileStorage(sp => StorageFactory.Blobs.AzureBlobStorageWithSas(configuration.GetConnectionString("AzureStorageSasUrl")))
|
||||
.UseIdentity(identity =>
|
||||
{
|
||||
if (useMongoDb)
|
||||
|
|
|
|||
|
|
@ -96,8 +96,5 @@
|
|||
"SweepInterval": "00:00:10:00",
|
||||
"BatchSize": 1000
|
||||
}
|
||||
},
|
||||
"Quartz": {
|
||||
|
||||
}
|
||||
}
|
||||
31
src/modules/Elsa.FileStorage/Activities/OpenFile.cs
Normal file
31
src/modules/Elsa.FileStorage/Activities/OpenFile.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Core;
|
||||
using Elsa.Workflows.Core.Attributes;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
|
||||
namespace Elsa.FileStorage.Activities;
|
||||
|
||||
/// <summary>
|
||||
/// Save a file to the configured storage provider.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "Storage", "Open a file from the configured storage provider.", Kind = ActivityKind.Task)]
|
||||
public class OpenFile : CodeActivity<Stream>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path to save the file to.
|
||||
/// </summary>
|
||||
[Input(Description = "The path to the file to open.")]
|
||||
public Input<string> Path { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var path = Path.Get(context);
|
||||
var blobStorageProvider = context.GetRequiredService<IBlobStorageProvider>();
|
||||
var blobStorage = blobStorageProvider.GetBlobStorage();
|
||||
var data = await blobStorage.OpenReadAsync(path, cancellationToken);
|
||||
|
||||
Result.Set(context, data);
|
||||
}
|
||||
}
|
||||
92
src/modules/Elsa.FileStorage/Activities/SaveFile.cs
Normal file
92
src/modules/Elsa.FileStorage/Activities/SaveFile.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using System.Collections;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Core;
|
||||
using Elsa.Workflows.Core.Attributes;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.FileStorage.Activities;
|
||||
|
||||
/// <summary>
|
||||
/// Save a file to the configured storage provider.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "Storage", "Save a file to the configured storage provider.", Kind = ActivityKind.Task)]
|
||||
public class SaveFile : CodeActivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the file data to save.
|
||||
/// </summary>
|
||||
[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<object> Data { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to save the file to.
|
||||
/// </summary>
|
||||
[Input(Description = "The path to save the file to.")]
|
||||
public Input<string> Path { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to append to the file if it already exists.
|
||||
/// </summary>
|
||||
[Input(Description = "Whether to append to the file if it already exists.")]
|
||||
public Input<bool> Append { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var data = await ResolveAsStreamAsync(Data.Get(context), cancellationToken);
|
||||
var path = Path.Get(context);
|
||||
var append = Append.GetOrDefault(context);
|
||||
var blobStorageProvider = context.GetRequiredService<IBlobStorageProvider>();
|
||||
var blobStorage = blobStorageProvider.GetBlobStorage();
|
||||
await blobStorage.WriteAsync(path, data, append, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Stream> 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<object>().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<Stream> 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;
|
||||
}
|
||||
}
|
||||
15
src/modules/Elsa.FileStorage/Contracts/IStorageProvider.cs
Normal file
15
src/modules/Elsa.FileStorage/Contracts/IStorageProvider.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using FluentStorage.Blobs;
|
||||
|
||||
namespace Elsa.FileStorage;
|
||||
|
||||
/// <summary>
|
||||
/// A provider of <see cref="IBlobStorage"/>. The point of this interface is to provide a wrapper for actual <see cref="IBlobStorage"/> implementations.
|
||||
/// This prevents collisions when the application uses multiple <see cref="IBlobStorage"/> implementations.
|
||||
/// </summary>
|
||||
public interface IBlobStorageProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IBlobStorage"/>.
|
||||
/// </summary>
|
||||
IBlobStorage GetBlobStorage();
|
||||
}
|
||||
24
src/modules/Elsa.FileStorage/Elsa.FileStorage.csproj
Normal file
24
src/modules/Elsa.FileStorage/Elsa.FileStorage.csproj
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\..\common.props" />
|
||||
<Import Project="..\..\..\packages.props" />
|
||||
<Import Project="..\..\..\configureawait.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
|
||||
<Description>
|
||||
Provides activities to save and load files to and from a confogurable storage provider.
|
||||
</Description>
|
||||
<PackageTags>elsa module activities storage</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
src/modules/Elsa.FileStorage/Extensions/ModuleExtensions.cs
Normal file
31
src/modules/Elsa.FileStorage/Extensions/ModuleExtensions.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.FileStorage.Features;
|
||||
using FluentStorage.Blobs;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IModule"/> to install the Storage feature.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Installs the Storage feature.
|
||||
/// </summary>
|
||||
public static IModule UseFileStorage(this IModule module, Func<IServiceProvider, IBlobStorage> blobStorage)
|
||||
{
|
||||
return module.UseFileStorage(feature => feature.BlobStorage = blobStorage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the Storage feature.
|
||||
/// </summary>
|
||||
public static IModule UseFileStorage(this IModule module, Action<FileStorageFeature>? configure = default)
|
||||
{
|
||||
module.Use(configure);
|
||||
return module;
|
||||
}
|
||||
}
|
||||
46
src/modules/Elsa.FileStorage/Features/BlobStorageFeature.cs
Normal file
46
src/modules/Elsa.FileStorage/Features/BlobStorageFeature.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.FileStorage.Services;
|
||||
using FluentStorage;
|
||||
using FluentStorage.Blobs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.FileStorage.Features;
|
||||
|
||||
/// <summary>
|
||||
/// The Storage feature provides activities to interact with a storage provider.
|
||||
/// </summary>
|
||||
public class FileStorageFeature : FeatureBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public FileStorageFeature(IModule module) : base(module)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The blob storage to use.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IBlobStorage> BlobStorage { get; set; } = _ => StorageFactory.Blobs.DirectoryFiles(GetDefaultStorageDirectory());
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Module.AddActivitiesFrom<FileStorageFeature>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.AddSingleton<IBlobStorageProvider>(sp => new BlobStorageProvider(BlobStorage(sp)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default workflows directory.
|
||||
/// </summary>
|
||||
/// <returns>The default workflows directory.</returns>
|
||||
public static string GetDefaultStorageDirectory()
|
||||
{
|
||||
return Path.Combine(Path.GetTempPath(), "Elsa", "Storage", "Blobs");
|
||||
}
|
||||
}
|
||||
3
src/modules/Elsa.FileStorage/FodyWeavers.xml
Normal file
3
src/modules/Elsa.FileStorage/FodyWeavers.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
22
src/modules/Elsa.FileStorage/Services/BlobStorageProvider.cs
Normal file
22
src/modules/Elsa.FileStorage/Services/BlobStorageProvider.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using FluentStorage.Blobs;
|
||||
|
||||
namespace Elsa.FileStorage.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A provider of <see cref="IBlobStorage"/>.
|
||||
/// </summary>
|
||||
public class BlobStorageProvider : IBlobStorageProvider
|
||||
{
|
||||
private readonly IBlobStorage _blobStorage;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlobStorageProvider"/> class.
|
||||
/// </summary>
|
||||
public BlobStorageProvider(IBlobStorage blobStorage)
|
||||
{
|
||||
_blobStorage = blobStorage;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IBlobStorage GetBlobStorage() => _blobStorage;
|
||||
}
|
||||
|
|
@ -241,7 +241,9 @@ public class WriteFileHttpResponse : Activity
|
|||
|
||||
private async Task SendFileStream(ActivityExecutionContext context, HttpContext httpContext, Stream source, string contentType, string filename, EntityTagHeaderValue? eTag)
|
||||
{
|
||||
source.Seek(0, SeekOrigin.Begin);
|
||||
if(source.CanSeek)
|
||||
source.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var enableResumableDownloads = EnableResumableDownloads.GetOrDefault(context, () => false);
|
||||
|
||||
var result = new FileStreamResult(source, contentType)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,5 @@ public interface IBlobStorageProvider
|
|||
/// <summary>
|
||||
/// Gets the <see cref="IBlobStorage"/>.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IBlobStorage"/>.</returns>
|
||||
IBlobStorage GetBlobStorage();
|
||||
}
|
||||
Loading…
Reference in a new issue