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

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;
using XFEToolBox.Core.Downloads;
using XFEToolBox.Server.Profiles.Data;
using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

namespace XFEToolBox.Server.Services;

public partial class SoftwareCatalogService : ServerCoreStandardServiceBase
{
    public string SoftwareStorageRoot { get; set; } = string.Empty;

    [EntryPoint("v1/software/list")]
    public async Task GetSoftwareCatalogEntryPoint()
    {
        if (!TryGetString("search", out var search) || !TryGetString("category", out var category))
        {
            await CloseWithError("search 和 category 必须是字符串。", HttpStatusCode.BadRequest);
            return;
        }

        IEnumerable<SoftwareCatalogItem> filtered = MainDataProfile.SoftwareCatalog
            .Where(item => item.Enabled && item.Published)
            .Select(ToPublicItem)
            .Where(item => item.Channels.Length > 0);

        if (!string.IsNullOrWhiteSpace(search))
        {
            filtered = filtered.Where(item =>
                item.Name.Contains(search, StringComparison.OrdinalIgnoreCase) ||
                item.Summary.Contains(search, StringComparison.OrdinalIgnoreCase) ||
                item.Description.Contains(search, StringComparison.OrdinalIgnoreCase) ||
                item.Publisher.Contains(search, StringComparison.OrdinalIgnoreCase) ||
                item.Tags.Any(tag => tag.Contains(search, StringComparison.OrdinalIgnoreCase)));
        }

        if (!string.IsNullOrWhiteSpace(category))
            filtered = filtered.Where(item => string.Equals(item.Category, category, StringComparison.OrdinalIgnoreCase));

        var items = filtered
            .OrderByDescending(item => item.Featured)
            .ThenBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
            .ToArray();
        var categories = MainDataProfile.SoftwareCatalog
            .Where(item => item.Enabled && item.Published && !string.IsNullOrWhiteSpace(item.Category))
            .Select(item => item.Category.Trim())
            .Distinct(StringComparer.CurrentCultureIgnoreCase)
            .OrderBy(value => value, StringComparer.CurrentCultureIgnoreCase)
            .ToArray();

        await Close(new SoftwareCatalogResponse { Items = items, Categories = categories });
    }

    [EntryPoint("v1/software/download")]
    public async Task DownloadSoftwareEntryPoint()
    {
        var softwareId = GetRequiredString("softwareId");
        var channelId = GetRequiredString("channelId");
        var software = MainDataProfile.SoftwareCatalog.FirstOrDefault(item =>
            item.Enabled && item.Published && string.Equals(item.Id, softwareId, StringComparison.OrdinalIgnoreCase));
        var channel = software?.GetEffectiveChannels().FirstOrDefault(item =>
            item.Enabled && string.Equals(item.Id, channelId, StringComparison.OrdinalIgnoreCase));
        if (software is null || channel is null)
        {
            await CloseWithError("软件或下载渠道不存在。", HttpStatusCode.NotFound);
            return;
        }

        if (channel.Mode != SoftwareDownloadMode.Server || string.IsNullOrWhiteSpace(channel.StorageKey))
        {
            await CloseWithError("该渠道不是服务器文件。", HttpStatusCode.BadRequest);
            return;
        }

        var fullPath = ResolveStoragePath(channel.StorageKey);
        if (fullPath is null || !File.Exists(fullPath))
        {
            await CloseWithError("服务器文件不存在。", HttpStatusCode.NotFound);
            return;
        }

        await using var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920,
            FileOptions.Asynchronous | FileOptions.SequentialScan);
        await Close(stream);
    }

    private SoftwareCatalogItem ToPublicItem(SoftwareCatalogItem source) => new()
    {
        Id = source.Id,
        Name = source.Name,
        Summary = source.Summary,
        Description = source.Description,
        Publisher = source.Publisher,
        Category = source.Category,
        Version = source.Version,
        WebsiteUrl = source.WebsiteUrl,
        IconUrl = source.IconUrl,
        Notice = source.Notice,
        Tags = source.Tags,
        Featured = source.Featured,
        Enabled = source.Enabled,
        Published = source.Published,
        Channels = source.GetEffectiveChannels()
            .Where(IsAvailableChannel)
            .Select(channel => new SoftwareDownloadChannel
            {
                Id = channel.Id,
                Name = channel.Name,
                Url = channel.Url,
                Mode = channel.Mode,
                FileName = channel.FileName,
                Sha256 = channel.Sha256,
                Enabled = channel.Enabled,
                SortOrder = channel.SortOrder
            })
            .ToArray()
    };

    private bool IsAvailableChannel(SoftwareDownloadChannel channel)
    {
        if (!channel.Enabled || string.IsNullOrWhiteSpace(channel.Name)) return false;
        return channel.Mode == SoftwareDownloadMode.Server
            ? ResolveStoragePath(channel.StorageKey) is { } path && File.Exists(path)
            : IsWebAddress(channel.Url);
    }

    private string? ResolveStoragePath(string storageKey)
    {
        if (string.IsNullOrWhiteSpace(SoftwareStorageRoot) || 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 bool TryGetString(string propertyName, out string? value)
    {
        try
        {
            value = Json?[propertyName]?.GetValue<string>()?.Trim();
            return true;
        }
        catch (Exception exception) when (exception is InvalidOperationException or FormatException)
        {
            value = null;
            return false;
        }
    }

    private string? GetRequiredString(string propertyName) =>
        TryGetString(propertyName, out var value) && !string.IsNullOrWhiteSpace(value) ? value : null;

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