83 lines
7.1 KiB
C#
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;
|
|
}
|
|
}
|