- 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.
180 lines
7.8 KiB
C#
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;
|
|
}
|
|
}
|