| | | 1 | | using System.Text.Json; |
| | | 2 | | using System.Text.Json.Nodes; |
| | | 3 | | using Elsa.Platform.Integration.Options; |
| | | 4 | | using Microsoft.Extensions.Hosting; |
| | | 5 | | using Microsoft.Extensions.Options; |
| | | 6 | | |
| | | 7 | | namespace Elsa.Platform.Integration.Services; |
| | | 8 | | |
| | 2 | 9 | | public sealed class FileShellConfigurationOverlayStore( |
| | 2 | 10 | | IOptions<ElsaPlatformIntegrationOptions> options, |
| | 2 | 11 | | IHostEnvironment environment) : IShellConfigurationOverlayStore |
| | | 12 | | { |
| | 1 | 13 | | private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) |
| | 1 | 14 | | { |
| | 1 | 15 | | WriteIndented = true |
| | 1 | 16 | | }; |
| | 2 | 17 | | private readonly SemaphoreSlim _semaphore = new(1, 1); |
| | | 18 | | |
| | | 19 | | public async Task<bool> ConfigureFeaturesAsync( |
| | | 20 | | string shellId, |
| | | 21 | | IReadOnlyDictionary<string, JsonElement>? enabledFeatures, |
| | | 22 | | IReadOnlyList<string>? disabledFeatures, |
| | | 23 | | CancellationToken cancellationToken = default) |
| | | 24 | | { |
| | 1 | 25 | | await _semaphore.WaitAsync(cancellationToken); |
| | | 26 | | try |
| | | 27 | | { |
| | 1 | 28 | | var root = await LoadAsync(cancellationToken); |
| | 1 | 29 | | var before = Serialize(root); |
| | 1 | 30 | | var features = EnsureObject(root, "CShells", "Shells", shellId, "Features"); |
| | | 31 | | |
| | 4 | 32 | | foreach (var feature in enabledFeatures ?? new Dictionary<string, JsonElement>()) |
| | 1 | 33 | | features[feature.Key] = ToNode(feature.Value) ?? new JsonObject(); |
| | | 34 | | |
| | 4 | 35 | | foreach (var featureId in disabledFeatures ?? []) |
| | 1 | 36 | | features.Remove(featureId); |
| | | 37 | | |
| | 1 | 38 | | var state = EnsureObject(root, "Elsa", "PlatformIntegration", "Shells", shellId); |
| | 1 | 39 | | var disabledFeatureArray = new JsonArray(); |
| | 4 | 40 | | foreach (var featureId in disabledFeatures ?? []) |
| | 1 | 41 | | disabledFeatureArray.Add(featureId); |
| | 1 | 42 | | state["DisabledFeatures"] = disabledFeatureArray; |
| | | 43 | | |
| | 1 | 44 | | return await SaveIfChangedAsync(root, before, cancellationToken); |
| | | 45 | | } |
| | | 46 | | finally |
| | | 47 | | { |
| | 1 | 48 | | _semaphore.Release(); |
| | | 49 | | } |
| | 1 | 50 | | } |
| | | 51 | | |
| | | 52 | | public async Task<bool> ConfigureSettingsAsync( |
| | | 53 | | string shellId, |
| | | 54 | | JsonElement settings, |
| | | 55 | | CancellationToken cancellationToken = default) |
| | | 56 | | { |
| | 1 | 57 | | await _semaphore.WaitAsync(cancellationToken); |
| | | 58 | | try |
| | | 59 | | { |
| | 1 | 60 | | var root = await LoadAsync(cancellationToken); |
| | 1 | 61 | | var before = Serialize(root); |
| | 1 | 62 | | var configuration = EnsureObject(root, "CShells", "Shells", shellId, "Configuration"); |
| | 1 | 63 | | if (ToNode(settings) is JsonObject settingsObject) |
| | 1 | 64 | | Merge(configuration, settingsObject); |
| | | 65 | | |
| | 1 | 66 | | return await SaveIfChangedAsync(root, before, cancellationToken); |
| | | 67 | | } |
| | | 68 | | finally |
| | | 69 | | { |
| | 1 | 70 | | _semaphore.Release(); |
| | | 71 | | } |
| | 1 | 72 | | } |
| | | 73 | | |
| | | 74 | | private async Task<JsonObject> LoadAsync(CancellationToken cancellationToken) |
| | | 75 | | { |
| | 2 | 76 | | var path = GetPath(); |
| | 2 | 77 | | if (!File.Exists(path)) |
| | 2 | 78 | | return new JsonObject(); |
| | | 79 | | |
| | 0 | 80 | | await using var stream = File.OpenRead(path); |
| | 0 | 81 | | return await JsonNode.ParseAsync(stream, cancellationToken: cancellationToken) as JsonObject |
| | 0 | 82 | | ?? throw new InvalidOperationException("The Platform shell overlay file must contain a JSON object."); |
| | 2 | 83 | | } |
| | | 84 | | |
| | | 85 | | private async Task<bool> SaveIfChangedAsync(JsonObject root, string before, CancellationToken cancellationToken) |
| | | 86 | | { |
| | 2 | 87 | | var after = Serialize(root); |
| | 2 | 88 | | if (string.Equals(before, after, StringComparison.Ordinal)) |
| | 0 | 89 | | return false; |
| | | 90 | | |
| | 2 | 91 | | var path = GetPath(); |
| | 2 | 92 | | var directory = Path.GetDirectoryName(path); |
| | 2 | 93 | | if (!string.IsNullOrWhiteSpace(directory)) |
| | 2 | 94 | | Directory.CreateDirectory(directory); |
| | | 95 | | |
| | 2 | 96 | | var tempPath = $"{path}.{Guid.NewGuid():N}.tmp"; |
| | 2 | 97 | | await File.WriteAllTextAsync(tempPath, after, cancellationToken); |
| | 2 | 98 | | File.Move(tempPath, path, overwrite: true); |
| | 2 | 99 | | return true; |
| | 2 | 100 | | } |
| | | 101 | | |
| | | 102 | | private string GetPath() |
| | | 103 | | { |
| | 4 | 104 | | var path = options.Value.ShellOverlayPath; |
| | 4 | 105 | | return Path.IsPathRooted(path) ? path : Path.Combine(environment.ContentRootPath, path); |
| | | 106 | | } |
| | | 107 | | |
| | | 108 | | private static JsonObject EnsureObject(JsonObject root, params string[] path) |
| | | 109 | | { |
| | 3 | 110 | | JsonObject current = root; |
| | 30 | 111 | | foreach (var segment in path) |
| | | 112 | | { |
| | 12 | 113 | | if (current[segment] is not JsonObject child) |
| | | 114 | | { |
| | 12 | 115 | | child = new JsonObject(); |
| | 12 | 116 | | current[segment] = child; |
| | | 117 | | } |
| | | 118 | | |
| | 12 | 119 | | current = child; |
| | | 120 | | } |
| | | 121 | | |
| | 3 | 122 | | return current; |
| | | 123 | | } |
| | | 124 | | |
| | | 125 | | private static JsonNode? ToNode(JsonElement element) => |
| | 2 | 126 | | element.ValueKind switch |
| | 2 | 127 | | { |
| | 0 | 128 | | JsonValueKind.Undefined or JsonValueKind.Null => null, |
| | 2 | 129 | | _ => JsonNode.Parse(element.GetRawText()) |
| | 2 | 130 | | }; |
| | | 131 | | |
| | | 132 | | private static void Merge(JsonObject target, JsonObject source) |
| | | 133 | | { |
| | 4 | 134 | | foreach (var property in source) |
| | | 135 | | { |
| | 1 | 136 | | if (property.Value is JsonObject sourceObject && target[property.Key] is JsonObject targetObject) |
| | | 137 | | { |
| | 0 | 138 | | Merge(targetObject, sourceObject); |
| | 0 | 139 | | continue; |
| | | 140 | | } |
| | | 141 | | |
| | 1 | 142 | | target[property.Key] = property.Value?.DeepClone(); |
| | | 143 | | } |
| | 1 | 144 | | } |
| | | 145 | | |
| | | 146 | | private static string Serialize(JsonObject root) => |
| | 4 | 147 | | root.ToJsonString(JsonOptions); |
| | | 148 | | } |