using FileService.Application.Ports; using FileService.Application.StorageContracts; using FileService.Domain.ValueObjects; using IM.Commons; using IM.InitCommon; using Microsoft.Extensions.Options; using StackExchange.Redis; namespace FileService.Infrastructure.Storage { public class LocalStorageAdapter(IStorageRedisCache redis, IOptions options) : IObjectStoragePort { private readonly IStorageRedisCache redis = redis; private readonly IOptions options = options; public string ProviderCode => "Local"; public async Task CompleteUploadAsync(CompleteUploadCommand command, CancellationToken token) { var res = await MergeAsync(command.UploadSessionId, command.ObjectKey, command.Parts); return new CompleteUploadResult(new StorageLocation( storageProvider: command.ProviderCode, bucket: command.Bucket, objectKey: command.ObjectKey, region: command.Region ), null, command.Parts.Sum(x => x.Size).Value); } public async Task> MergeAsync(string sessionId, string objectKey, IReadOnlyList parts) { var rootPath = options.Value.Providers[options.Value.DefaultProviderCode].LocalRootPath; var tempPath = Path.Combine(rootPath, "temp"); // 项目根目录下 uploads // 最终文件存储路径(这里可以用你之前 ObjectNameGenerator 生成的名字) var finalPath = Path.Combine(rootPath, objectKey); var finalDir = Path.GetDirectoryName(finalPath); Directory.CreateDirectory(finalDir); var storageCache = await redis.GetAsync(sessionId); var totalChunks = storageCache.TotalPartCount; try { using (var finalStream = new FileStream(finalPath, FileMode.Create)) { for (var i = 1; i <= totalChunks; i++) { var progress = (i * 100.0 / totalChunks); if (i % 5 == 0 || i == totalChunks) { //await _redis.HashSetAsync(RedisKeys.MergeStatus(taskId), new HashEntry[] //{ // new("status", "processing"), // new("progress", progress.ToString("F2")) //}); } var chunkPath = Path.Combine(tempPath, $"{i}.part.tmp"); if (!File.Exists(chunkPath)) return Result.Fail(ResultCode.CHUNK_NOT_FOUND); using (var chunkStream = new FileStream(chunkPath, FileMode.Open)) { await chunkStream.CopyToAsync(finalStream); } } Directory.Delete(tempPath, true); await redis.DeleteAsync(sessionId); } return Result.Success(); } catch (Exception e) { //_logger.LogError(e, e.Message); throw; } } public async Task GenerateUploadUrlAsync(GenerateUploadUrlCommand command, CancellationToken token) { var baseUrl = options.Value.Providers[options.Value.DefaultProviderCode].LocalUploadApiBaseUrl; return new PresignedUrl( baseUrl + $"?partNumber={command.PartNumber}", new Dictionary(), ExpiresAt: DateTimeOffset.Now.Add(options.Value.Providers[options.Value.DefaultProviderCode].UploadUrlExpiresIn) ); } public async Task InitUploadAsync(InitiateUploadCommand command, CancellationToken token) { var sessionId = Guid.NewGuid(); var location = new StorageLocation(); return new InitiateUploadResult(sessionId.ToString(),location); } } }