feat: add file management entities and services for chat message handling

This commit is contained in:
2026-01-05 15:40:06 +03:30
parent 3d2b5ff6bd
commit d2dd67343b
39 changed files with 3548 additions and 41 deletions

View File

@@ -0,0 +1,108 @@
using GozareshgirProgramManager.Application.Services.FileManagement;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement;
/// <summary>
/// سرویس ذخیره‌سازی فایل در سیستم فایل محلی
/// </summary>
public class LocalFileStorageService : IFileStorageService
{
private readonly string _uploadBasePath;
private readonly string _baseUrl;
public LocalFileStorageService(IConfiguration configuration,
IHttpContextAccessor httpContextAccessor, IHostEnvironment env)
{
// محاسبه مسیر پایه: اگر env نبود، از مسیر فعلی استفاده کن
var contentRoot = env.ContentRootPath;
_uploadBasePath = Path.Combine(contentRoot, "Storage");
// Base URL برای دسترسی به فایل‌ها
var request = httpContextAccessor.HttpContext?.Request;
if (request != null)
{
_baseUrl = $"{request.Scheme}://{request.Host}/uploads";
}
else
{
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/uploads";
}
// ایجاد پوشه اگر وجود نداشت
if (!Directory.Exists(_uploadBasePath))
{
Directory.CreateDirectory(_uploadBasePath);
}
}
public async Task<(string StoragePath, string StorageUrl)> UploadAsync(
Stream fileStream,
string uniqueFileName,
string category)
{
// ایجاد پوشه دسته‌بندی (مثلاً: Uploads/TaskChatMessage)
var categoryPath = Path.Combine(_uploadBasePath, category);
if (!Directory.Exists(categoryPath))
{
Directory.CreateDirectory(categoryPath);
}
// ایجاد زیرپوشه بر اساس تاریخ (مثلاً: Uploads/TaskChatMessage/2026/01)
var datePath = Path.Combine(categoryPath, DateTime.Now.Year.ToString(),
DateTime.Now.Month.ToString("00"));
if (!Directory.Exists(datePath))
{
Directory.CreateDirectory(datePath);
}
// مسیر کامل فایل
var storagePath = Path.Combine(datePath, uniqueFileName);
// ذخیره فایل
await using var fileStreamOutput = new FileStream(storagePath, FileMode.Create, FileAccess.Write);
await fileStream.CopyToAsync(fileStreamOutput);
// URL فایل
var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath)
.Replace("\\", "/");
var storageUrl = $"{_baseUrl}/{relativePath}";
return (storagePath, storageUrl);
}
public Task DeleteAsync(string storagePath)
{
if (File.Exists(storagePath))
{
File.Delete(storagePath);
}
return Task.CompletedTask;
}
public Task<Stream?> GetFileStreamAsync(string storagePath)
{
if (!File.Exists(storagePath))
{
return Task.FromResult<Stream?>(null);
}
var stream = new FileStream(storagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return Task.FromResult<Stream?>(stream);
}
public Task<bool> ExistsAsync(string storagePath)
{
return Task.FromResult(File.Exists(storagePath));
}
public string GetFileUrl(string storagePath)
{
var relativePath = Path.GetRelativePath(_uploadBasePath, storagePath)
.Replace("\\", "/");
return $"{_baseUrl}/{relativePath}";
}
}

View File

@@ -0,0 +1,111 @@
using GozareshgirProgramManager.Application.Services.FileManagement;
using Microsoft.Extensions.Configuration;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Jpeg;
namespace GozareshgirProgramManager.Infrastructure.Services.FileManagement;
/// <summary>
/// سرویس تولید thumbnail با استفاده از ImageSharp
/// </summary>
public class ThumbnailGeneratorService : IThumbnailGeneratorService
{
private readonly string _thumbnailBasePath;
private readonly string _baseUrl;
public ThumbnailGeneratorService(IConfiguration configuration)
{
_thumbnailBasePath = configuration["FileStorage:ThumbnailPath"]
?? Path.Combine(Directory.GetCurrentDirectory(), "Uploads", "Thumbnails");
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "http://localhost:5000/uploads";
if (!Directory.Exists(_thumbnailBasePath))
{
Directory.CreateDirectory(_thumbnailBasePath);
}
}
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateImageThumbnailAsync(
string imagePath,
int width = 200,
int height = 200)
{
try
{
if (!File.Exists(imagePath))
{
return null;
}
// بارگذاری تصویر
using var image = await Image.LoadAsync(imagePath);
// Resize با حفظ نسبت
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(width, height),
Mode = ResizeMode.Max
}));
// نام فایل thumbnail
var fileName = Path.GetFileNameWithoutExtension(imagePath);
var extension = Path.GetExtension(imagePath);
var thumbnailFileName = $"{fileName}_thumb{extension}";
// مسیر ذخیره thumbnail
var thumbnailPath = Path.Combine(_thumbnailBasePath, thumbnailFileName);
// ذخیره thumbnail با کیفیت 80
await image.SaveAsync(thumbnailPath, new JpegEncoder { Quality = 80 });
// URL thumbnail
var thumbnailUrl = $"{_baseUrl}/thumbnails/{thumbnailFileName}";
return (thumbnailPath, thumbnailUrl);
}
catch (Exception)
{
// در صورت خطا null برمی‌گردانیم
return null;
}
}
public async Task<(string ThumbnailPath, string ThumbnailUrl)?> GenerateVideoThumbnailAsync(string videoPath)
{
// TODO: برای Video thumbnail باید از FFmpeg استفاده کنیم
// فعلاً یک placeholder image برمی‌گردانیم
await Task.CompletedTask;
return null;
}
public Task DeleteThumbnailAsync(string thumbnailPath)
{
if (File.Exists(thumbnailPath))
{
File.Delete(thumbnailPath);
}
return Task.CompletedTask;
}
public async Task<(int Width, int Height)?> GetImageDimensionsAsync(string imagePath)
{
try
{
if (!File.Exists(imagePath))
{
return null;
}
var imageInfo = await Image.IdentifyAsync(imagePath);
return (imageInfo.Width, imageInfo.Height);
}
catch (Exception)
{
return null;
}
}
}