XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.RegularExpressions;
using XFEToolBox.Core.Downloads;
using XFEToolBox.Core.Models.Users;
using XFEToolBox.Server.Profiles.Data;
using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Utilities.Server.Services.CoreService;

namespace XFEToolBox.Server.Services;

public partial class SoftwareAdminService : ServerCoreUserServiceBase
{
    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
    private static readonly Regex IdentifierPattern = new("^[a-zA-Z0-9._-]{1,64}$", RegexOptions.Compiled);

    public string SoftwareStorageRoot { get; set; } = string.Empty;
    public long MaxSoftwareBytes { get; set; }

    [EntryPoint("v1/manage/software/list")]
    public async Task ListSoftwareEntryPoint()
    {
        if (!await VerifyAdministrator()) return;
        await Close(MainDataProfile.SoftwareCatalog
            .OrderBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
            .ToArray());
    }

    [EntryPoint("v1/manage/software/upsert")]
    public async Task UpsertSoftwareEntryPoint()
    {
        if (!await VerifyAdministrator()) return;

        SoftwareCatalogItem? software;
        try
        {
            var softwareJson = Json?["software"]?.ToString();
            software = string.IsNullOrWhiteSpace(softwareJson)
                ? null
                : JsonSerializer.Deserialize<SoftwareCatalogItem>(softwareJson, JsonOptions);
        }
        catch (JsonException) { software = null; }

        var validationError = Validate(software);
        if (validationError is not null)
        {
            await CloseWithError(validationError, HttpStatusCode.BadRequest);
            return;
        }

        var existingIndex = -1;
        for (var index = 0; index < MainDataProfile.SoftwareCatalog.Count; index++)
        {
            if (!string.Equals(MainDataProfile.SoftwareCatalog[index].Id, software!.Id, StringComparison.OrdinalIgnoreCase)) continue;
            existingIndex = index;
            break;
        }
        if (existingIndex >= 0)
        {
            PreserveStoredFiles(MainDataProfile.SoftwareCatalog[existingIndex], software!);
            MainDataProfile.SoftwareCatalog[existingIndex] = software!;
        }
        else
        {
            MainDataProfile.SoftwareCatalog.Add(software!);
        }

        MainDataProfile.SaveProfile();
        await Close(software!);
    }

    [EntryPoint("v1/manage/software/publication")]
    public async Task SetPublicationEntryPoint()
    {
        if (!await VerifyAdministrator()) return;
        var software = FindSoftware(GetString("softwareId"));
        var published = GetNullableBoolean("published");
        if (software is null || !published.HasValue)
        {
            await CloseWithError("软件不存在,或发布状态无效。", HttpStatusCode.BadRequest);
            return;
        }

        software.Published = published.Value;
        MainDataProfile.SaveProfile();
        await Close(software);
    }

    [EntryPoint("v1/manage/software/upload")]
    public async Task UploadSoftwareFileEntryPoint()
    {
        if (!await VerifyAdministrator()) return;
        var software = FindSoftware(GetString("softwareId"));
        var channelId = GetString("channelId");
        var fileName = Path.GetFileName(GetString("fileName") ?? "software.bin");
        var fileBase64 = GetString("fileBase64", trim: false);
        var channel = software?.Channels.FirstOrDefault(item =>
            string.Equals(item.Id, channelId, StringComparison.OrdinalIgnoreCase));
        if (software is null || channel is null || fileBase64 is null)
        {
            await CloseWithError("软件、渠道或文件内容无效。", HttpStatusCode.BadRequest);
            return;
        }

        if (MaxSoftwareBytes <= 0 || fileBase64.Length > checked(MaxSoftwareBytes * 2))
        {
            await CloseWithError("软件文件超过服务器限制。", HttpStatusCode.RequestEntityTooLarge);
            return;
        }

        byte[] bytes;
        try { bytes = Convert.FromBase64String(fileBase64); }
        catch (FormatException)
        {
            await CloseWithError("软件文件 Base64 无效。", HttpStatusCode.BadRequest);
            return;
        }

        if (bytes.LongLength > MaxSoftwareBytes)
        {
            await CloseWithError("软件文件超过服务器限制。", HttpStatusCode.RequestEntityTooLarge);
            return;
        }

        var relativeDirectory = Path.Combine(SanitizeSegment(software.Id), SanitizeSegment(channel.Id));
        var directory = Path.Combine(SoftwareStorageRoot, relativeDirectory);
        Directory.CreateDirectory(directory);
        var storedName = $"{Guid.NewGuid():N}_{SanitizeFileName(fileName)}";
        var fullPath = Path.Combine(directory, storedName);
        await File.WriteAllBytesAsync(fullPath, bytes);

        var oldPath = ResolveStoragePath(channel.StorageKey);
        channel.StorageKey = Path.Combine(relativeDirectory, storedName).Replace(Path.DirectorySeparatorChar, '/');
        channel.Mode = SoftwareDownloadMode.Server;
        channel.Url = string.Empty;
        channel.FileName = fileName;
        channel.Sha256 = Convert.ToHexString(SHA256.HashData(bytes));
        channel.Enabled = true;
        MainDataProfile.SaveProfile();
        if (oldPath is not null && File.Exists(oldPath) && !string.Equals(oldPath, fullPath, StringComparison.OrdinalIgnoreCase))
            File.Delete(oldPath);

        await Close(software);
    }

