Files
IM_NEW/FileService.WebApi/Controllers/ManagementController.cs
T

83 lines
7.1 KiB
C#

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;
}
}