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; /// /// 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. /// /// /// 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. /// public class ConfirmUploadCommandHandler : IRequestHandler { private readonly IFileRepository _fileRepository; private readonly IQuotaRepository _quotaRepository; private readonly IStorageProviderFactory _storageProviderFactory; private readonly StorageSettings _settings; private readonly IRedisCacheService _cache; private readonly ILogger _logger; public ConfirmUploadCommandHandler( IFileRepository fileRepository, IQuotaRepository quotaRepository, IStorageProviderFactory storageProviderFactory, IOptions settings, IRedisCacheService cache, ILogger logger) { _fileRepository = fileRepository; _quotaRepository = quotaRepository; _storageProviderFactory = storageProviderFactory; _settings = settings.Value; _cache = cache; _logger = logger; } public async Task 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."); } } /// /// EN: Validate that object key belongs to the specified user. /// VI: Xác minh object key thuộc về user được chỉ định. /// /// /// EN: Object key format: {accessLevel}/{userId}/{date}/{fileId}_{filename} /// VI: Định dạng object key: {accessLevel}/{userId}/{date}/{fileId}_{filename} /// 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; } }