1- using System . IO . Compression ;
2- using System . Text . Json ;
31using Elsa . Abstractions ;
42using Elsa . Common . Models ;
53using Elsa . Workflows . Management ;
6- using Elsa . Workflows . Management . Entities ;
7- using Elsa . Workflows . Management . Filters ;
8- using Elsa . Workflows . Management . Mappers ;
9- using Elsa . Workflows . Management . Models ;
10- using Humanizer ;
114using JetBrains . Annotations ;
125
136namespace Elsa . Workflows . Api . Endpoints . WorkflowDefinitions . Export ;
@@ -16,26 +9,8 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Export;
169/// Exports the specified workflow definition as JSON download.
1710/// </summary>
1811[ UsedImplicitly ]
19- internal class Export : ElsaEndpoint < Request >
12+ internal class Export ( IWorkflowDefinitionExporter exporter ) : ElsaEndpoint < Request >
2013{
21- private readonly IApiSerializer _serializer ;
22- private readonly IWorkflowDefinitionStore _store ;
23- private readonly IWorkflowReferenceGraphBuilder _workflowReferenceGraphBuilder ;
24- private readonly WorkflowDefinitionMapper _workflowDefinitionMapper ;
25-
26- /// <inheritdoc />
27- public Export (
28- IWorkflowDefinitionStore store ,
29- IApiSerializer serializer ,
30- WorkflowDefinitionMapper workflowDefinitionMapper ,
31- IWorkflowReferenceGraphBuilder workflowReferenceGraphBuilder )
32- {
33- _store = store ;
34- _serializer = serializer ;
35- _workflowDefinitionMapper = workflowDefinitionMapper ;
36- _workflowReferenceGraphBuilder = workflowReferenceGraphBuilder ;
37- }
38-
3914 /// <inheritdoc />
4015 public override void Configure ( )
4116 {
@@ -48,165 +23,33 @@ public override void Configure()
4823 public override async Task HandleAsync ( Request request , CancellationToken cancellationToken )
4924 {
5025 if ( request . DefinitionId != null )
51- await DownloadSingleWorkflowAsync ( request . DefinitionId , request . VersionOptions , request . IncludeConsumingWorkflows , cancellationToken ) ;
52- else if ( request . Ids != null )
53- await DownloadMultipleWorkflowsAsync ( request . Ids , request . IncludeConsumingWorkflows , cancellationToken ) ;
54- else await Send . NoContentAsync ( cancellationToken ) ;
55- }
56-
57- private async Task DownloadMultipleWorkflowsAsync ( ICollection < string > ids , bool includeConsumingWorkflows , CancellationToken cancellationToken )
58- {
59- var definitions = ( await _store . FindManyAsync ( new ( )
60- {
61- Ids = ids
62- } , cancellationToken ) ) . ToList ( ) ;
63-
64- if ( includeConsumingWorkflows )
65- definitions = await IncludeConsumersAsync ( definitions , cancellationToken ) ;
66-
67- if ( ! definitions . Any ( ) )
68- {
69- await Send . NoContentAsync ( cancellationToken ) ;
70- return ;
71- }
72-
73- await WriteZipResponseAsync ( definitions , cancellationToken ) ;
74- }
75-
76- private async Task DownloadSingleWorkflowAsync ( string definitionId , string ? versionOptions , bool includeConsumingWorkflows , CancellationToken cancellationToken )
77- {
78- var parsedVersionOptions = string . IsNullOrEmpty ( versionOptions ) ? VersionOptions . Latest : VersionOptions . FromString ( versionOptions ) ;
79- var definition = ( await _store . FindManyAsync ( new ( )
80- {
81- DefinitionId = definitionId ,
82- VersionOptions = parsedVersionOptions
83- } , cancellationToken ) ) . FirstOrDefault ( ) ;
84-
85- if ( definition == null )
8626 {
87- await Send . NotFoundAsync ( cancellationToken ) ;
88- return ;
89- }
90-
91- if ( includeConsumingWorkflows )
92- {
93- var definitions = await IncludeConsumersAsync ( [ definition ] , cancellationToken ) ;
94- await WriteZipResponseAsync ( definitions , cancellationToken ) ;
95- return ;
96- }
97-
98- var model = await CreateWorkflowModelAsync ( definition , cancellationToken ) ;
99- var binaryJson = await SerializeWorkflowDefinitionAsync ( model , cancellationToken ) ;
100- var fileName = GetFileName ( model ) ;
101-
102- await Send . BytesAsync ( binaryJson , fileName , cancellation : cancellationToken ) ;
103- }
27+ var versionOptions = string . IsNullOrEmpty ( request . VersionOptions ) ? VersionOptions . Latest : VersionOptions . FromString ( request . VersionOptions ) ;
28+ var result = await exporter . ExportAsync ( request . DefinitionId , versionOptions , request . IncludeConsumingWorkflows , cancellationToken ) ;
10429
105- /// <summary>
106- /// Recursively discovers all consuming workflow definitions and includes them.
107- /// Consumers are always resolved at <see cref="VersionOptions.Latest"/>, regardless of the version used for the initial definitions.
108- /// </summary>
109- private async Task < List < WorkflowDefinition > > IncludeConsumersAsync ( List < WorkflowDefinition > definitions , CancellationToken cancellationToken )
110- {
111- var initialDefinitionIds = definitions . Select ( d => d . DefinitionId ) . ToList ( ) ;
112- var graph = await _workflowReferenceGraphBuilder . BuildGraphAsync ( initialDefinitionIds , cancellationToken ) ;
113-
114- // Find any consumer definitions not already in our list.
115- var newDefinitionIds = graph . ConsumerDefinitionIds . Except ( initialDefinitionIds ) . ToList ( ) ;
116-
117- if ( newDefinitionIds . Count > 0 )
118- {
119- var consumerDefinitions = await _store . FindManyAsync ( new WorkflowDefinitionFilter
30+ if ( result == null )
12031 {
121- DefinitionIds = newDefinitionIds . ToArray ( ) ,
122- VersionOptions = VersionOptions . Latest
123- } , cancellationToken ) ;
32+ await Send . NotFoundAsync ( cancellationToken ) ;
33+ return ;
34+ }
12435
125- definitions = definitions . Concat ( consumerDefinitions ) . ToList ( ) ;
36+ await Send . BytesAsync ( result . Data , result . FileName , cancellation : cancellationToken ) ;
12637 }
127-
128- return definitions ;
129- }
130-
131- private async Task WriteZipResponseAsync ( List < WorkflowDefinition > definitions , CancellationToken cancellationToken )
132- {
133- var zipStream = new MemoryStream ( ) ;
134- var sortedDefinitions = definitions . OrderBy ( d => d . DefinitionId ) . ToList ( ) ;
135-
136- // NOTE:
137- // - ZIP timestamps cannot be earlier than 1980-01-01 (the ZIP format's minimum).
138- // - We intentionally use a fixed timestamp (instead of DateTimeOffset.UtcNow) to keep exports deterministic.
139- // This avoids producing different ZIP bytes for identical exports, which helps tests, caching, and diffing.
140- var zipEpoch = new DateTimeOffset ( 1980 , 1 , 1 , 0 , 0 , 0 , TimeSpan . Zero ) ;
141-
142- #if NET10_0_OR_GREATER
143- await using ( var zipArchive = new ZipArchive ( zipStream , ZipArchiveMode . Create , true ) )
38+ else if ( request . Ids != null )
14439 {
145- // Create a JSON file for each workflow definition:
146- foreach ( var definition in sortedDefinitions )
40+ var result = await exporter . ExportManyAsync ( request . Ids , request . IncludeConsumingWorkflows , cancellationToken ) ;
41+
42+ if ( result == null )
14743 {
148- var model = await CreateWorkflowModelAsync ( definition , cancellationToken ) ;
149- var binaryJson = await SerializeWorkflowDefinitionAsync ( model , cancellationToken ) ;
150- var fileName = GetFileName ( model ) ;
151- var entry = zipArchive . CreateEntry ( fileName , CompressionLevel . Optimal ) ;
152- entry . LastWriteTime = zipEpoch ;
153- await using var entryStream = await entry . OpenAsync ( cancellationToken ) ;
154- await entryStream . WriteAsync ( binaryJson , cancellationToken ) ;
44+ await Send . NoContentAsync ( cancellationToken ) ;
45+ return ;
15546 }
47+
48+ await Send . BytesAsync ( result . Data , result . FileName , cancellation : cancellationToken ) ;
15649 }
157- #else
158- using ( var zipArchive = new ZipArchive ( zipStream , ZipArchiveMode . Create , true ) )
50+ else
15951 {
160- // Create a JSON file for each workflow definition:
161- foreach ( var definition in sortedDefinitions )
162- {
163- var model = await CreateWorkflowModelAsync ( definition , cancellationToken ) ;
164- var binaryJson = await SerializeWorkflowDefinitionAsync ( model , cancellationToken ) ;
165- var fileName = GetFileName ( model ) ;
166- var entry = zipArchive . CreateEntry ( fileName , CompressionLevel . Optimal ) ;
167- entry . LastWriteTime = zipEpoch ;
168- await using var entryStream = entry . Open ( ) ;
169- await entryStream . WriteAsync ( binaryJson , cancellationToken ) ;
170- }
52+ await Send . NoContentAsync ( cancellationToken ) ;
17153 }
172- #endif
173-
174- // Send the zip file to the client:
175- zipStream . Position = 0 ;
176- await Send . BytesAsync ( zipStream . ToArray ( ) , "workflow-definitions.zip" , cancellation : cancellationToken ) ;
177- }
178-
179- private string GetFileName ( WorkflowDefinitionModel definition )
180- {
181- var hasWorkflowName = ! string . IsNullOrWhiteSpace ( definition . Name ) ;
182- var workflowName = hasWorkflowName ? definition . Name ! . Trim ( ) : definition . DefinitionId ;
183- var fileName = $ "workflow-definition-{ workflowName . Underscore ( ) . Dasherize ( ) . ToLowerInvariant ( ) } -{ definition . DefinitionId } .json";
184- return fileName ;
185- }
186-
187- private async Task < byte [ ] > SerializeWorkflowDefinitionAsync ( WorkflowDefinitionModel model , CancellationToken cancellationToken )
188- {
189- var serializerOptions = _serializer . GetOptions ( ) ;
190- var document = JsonSerializer . SerializeToDocument ( model , serializerOptions ) ;
191- var rootElement = document . RootElement ;
192-
193- using var output = new MemoryStream ( ) ;
194- await using var writer = new Utf8JsonWriter ( output ) ;
195-
196- writer . WriteStartObject ( ) ;
197- writer . WriteString ( "$schema" , "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json" ) ;
198-
199- foreach ( var property in rootElement . EnumerateObject ( ) )
200- property . WriteTo ( writer ) ;
201-
202- writer . WriteEndObject ( ) ;
203-
204- await writer . FlushAsync ( cancellationToken ) ;
205- return output . ToArray ( ) ;
206- }
207-
208- private async Task < WorkflowDefinitionModel > CreateWorkflowModelAsync ( WorkflowDefinition definition , CancellationToken cancellationToken )
209- {
210- return await _workflowDefinitionMapper . MapAsync ( definition , cancellationToken ) ;
21154 }
21255}
0 commit comments