using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Platform.Analytics.Options; using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Platform.Analytics.Services; public interface ICasContentReader { Task OpenReadAsync(string casUri, CancellationToken cancellationToken); } public sealed record CasContent(Stream Stream, long? Length); public sealed class FileCasContentReader : ICasContentReader { private readonly AnalyticsCasOptions _options; private readonly ILogger _logger; public FileCasContentReader( IOptions options, ILogger logger) { _options = options?.Value.Cas ?? new AnalyticsCasOptions(); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public Task OpenReadAsync(string casUri, CancellationToken cancellationToken) { if (!TryParseCasUri(casUri, out var reference)) { _logger.LogWarning("Unsupported CAS URI '{CasUri}'.", casUri); return Task.FromResult(null); } if (string.IsNullOrWhiteSpace(_options.RootPath)) { _logger.LogWarning("CAS root path not configured; skipping {CasUri}.", casUri); return Task.FromResult(null); } var root = Path.GetFullPath(_options.RootPath); foreach (var candidate in ExpandKeyCandidates(reference.Key)) { var keyPath = candidate.Replace('/', Path.DirectorySeparatorChar); var resolved = Path.GetFullPath(Path.Combine(root, reference.Bucket, keyPath)); if (!resolved.StartsWith(root, StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning("CAS URI '{CasUri}' resolved outside root '{Root}'.", casUri, root); return Task.FromResult(null); } if (!File.Exists(resolved)) { continue; } var stream = new FileStream(resolved, FileMode.Open, FileAccess.Read, FileShare.Read); var length = new FileInfo(resolved).Length; return Task.FromResult(new CasContent(stream, length)); } _logger.LogWarning("CAS object not found at '{Key}' for '{CasUri}'.", reference.Key, casUri); return Task.FromResult(null); } private bool TryParseCasUri(string casUri, out CasReference reference) { reference = default!; if (string.IsNullOrWhiteSpace(casUri)) { return false; } if (!Uri.TryCreate(casUri, UriKind.Absolute, out var uri) || !string.Equals(uri.Scheme, "cas", StringComparison.OrdinalIgnoreCase)) { return false; } var bucket = uri.Host; var key = uri.AbsolutePath.TrimStart('/'); if (string.IsNullOrWhiteSpace(bucket)) { if (string.IsNullOrWhiteSpace(_options.DefaultBucket)) { return false; } bucket = _options.DefaultBucket!; } if (string.IsNullOrWhiteSpace(key)) { return false; } reference = new CasReference(casUri, bucket, key); return true; } private static IEnumerable ExpandKeyCandidates(string key) { yield return key; var colonIndex = key.IndexOf(':'); if (colonIndex <= 0 || colonIndex >= key.Length - 1) { yield break; } var prefix = key[..colonIndex]; var suffix = key[(colonIndex + 1)..]; yield return $"{prefix}/{suffix}"; yield return suffix; } } public sealed record CasReference(string Uri, string Bucket, string Key);