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

252 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 📁 معماری مدیریت فایل و تصاویر — 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<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
```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('/')}";
}
```