Files
docs/cms/FILE-MANAGEMENT-ARCHITECTURE.md
T

9.1 KiB
Raw Blame History

📁 معماری مدیریت فایل و تصاویر — 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_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. مناسب برای بارگذاری تصاویر در تگ <img> و کاهش پهنای باند.

ویژگی مقدار
مسیر 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

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('/')}";
}