| | | 1 | | using System.IO.Compression; |
| | | 2 | | using System.Text.Json; |
| | | 3 | | using System.Text.Json.Nodes; |
| | | 4 | | using Elsa.Abstractions; |
| | | 5 | | using Elsa.Common.Entities; |
| | | 6 | | using Elsa.Common.Models; |
| | | 7 | | using Elsa.Workflows.Api.Endpoints.WorkflowInstances.Get; |
| | | 8 | | using Elsa.Workflows.Api.Models; |
| | | 9 | | using Elsa.Workflows.Management; |
| | | 10 | | using Elsa.Workflows.Management.Entities; |
| | | 11 | | using Elsa.Workflows.Management.Filters; |
| | | 12 | | using Elsa.Workflows.Runtime; |
| | | 13 | | using Elsa.Workflows.Runtime.Entities; |
| | | 14 | | using Elsa.Workflows.Runtime.Filters; |
| | | 15 | | using Elsa.Workflows.Runtime.OrderDefinitions; |
| | | 16 | | using Elsa.Workflows.State; |
| | | 17 | | using JetBrains.Annotations; |
| | | 18 | | |
| | | 19 | | namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Export; |
| | | 20 | | |
| | | 21 | | /// <summary> |
| | | 22 | | /// Exports the specified workflow instances as JSON downloads. When selecting multiple instances, a zip file will be do |
| | | 23 | | /// </summary> |
| | | 24 | | [UsedImplicitly] |
| | | 25 | | internal class Export : ElsaEndpointWithMapper<Request, WorkflowInstanceMapper> |
| | | 26 | | { |
| | | 27 | | private readonly IWorkflowInstanceStore _workflowInstanceStore; |
| | | 28 | | private readonly IActivityExecutionStore _activityExecutionStore; |
| | | 29 | | private readonly IWorkflowExecutionLogStore _workflowExecutionLogStore; |
| | | 30 | | private readonly IBookmarkStore _bookmarkStore; |
| | | 31 | | private readonly IWorkflowStateSerializer _workflowStateSerializer; |
| | | 32 | | private readonly IPayloadSerializer _payloadSerializer; |
| | | 33 | | private readonly ISafeSerializer _safeSerializer; |
| | | 34 | | |
| | | 35 | | /// <inheritdoc /> |
| | 1 | 36 | | public Export( |
| | 1 | 37 | | IWorkflowInstanceStore workflowInstanceStore, |
| | 1 | 38 | | IActivityExecutionStore activityExecutionStore, |
| | 1 | 39 | | IWorkflowExecutionLogStore workflowExecutionLogStore, |
| | 1 | 40 | | IBookmarkStore bookmarkStore, |
| | 1 | 41 | | IWorkflowStateSerializer workflowStateSerializer, |
| | 1 | 42 | | IPayloadSerializer payloadSerializer, |
| | 1 | 43 | | ISafeSerializer safeSerializer) |
| | | 44 | | { |
| | 1 | 45 | | _workflowInstanceStore = workflowInstanceStore; |
| | 1 | 46 | | _activityExecutionStore = activityExecutionStore; |
| | 1 | 47 | | _workflowExecutionLogStore = workflowExecutionLogStore; |
| | 1 | 48 | | _bookmarkStore = bookmarkStore; |
| | 1 | 49 | | _workflowStateSerializer = workflowStateSerializer; |
| | 1 | 50 | | _payloadSerializer = payloadSerializer; |
| | 1 | 51 | | _safeSerializer = safeSerializer; |
| | 1 | 52 | | } |
| | | 53 | | |
| | | 54 | | /// <inheritdoc /> |
| | | 55 | | public override void Configure() |
| | | 56 | | { |
| | 1 | 57 | | Routes("/bulk-actions/export/workflow-instances", "/workflow-instances/{id}/export"); |
| | 1 | 58 | | Verbs(FastEndpoints.Http.GET, FastEndpoints.Http.POST); |
| | 1 | 59 | | ConfigurePermissions("read:workflow-instances"); |
| | 1 | 60 | | } |
| | | 61 | | |
| | | 62 | | /// <inheritdoc /> |
| | | 63 | | public override Task HandleAsync(Request request, CancellationToken cancellationToken) |
| | | 64 | | { |
| | 0 | 65 | | if (request.Id != null || request.Ids.Count == 1) |
| | 0 | 66 | | return DownloadSingleInstanceAsync(request, request.Id ?? request.Ids.First(), cancellationToken); |
| | 0 | 67 | | return DownloadMultipleInstancesAsync(request, cancellationToken); |
| | | 68 | | } |
| | | 69 | | |
| | | 70 | | private async Task DownloadMultipleInstancesAsync(Request request, CancellationToken cancellationToken) |
| | | 71 | | { |
| | 0 | 72 | | var instances = (await _workflowInstanceStore.FindManyAsync(new WorkflowInstanceFilter { Ids = request.Ids }, ca |
| | | 73 | | |
| | 0 | 74 | | if (!instances.Any()) |
| | | 75 | | { |
| | 0 | 76 | | await Send.NoContentAsync(cancellationToken); |
| | 0 | 77 | | return; |
| | | 78 | | } |
| | | 79 | | |
| | 0 | 80 | | var zipStream = new MemoryStream(); |
| | 0 | 81 | | using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) |
| | | 82 | | { |
| | | 83 | | // Create a JSON file for each workflow definition: |
| | 0 | 84 | | foreach (var instance in instances) |
| | | 85 | | { |
| | 0 | 86 | | var model = await CreateExportModelAsync(request, instance, cancellationToken); |
| | 0 | 87 | | var binaryJson = SerializeWorkflowInstance(model); |
| | 0 | 88 | | var fileName = GetFileName(instance.WorkflowState); |
| | 0 | 89 | | var entry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal); |
| | 0 | 90 | | await using var entryStream = entry.Open(); |
| | 0 | 91 | | await entryStream.WriteAsync(binaryJson, cancellationToken); |
| | 0 | 92 | | } |
| | 0 | 93 | | } |
| | | 94 | | |
| | | 95 | | // Send the zip file to the client: |
| | 0 | 96 | | zipStream.Position = 0; |
| | 0 | 97 | | await Send.BytesAsync(zipStream.ToArray(), "workflow-instances.zip", cancellation: cancellationToken); |
| | 0 | 98 | | } |
| | | 99 | | |
| | | 100 | | private async Task DownloadSingleInstanceAsync(Request request, string id, CancellationToken cancellationToken) |
| | | 101 | | { |
| | 0 | 102 | | var instance = (await _workflowInstanceStore.FindManyAsync(new WorkflowInstanceFilter { Id = id }, cancellationT |
| | | 103 | | |
| | 0 | 104 | | if (instance == null) |
| | | 105 | | { |
| | 0 | 106 | | await Send.NotFoundAsync(cancellationToken); |
| | 0 | 107 | | return; |
| | | 108 | | } |
| | | 109 | | |
| | 0 | 110 | | var model = await CreateExportModelAsync(request, instance, cancellationToken); |
| | 0 | 111 | | var binaryJson = SerializeWorkflowInstance(model); |
| | 0 | 112 | | var fileName = GetFileName(instance.WorkflowState); |
| | | 113 | | |
| | 0 | 114 | | await Send.BytesAsync(binaryJson, fileName, cancellation: cancellationToken); |
| | 0 | 115 | | } |
| | | 116 | | |
| | | 117 | | private async Task<ExportedWorkflowState> CreateExportModelAsync(Request request, WorkflowInstance instance, Cancell |
| | | 118 | | { |
| | 0 | 119 | | var workflowState = instance.WorkflowState; |
| | 0 | 120 | | var executionLogRecords = request.IncludeWorkflowExecutionLog ? await LoadWorkflowExecutionLogRecordsAsync(workf |
| | 0 | 121 | | var activityExecutionLogRecords = request.IncludeActivityExecutionLog ? await LoadActivityExecutionLogRecordsAsy |
| | 0 | 122 | | var bookmarks = request.IncludeBookmarks ? await LoadBookmarksAsync(workflowState.Id, cancellationToken) : null; |
| | 0 | 123 | | var workflowStateElement = _workflowStateSerializer.SerializeToElement(workflowState); |
| | 0 | 124 | | var bookmarksElement = bookmarks != null ? SerializeBookmarks(bookmarks) : default(JsonElement?); |
| | 0 | 125 | | var executionLogRecordsElement = executionLogRecords != null ? _safeSerializer.SerializeToElement(executionLogRe |
| | 0 | 126 | | var activityExecutionLogRecordsElement = activityExecutionLogRecords != null ? _safeSerializer.SerializeToElemen |
| | 0 | 127 | | var model = new ExportedWorkflowState(workflowStateElement, bookmarksElement, activityExecutionLogRecordsElement |
| | 0 | 128 | | return model; |
| | 0 | 129 | | } |
| | | 130 | | |
| | | 131 | | private JsonElement SerializeBookmarks(IEnumerable<StoredBookmark> bookmarks) |
| | | 132 | | { |
| | 0 | 133 | | var jsonBookmarkNodes = bookmarks.Select(x => new JsonObject |
| | 0 | 134 | | { |
| | 0 | 135 | | ["id"] = x.Id, |
| | 0 | 136 | | ["activityTypeName"] = x.Name, |
| | 0 | 137 | | ["workflowInstanceId"] = x.WorkflowInstanceId, |
| | 0 | 138 | | ["activityInstanceId"] = x.ActivityInstanceId, |
| | 0 | 139 | | ["hash"] = x.Hash, |
| | 0 | 140 | | ["correlationId"] = x.CorrelationId, |
| | 0 | 141 | | ["createdAt"] = x.CreatedAt, |
| | 0 | 142 | | ["payload"] = JsonObject.Create(_payloadSerializer.SerializeToElement(x.Payload!)), |
| | 0 | 143 | | ["metadata"] = JsonObject.Create(_payloadSerializer.SerializeToElement(x.Metadata!)) |
| | 0 | 144 | | }).Cast<JsonNode>().ToArray(); |
| | | 145 | | |
| | 0 | 146 | | var jsonBookmarkArray = new JsonArray(jsonBookmarkNodes); |
| | 0 | 147 | | return JsonSerializer.SerializeToElement(jsonBookmarkArray); |
| | | 148 | | } |
| | | 149 | | |
| | | 150 | | private async Task<IEnumerable<StoredBookmark>> LoadBookmarksAsync(string workflowInstanceId, CancellationToken canc |
| | | 151 | | { |
| | 0 | 152 | | var filter = new BookmarkFilter { WorkflowInstanceId = workflowInstanceId }; |
| | 0 | 153 | | return await _bookmarkStore.FindManyAsync(filter, cancellationToken); |
| | 0 | 154 | | } |
| | | 155 | | |
| | | 156 | | private async Task<IEnumerable<ActivityExecutionRecord>> LoadActivityExecutionLogRecordsAsync(string workflowInstanc |
| | | 157 | | { |
| | 0 | 158 | | var filter = new ActivityExecutionRecordFilter { WorkflowInstanceId = workflowInstanceId }; |
| | 0 | 159 | | var order = new ActivityExecutionRecordOrder<DateTimeOffset>(x => x.StartedAt, OrderDirection.Ascending); |
| | 0 | 160 | | return await _activityExecutionStore.FindManyAsync(filter, order, cancellationToken); |
| | 0 | 161 | | } |
| | | 162 | | |
| | | 163 | | private async Task<IEnumerable<WorkflowExecutionLogRecord>> LoadWorkflowExecutionLogRecordsAsync(string workflowInst |
| | | 164 | | { |
| | 0 | 165 | | var filter = new WorkflowExecutionLogRecordFilter { WorkflowInstanceId = workflowInstanceId }; |
| | 0 | 166 | | var order = new WorkflowExecutionLogRecordOrder<DateTimeOffset>(x => x.Timestamp, OrderDirection.Ascending); |
| | 0 | 167 | | var page = await _workflowExecutionLogStore.FindManyAsync(filter, PageArgs.All, order, cancellationToken); |
| | 0 | 168 | | return page.Items; |
| | 0 | 169 | | } |
| | | 170 | | |
| | | 171 | | private static string GetFileName(WorkflowState instance) |
| | | 172 | | { |
| | 0 | 173 | | var fileName = $"workflow-instance-{instance.Id.ToLowerInvariant()}.json"; |
| | 0 | 174 | | return fileName; |
| | | 175 | | } |
| | | 176 | | |
| | | 177 | | private static byte[] SerializeWorkflowInstance(ExportedWorkflowState model) |
| | | 178 | | { |
| | 0 | 179 | | var binaryJson = JsonSerializer.SerializeToUtf8Bytes(model); |
| | 0 | 180 | | return binaryJson; |
| | | 181 | | } |
| | | 182 | | } |