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" }; } }