using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RobotNet10.StorageManager; using SixLabors.ImageSharp; namespace RobotNet10.MapManager.Services; /// /// FileSystem-based image storage implementation using StorageManager /// Stores images in local folder with naming: {layoutLevelId}.png /// public class FileSystemImageStorageService : IImageStorageService, IDisposable { private readonly ILogger _logger; private readonly StorageManager.StorageManager _storageManager; private const string ImagePath = "layoutImages"; // Empty path means files are stored directly in LocalFolder private const string ContentType = "image/png"; public FileSystemImageStorageService(IOptionsMonitor optionsSnapshot, ILogger logger) { _logger = logger; var config = optionsSnapshot.Get("LayoutImages"); ArgumentNullException.ThrowIfNull(config); _storageManager = new StorageManager.StorageManager(config); if (_logger.IsEnabled(LogLevel.Information)) { _logger.LogInformation("FileSystemImageStorageService initialized"); } } private static string GetObjectName(Guid layoutLevelId) => layoutLevelId.ToString(); public async Task SaveImageAsync(Guid layoutLevelId, Stream imageStream, CancellationToken cancellationToken = default) { var objectName = GetObjectName(layoutLevelId); try { // Reset stream position if seekable if (imageStream.CanSeek) { imageStream.Position = 0; } // Get stream size - handle cases where Length might not be available long size = imageStream.Length; // If size is 0 or stream doesn't support Length, copy to MemoryStream if (size == 0 || !imageStream.CanSeek) { using var memoryStream = new MemoryStream(); await imageStream.CopyToAsync(memoryStream, cancellationToken); size = memoryStream.Length; memoryStream.Position = 0; await _storageManager.UploadAsync(ImagePath, objectName, memoryStream, size, ContentType, cancellationToken); } else { // Stream has valid length and is seekable, use directly await _storageManager.UploadAsync(ImagePath, objectName, imageStream, size, ContentType, cancellationToken); } } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Failed to save image for layout level {LevelId}", layoutLevelId); throw; } } public async Task GetImageAsync(Guid layoutLevelId, CancellationToken cancellationToken = default) { var objectName = GetObjectName(layoutLevelId); try { var exists = await _storageManager.ExistsAsync(ImagePath, objectName, cancellationToken); if (!exists) { if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for layout level {LevelId}", layoutLevelId); return null; } var stream = await _storageManager.GetFileAsync(ImagePath, objectName, cancellationToken); return stream; } catch (FileNotFoundException) { if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for layout level {LevelId}", layoutLevelId); return null; } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Failed to read image for layout level {LevelId}", layoutLevelId); throw; } } public async Task DeleteImageAsync(Guid layoutLevelId, CancellationToken cancellationToken = default) { var objectName = GetObjectName(layoutLevelId); try { var exists = await _storageManager.ExistsAsync(ImagePath, objectName, cancellationToken); if (!exists) { if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for deletion: {LevelId}", layoutLevelId); return false; } await _storageManager.DeleteAsync(ImagePath, objectName, cancellationToken); return true; } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Failed to delete image for layout level {LevelId}", layoutLevelId); throw; } } public async Task ImageExistsAsync(Guid layoutLevelId, CancellationToken cancellationToken = default) { var objectName = GetObjectName(layoutLevelId); return await _storageManager.ExistsAsync(ImagePath, objectName, cancellationToken); } public async Task<(int width, int height)> GetImageDimensionsAsync(Stream imageStream, CancellationToken cancellationToken = default) { try { // Reset stream position if seekable if (imageStream.CanSeek) { imageStream.Position = 0; } // Load image to get dimensions using var image = await Image.LoadAsync(imageStream, cancellationToken); return (image.Width, image.Height); } catch (Exception ex) { _logger.LogError(ex, "Failed to extract image dimensions"); throw new InvalidOperationException("Invalid image format or corrupted file", ex); } } public void Dispose() { _storageManager?.Dispose(); GC.SuppressFinalize(this); } }