using System.IO;
using System.Net.Http;
using CMSMicroservice.Application.Common.FileManager;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
namespace CMSMicroservice.Infrastructure.Services;
///
/// فایلمنیجر محلی — فایلها روی دیسک ذخیره میشوند
/// مسیر نسبی در دیتابیس ذخیره میشود
/// موقع واکشی: فایل از دیسک خوانده و به base64 data-URI تبدیل میشود
///
public sealed class LocalFileManager : IFileManager
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly string _uploadRoot;
private readonly string _fmsBaseUrl;
// ── تنظیمات بهینهسازی تصویر ──
private const int MainMaxWidth = 1200;
private const int MainMaxHeight = 1200;
private const int ThumbMaxWidth = 300;
private const int ThumbMaxHeight = 300;
private const int JpegQuality = 75;
public LocalFileManager(IConfiguration configuration, IHttpClientFactory httpClientFactory, ILogger logger)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
// مسیر ذخیره فایلها — پیشفرض: پوشه Uploads در کنار WebApi
_uploadRoot = configuration["FileStorage:UploadPath"]
?? Path.Combine(AppContext.BaseDirectory, "Uploads");
_fmsBaseUrl = configuration["FMS:Address"]?.TrimEnd('/') ?? "https://dl.afrino.co";
Directory.CreateDirectory(_uploadRoot);
_logger.LogInformation("LocalFileManager initialized — UploadRoot: {Root}", _uploadRoot);
}
// ────────────────────────────────────────────────────
// آپلود فایل خام → ذخیره روی دیسک → برگرداندن مسیر نسبی
// ────────────────────────────────────────────────────
public async Task UploadAsync(
string directory, byte[] fileBytes, string mime,
string? fileName = null, CancellationToken ct = default)
{
if (fileBytes is not { Length: > 0 })
throw new FileUploadException("فایلی برای آپلود ارسال نشده است");
try
{
var ext = GetExtension(mime, fileName);
var uniqueName = $"{Guid.NewGuid():N}{ext}";
var relativePath = Path.Combine(directory, uniqueName).Replace('\\', '/');
var fullPath = Path.Combine(_uploadRoot, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
await File.WriteAllBytesAsync(fullPath, fileBytes, ct);
_logger.LogInformation(
"File saved — Path: {Path}, Size: {Size}KB",
relativePath, fileBytes.Length / 1024);
return new UploadedFile(0, relativePath);
}
catch (FileUploadException) { throw; }
catch (Exception ex)
{
_logger.LogError(ex, "خطا در ذخیره فایل — Directory: {Dir}", directory);
throw new FileUploadException($"خطا در ذخیره فایل: {ex.Message}", ex);
}
}
// ────────────────────────────────────────────────────
// آپلود تصویر + بندانگشتی → ذخیره روی دیسک
// ────────────────────────────────────────────────────
public async Task UploadImageAsync(
string directory, byte[] fileBytes, string mime,
string? fileName = null, CancellationToken ct = default)
{
if (fileBytes is not { Length: > 0 })
throw new FileUploadException("تصویری برای آپلود ارسال نشده است");
var baseName = Guid.NewGuid().ToString("N");
// ① بهینهسازی و ذخیره تصویر اصلی
var mainBytes = await OptimizeAsync(fileBytes, MainMaxWidth, MainMaxHeight);
var mainRelative = Path.Combine(directory, $"{baseName}.jpg").Replace('\\', '/');
var mainFull = Path.Combine(_uploadRoot, mainRelative);
Directory.CreateDirectory(Path.GetDirectoryName(mainFull)!);
await File.WriteAllBytesAsync(mainFull, mainBytes, ct);
var main = new UploadedFile(0, mainRelative);
// ② ساخت و ذخیره بندانگشتی
var thumbBytes = await OptimizeAsync(fileBytes, ThumbMaxWidth, ThumbMaxHeight);
var thumbRelative = Path.Combine(directory, $"{baseName}_thumb.jpg").Replace('\\', '/');
var thumbFull = Path.Combine(_uploadRoot, thumbRelative);
await File.WriteAllBytesAsync(thumbFull, thumbBytes, ct);
var thumb = new UploadedFile(0, thumbRelative);
_logger.LogInformation(
"Image saved — Main: {MainPath} ({MainKB}KB), Thumb: {ThumbPath} ({ThumbKB}KB)",
mainRelative, mainBytes.Length / 1024,
thumbRelative, thumbBytes.Length / 1024);
return new UploadedImage(main, thumb);
}
// ────────────────────────────────────────────────────
// حذف فایل از دیسک
// ────────────────────────────────────────────────────
public Task DeleteAsync(long fileId, CancellationToken ct = default)
{
_logger.LogWarning("DeleteAsync called with fileId={Id} — file deletion by ID not supported in disk mode", fileId);
return Task.CompletedTask;
}
// ────────────────────────────────────────────────────
// خواندن فایل از دیسک → تبدیل به base64 data-URI
// ────────────────────────────────────────────────────
public string ResolveImageUrl(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
// اگر از قبل data-URI یا URL مطلق هست، همان را برگردان
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
return path;
try
{
var fullPath = Path.Combine(_uploadRoot, path.TrimStart('/'));
if (!File.Exists(fullPath))
{
_logger.LogWarning("Image file not found on disk, trying FMS fallback: {Path}", fullPath);
// ── FMS Fallback: دانلود از dl.afrino.co و کش محلی (برای مهاجرت) ──
if (!TryDownloadFromFms(path.TrimStart('/'), fullPath))
return string.Empty;
_logger.LogInformation("Downloaded and cached from FMS: {Path}", path);
}
var bytes = File.ReadAllBytes(fullPath);
var mime = GetMimeFromExtension(Path.GetExtension(fullPath));
return $"data:{mime};base64,{Convert.ToBase64String(bytes)}";
}
catch (Exception ex)
{
_logger.LogError(ex, "Error reading image from disk: {Path}", path);
return string.Empty;
}
}
// ────────────────────────────────────────────────────
// FMS Fallback — دانلود از سرور قدیمی و کش محلی (مهاجرت)
// ────────────────────────────────────────────────────
private bool TryDownloadFromFms(string relativePath, string localPath)
{
try
{
var fmsUrl = $"{_fmsBaseUrl}/{relativePath}";
_logger.LogInformation("Attempting FMS download: {Url}", fmsUrl);
using var client = _httpClientFactory.CreateClient("FMS");
using var response = client.Send(new HttpRequestMessage(HttpMethod.Get, fmsUrl),
HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("FMS returned {Status} for {Url}", response.StatusCode, fmsUrl);
return false;
}
// ذخیره روی دیسک
var directory = Path.GetDirectoryName(localPath)!;
Directory.CreateDirectory(directory);
using var responseStream = response.Content.ReadAsStream();
using var fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None);
responseStream.CopyTo(fileStream);
_logger.LogInformation("Cached FMS file locally: {Path} ({Size} bytes)", relativePath, fileStream.Length);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to download from FMS: {Path}", relativePath);
return false;
}
}
// ────────────────────────────────────────────────────
// بهینهسازی تصویر (ریسایز + فشردهسازی JPEG)
// ────────────────────────────────────────────────────
private static async Task OptimizeAsync(byte[] imageBytes, int maxWidth, int maxHeight)
{
using var image = SixLabors.ImageSharp.Image.Load(imageBytes);
if (image.Width > maxWidth || image.Height > maxHeight)
{
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(maxWidth, maxHeight),
Mode = ResizeMode.Max
}));
}
using var ms = new MemoryStream();
await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = JpegQuality });
return ms.ToArray();
}
// ────────────────────────────────────────────────────
// پسوند فایل از mime type
// ────────────────────────────────────────────────────
private static string GetExtension(string mime, string? fileName)
{
if (!string.IsNullOrEmpty(fileName))
{
var ext = Path.GetExtension(fileName);
if (!string.IsNullOrEmpty(ext))
return ext.ToLowerInvariant();
}
return mime.ToLowerInvariant() switch
{
"image/jpeg" or "image/jpg" => ".jpg",
"image/png" => ".png",
"image/gif" => ".gif",
"image/webp" => ".webp",
"image/svg+xml" => ".svg",
"application/pdf" => ".pdf",
_ => ".bin"
};
}
private static string GetMimeFromExtension(string extension)
{
return extension.ToLowerInvariant() switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".webp" => "image/webp",
".svg" => "image/svg+xml",
".pdf" => "application/pdf",
_ => "application/octet-stream"
};
}
}