9.1 KiB
📁 معماری مدیریت فایل و تصاویر — 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
public interface IFileManager
{
Task<UploadResult> UploadAsync(string folder, byte[] file, string mime, string? fileName, CancellationToken ct);
Task<ImageUploadResult> 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<IFileManager, LocalFileManager>() |
۳.۳ ImagePathResolverInterceptor — gRPC Interceptor
مسیر: WebApi/Interceptors/ImagePathResolverInterceptor.cs
اینترسپتور خودکار تمام فیلدهای تصویری را در responseهای gRPC پیدا کرده و مسیر نسبی را به data URI تبدیل میکند.
فیلدهای شناساییشده:
image_path,thumbnail_path,image_thumbnail_pathfeatured_image_path,featured_image_thumbnail_pathhero_image_path,product_thumbnail_pathavatar_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. مناسب برای بارگذاری تصاویر در تگ <img> و کاهش پهنای باند.
| ویژگی | مقدار |
|---|---|
| مسیر | GET /uploads/{**path} |
| احراز هویت | [AllowAnonymous] — عمومی |
| کش مرورگر | ResponseCache 86400 ثانیه (۲۴ ساعت) |
| Content-Type | تشخیص خودکار از پسوند فایل (FileExtensionContentTypeProvider) |
| Range Requests | ✅ فعال (enableRangeProcessing: true) |
| محافظت مسیر | جلوگیری از path traversal (.., \, Path.GetFullPath validation) |
FMS Fallback:
اگر فایل محلی وجود نداشته باشد و تنظیم FMS:Address پر باشد:
- فایل از
{FMS:Address}/{relativePath}دانلود میشود - Content-Type بررسی میشود (فقط
image/*وapplication/pdfمجاز) - فایل روی دیسک محلی ذخیره و کش میشود
- سپس فایل محلی سرو میشود
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
message ImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
استفاده در: CreateDiscountProductRequest, UpdateDiscountProductRequest
BlogPost
message BlogImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
استفاده در: CreateBlogPostRequest, UpdateBlogPostRequest
SitePage
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)
{
"FileStorage": {
"UploadPath": "/app/Uploads"
}
}
محدودیت حجم gRPC
// Program.cs
services.AddGrpc(o => o.MaxReceiveMessageSize = 50 * 1024 * 1024); // 50MB
FrontOffice — UrlUtility.GetImageUrl()
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('/')}";
}