This commit is contained in:
2026-08-31 14:42:22 +08:00
72 changed files with 2479 additions and 168 deletions
@@ -35,5 +35,12 @@ namespace FileService.Infrastructure.Reposites
{
return await db.Files.FirstOrDefaultAsync(x => x.Id == id);
}
public async Task<UploadFile?> FindByCheckSumGlobalAsync(string algorithm, string value)
{
return await db.Files.FirstOrDefaultAsync(
x => x.CheckSum.Algorithm == algorithm &&
x.CheckSum.Value == value
);
}
}
}
@@ -16,6 +16,60 @@ namespace FileService.Infrastructure.Storage
public string ProviderCode => "Local";
/// <summary>
/// 单次直传:直接写入 LocalRootPath/{bucket}/{objectKey}。
/// bucket 传公开桶名即落到公开目录,可被静态托管直链访问。
/// </summary>
public async Task<StorageLocation> PutObjectAsync(PutObjectCommand command, CancellationToken token)
{
var fullPath = Path.Combine(providerOptions.LocalRootPath!, command.Bucket, command.ObjectKey);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
await using (var fs = new FileStream(fullPath, FileMode.Create))
{
await command.Content.CopyToAsync(fs, token);
await fs.FlushAsync(token);
}
return new StorageLocation(
storageProvider: ProviderCode,
bucket: command.Bucket,
objectKey: command.ObjectKey,
region: providerOptions.Region);
}
/// <summary>
/// 公开桶文件返回静态托管直链;私有文件返回 null。
/// </summary>
public string? GetPublicUrl(StorageLocation location)
{
if (string.IsNullOrEmpty(providerOptions.PublicBucket) ||
!string.Equals(location.Bucket, providerOptions.PublicBucket, StringComparison.OrdinalIgnoreCase))
{
return null;
}
var baseUrl = (providerOptions.PublicBaseUrl ?? providerOptions.LocalUploadApiBaseUrl ?? string.Empty)
.TrimEnd('/');
var key = location.ObjectKey.Replace('\\', '/').TrimStart('/');
return $"{baseUrl}/static/{key}";
}
/// <summary>
/// 打开本地文件读取流:LocalRootPath/{bucket}/{objectKey}。
/// </summary>
public Task<Stream> OpenReadAsync(StorageLocation location, CancellationToken token)
{
var fullPath = Path.Combine(providerOptions.LocalRootPath!, location.Bucket, location.ObjectKey);
if (!File.Exists(fullPath))
{
throw new FileNotFoundException(fullPath);
}
Stream stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return Task.FromResult(stream);
}
public async Task<CompleteUploadResult> CompleteUploadAsync(CompleteUploadCommand command, CancellationToken token)
{
var res = await MergeAsync(command.UploadSessionId, command.ObjectKey, command.Parts);
@@ -20,18 +20,40 @@ namespace FileService.Infrastructure.Storage
public async Task SetAsync(UploadRuntimeCache upload)
{
string key = RedisHelper.GetUploadInfoKey(upload.UploadSessionId);
await redis.SetAsync<UploadRuntimeCache>(key, upload);
// 过期时间取 UploadRuntimeCache 的 ExpireAt 字段,兜底默认 24 小时
var ttl = upload.ExpireAt > upload.CreatedAt
? upload.ExpireAt - upload.CreatedAt
: TimeSpan.FromHours(24);
await redis.SetAsync<UploadRuntimeCache>(key, upload, ttl);
// 维护 taskId → sessionId 映射,用于 DeleteByTaskId 快速定位
var mapKey = RedisHelper.GetUploadTaskSessionMapKey(upload.TaskId);
await redis.SetAsync(mapKey, upload.UploadSessionId, ttl);
}
public async Task DeleteAsync(string sessionId)
{
string key = RedisHelper.GetUploadInfoKey(sessionId);
// 先取出缓存以清理映射 key
var cache = await redis.GetAsync<UploadRuntimeCache>(key);
if (cache != null)
{
var mapKey = RedisHelper.GetUploadTaskSessionMapKey(cache.TaskId);
await redis.RemoveAsync(mapKey);
}
await redis.RemoveAsync(key);
}
public Task DeleteByTaskIdAsync(string taskId)
public async Task DeleteByTaskIdAsync(string taskId)
{
throw new NotImplementedException();
var mapKey = RedisHelper.GetUploadTaskSessionMapKey(taskId);
var sessionId = await redis.GetAsync<string>(mapKey);
if (sessionId != null)
{
await DeleteAsync(sessionId);
}
}
public async Task<UploadRuntimeCache?> GetAsync(string sessionId)