using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MarcusLaw.OutlookAddin.Core.Models; using Serilog; namespace MarcusLaw.OutlookAddin.Core.Services { public sealed class DiskRetryQueue : IRetryQueue { private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, PropertyNameCaseInsensitive = true }; private static readonly UTF8Encoding Utf8NoBom = new UTF8Encoding(false); private readonly string _directory; private readonly ILogger _logger; public DiskRetryQueue(ILogger logger) : this(DefaultDirectory(), logger) { } public DiskRetryQueue(string directory, ILogger logger) { if (string.IsNullOrWhiteSpace(directory)) throw new ArgumentException("directory required", nameof(directory)); _directory = directory; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public string Directory => _directory; public Task EnqueueAsync(RetryQueueItem item, CancellationToken cancellationToken = default) { if (item == null) throw new ArgumentNullException(nameof(item)); EnsureDirectory(); if (string.IsNullOrEmpty(item.Id)) { item.Id = Guid.NewGuid().ToString("N"); } if (item.EnqueuedAt == default) { item.EnqueuedAt = DateTimeOffset.UtcNow; } var finalPath = Path.Combine(_directory, item.Id + ".json"); var tempPath = finalPath + ".tmp"; var json = JsonSerializer.Serialize(item, JsonOptions); File.WriteAllText(tempPath, json, Utf8NoBom); if (File.Exists(finalPath)) { File.Delete(finalPath); } File.Move(tempPath, finalPath); _logger.Information("Retry queue enqueued {Id} (messageId={MessageId})", item.Id, item.Request?.MessageId); return Task.FromResult(item.Id); } public Task> ListAllAsync(CancellationToken cancellationToken = default) { var result = new List(); if (!System.IO.Directory.Exists(_directory)) { return Task.FromResult>(result); } foreach (var path in System.IO.Directory.EnumerateFiles(_directory, "*.json")) { cancellationToken.ThrowIfCancellationRequested(); try { var json = File.ReadAllText(path, Encoding.UTF8); var item = JsonSerializer.Deserialize(json, JsonOptions); if (item != null) { if (string.IsNullOrEmpty(item.Id)) { item.Id = Path.GetFileNameWithoutExtension(path); } result.Add(item); } } catch (JsonException ex) { _logger.Warning(ex, "Retry queue: skipping corrupt file {Path}", path); } catch (IOException ex) { _logger.Warning(ex, "Retry queue: cannot read {Path}", path); } } return Task.FromResult>(result); } public Task RemoveAsync(string id, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("id required", nameof(id)); var path = Path.Combine(_directory, id + ".json"); if (File.Exists(path)) { File.Delete(path); _logger.Information("Retry queue removed {Id}", id); } return Task.CompletedTask; } private void EnsureDirectory() { if (!System.IO.Directory.Exists(_directory)) { System.IO.Directory.CreateDirectory(_directory); } } private static string DefaultDirectory() { return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MarcusLaw", "OutlookAddin", "retry-queue"); } } }