171 lines
7.2 KiB
C#
171 lines
7.2 KiB
C#
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Assets;
|
|
using Tiku.Application.Jobs;
|
|
using Tiku.Application.Storage;
|
|
using Tiku.Domain.Common;
|
|
using Tiku.Domain.Content;
|
|
|
|
namespace Tiku.Infrastructure.Assets;
|
|
|
|
internal sealed class AssetUploadManagementService(AssetManagementDependencies dependencies)
|
|
: AssetManagementServiceBase(dependencies), IAssetUploadManagementService
|
|
{
|
|
public async Task<AssetUploadSignResult> SignUploadAsync(
|
|
AssetManagementActor actor,
|
|
AssetUploadSignCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(command.FileName);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(command.MimeType);
|
|
|
|
var storageConfig = await ResolveObjectStorageConfigAsync(actor.TenantId, cancellationToken);
|
|
var provider = storageConfig.Provider;
|
|
var bucket = storageConfig.Bucket;
|
|
var mimeType = objectStorageService.ValidateMimeType(command.MimeType.Trim());
|
|
var fileSizeBytes = objectStorageService.ValidateFileSize(command.FileSizeBytes);
|
|
var asset = await ResolveUploadAssetAsync(actor, command, provider, bucket, mimeType, fileSizeBytes,
|
|
cancellationToken);
|
|
var objectKey = objectStorageService.ValidateObjectKey(
|
|
actor.TenantId,
|
|
string.IsNullOrWhiteSpace(command.ObjectKey)
|
|
? CreateObjectKey(actor.TenantId, asset.Id, command.FileName)
|
|
: command.ObjectKey.Trim());
|
|
|
|
objectStorageService.AssertUploadProvider(provider);
|
|
objectStorageService.AssertWritableLocation(new StorageAssetLocation(provider, bucket, objectKey));
|
|
|
|
asset.StorageProvider = ToAssetStorageProvider(provider);
|
|
asset.Bucket = bucket;
|
|
asset.ObjectKey = objectKey;
|
|
asset.FileName = command.FileName.Trim();
|
|
asset.MimeType = mimeType;
|
|
asset.FileSizeBytes = fileSizeBytes;
|
|
asset.ChecksumSha256 = NormalizeChecksum(command.ChecksumSha256);
|
|
asset.UploadStatus = AssetUploadStatus.Pending;
|
|
asset.VerifiedAt = null;
|
|
asset.VerifiedBy = null;
|
|
asset.VerificationDetails = JsonDefaults.Object();
|
|
asset.SecurityScanStatus = AssetSecurityScanStatus.Pending;
|
|
asset.PreviewStatus = ResolveInitialPreviewStatus(asset.AssetType, mimeType);
|
|
asset.UpdatedBy = actor.UserId;
|
|
|
|
var expiresIn = ResolveUploadTtl(command.ExpiresInSeconds);
|
|
var upload = await objectStorageService.SignUploadAsync(
|
|
new ObjectStorageUploadSignRequest(
|
|
actor.TenantId,
|
|
provider,
|
|
bucket,
|
|
objectKey,
|
|
asset.FileName,
|
|
mimeType,
|
|
fileSizeBytes,
|
|
expiresIn,
|
|
true),
|
|
cancellationToken);
|
|
|
|
await contentAssetPersistence.SaveChangesAsync(cancellationToken);
|
|
return new AssetUploadSignResult(ToItem(asset), upload);
|
|
}
|
|
|
|
public async Task<AssetUploadConfirmResult> ConfirmUploadAsync(
|
|
AssetManagementActor actor,
|
|
AssetUploadConfirmCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var asset = await contentAssetPersistence.ContentAssets
|
|
.SingleOrDefaultAsync(
|
|
item =>
|
|
item.TenantId == actor.TenantId &&
|
|
item.Id == command.AssetId &&
|
|
item.Status == ContentStatus.Active,
|
|
cancellationToken);
|
|
|
|
if (asset is null) throw new AssetManagementException("Asset was not found.", "asset_not_found");
|
|
|
|
if (string.IsNullOrWhiteSpace(asset.ObjectKey) || string.IsNullOrWhiteSpace(asset.Bucket))
|
|
throw new AssetManagementException("Asset does not have a writable object location.",
|
|
"asset_location_missing");
|
|
|
|
var accountedBytesBefore = AccountedStorageBytes(asset);
|
|
var provider = ToObjectStorageProvider(asset.StorageProvider);
|
|
var declaredMimeType = string.IsNullOrWhiteSpace(command.MimeType) ? asset.MimeType : command.MimeType.Trim();
|
|
var declaredSize = command.FileSizeBytes ?? asset.FileSizeBytes;
|
|
var declaredChecksum = NormalizeChecksum(command.ChecksumSha256) ?? asset.ChecksumSha256;
|
|
var metadata = await objectStorageService.HeadObjectAsync(
|
|
new ObjectStorageHeadRequest(
|
|
actor.TenantId,
|
|
provider,
|
|
asset.Bucket,
|
|
asset.ObjectKey,
|
|
declaredMimeType,
|
|
declaredSize,
|
|
declaredChecksum),
|
|
cancellationToken);
|
|
|
|
asset.VerificationDetails = JsonSerializer.SerializeToElement(new
|
|
{
|
|
metadata.Exists,
|
|
metadata.SizeBytes,
|
|
metadata.MimeType,
|
|
metadata.ChecksumSha256,
|
|
metadata.ETag,
|
|
metadata.LastModified,
|
|
metadata.VerificationSource,
|
|
declared = new
|
|
{
|
|
MimeType = declaredMimeType,
|
|
FileSizeBytes = declaredSize,
|
|
ChecksumSha256 = declaredChecksum
|
|
}
|
|
});
|
|
|
|
if (!metadata.Exists)
|
|
{
|
|
asset.UploadStatus = AssetUploadStatus.Failed;
|
|
asset.UpdatedBy = actor.UserId;
|
|
await contentAssetPersistence.SaveChangesAsync(cancellationToken);
|
|
throw new AssetManagementException("Uploaded object was not found in object storage.",
|
|
"asset_upload_missing");
|
|
}
|
|
|
|
if (metadata.SizeBytes is not { } verifiedSizeBytes)
|
|
{
|
|
asset.UploadStatus = AssetUploadStatus.Failed;
|
|
asset.UpdatedBy = actor.UserId;
|
|
await contentAssetPersistence.SaveChangesAsync(cancellationToken);
|
|
throw new AssetManagementException(
|
|
"Object storage did not return a verified asset size.",
|
|
"asset_upload_size_unverified");
|
|
}
|
|
|
|
asset.UploadStatus = AssetUploadStatus.Verified;
|
|
asset.VerifiedAt = DateTimeOffset.UtcNow;
|
|
asset.VerifiedBy = actor.UserId;
|
|
asset.VerifiedSizeBytes = verifiedSizeBytes;
|
|
asset.VerifiedChecksumSha256 = NormalizeChecksum(metadata.ChecksumSha256) ?? declaredChecksum;
|
|
asset.MimeType = metadata.MimeType ?? declaredMimeType;
|
|
asset.FileSizeBytes = verifiedSizeBytes;
|
|
asset.SecurityScanStatus = AssetSecurityScanStatus.Pending;
|
|
asset.UpdatedBy = actor.UserId;
|
|
var accountedBytesAfter = AccountedStorageBytes(asset);
|
|
await SaveWithStorageQuotaAdjustmentAsync(
|
|
actor.TenantId,
|
|
accountedBytesAfter - accountedBytesBefore,
|
|
cancellationToken);
|
|
|
|
await backgroundJobService.EnqueueAsync(
|
|
new CreateBackgroundJobCommand(
|
|
actor.TenantId,
|
|
"asset_security_scan",
|
|
JsonSerializer.SerializeToElement(new { assetId = asset.Id }),
|
|
MaxRetries: 5,
|
|
IdempotencyKey:
|
|
$"asset:{asset.Id:N}:{asset.VerifiedChecksumSha256 ?? asset.VerifiedAt?.UtcTicks.ToString()}",
|
|
IsSystemJob: true),
|
|
cancellationToken);
|
|
|
|
return new AssetUploadConfirmResult(ToItem(asset), metadata);
|
|
}
|
|
}
|