using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using RobotNet10.StorageManager; using SixLabors.ImageSharp; namespace RobotNet10.FleetManager.Services; /// /// Service implementation for robot model image storage using StorageManager /// Stores images with naming: {robotModelId}.png /// public class RobotModelImageStorageService : IRobotModelImageStorageService, IDisposable { private readonly ILogger _logger; private readonly StorageManager.StorageManager _storageManager; private const string ImagePath = "robotModelImages"; private const string ContentType = "image/png"; public RobotModelImageStorageService(IOptionsMonitor optionsSnapshot, ILogger logger) { _logger = logger; var config = optionsSnapshot.Get("RobotModelImages"); ArgumentNullException.ThrowIfNull(config); _storageManager = new StorageManager.StorageManager(config); if (_logger.IsEnabled(LogLevel.Information)) { _logger.LogInformation("RobotModelImageStorageService initialized"); } } private static string GetObjectName(Guid robotModelId) => robotModelId.ToString(); public async Task SaveImageAsync(Guid robotModelId, Stream imageStream, CancellationToken cancellationToken = default) { var objectName = GetObjectName(robotModelId); 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 robot model {RobotModelId}", robotModelId); throw; } } public async Task GetImageAsync(Guid robotModelId, CancellationToken cancellationToken = default) { var objectName = GetObjectName(robotModelId); try { var exists = await _storageManager.ExistsAsync(ImagePath, objectName, cancellationToken); if (!exists) { if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for robot model {RobotModelId}", robotModelId); 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 robot model {RobotModelId}", robotModelId); return null; } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Error)) _logger.LogError(ex, "Failed to read image for robot model {RobotModelId}", robotModelId); throw; } } public async Task DeleteImageAsync(Guid robotModelId, CancellationToken cancellationToken = default) { var objectName = GetObjectName(robotModelId); try { var exists = await _storageManager.ExistsAsync(ImagePath, objectName, cancellationToken); if (!exists) { if (_logger.IsEnabled(LogLevel.Warning)) _logger.LogWarning("Image not found for deletion: {RobotModelId}", robotModelId); 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 robot model {RobotModelId}", robotModelId); throw; } } public async Task ImageExistsAsync(Guid robotModelId, CancellationToken cancellationToken = default) { var objectName = GetObjectName(robotModelId); 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); } }