fix: align backend APIs and upload flow
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using FileService.Application.UploadFile;
|
||||
using FileService.Infrastructure;
|
||||
using IM.ASPNETCore;
|
||||
using IM.Commons;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -51,7 +52,8 @@ namespace FileService.WebApi.Controllers.File
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(Guid id)
|
||||
{
|
||||
var res = await service.GetFileInfoAsync(id);
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var res = await service.GetFileInfoAsync(id, Guid.Parse(userId));
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
@@ -65,11 +67,15 @@ namespace FileService.WebApi.Controllers.File
|
||||
var res = await service.OpenDownloadAsync(id, Guid.Parse(userId));
|
||||
if (res.Data == null)
|
||||
{
|
||||
if (res.Code == (int)ResultCode.PERMISSION_DENIED)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, res);
|
||||
}
|
||||
return NotFound(res);
|
||||
}
|
||||
|
||||
Response.Headers["Cache-Control"] = "private,max-age=86400";
|
||||
return File(res.Data.Content, res.Data.ContentType);
|
||||
return File(res.Data.Content, res.Data.ContentType, enableRangeProcessing: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace FileService.WebApi.Controllers.FileTask
|
||||
{
|
||||
public class CompleteTaskRequest
|
||||
{
|
||||
public string SessionId { get; set; }
|
||||
public List<UploadPart> Parts { get; set; }
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
public List<UploadPart> Parts { get; set; } = [];
|
||||
}
|
||||
|
||||
public class CompleteTaskRequestValidator : AbstractValidator<CompleteTaskRequest>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using FileService.Application.UploadFileTask;
|
||||
using FileService.Application.UploadFileTask;
|
||||
using FileService.Infrastructure;
|
||||
using IM.ASPNETCore;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -28,6 +28,8 @@ namespace FileService.WebApi.Controllers.FileTask
|
||||
var res = await service.InitTaskAsync(new UploadTaskInitCommand(
|
||||
UploaderId: Guid.Parse(userId),
|
||||
ConversationId: request.ConversationId,
|
||||
ChatType: request.ChatType,
|
||||
TargetId: request.TargetId,
|
||||
FileName: request.FileName,
|
||||
FileSize: request.FileSize,
|
||||
contentType: request.ContentType,
|
||||
@@ -44,6 +46,13 @@ namespace FileService.WebApi.Controllers.FileTask
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpGet("status")]
|
||||
public async Task<IActionResult> Status(Guid taskId)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return Ok(await service.GetStatusAsync(taskId, Guid.Parse(userId)));
|
||||
}
|
||||
|
||||
[HttpGet("Getuploadurl")]
|
||||
public async Task<IActionResult> GetUploadUrl(string sessionId, int partNum)
|
||||
{
|
||||
@@ -53,19 +62,20 @@ namespace FileService.WebApi.Controllers.FileTask
|
||||
}
|
||||
|
||||
[HttpPost("complete")]
|
||||
[UnitOfWork(typeof(FileDbContext))]
|
||||
public async Task<IActionResult> Complete([FromBody] CompleteTaskRequest request)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var res = await service.CompleteTaskAsync(new UploadTaskCompleteCommand(request.SessionId, Guid.Parse(userId), request.Parts));
|
||||
return Ok(res);
|
||||
return res.Succeeded ? Accepted(res) : Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("local/parts/upload")]
|
||||
public async Task<IActionResult> LocalUpload(string sessionId, int partNumber, IFormFile file)
|
||||
public async Task<IActionResult> LocalUpload([FromForm] string sessionId, [FromForm] int partNumber, IFormFile file)
|
||||
{
|
||||
//var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var stream = file.OpenReadStream();
|
||||
var res = await service.UploadPartAsync(new UploadPartCommand(stream, sessionId, partNumber, file.Length));
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
await using var stream = file.OpenReadStream();
|
||||
var res = await service.UploadPartAsync(new UploadPartCommand(stream, sessionId, partNumber, file.Length), Guid.Parse(userId));
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,30 @@ namespace FileService.WebApi.Controllers.FileTask
|
||||
public class FileTaskInitRequest
|
||||
{
|
||||
public Guid ConversationId { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public string? ChatType { get; set; }
|
||||
public Guid? TargetId { get; set; }
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
public string ContentType { get; set; }
|
||||
public string CheckSum { get; set; }
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
public string CheckSum { get; set; } = string.Empty;
|
||||
|
||||
}
|
||||
public class FileTaskInitRequestValidator: AbstractValidator<FileTaskInitRequest>
|
||||
{
|
||||
|
||||
public FileTaskInitRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.FileName).NotEmpty().MaximumLength(255);
|
||||
RuleFor(x => x.FileSize).GreaterThan(0);
|
||||
RuleFor(x => x.ContentType).NotEmpty().MaximumLength(255);
|
||||
RuleFor(x => x.CheckSum).NotEmpty().MaximumLength(128);
|
||||
RuleFor(x => x.ChatType)
|
||||
.Must(value => string.IsNullOrWhiteSpace(value) ||
|
||||
value.Equals("PRIVATE", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("GROUP", StringComparison.OrdinalIgnoreCase))
|
||||
.WithMessage("chatType 必须为 PRIVATE 或 GROUP");
|
||||
RuleFor(x => x.TargetId)
|
||||
.NotEmpty()
|
||||
.When(x => !string.IsNullOrWhiteSpace(x.ChatType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FileService.Infrastructure;
|
||||
using FileService.Infrastructure.Storage;
|
||||
using IM.InitCommon;
|
||||
using IM.InitCommon.Management;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace FileService.WebApi.Controllers;
|
||||
[ApiController, Route("internal/management/storage")]
|
||||
public class ManagementController(FileDbContext db, IOptionsSnapshot<StorageOptions> current, IConfiguration config) : ControllerBase
|
||||
{
|
||||
[HttpGet("summary")]
|
||||
public async Task<object> Summary(string? provider, string? type, DateTimeOffset? from, DateTimeOffset? to)
|
||||
{
|
||||
var files = db.Files.AsNoTracking().AsQueryable(); var tasks = db.Tasks.AsNoTracking().AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(provider)) { files = files.Where(x => x.StorageLocation.StorageProvider == provider); tasks = tasks.Where(x => x.StorageLocation.StorageProvider == provider); }
|
||||
if (!string.IsNullOrWhiteSpace(type)) { files = files.Where(x => x.ContentType.Value == type); tasks = tasks.Where(x => x.ContentType.Value == type); }
|
||||
if (from.HasValue) { files = files.Where(x => x.CreationTime >= from); tasks = tasks.Where(x => x.CreationTime >= from); }
|
||||
if (to.HasValue) { files = files.Where(x => x.CreationTime < to); tasks = tasks.Where(x => x.CreationTime < to); }
|
||||
var totals = await files.GroupBy(x => new { provider = x.StorageLocation.StorageProvider, type = x.ContentType.Value }).Select(g => new { g.Key.provider, g.Key.type, count = g.Count(), bytes = g.Sum(x => x.FileSize) }).ToListAsync();
|
||||
var states = await tasks.GroupBy(x => x.State).Select(g => new { state = g.Key.ToString(), count = g.Count() }).ToListAsync();
|
||||
var capacities = current.Value.Providers.Select(x => {
|
||||
long? total = null, available = null; string status = "未提供";
|
||||
if (x.Value.ProviderType == StorageProviderType.Local) try { var drive = new DriveInfo(Path.GetPathRoot(Path.GetFullPath(x.Value.LocalRootPath!))!); total = drive.TotalSize; available = drive.AvailableFreeSpace; status = "可用"; } catch { status = "不可用"; }
|
||||
return new { provider = x.Key, total, available, status };
|
||||
}).ToArray();
|
||||
return new { totals, tasks = states, capacities, checkedAt = DateTime.UtcNow };
|
||||
}
|
||||
[HttpPost("validate")]
|
||||
public async Task<object> Validate(InfrastructureEnvelope input)
|
||||
{
|
||||
var next = Parse(input);
|
||||
foreach (var (code, old) in current.Value.Providers) {
|
||||
var referenced = await db.Files.IgnoreQueryFilters().AnyAsync(x => x.StorageLocation.StorageProvider == code) || await db.Tasks.IgnoreQueryFilters().AnyAsync(x => x.StorageLocation.StorageProvider == code);
|
||||
if (!referenced) continue;
|
||||
if (!next.Providers.TryGetValue(code, out var p) || !p.Enabled || p.ProviderType != old.ProviderType || p.Bucket != old.Bucket || p.PublicBucket != old.PublicBucket || p.Endpoint != old.Endpoint || p.Region != old.Region || p.LocalRootPath != old.LocalRootPath || p.PublicBaseUrl != old.PublicBaseUrl)
|
||||
throw new IM.DomainCommons.DomainException("已有文件或上传任务引用该提供商,不能移除或更改定位参数");
|
||||
}
|
||||
return new { valid = true };
|
||||
}
|
||||
[HttpPost("test")]
|
||||
public async Task<object> Test(InfrastructureEnvelope input, CancellationToken ct)
|
||||
{
|
||||
await Validate(input); var next = Parse(input);
|
||||
foreach (var (code, provider) in next.Providers.Where(x => x.Value.Enabled)) {
|
||||
if (provider.ProviderType == StorageProviderType.Local) {
|
||||
var path = LocalStorageAdapter.SafePath(provider.LocalRootPath!, "im-admin-connectivity", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
try { await System.IO.File.WriteAllTextAsync(path, "IM connection test", ct); await System.IO.File.ReadAllTextAsync(path, ct); }
|
||||
finally { if (System.IO.File.Exists(path)) System.IO.File.Delete(path); }
|
||||
} else { using var adapter = new S3StorageAdapter(code, provider); await adapter.Test(ct); }
|
||||
}
|
||||
return new { tested = true };
|
||||
}
|
||||
StorageOptions Parse(InfrastructureEnvelope input)
|
||||
{
|
||||
var next = input.Value.Deserialize<StorageOptions>(new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? throw new IM.DomainCommons.DomainException("配置格式错误");
|
||||
if (next.Providers is null || !next.Providers.TryGetValue(next.DefaultProviderCode, out var chosen) || !chosen.Enabled) throw new IM.DomainCommons.DomainException("默认提供商无效");
|
||||
var secrets = JsonNode.Parse(string.IsNullOrEmpty(input.Secret) ? "{}" : input.Secret)!;
|
||||
foreach (var (code, p) in next.Providers) {
|
||||
if (p.ProviderCode != code || string.IsNullOrWhiteSpace(p.Bucket) || p.Bucket.IndexOfAny(['/', '\\']) >= 0 || p.PublicBucket?.IndexOfAny(['/', '\\']) >= 0 || p.Bucket is "." or ".." || p.PublicBucket is "." or "..") throw new IM.DomainCommons.DomainException("提供商或存储桶名称无效");
|
||||
if (p.DefaultPartSizeBytes < p.MinPartSizeBytes || p.MinPartSizeBytes < 1 || p.MaxPartCount < 1 || p.MaxObjectSizeBytes < 1) throw new IM.DomainCommons.DomainException("分片或容量限制无效");
|
||||
if (p.ProviderType == StorageProviderType.Local) {
|
||||
if (code != "Local" || string.IsNullOrWhiteSpace(p.LocalRootPath)) throw new IM.DomainCommons.DomainException("本地提供商编码必须为 Local");
|
||||
var root = Path.GetFullPath(p.LocalRootPath);
|
||||
var allowed = config.GetSection("Management:AllowedStorageRoots").Get<string[]>() ?? [];
|
||||
if (!allowed.Any(x => { var path = Path.GetFullPath(x).TrimEnd(Path.DirectorySeparatorChar); return root == path || root.StartsWith(path + Path.DirectorySeparatorChar, OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); })) throw new IM.DomainCommons.DomainException("本地目录未被部署允许");
|
||||
LocalStorageAdapter.SafePath(root, "im-admin-connectivity", "check");
|
||||
} else {
|
||||
if (p.ProviderType is not StorageProviderType.AwsS3 and not StorageProviderType.Minio || !Uri.TryCreate(p.Endpoint, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https") || !string.IsNullOrEmpty(uri.UserInfo)) throw new IM.DomainCommons.DomainException("不支持的存储端点");
|
||||
var allowed = config.GetSection("Management:AllowedInfrastructureHosts").Get<string[]>() ?? [];
|
||||
if (!allowed.Contains(uri.Host, StringComparer.OrdinalIgnoreCase)) throw new IM.DomainCommons.DomainException("存储主机未被部署允许");
|
||||
p.AccessKeyId = secrets[code]?["accessKeyId"]?.GetValue<string>(); p.AccessKeySecret = secrets[code]?["accessKeySecret"]?.GetValue<string>();
|
||||
if (string.IsNullOrWhiteSpace(p.AccessKeyId) || string.IsNullOrWhiteSpace(p.AccessKeySecret)) throw new IM.DomainCommons.DomainException("缺少存储凭据");
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user