Files
pos-system/services/storage-service-net/src/StorageService.API/Application/Commands/ConfirmUploadCommandHandler.cs
Ho Ngoc Hai 9c648c2d35 feat(cdn): Add CDN URL retrieval for public files and enhance caching mechanisms
- Introduced a new endpoint to retrieve CDN URLs for public files, falling back to pre-signed URLs when necessary.
- Enhanced caching for file metadata retrieval in GetFileQueryHandler to improve performance.
- Updated file handling commands to invalidate relevant caches upon file operations.
- Added configuration settings for CDN in appsettings.json to manage CDN behavior.
- Implemented new data models for CDN URL responses and integrated them into the API response structure.
2026-01-14 00:20:31 +07:00

180 lines
7.8 KiB
C#

using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StorageService.API.Application.Queries;
using StorageService.Domain.AggregatesModel.FileAggregate;
using StorageService.Domain.AggregatesModel.QuotaAggregate;
using StorageService.Infrastructure.Caching;
using StorageService.Infrastructure.Configuration;
using StorageService.Infrastructure.Storage;
namespace StorageService.API.Application.Commands;
/// <summary>
/// EN: Handler for ConfirmUploadCommand - confirms direct upload and saves metadata.
/// VI: Handler cho ConfirmUploadCommand - xác nhận upload trực tiếp và lưu metadata.
/// </summary>
/// <remarks>
/// EN: Called after client successfully uploads file using pre-signed URL.
/// Verifies file exists in storage, saves metadata to database, and updates quota.
/// VI: Được gọi sau khi client upload file thành công bằng pre-signed URL.
/// Xác minh file tồn tại trong storage, lưu metadata vào database, và cập nhật quota.
/// </remarks>
public class ConfirmUploadCommandHandler : IRequestHandler<ConfirmUploadCommand, ConfirmUploadResult>
{
private readonly IFileRepository _fileRepository;
private readonly IQuotaRepository _quotaRepository;
private readonly IStorageProviderFactory _storageProviderFactory;
private readonly StorageSettings _settings;
private readonly IRedisCacheService _cache;
private readonly ILogger<ConfirmUploadCommandHandler> _logger;
public ConfirmUploadCommandHandler(
IFileRepository fileRepository,
IQuotaRepository quotaRepository,
IStorageProviderFactory storageProviderFactory,
IOptions<StorageSettings> settings,
IRedisCacheService cache,
ILogger<ConfirmUploadCommandHandler> logger)
{
_fileRepository = fileRepository;
_quotaRepository = quotaRepository;
_storageProviderFactory = storageProviderFactory;
_settings = settings.Value;
_cache = cache;
_logger = logger;
}
public async Task<ConfirmUploadResult> Handle(ConfirmUploadCommand request, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation(
"Confirm upload for user {UserId}, objectKey: {ObjectKey}",
request.UserId, request.ObjectKey);
// EN: Validate object key belongs to user
// VI: Kiểm tra object key thuộc về user
if (!ValidateObjectKeyOwnership(request.ObjectKey, request.UserId))
{
_logger.LogWarning(
"Object key {ObjectKey} does not belong to user {UserId}",
request.ObjectKey, request.UserId);
return ConfirmUploadResult.Fail("Invalid object key.");
}
var bucketName = _settings.DefaultBucket;
var provider = _storageProviderFactory.GetProvider();
// EN: Verify file exists in storage / VI: Xác minh file tồn tại trong storage
var exists = await provider.ExistsAsync(bucketName, request.ObjectKey, cancellationToken);
if (!exists)
{
_logger.LogWarning(
"File not found in storage: {Bucket}/{ObjectKey}",
bucketName, request.ObjectKey);
return ConfirmUploadResult.Fail("File not found in storage. Upload may have failed.");
}
// EN: Check if already confirmed (idempotency)
// VI: Kiểm tra đã xác nhận chưa (idempotency)
var existingFile = await _fileRepository.GetByObjectKeyAsync(request.ObjectKey, cancellationToken);
if (existingFile != null)
{
_logger.LogInformation(
"Upload already confirmed for objectKey: {ObjectKey}, fileId: {FileId}",
request.ObjectKey, existingFile.Id);
return ConfirmUploadResult.Ok(existingFile.Id, existingFile.ToDto());
}
// EN: Get or create quota / VI: Lấy hoặc tạo quota
var quota = await _quotaRepository.GetOrCreateAsync(request.UserId, cancellationToken);
// EN: Double-check quota (in case of concurrent uploads)
// VI: Kiểm tra lại quota (phòng trường hợp upload đồng thời)
if (!quota.CanUpload(request.FileSizeBytes))
{
_logger.LogWarning(
"Quota exceeded for user {UserId} during confirmation",
request.UserId);
// EN: Delete the uploaded file since quota exceeded
// VI: Xóa file đã upload vì vượt quota
await provider.DeleteAsync(bucketName, request.ObjectKey, cancellationToken);
return ConfirmUploadResult.Fail(
"Storage quota exceeded. The uploaded file has been removed.");
}
// EN: Create file metadata / VI: Tạo metadata file
var storageFile = new StorageFile(
request.FileName,
bucketName,
request.ObjectKey,
request.ContentType,
request.FileSizeBytes,
request.UserId,
provider.ProviderType,
request.AccessLevel,
request.TenantId,
checksum: null);
await _fileRepository.AddAsync(storageFile, cancellationToken);
// EN: Update quota / VI: Cập nhật quota
quota.AddUsage(request.FileSizeBytes);
_quotaRepository.Update(quota);
// EN: Save changes / VI: Lưu thay đổi
await _fileRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
// EN: Invalidate quota cache so next query gets updated value
// VI: Invalidate quota cache để query tiếp theo lấy giá trị mới
if (Guid.TryParse(request.UserId, out var userId))
{
await _cache.DeleteAsync(CacheKeys.UserQuota(userId), cancellationToken);
// EN: Invalidate user files list cache (for search results refresh)
// VI: Invalidate cache danh sách file của user (để refresh kết quả search)
await _cache.DeleteByPatternAsync(CacheKeys.UserFilesPattern(userId), cancellationToken);
}
_logger.LogInformation(
"Upload confirmed successfully: fileId={FileId}, objectKey={ObjectKey}, user={UserId}",
storageFile.Id, request.ObjectKey, request.UserId);
return ConfirmUploadResult.Ok(storageFile.Id, storageFile.ToDto());
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error confirming upload for user {UserId}, objectKey: {ObjectKey}",
request.UserId, request.ObjectKey);
return ConfirmUploadResult.Fail("An error occurred while confirming upload.");
}
}
/// <summary>
/// EN: Validate that object key belongs to the specified user.
/// VI: Xác minh object key thuộc về user được chỉ định.
/// </summary>
/// <remarks>
/// EN: Object key format: {accessLevel}/{userId}/{date}/{fileId}_{filename}
/// VI: Định dạng object key: {accessLevel}/{userId}/{date}/{fileId}_{filename}
/// </remarks>
private static bool ValidateObjectKeyOwnership(string objectKey, string userId)
{
if (string.IsNullOrEmpty(objectKey))
return false;
var parts = objectKey.Split('/');
if (parts.Length < 3)
return false;
// EN: Format: {prefix}/{userId}/{date}/...
// VI: Định dạng: {prefix}/{userId}/{date}/...
var ownerUserId = parts[1];
return ownerUserId == userId;
}
}