    [EntryPoint("v1/manage/software/delete")]
    public async Task DeleteSoftwareEntryPoint()
    {
        if (!await VerifyAdministrator()) return;
        var software = FindSoftware(GetString("softwareId"));
        if (software is null)
        {
            await CloseWithError("软件不存在。", HttpStatusCode.NotFound);
            return;
        }

        MainDataProfile.SoftwareCatalog.Remove(software);
        MainDataProfile.SaveProfile();
        var softwareDirectory = Path.Combine(SoftwareStorageRoot, SanitizeSegment(software.Id));
        if (Directory.Exists(softwareDirectory)) Directory.Delete(softwareDirectory, recursive: true);
        await Close(new { software.Id, deleted = true });
    }

    private static string? Validate(SoftwareCatalogItem? software)
    {
        if (software is null) return "软件信息无效。";
        software.Id = software.Id.Trim();
        software.Name = software.Name.Trim();
        if (!IdentifierPattern.IsMatch(software.Id)) return "软件标识只能包含字母、数字、点、下划线和短横线,长度不超过 64。";
        if (software.Name.Length is < 1 or > 80) return "软件名称长度应为 1-80 个字符。";
        if (software.Channels.Length == 0) return "请至少添加一个下载渠道。";

        var channelIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var channel in software.Channels)
        {
            channel.Id = channel.Id.Trim();
            channel.Name = channel.Name.Trim();
            if (!IdentifierPattern.IsMatch(channel.Id)) return "渠道标识格式无效。";
            if (channel.Name.Length is < 1 or > 40) return "渠道名称长度应为 1-40 个字符。";
            if (!channelIds.Add(channel.Id)) return "渠道标识不能重复。";
            if (channel.Mode != SoftwareDownloadMode.Server && !IsWebAddress(channel.Url))
                return $"渠道“{channel.Name}”需要有效的 HTTP(S) 地址。";
        }
        return null;
    }

    private static void PreserveStoredFiles(SoftwareCatalogItem existing, SoftwareCatalogItem updated)
    {
        foreach (var channel in updated.Channels.Where(channel => string.IsNullOrWhiteSpace(channel.StorageKey)))
        {
            var oldChannel = existing.Channels.FirstOrDefault(item =>
                string.Equals(item.Id, channel.Id, StringComparison.OrdinalIgnoreCase));
            if (oldChannel?.Mode != SoftwareDownloadMode.Server) continue;
            channel.StorageKey = oldChannel.StorageKey;
            if (channel.Mode == SoftwareDownloadMode.Server)
            {
                channel.FileName = string.IsNullOrWhiteSpace(channel.FileName) ? oldChannel.FileName : channel.FileName;
                channel.Sha256 = string.IsNullOrWhiteSpace(channel.Sha256) ? oldChannel.Sha256 : channel.Sha256;
            }
        }
    }

    private SoftwareCatalogItem? FindSoftware(string? id) => MainDataProfile.SoftwareCatalog.FirstOrDefault(item =>
        string.Equals(item.Id, id, StringComparison.OrdinalIgnoreCase));

    private async Task<bool> VerifyAdministrator()
    {
        if (User.PermissionLevel >= (int)ToolBoxUserRole.Administrator) return true;
        await CloseWithError("需要管理员权限。", HttpStatusCode.Forbidden);
        return false;
    }

    private string? GetString(string propertyName, bool trim = true)
    {
        try
        {
            var value = Json?[propertyName]?.GetValue<string>();
            if (string.IsNullOrWhiteSpace(value)) return null;
            return trim ? value.Trim() : value;
        }
        catch (Exception exception) when (exception is InvalidOperationException or FormatException) { return null; }
    }

    private bool? GetNullableBoolean(string propertyName)
    {
        try { return Json?[propertyName]?.GetValue<bool>(); }
        catch (Exception exception) when (exception is InvalidOperationException or FormatException) { return null; }
    }

    private string? ResolveStoragePath(string storageKey)
    {
        if (string.IsNullOrWhiteSpace(storageKey)) return null;
        var root = Path.GetFullPath(SoftwareStorageRoot);
        var path = Path.GetFullPath(Path.Combine(root, storageKey.Replace('/', Path.DirectorySeparatorChar)));
        return path.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ? path : null;
    }

    private static string SanitizeSegment(string value) =>
        new(value.Where(character => char.IsLetterOrDigit(character) || character is '-' or '_' or '.').ToArray());

    private static string SanitizeFileName(string value)
    {
        foreach (var character in Path.GetInvalidFileNameChars()) value = value.Replace(character, '_');
        return string.IsNullOrWhiteSpace(value) ? "software.bin" : value;
    }

    private static bool IsWebAddress(string value) =>
        Uri.TryCreate(value, UriKind.Absolute, out var uri) &&
        (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}