using System.Text.Json; namespace AIStudio.Assistants.BeyondFactsLab; public sealed partial class BeyondFactsLabStore { /// /// The folder of a preparation which holds its results, one file for each kind. /// private const string RESULTS_DIRECTORY_NAME = "results"; /// /// Loads a result of a preparation. /// /// The ID of the preparation. /// The kind of result. /// The cancellation token. /// The result, or an empty one while the preparation has none. /// This version cannot open the result. public async Task LoadResultAsync(Guid projectId, BflResultKind kind, CancellationToken token = default) { await this.InitializeAsync(token); return await this.LoadResultWithoutInitializeAsync(projectId, kind, token); } /// /// Loads every result of a preparation. /// /// The ID of the preparation. /// The cancellation token. /// The results this version can open, by their kinds. public async Task> LoadResultsAsync(Guid projectId, CancellationToken token = default) { await this.InitializeAsync(token); var results = new Dictionary(); foreach (var kind in BflResultKind.All) { try { results[kind] = await this.LoadResultWithoutInitializeAsync(projectId, kind, token); } catch (InvalidDataException exception) { logger.LogWarning(exception, "Could not open the result '{Kind}' of the preparation '{ProjectId}' of the BeyondFacts Lab.", kind, projectId); } } return results; } /// /// Stores a result of a preparation and counts it as a change of the preparation. /// /// The ID of the preparation. /// The kind of result. /// The result. /// The cancellation token. /// The preparation with its new time of change. /// The preparation does not exist. /// This version cannot open the preparation or the stored result, so it does not write them either. public async Task SaveResultAsync(Guid projectId, BflResultKind kind, BflResult result, CancellationToken token = default) { await this.InitializeAsync(token); var gate = this.GetLock(projectId); await gate.WaitAsync(token); try { var project = await this.LoadRequiredWithoutInitializeAsync(projectId, token); // A result which a newer version stored, or which is damaged, stays as it is: await this.LoadResultWithoutInitializeAsync(projectId, kind, token); result.SchemaVersion = BflResult.CURRENT_SCHEMA_VERSION; await WriteTextAtomicAsync(this.ResultPath(projectId, kind), JsonSerializer.Serialize(result, JSON_OPTIONS), token); project.ModifiedAtUtc = DateTimeOffset.UtcNow; await this.StoreProjectAsync(project, token); return project; } finally { gate.Release(); } } /// /// Reads a result of a preparation without the first start of the store. /// /// The ID of the preparation. /// The kind of result. /// The cancellation token. /// The result, or an empty one when there is no file. /// This version cannot open the result. private async Task LoadResultWithoutInitializeAsync(Guid projectId, BflResultKind kind, CancellationToken token) { var path = this.ResultPath(projectId, kind); if (!File.Exists(path)) return new BflResult(); BflResult? result; try { await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, true); result = await JsonSerializer.DeserializeAsync(stream, JSON_OPTIONS, token); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) { throw new InvalidDataException("This version of AI Studio cannot open the result.", exception); } if (result is null || result.SchemaVersion is < 1 or > BflResult.CURRENT_SCHEMA_VERSION || !IsReadable(result.Draft) || !IsReadable(result.Confirmed)) throw new InvalidDataException("This version of AI Studio cannot open the result."); return result; } /// /// Tells whether a version of a result, which may be missing, has everything this version of AI Studio reads. /// /// The version, or null. private static bool IsReadable(BflResultVersion? version) => version is null || !IsNull(version.Text) && !IsNull(version.WorkStateRevisions); /// /// Gets the file of a result of a preparation. /// /// The ID of the preparation. /// The kind of result. private string ResultPath(Guid projectId, BflResultKind kind) => Path.Combine(this.ProjectDirectory(projectId), RESULTS_DIRECTORY_NAME, kind.FileName); }