Initial commit
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RobotNet10.StorageManager;
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace RobotNet10.MapManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// FileSystem-based image storage implementation using StorageManager
|
||||
/// Stores images in local folder with naming: {layoutLevelId}.png
|
||||
/// </summary>
|
||||
public class FileSystemImageStorageService : IImageStorageService, IDisposable
|
||||
{
|
||||
private readonly ILogger<FileSystemImageStorageService> _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<StorageConfig> optionsSnapshot, ILogger<FileSystemImageStorageService> 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<Stream?> 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<bool> 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<bool> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user