# 📁 معماری مدیریت فایل و تصاویر — CMS > **تاریخ:** ۱۴۰۴/۱۱/۲۸ (February 17, 2026) > **وضعیت:** ✅ عملیاتی > **Build:** 0 Error (هر ۳ پروژه) ✅ --- ## ۱. پیش‌زمینه سیستم قبلی از **FMS (File Management Service)** در آدرس `https://dl.afrino.co` استفاده می‌کرد که غیرقابل دسترس/ناسازگار شده بود. در چندین فاز، معماری فایل‌ها به صورت کامل بازنویسی شد: | فاز | شرح | وضعیت | |-----|------|-------| | ۱. حذف FMS | حذف کامل ۳ فایل مرده FMS | ✅ | | ۲. حالت base64 | ذخیره data URI مستقیم در DB | ✅ (بازنشسته) | | ۳. ذخیره دیسکی | فایل در دیسک + مسیر در DB + تبدیل به base64 هنگام serve | ✅ | | ۴. **سرو HTTP عمومی** | **اندپوینت `/uploads/{path}` + Fallback FMS** | **✅ جدید** | --- ## ۲. معماری نهایی ``` BackOffice (Blazor WASM) │ │ MudFileUpload → IBrowserFile → byte[] → gRPC ImageFileModel │ ▼ CMS gRPC Service │ │ proto ImageFileModel → Command.ImageFileBytes │ ▼ MediatR Handler │ │ IFileManager.UploadImageAsync(folder, bytes, mime, name) │ ▼ LocalFileManager │ ├─ Main Image → Uploads/Images/{folder}/{guid}.jpg (1200×1200, JPEG Q75) ├─ Thumbnail → Uploads/Images/{folder}/{guid}_thumb.jpg (300×300, JPEG Q75) │ │ Returns: { Main.Path, Thumbnail.Path } (relative paths stored in DB) │ ▼ ImagePathResolverInterceptor (gRPC response) │ │ Walks all response fields → reads file from disk → data:{mime};base64,{bytes} │ ▼ BackOffice / FrontOffice ← receives base64 data URI directly in proto fields ``` --- ## ۳. اجزای کلیدی ### ۳.۱ `IFileManager` — Interface **مسیر:** `Application/Common/FileManager/IFileManager.cs` ```csharp public interface IFileManager { Task UploadAsync(string folder, byte[] file, string mime, string? fileName, CancellationToken ct); Task UploadImageAsync(string folder, byte[] file, string mime, string? fileName, CancellationToken ct); Task DeleteAsync(string path, CancellationToken ct); string? ResolveImageUrl(string? path); } ``` - **`UploadAsync`** — آپلود فایل خام - **`UploadImageAsync`** — بهینه‌سازی + ساخت thumbnail خودکار (SixLabors.ImageSharp) - **`ResolveImageUrl`** — تبدیل مسیر نسبی به data URI (base64) ### ۳.۲ `LocalFileManager` — پیاده‌سازی **مسیر:** `Infrastructure/Services/LocalFileManager.cs` | ویژگی | مقدار | |-------|-------| | ریشه آپلود | `FileStorage:UploadPath` یا `AppContext.BaseDirectory/Uploads` | | فرمت تصویر اصلی | JPEG, Quality 75, حداکثر 1200×1200 | | فرمت thumbnail | JPEG, Quality 75, حداکثر 300×300 | | نام‌گذاری فایل | `{Guid}.jpg` + `{Guid}_thumb.jpg` | | DI Registration | `services.AddSingleton()` | ### ۳.۳ `ImagePathResolverInterceptor` — gRPC Interceptor **مسیر:** `WebApi/Interceptors/ImagePathResolverInterceptor.cs` اینترسپتور **خودکار** تمام فیلدهای تصویری را در response‌های gRPC پیدا کرده و مسیر نسبی را به data URI تبدیل می‌کند. **فیلدهای شناسایی‌شده:** - `image_path`, `thumbnail_path`, `image_thumbnail_path` - `featured_image_path`, `featured_image_thumbnail_path` - `hero_image_path`, `product_thumbnail_path` - `avatar_path`, `avatar_url` **قابلیت‌ها:** - Walk بازگشتی پیام‌های proto - پشتیبانی از `string` ساده و `Google.Protobuf.WellKnownTypes.StringValue` - پشتیبانی از فیلدهای `repeated` (collection‌های تو در تو) - اگر مقدار `data:` یا `http` باشد → رد می‌شود (تبدیل نمی‌شود) ### ۳.۴ `LoggingBehaviour` — پاکسازی لاگ **مسیر:** `WebApi/Common/Behaviours/LoggingBehaviour.cs` - فرمت لاگ: `JsonFormatter.Default.Format()` به جای `{@Request}` - پاکسازی فیلدهای باینری با regex (`File`, `ImageFile`, `image_file`, `file`) - محدودیت طول لاگ: حداکثر 2000 کاراکتر ### ۳.۵ `UploadsController` — سرو عمومی فایل‌ها (HTTP) 🆕 **مسیر:** `WebApi/Controllers/UploadsController.cs` اندپوینت عمومی REST برای سرو مستقیم تصاویر بدون نیاز به base64. مناسب برای بارگذاری تصاویر در تگ `` و کاهش پهنای باند. | ویژگی | مقدار | |-------|-------| | مسیر | `GET /uploads/{**path}` | | احراز هویت | `[AllowAnonymous]` — عمومی | | کش مرورگر | `ResponseCache 86400` ثانیه (۲۴ ساعت) | | Content-Type | تشخیص خودکار از پسوند فایل (`FileExtensionContentTypeProvider`) | | Range Requests | ✅ فعال (`enableRangeProcessing: true`) | | محافظت مسیر | جلوگیری از path traversal (`..`, `\`, `Path.GetFullPath` validation) | **FMS Fallback:** اگر فایل محلی وجود نداشته باشد و تنظیم `FMS:Address` پر باشد: 1. فایل از `{FMS:Address}/{relativePath}` دانلود می‌شود 2. Content-Type بررسی می‌شود (فقط `image/*` و `application/pdf` مجاز) 3. فایل روی دیسک محلی ذخیره و کش می‌شود 4. سپس فایل محلی سرو می‌شود ``` Client → GET /uploads/Images/BlogPosts/abc.jpg │ ├─ فایل محلی وجود دارد? → سرو مستقیم از دیسک │ └─ فایل محلی وجود ندارد? └─ FMS:Address تنظیم شده? ├─ بله → دانلود از dl.afrino.co → ذخیره محلی → سرو └─ خیر → 404 Not Found ``` **وابستگی‌ها:** - `IHttpClientFactory` با named client `"FMS"` (timeout: 30 ثانیه) - ثبت در `Program.cs`: `builder.Services.AddHttpClient("FMS", ...)` --- ## ۴. Proto Messages — ImageFileModel هر حوزه (DiscountProduct, BlogPost, SitePage) پیام مستقل `ImageFileModel` خود را دارد: ### DiscountProduct ```protobuf message ImageFileModel { bytes file = 1; string mime = 2; string file_name = 3; } ``` **استفاده در:** `CreateDiscountProductRequest`, `UpdateDiscountProductRequest` ### BlogPost ```protobuf message BlogImageFileModel { bytes file = 1; string mime = 2; string file_name = 3; } ``` **استفاده در:** `CreateBlogPostRequest`, `UpdateBlogPostRequest` ### SitePage ```protobuf message SitePageImageFileModel { bytes file = 1; string mime = 2; string file_name = 3; } ``` **استفاده در:** `UpdateSitePageRequest`, `CreateSitePageSectionRequest`, `UpdateSitePageSectionRequest` --- ## ۵. جریان آپلود تصویر (مثال: BlogPost) ``` 1. کاربر در BackOffice → MudFileUpload → انتخاب فایل 2. BlogPostEditDialog.OnImageSelected() → IBrowserFile.OpenReadStream() → byte[] + ContentType + FileName → پیش‌نمایش base64 در UI 3. Submit → BlogPostEditDto { ImageFile = bytes, ImageMime, ImageFileName } 4. BlogPostService.CreateAsync() → BlogImageFileModel { File = ByteString.CopyFrom(bytes), Mime, FileName } → gRPC CreateBlogPostRequest 5. CMS BlogPostService (gRPC) → CreateBlogPostCommand { ImageFileBytes = request.ImageFile.File.ToByteArray(), ... } 6. CreateBlogPostCommandHandler.Handle() → _fileManager.UploadImageAsync("Images/BlogPosts", bytes, mime, name) → post.FeaturedImagePath = result.Main.Path → post.FeaturedImageThumbnailPath = result.Thumbnail.Path 7. Response → ImagePathResolverInterceptor → featured_image_path → data:image/jpeg;base64,... → featured_image_thumbnail_path → data:image/jpeg;base64,... 8. BackOffice / FrontOffice → نمایش مستقیم base64 data URI ``` --- ## ۶. فایل‌های حذف‌شده (کد مرده FMS) | فایل | شرح | |------|------| | `Infrastructure/Services/FmsFileManager.cs` | پیاده‌سازی قدیمی FMS (HTTP upload) | | `Application/Common/FileManager/FileManagementService.cs` | سرویس قدیمی مدیریت فایل | | `Application/Common/FileManager/IFileManagementService.cs` | اینترفیس قدیمی | --- ## ۷. تنظیمات ### `appsettings.json` (CMS) ```json { "FileStorage": { "UploadPath": "/app/Uploads" } } ``` ### محدودیت حجم gRPC ```csharp // Program.cs services.AddGrpc(o => o.MaxReceiveMessageSize = 50 * 1024 * 1024); // 50MB ``` ### FrontOffice — `UrlUtility.GetImageUrl()` ```csharp public static string GetImageUrl(string? path) { if (string.IsNullOrWhiteSpace(path)) return string.Empty; if (path.StartsWith("data:") || path.StartsWith("http")) return path; return $"{DownloadUrl?.TrimEnd('/')}/{path.TrimStart('/')}"; } ```