mirror of
https://github.com/jackqqq123/luban_ui_internal.git
synced 2025-11-15 13:48:24 +08:00
feat: Luban.Core初步实现,提供能力:
- RegistService 装饰器实现服务注册 - 下载服务 - 文件服务 - 多线程方法 - 解压缩服务
This commit is contained in:
83
.github/instructions/prompt.instructions.md
vendored
83
.github/instructions/prompt.instructions.md
vendored
@@ -3,6 +3,52 @@ applyTo: '**'
|
||||
---
|
||||
- 说中文
|
||||
- 这是一个基于Avalonia的项目管理工程,目标是让非程序员也能轻松使用luban管理配置表。
|
||||
- 命令行是PowerShell使用分号作为命令分隔符
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 🏗️ LubanHub.Core 核心能力库
|
||||
LubanHub.Core 提供了项目的底层核心能力,**优先使用这些服务**来实现功能,避免重复造轮子:
|
||||
|
||||
#### 📥 下载服务 (ICoreDownloadService)
|
||||
- **默认下载目录**: 系统用户数据目录的LubanHub子目录 (`%AppData%\LubanHub\Downloads`)
|
||||
- **目录管理**: 获取/设置下载目录
|
||||
- **进度追踪**: 实时下载进度、速度、文件名显示
|
||||
- **取消支持**: 支持CancellationToken取消下载
|
||||
- **使用场景**: Luban版本下载、模板下载、资源文件下载
|
||||
|
||||
#### 📁 文件管理服务 (ICoreFileService)
|
||||
- **文件操作**: 创建、删除、复制、移动文件
|
||||
- **目录操作**: 创建、删除、遍历目录
|
||||
- **异步读写**: ReadAllTextAsync/WriteAllTextAsync
|
||||
- **信息获取**: FileInfo/DirectoryInfo
|
||||
- **使用场景**: 配置文件操作、项目文件管理、模板文件处理
|
||||
|
||||
#### ⚙️ 进程调用服务 (ICoreProcessService)
|
||||
- **同步执行**: 等待进程完成并获取输出
|
||||
- **异步执行**: 实时获取stdout/stderr输出
|
||||
- **后台进程**: 启动但不等待的进程
|
||||
- **进程管理**: 检查进程状态、杀死进程
|
||||
- **使用场景**: 调用Luban命令行、Git操作、外部工具集成
|
||||
|
||||
#### 🗜️ 解压缩服务 (ICoreCompressionService)
|
||||
- **ZIP支持**: 完整的ZIP解压缩功能
|
||||
- **进度追踪**: 解压进度回调
|
||||
- **格式检测**: 自动识别压缩格式
|
||||
- **扩展支持**: 预留RAR/7Z接口(需第三方库)
|
||||
- **使用场景**: Luban安装包解压、模板包解压、资源包处理
|
||||
|
||||
#### 🔧 依赖注入集成
|
||||
```csharp
|
||||
// 在App.axaml.cs中已配置
|
||||
services.AddCoreServices(); // 自动注册所有Core服务
|
||||
```
|
||||
|
||||
#### ⚠️ 扩展原则
|
||||
- **优先使用**: 实现新功能时,首先检查Core是否已提供相关服务
|
||||
- **必要扩展**: 只有在Core无法满足需求时,才考虑扩展Core
|
||||
- **接口设计**: 新增Core功能需要定义接口,保持架构一致性
|
||||
- **日志集成**: 所有Core服务已集成Microsoft.Extensions.Logging
|
||||
|
||||
## UI设计规范
|
||||
|
||||
@@ -62,3 +108,40 @@ applyTo: '**'
|
||||
- 输入框支持焦点状态
|
||||
- 列表项支持选中和悬停状态
|
||||
- 过渡动画让交互更流畅
|
||||
|
||||
## 开发指导
|
||||
|
||||
### 📋 功能实现流程
|
||||
1. **需求分析**: 明确功能需求和用户场景
|
||||
2. **Core检查**: 检查LubanHub.Core是否已提供相关服务
|
||||
3. **服务利用**: 优先使用Core服务实现功能逻辑
|
||||
4. **UI设计**: 按照设计规范创建用户界面
|
||||
5. **进度显示**: 长时间操作使用底部进度条显示状态
|
||||
6. **错误处理**: 合理的异常处理和用户提示
|
||||
|
||||
### 🎯 常见场景示例
|
||||
- **文件下载**: 使用ICoreDownloadService,进度显示在底部
|
||||
- **Luban调用**: 使用ICoreProcessService执行命令行
|
||||
- **配置管理**: 使用ICoreFileService读写配置文件
|
||||
- **安装包处理**: 使用ICoreCompressionService解压
|
||||
- **目录选择**: 集成系统文件对话框(需要时扩展Core)
|
||||
|
||||
### 🔄 服务扩展指南
|
||||
当需要新功能且Core无法满足时:
|
||||
1. 在`LubanHub.Core/Interfaces`添加接口定义
|
||||
2. 在`LubanHub.Core/Services`添加实现类
|
||||
3. 在`ServiceCollectionExtensions`中注册服务
|
||||
4. 更新本文档说明新增能力
|
||||
5. 在App层通过依赖注入使用新服务
|
||||
|
||||
### 📊 UI状态管理
|
||||
- **ViewModelBase**: 所有ViewModel继承此基类
|
||||
- **属性绑定**: 使用SetProperty方法通知UI更新
|
||||
- **命令模式**: 使用RelayCommand处理用户交互
|
||||
- **进度显示**: 使用DownloadProgressViewModel显示长时间操作
|
||||
|
||||
### 🎨 主题和样式
|
||||
- **主题切换**: 使用ThemeManager管理深色/浅色主题
|
||||
- **资源绑定**: 所有颜色使用DynamicResource绑定
|
||||
- **样式复用**: 在Styles.axaml中定义通用样式
|
||||
- **响应式**: 支持运行时主题切换
|
||||
@@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{06401A04-D86
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.App", "src\LubanHub.App\LubanHub.App.csproj", "{9A66E728-EA8A-4644-9CC2-2C056479AF5A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.Core", "src\LubanHub.Core\LubanHub.Core.csproj", "{B8F5E9A2-1234-4567-890A-BCDEF0123456}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -20,8 +22,13 @@ Global
|
||||
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{9A66E728-EA8A-4644-9CC2-2C056479AF5A} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
|
||||
{B8F5E9A2-1234-4567-890A-BCDEF0123456} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -82,9 +82,37 @@ UI层
|
||||
|
||||
---
|
||||
|
||||
## 五、开发建议
|
||||
## 五、架构原则
|
||||
|
||||
### 1. 服务注册原则
|
||||
- **优先使用装饰器注册服务**:所有服务类使用 `[RegistService]` 特性标记,实现自动发现和注册
|
||||
- **避免手工注册**:减少在 DI 容器中手动添加服务,降低耦合度和维护成本
|
||||
- **支持生命周期配置**:通过装饰器参数指定服务生命周期(Singleton/Scoped/Transient)
|
||||
|
||||
### 2. 依赖方向原则
|
||||
- **LubanHub.Core 不能引用任何其他服务**:作为基础能力库,Core 必须保持纯净,只能依赖系统库和第三方基础库
|
||||
- **其他服务可以引用 Core**:业务服务层可以依赖 Core 提供的基础能力
|
||||
- **业务服务间避免相互引用**:各业务服务应保持独立,通过事件或接口解耦
|
||||
|
||||
### 3. 接口设计原则
|
||||
- **Core 服务必须定义接口**:如 `ICoreFileService`、`ICoreDownloadService` 等
|
||||
- **业务服务建议定义接口**:便于测试和扩展
|
||||
- **接口与实现分离**:接口放在 Interfaces 目录,实现放在 Services 目录
|
||||
|
||||
### 4. 装饰器使用示例
|
||||
```csharp
|
||||
[RegistService(ServiceLifetime.Singleton, typeof(ICoreFileService))]
|
||||
public class CoreFileService : ICoreFileService
|
||||
{
|
||||
// 服务实现
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、开发建议
|
||||
|
||||
- 每个服务建议定义接口(如 IFileService),便于测试和扩展
|
||||
- Core层实现时注意跨平台兼容
|
||||
- 业务服务只依赖Core,不直接操作系统API
|
||||
- UI层只做展示和交互,所有逻辑下沉到服务
|
||||
- 使用装饰器模式实现服务的自动发现和注册
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LubanHub.App.Services;
|
||||
using LubanHub.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace LubanHub.App;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public static ServiceProvider? ServiceProvider { get; private set; }
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("正在初始化应用程序...");
|
||||
|
||||
// 配置服务
|
||||
var services = new ServiceCollection();
|
||||
ConfigureServices(services);
|
||||
ServiceProvider = services.BuildServiceProvider();
|
||||
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
Console.WriteLine("XAML加载完成。");
|
||||
}
|
||||
@@ -23,6 +35,27 @@ public partial class App : Application
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureServices(ServiceCollection services)
|
||||
{
|
||||
// 添加日志
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Debug);
|
||||
});
|
||||
|
||||
// 添加Core服务
|
||||
services.AddCoreServices();
|
||||
|
||||
// 添加App层服务
|
||||
services.AddSingleton<IDownloadProgressService, DownloadProgressService>();
|
||||
services.AddSingleton<IAppDownloadService, AppDownloadService>();
|
||||
services.AddSingleton<IExampleModuleService, ExampleModuleService>();
|
||||
|
||||
// 添加ViewModels等其他服务
|
||||
// services.AddSingleton<MainWindowViewModel>();
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
try
|
||||
@@ -31,6 +64,7 @@ public partial class App : Application
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
desktop.Exit += (s, e) => ServiceProvider?.Dispose();
|
||||
Console.WriteLine("主窗口已创建。");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||
<RootNamespace>LubanHub.App</RootNamespace>
|
||||
<AssemblyName>LubanHub.App</AssemblyName>
|
||||
</PropertyGroup>
|
||||
@@ -15,10 +15,17 @@
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.6">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LubanHub.Core\LubanHub.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LubanHub.App.Views"
|
||||
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
|
||||
x:Class="LubanHub.App.MainWindow"
|
||||
Title="LubanHub"
|
||||
MinWidth="1000" MinHeight="600">
|
||||
|
||||
<Grid>
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="200"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -227,7 +229,7 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0"
|
||||
Text="C:\Users\Downloads\LubanHub"
|
||||
Name="DownloadDirectoryTextBox"
|
||||
Background="{DynamicResource InputBackgroundBrush}"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
@@ -235,8 +237,13 @@
|
||||
<Button Grid.Column="1"
|
||||
Content="浏览..."
|
||||
Classes="primary"
|
||||
Padding="{DynamicResource ButtonPadding}"/>
|
||||
Padding="{DynamicResource ButtonPadding}"
|
||||
Click="OnBrowseDownloadDirectoryClick"/>
|
||||
</Grid>
|
||||
<Button Content="🧪 测试其他模块下载"
|
||||
Margin="0,10,0,0"
|
||||
Padding="10,5"
|
||||
Click="OnTestModuleDownloadClick"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel>
|
||||
@@ -260,4 +267,8 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- 底部下载进度区域 -->
|
||||
<views:DownloadProgressView Grid.Row="1" DataContext="{Binding DownloadProgress}" />
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -2,6 +2,10 @@ using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Controls.Selection;
|
||||
using LubanHub.App.Services;
|
||||
using LubanHub.App.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using LubanHub.Core.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace LubanHub.App;
|
||||
|
||||
@@ -19,11 +23,15 @@ public partial class MainWindow : Window
|
||||
private Button? _installButton;
|
||||
private Button? _settingsButton;
|
||||
private ComboBox? _themeComboBox;
|
||||
private TextBox? _downloadDirectoryTextBox;
|
||||
|
||||
private MainWindowViewModel? _viewModel;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeComponents();
|
||||
InitializeViewModel();
|
||||
|
||||
// 订阅主题变化事件
|
||||
ThemeManager.ThemeChanged += OnThemeChanged;
|
||||
@@ -33,6 +41,31 @@ public partial class MainWindow : Window
|
||||
UpdateThemeComboBox();
|
||||
}
|
||||
|
||||
private void InitializeViewModel()
|
||||
{
|
||||
try
|
||||
{
|
||||
_viewModel = new MainWindowViewModel();
|
||||
DataContext = _viewModel;
|
||||
|
||||
// 更新下载目录文本框
|
||||
UpdateDownloadDirectoryTextBox();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 如果ViewModel初始化失败,记录错误但不中断应用启动
|
||||
Console.WriteLine($"初始化ViewModel时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDownloadDirectoryTextBox()
|
||||
{
|
||||
if (_downloadDirectoryTextBox != null && _viewModel != null)
|
||||
{
|
||||
_downloadDirectoryTextBox.Text = _viewModel.DownloadDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeComponents()
|
||||
{
|
||||
// 获取面板引用
|
||||
@@ -48,7 +81,10 @@ public partial class MainWindow : Window
|
||||
_projectButton = this.FindControl<Button>("ProjectButton");
|
||||
_installButton = this.FindControl<Button>("InstallButton");
|
||||
_settingsButton = this.FindControl<Button>("SettingsButton");
|
||||
|
||||
// 获取其他控件引用
|
||||
_themeComboBox = this.FindControl<ComboBox>("ThemeComboBox");
|
||||
_downloadDirectoryTextBox = this.FindControl<TextBox>("DownloadDirectoryTextBox");
|
||||
}
|
||||
|
||||
private void ShowPanel(Grid? targetPanel)
|
||||
@@ -130,4 +166,36 @@ public partial class MainWindow : Window
|
||||
_themeComboBox.SelectedIndex = ThemeManager.CurrentTheme == ThemeVariant.Dark ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowseDownloadDirectoryClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 简单的目录选择实现,实际项目中应该使用文件对话框
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.ChangeDownloadDirectoryCommand.Execute(null);
|
||||
UpdateDownloadDirectoryTextBox();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"浏览下载目录时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTestModuleDownloadClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.TestModuleDownloadCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"测试模块下载时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/LubanHub.App/Services/AppDownloadService.cs
Normal file
118
src/LubanHub.App/Services/AppDownloadService.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using LubanHub.Core.Interfaces;
|
||||
using LubanHub.Core.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 应用层下载服务接口
|
||||
/// </summary>
|
||||
public interface IAppDownloadService
|
||||
{
|
||||
/// <summary>
|
||||
/// 下载文件并自动显示进度
|
||||
/// </summary>
|
||||
Task<string> DownloadFileAsync(string url, string? fileName = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件到指定目录并自动显示进度
|
||||
/// </summary>
|
||||
Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前下载目录
|
||||
/// </summary>
|
||||
string GetDownloadDirectory();
|
||||
|
||||
/// <summary>
|
||||
/// 设置下载目录
|
||||
/// </summary>
|
||||
void SetDownloadDirectory(string path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用层下载服务实现
|
||||
/// </summary>
|
||||
public class AppDownloadService : IAppDownloadService
|
||||
{
|
||||
private readonly ICoreDownloadService _coreDownloadService;
|
||||
private readonly IDownloadProgressService _progressService;
|
||||
private readonly ILogger<AppDownloadService> _logger;
|
||||
|
||||
public AppDownloadService(
|
||||
ICoreDownloadService coreDownloadService,
|
||||
IDownloadProgressService progressService,
|
||||
ILogger<AppDownloadService> logger)
|
||||
{
|
||||
_coreDownloadService = coreDownloadService;
|
||||
_progressService = progressService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFileAsync(string url, string? fileName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var progress = new Progress<DownloadProgressInfo>(progressInfo =>
|
||||
{
|
||||
_progressService.ReportProgress(progressInfo);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("开始下载文件: {Url}", url);
|
||||
var filePath = await _coreDownloadService.DownloadFileAsync(url, fileName, progress, cancellationToken);
|
||||
|
||||
// 下载完成后延迟隐藏进度条
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(2000, CancellationToken.None); // 2秒后隐藏
|
||||
_progressService.HideProgress();
|
||||
});
|
||||
|
||||
_logger.LogInformation("文件下载完成: {FilePath}", filePath);
|
||||
return filePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "下载文件时出错: {Url}", url);
|
||||
_progressService.HideProgress();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var progress = new Progress<DownloadProgressInfo>(progressInfo =>
|
||||
{
|
||||
_progressService.ReportProgress(progressInfo);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("开始下载文件到指定目录: {Url} -> {Directory}", url, targetDirectory);
|
||||
var filePath = await _coreDownloadService.DownloadFileAsync(url, targetDirectory, fileName, progress, cancellationToken);
|
||||
|
||||
// 下载完成后延迟隐藏进度条
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(2000, CancellationToken.None); // 2秒后隐藏
|
||||
_progressService.HideProgress();
|
||||
});
|
||||
|
||||
_logger.LogInformation("文件下载完成: {FilePath}", filePath);
|
||||
return filePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "下载文件时出错: {Url} -> {Directory}", url, targetDirectory);
|
||||
_progressService.HideProgress();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDownloadDirectory() => _coreDownloadService.GetDownloadDirectory();
|
||||
|
||||
public void SetDownloadDirectory(string path) => _coreDownloadService.SetDownloadDirectory(path);
|
||||
}
|
||||
28
src/LubanHub.App/Services/DownloadProgressService.cs
Normal file
28
src/LubanHub.App/Services/DownloadProgressService.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using LubanHub.Core.Models;
|
||||
using System;
|
||||
|
||||
namespace LubanHub.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 全局下载进度服务实现
|
||||
/// </summary>
|
||||
public class DownloadProgressService : IDownloadProgressService
|
||||
{
|
||||
public event Action<DownloadProgressInfo>? ProgressChanged;
|
||||
|
||||
public void ReportProgress(DownloadProgressInfo progressInfo)
|
||||
{
|
||||
ProgressChanged?.Invoke(progressInfo);
|
||||
}
|
||||
|
||||
public void HideProgress()
|
||||
{
|
||||
var hideInfo = new DownloadProgressInfo
|
||||
{
|
||||
IsCompleted = true,
|
||||
Progress = 0,
|
||||
FileName = string.Empty
|
||||
};
|
||||
ProgressChanged?.Invoke(hideInfo);
|
||||
}
|
||||
}
|
||||
55
src/LubanHub.App/Services/ExampleModuleService.cs
Normal file
55
src/LubanHub.App/Services/ExampleModuleService.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using LubanHub.App.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 示例模块服务,演示如何在其他模块中使用下载功能
|
||||
/// </summary>
|
||||
public interface IExampleModuleService
|
||||
{
|
||||
/// <summary>
|
||||
/// 下载Luban示例文件
|
||||
/// </summary>
|
||||
Task DownloadLubanExampleAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 示例模块服务实现
|
||||
/// </summary>
|
||||
public class ExampleModuleService : IExampleModuleService
|
||||
{
|
||||
private readonly IAppDownloadService _downloadService;
|
||||
private readonly ILogger<ExampleModuleService> _logger;
|
||||
|
||||
public ExampleModuleService(IAppDownloadService downloadService, ILogger<ExampleModuleService> logger)
|
||||
{
|
||||
_downloadService = downloadService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task DownloadLubanExampleAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("示例模块开始下载Luban示例文件");
|
||||
|
||||
// 模拟下载一个示例文件
|
||||
var testUrl = "https://httpbin.org/json";
|
||||
var fileName = "luban_example.json";
|
||||
|
||||
// 调用下载服务,进度会自动显示在UI上
|
||||
var filePath = await _downloadService.DownloadFileAsync(testUrl, fileName, CancellationToken.None);
|
||||
|
||||
_logger.LogInformation("示例模块下载完成: {FilePath}", filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "示例模块下载失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/LubanHub.App/Services/IDownloadProgressService.cs
Normal file
25
src/LubanHub.App/Services/IDownloadProgressService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using LubanHub.Core.Models;
|
||||
using System;
|
||||
|
||||
namespace LubanHub.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 全局下载进度服务接口
|
||||
/// </summary>
|
||||
public interface IDownloadProgressService
|
||||
{
|
||||
/// <summary>
|
||||
/// 下载进度变更事件
|
||||
/// </summary>
|
||||
event Action<DownloadProgressInfo>? ProgressChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 报告下载进度
|
||||
/// </summary>
|
||||
void ReportProgress(DownloadProgressInfo progressInfo);
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏下载进度
|
||||
/// </summary>
|
||||
void HideProgress();
|
||||
}
|
||||
104
src/LubanHub.App/ViewModels/DownloadProgressViewModel.cs
Normal file
104
src/LubanHub.App/ViewModels/DownloadProgressViewModel.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using LubanHub.Core.Models;
|
||||
using System;
|
||||
|
||||
namespace LubanHub.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 下载进度ViewModel
|
||||
/// </summary>
|
||||
public class DownloadProgressViewModel : ViewModelBase
|
||||
{
|
||||
private string _fileName = string.Empty;
|
||||
private double _progress;
|
||||
private bool _isVisible;
|
||||
private string _statusText = string.Empty;
|
||||
private string _speedText = string.Empty;
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get => _fileName;
|
||||
set => SetProperty(ref _fileName, value);
|
||||
}
|
||||
|
||||
public double Progress
|
||||
{
|
||||
get => _progress;
|
||||
set => SetProperty(ref _progress, value);
|
||||
}
|
||||
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set => SetProperty(ref _isVisible, value);
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
public string SpeedText
|
||||
{
|
||||
get => _speedText;
|
||||
set => SetProperty(ref _speedText, value);
|
||||
}
|
||||
|
||||
public void UpdateProgress(DownloadProgressInfo progressInfo)
|
||||
{
|
||||
FileName = progressInfo.FileName;
|
||||
Progress = progressInfo.Progress;
|
||||
IsVisible = true;
|
||||
|
||||
if (progressInfo.IsCompleted)
|
||||
{
|
||||
StatusText = "下载完成";
|
||||
SpeedText = string.Empty;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(progressInfo.ErrorMessage))
|
||||
{
|
||||
StatusText = $"下载出错: {progressInfo.ErrorMessage}";
|
||||
SpeedText = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var downloadedMB = progressInfo.DownloadedBytes / 1024.0 / 1024.0;
|
||||
var totalMB = progressInfo.TotalBytes / 1024.0 / 1024.0;
|
||||
|
||||
if (progressInfo.TotalBytes > 0)
|
||||
{
|
||||
StatusText = $"下载中: {downloadedMB:F1}MB / {totalMB:F1}MB";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = $"下载中: {downloadedMB:F1}MB";
|
||||
}
|
||||
|
||||
if (progressInfo.Speed > 0)
|
||||
{
|
||||
var speedKB = progressInfo.Speed / 1024.0;
|
||||
if (speedKB >= 1024)
|
||||
{
|
||||
SpeedText = $"{speedKB / 1024:F1}MB/s";
|
||||
}
|
||||
else
|
||||
{
|
||||
SpeedText = $"{speedKB:F1}KB/s";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SpeedText = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
IsVisible = false;
|
||||
Progress = 0;
|
||||
StatusText = string.Empty;
|
||||
SpeedText = string.Empty;
|
||||
FileName = string.Empty;
|
||||
}
|
||||
}
|
||||
154
src/LubanHub.App/ViewModels/MainWindowViewModel.cs
Normal file
154
src/LubanHub.App/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using LubanHub.App.Services;
|
||||
using LubanHub.Core.Interfaces;
|
||||
using LubanHub.Core.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace LubanHub.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// 主窗口ViewModel
|
||||
/// </summary>
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IAppDownloadService _downloadService;
|
||||
private readonly IDownloadProgressService _progressService;
|
||||
private readonly IExampleModuleService _exampleModuleService;
|
||||
private readonly ILogger<MainWindowViewModel> _logger;
|
||||
private string _downloadDirectory = string.Empty;
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
// 从依赖注入容器获取服务
|
||||
if (App.ServiceProvider != null)
|
||||
{
|
||||
_downloadService = App.ServiceProvider.GetRequiredService<IAppDownloadService>();
|
||||
_progressService = App.ServiceProvider.GetRequiredService<IDownloadProgressService>();
|
||||
_exampleModuleService = App.ServiceProvider.GetRequiredService<IExampleModuleService>();
|
||||
_logger = App.ServiceProvider.GetRequiredService<ILogger<MainWindowViewModel>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("ServiceProvider 未初始化");
|
||||
}
|
||||
|
||||
DownloadProgress = new DownloadProgressViewModel();
|
||||
|
||||
// 订阅全局下载进度事件
|
||||
_progressService.ProgressChanged += OnProgressChanged;
|
||||
|
||||
// 初始化下载目录
|
||||
_downloadDirectory = _downloadService.GetDownloadDirectory();
|
||||
|
||||
// 初始化命令
|
||||
ChangeDownloadDirectoryCommand = new RelayCommand(async () => await ChangeDownloadDirectoryAsync());
|
||||
TestModuleDownloadCommand = new RelayCommand(async () => await TestModuleDownloadAsync());
|
||||
}
|
||||
|
||||
public DownloadProgressViewModel DownloadProgress { get; }
|
||||
|
||||
public string DownloadDirectory
|
||||
{
|
||||
get => _downloadDirectory;
|
||||
set => SetProperty(ref _downloadDirectory, value);
|
||||
}
|
||||
|
||||
public ICommand ChangeDownloadDirectoryCommand { get; }
|
||||
public ICommand TestModuleDownloadCommand { get; }
|
||||
|
||||
private void OnProgressChanged(DownloadProgressInfo progressInfo)
|
||||
{
|
||||
// 在UI线程中更新进度
|
||||
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (progressInfo.IsCompleted && string.IsNullOrEmpty(progressInfo.FileName))
|
||||
{
|
||||
// 隐藏进度条
|
||||
DownloadProgress.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
DownloadProgress.UpdateProgress(progressInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Task ChangeDownloadDirectoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: 实现目录选择对话框
|
||||
// 这里先用一个简单的示例
|
||||
var newPath = @"C:\temp\LubanHub"; // 示例路径
|
||||
_downloadService.SetDownloadDirectory(newPath);
|
||||
DownloadDirectory = _downloadService.GetDownloadDirectory();
|
||||
|
||||
_logger.LogInformation("下载目录已更改为: {NewPath}", DownloadDirectory);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "更改下载目录时出错");
|
||||
return Task.FromException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TestModuleDownloadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("测试其他模块下载功能");
|
||||
await _exampleModuleService.DownloadLubanExampleAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "测试其他模块下载时出错");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 简单的命令实现
|
||||
/// </summary>
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<Task> _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private bool _isExecuting;
|
||||
|
||||
public RelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return !_isExecuting && (_canExecute?.Invoke() ?? true);
|
||||
}
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
return;
|
||||
|
||||
_isExecuting = true;
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
await _execute();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/LubanHub.App/ViewModels/ViewModelBase.cs
Normal file
29
src/LubanHub.App/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace LubanHub.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel基类
|
||||
/// </summary>
|
||||
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
46
src/LubanHub.App/Views/DownloadProgressView.axaml
Normal file
46
src/LubanHub.App/Views/DownloadProgressView.axaml
Normal file
@@ -0,0 +1,46 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LubanHub.App.Views.DownloadProgressView">
|
||||
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="16,8"
|
||||
IsVisible="{Binding IsVisible}">
|
||||
|
||||
<Grid RowDefinitions="Auto,4,Auto,4,Auto">
|
||||
|
||||
<!-- 第一行:文件名和速度 -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding FileName}"
|
||||
FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding SpeedText}"
|
||||
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
|
||||
FontSize="12" />
|
||||
</Grid>
|
||||
|
||||
<!-- 第二行:间隔 -->
|
||||
|
||||
<!-- 第三行:进度条 -->
|
||||
<ProgressBar Grid.Row="2"
|
||||
Value="{Binding Progress}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Height="4" />
|
||||
|
||||
<!-- 第四行:间隔 -->
|
||||
|
||||
<!-- 第五行:状态文本 -->
|
||||
<TextBlock Grid.Row="4"
|
||||
Text="{Binding StatusText}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
11
src/LubanHub.App/Views/DownloadProgressView.axaml.cs
Normal file
11
src/LubanHub.App/Views/DownloadProgressView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LubanHub.App.Views;
|
||||
|
||||
public partial class DownloadProgressView : UserControl
|
||||
{
|
||||
public DownloadProgressView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
37
src/LubanHub.Core/Attributes/RegistServiceAttribute.cs
Normal file
37
src/LubanHub.Core/Attributes/RegistServiceAttribute.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace LubanHub.Core.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// 服务注册特性,用于标记需要自动注册到DI容器的服务
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class RegistServiceAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务生命周期
|
||||
/// </summary>
|
||||
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Singleton;
|
||||
|
||||
/// <summary>
|
||||
/// 服务接口类型,如果不指定则自动推断
|
||||
/// </summary>
|
||||
public Type? ServiceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务注册优先级,数值越小优先级越高
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化服务注册特性
|
||||
/// </summary>
|
||||
/// <param name="lifetime">服务生命周期</param>
|
||||
/// <param name="serviceType">服务接口类型</param>
|
||||
public RegistServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Singleton, Type? serviceType = null)
|
||||
{
|
||||
Lifetime = lifetime;
|
||||
ServiceType = serviceType;
|
||||
}
|
||||
}
|
||||
41
src/LubanHub.Core/Interfaces/ICoreCompressionService.cs
Normal file
41
src/LubanHub.Core/Interfaces/ICoreCompressionService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 解压缩服务接口
|
||||
/// </summary>
|
||||
public interface ICoreCompressionService
|
||||
{
|
||||
/// <summary>
|
||||
/// 解压ZIP文件
|
||||
/// </summary>
|
||||
Task ExtractZipAsync(string zipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 创建ZIP文件
|
||||
/// </summary>
|
||||
Task CreateZipAsync(string sourcePath, string zipFilePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 解压RAR文件 (如果支持)
|
||||
/// </summary>
|
||||
Task ExtractRarAsync(string rarFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 解压7Z文件 (如果支持)
|
||||
/// </summary>
|
||||
Task Extract7ZAsync(string sevenZipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否支持指定格式
|
||||
/// </summary>
|
||||
bool IsFormatSupported(string fileExtension);
|
||||
|
||||
/// <summary>
|
||||
/// 自动检测格式并解压
|
||||
/// </summary>
|
||||
Task ExtractAsync(string archiveFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
37
src/LubanHub.Core/Interfaces/ICoreDownloadService.cs
Normal file
37
src/LubanHub.Core/Interfaces/ICoreDownloadService.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using LubanHub.Core.Models;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 下载服务接口
|
||||
/// </summary>
|
||||
public interface ICoreDownloadService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取默认下载目录
|
||||
/// </summary>
|
||||
string GetDefaultDownloadDirectory();
|
||||
|
||||
/// <summary>
|
||||
/// 设置下载目录
|
||||
/// </summary>
|
||||
void SetDownloadDirectory(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前下载目录
|
||||
/// </summary>
|
||||
string GetDownloadDirectory();
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件
|
||||
/// </summary>
|
||||
Task<string> DownloadFileAsync(string url, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件到指定目录
|
||||
/// </summary>
|
||||
Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
76
src/LubanHub.Core/Interfaces/ICoreFileService.cs
Normal file
76
src/LubanHub.Core/Interfaces/ICoreFileService.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 文件管理服务接口
|
||||
/// </summary>
|
||||
public interface ICoreFileService
|
||||
{
|
||||
/// <summary>
|
||||
/// 检查文件是否存在
|
||||
/// </summary>
|
||||
bool FileExists(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 检查目录是否存在
|
||||
/// </summary>
|
||||
bool DirectoryExists(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 创建目录
|
||||
/// </summary>
|
||||
void CreateDirectory(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
void DeleteFile(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 删除目录
|
||||
/// </summary>
|
||||
void DeleteDirectory(string path, bool recursive = false);
|
||||
|
||||
/// <summary>
|
||||
/// 复制文件
|
||||
/// </summary>
|
||||
void CopyFile(string sourceFile, string destFile, bool overwrite = false);
|
||||
|
||||
/// <summary>
|
||||
/// 移动文件
|
||||
/// </summary>
|
||||
void MoveFile(string sourceFile, string destFile);
|
||||
|
||||
/// <summary>
|
||||
/// 读取文件文本
|
||||
/// </summary>
|
||||
Task<string> ReadAllTextAsync(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 写入文件文本
|
||||
/// </summary>
|
||||
Task WriteAllTextAsync(string path, string content);
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件信息
|
||||
/// </summary>
|
||||
FileInfo GetFileInfo(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 获取目录信息
|
||||
/// </summary>
|
||||
DirectoryInfo GetDirectoryInfo(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 获取目录下的文件列表
|
||||
/// </summary>
|
||||
string[] GetFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||
|
||||
/// <summary>
|
||||
/// 获取目录下的子目录列表
|
||||
/// </summary>
|
||||
string[] GetDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||
}
|
||||
38
src/LubanHub.Core/Interfaces/ICoreProcessService.cs
Normal file
38
src/LubanHub.Core/Interfaces/ICoreProcessService.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using LubanHub.Core.Models;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 进程调用服务接口
|
||||
/// </summary>
|
||||
public interface ICoreProcessService
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行进程并等待完成
|
||||
/// </summary>
|
||||
Task<ProcessResult> ExecuteAsync(string fileName, string? arguments = null, string? workingDirectory = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 执行进程并实时获取输出
|
||||
/// </summary>
|
||||
Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
|
||||
Action<string>? onOutputReceived, Action<string>? onErrorReceived, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 启动进程但不等待完成
|
||||
/// </summary>
|
||||
System.Diagnostics.Process StartProcess(string fileName, string? arguments = null, string? workingDirectory = null);
|
||||
|
||||
/// <summary>
|
||||
/// 检查进程是否正在运行
|
||||
/// </summary>
|
||||
bool IsProcessRunning(string processName);
|
||||
|
||||
/// <summary>
|
||||
/// 杀死进程
|
||||
/// </summary>
|
||||
void KillProcess(string processName);
|
||||
}
|
||||
14
src/LubanHub.Core/LubanHub.Core.csproj
Normal file
14
src/LubanHub.Core/LubanHub.Core.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>LubanHub.Core</RootNamespace>
|
||||
<AssemblyName>LubanHub.Core</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
42
src/LubanHub.Core/Models/DownloadProgressInfo.cs
Normal file
42
src/LubanHub.Core/Models/DownloadProgressInfo.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace LubanHub.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 下载进度信息
|
||||
/// </summary>
|
||||
public class DownloadProgressInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 下载进度 (0-100)
|
||||
/// </summary>
|
||||
public double Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已下载字节数
|
||||
/// </summary>
|
||||
public long DownloadedBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总字节数
|
||||
/// </summary>
|
||||
public long TotalBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下载速度 (bytes/s)
|
||||
/// </summary>
|
||||
public long Speed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否完成
|
||||
/// </summary>
|
||||
public bool IsCompleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
27
src/LubanHub.Core/Models/ProcessResult.cs
Normal file
27
src/LubanHub.Core/Models/ProcessResult.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace LubanHub.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行结果
|
||||
/// </summary>
|
||||
public class ProcessResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 退出代码
|
||||
/// </summary>
|
||||
public int ExitCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准输出
|
||||
/// </summary>
|
||||
public string StandardOutput { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 标准错误
|
||||
/// </summary>
|
||||
public string StandardError { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否成功执行
|
||||
/// </summary>
|
||||
public bool IsSuccess => ExitCode == 0;
|
||||
}
|
||||
80
src/LubanHub.Core/ServiceCollectionExtensions.cs
Normal file
80
src/LubanHub.Core/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using LubanHub.Core.Attributes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LubanHub.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Core服务依赖注入扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加Core服务 - 通过反射自动发现并注册带有RegistService特性的服务
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCoreServices(this IServiceCollection services)
|
||||
{
|
||||
// 注册HttpClient(Core服务的依赖)
|
||||
services.AddSingleton<HttpClient>();
|
||||
|
||||
// 获取当前程序集
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
// 发现所有带有RegistService特性的类型
|
||||
var serviceTypes = assembly.GetTypes()
|
||||
.Where(type => type.IsClass && !type.IsAbstract && type.GetCustomAttribute<RegistServiceAttribute>() != null)
|
||||
.Select(type => new
|
||||
{
|
||||
Type = type,
|
||||
Attribute = type.GetCustomAttribute<RegistServiceAttribute>()!
|
||||
})
|
||||
.OrderBy(x => x.Attribute.Priority) // 按优先级排序
|
||||
.ToList();
|
||||
|
||||
// 注册服务
|
||||
foreach (var serviceInfo in serviceTypes)
|
||||
{
|
||||
var implementationType = serviceInfo.Type;
|
||||
var attribute = serviceInfo.Attribute;
|
||||
|
||||
// 确定服务接口类型
|
||||
Type serviceType;
|
||||
if (attribute.ServiceType != null)
|
||||
{
|
||||
// 使用特性指定的服务类型
|
||||
serviceType = attribute.ServiceType;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 自动推断:查找实现的第一个接口
|
||||
var interfaceType = implementationType.GetInterfaces().FirstOrDefault();
|
||||
if (interfaceType == null)
|
||||
{
|
||||
throw new InvalidOperationException($"服务 {implementationType.Name} 没有实现任何接口,且未在RegistService特性中指定ServiceType");
|
||||
}
|
||||
serviceType = interfaceType;
|
||||
}
|
||||
|
||||
// 根据生命周期注册服务
|
||||
switch (attribute.Lifetime)
|
||||
{
|
||||
case ServiceLifetime.Singleton:
|
||||
services.AddSingleton(serviceType, implementationType);
|
||||
break;
|
||||
case ServiceLifetime.Scoped:
|
||||
services.AddScoped(serviceType, implementationType);
|
||||
break;
|
||||
case ServiceLifetime.Transient:
|
||||
services.AddTransient(serviceType, implementationType);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(attribute.Lifetime), attribute.Lifetime, "不支持的服务生命周期");
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
178
src/LubanHub.Core/Services/CoreCompressionService.cs
Normal file
178
src/LubanHub.Core/Services/CoreCompressionService.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using LubanHub.Core.Attributes;
|
||||
using LubanHub.Core.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 解压缩服务实现
|
||||
/// </summary>
|
||||
[RegistService(ServiceLifetime.Singleton, typeof(ICoreCompressionService))]
|
||||
public class CoreCompressionService : ICoreCompressionService
|
||||
{
|
||||
private readonly ILogger<CoreCompressionService> _logger;
|
||||
private readonly ICoreFileService _fileService;
|
||||
|
||||
public CoreCompressionService(ILogger<CoreCompressionService> logger, ICoreFileService fileService)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileService = fileService;
|
||||
}
|
||||
|
||||
public Task ExtractZipAsync(string zipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_fileService.FileExists(zipFilePath))
|
||||
throw new FileNotFoundException($"ZIP文件不存在: {zipFilePath}");
|
||||
|
||||
_logger.LogInformation("开始解压ZIP文件: {ZipPath} -> {ExtractPath}", zipFilePath, extractPath);
|
||||
|
||||
// 确保提取目录存在
|
||||
if (!_fileService.DirectoryExists(extractPath))
|
||||
{
|
||||
_fileService.CreateDirectory(extractPath);
|
||||
}
|
||||
|
||||
using var archive = ZipFile.OpenRead(zipFilePath);
|
||||
var totalEntries = archive.Entries.Count;
|
||||
var extractedEntries = 0;
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 跳过目录条目
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
extractedEntries++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var destinationPath = Path.Combine(extractPath, entry.FullName);
|
||||
var destinationDir = Path.GetDirectoryName(destinationPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(destinationDir) && !_fileService.DirectoryExists(destinationDir))
|
||||
{
|
||||
_fileService.CreateDirectory(destinationDir);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
entry.ExtractToFile(destinationPath, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "解压文件时出错: {EntryName}", entry.FullName);
|
||||
}
|
||||
|
||||
extractedEntries++;
|
||||
var progressPercent = (double)extractedEntries / totalEntries * 100;
|
||||
progress?.Report(progressPercent);
|
||||
}
|
||||
|
||||
_logger.LogInformation("ZIP文件解压完成: {ZipPath}, 提取了 {Count} 个文件", zipFilePath, extractedEntries);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "解压ZIP文件时出错: {ZipPath}", zipFilePath);
|
||||
throw;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task CreateZipAsync(string sourcePath, string zipFilePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_fileService.DirectoryExists(sourcePath) && !_fileService.FileExists(sourcePath))
|
||||
throw new DirectoryNotFoundException($"源路径不存在: {sourcePath}");
|
||||
|
||||
_logger.LogInformation("开始创建ZIP文件: {SourcePath} -> {ZipPath}", sourcePath, zipFilePath);
|
||||
|
||||
var zipDir = Path.GetDirectoryName(zipFilePath);
|
||||
if (!string.IsNullOrEmpty(zipDir) && !_fileService.DirectoryExists(zipDir))
|
||||
{
|
||||
_fileService.CreateDirectory(zipDir);
|
||||
}
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
if (_fileService.FileExists(sourcePath))
|
||||
{
|
||||
// 压缩单个文件
|
||||
using var archive = ZipFile.Open(zipFilePath, ZipArchiveMode.Create);
|
||||
archive.CreateEntryFromFile(sourcePath, Path.GetFileName(sourcePath));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 压缩目录
|
||||
ZipFile.CreateFromDirectory(sourcePath, zipFilePath);
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
progress?.Report(100);
|
||||
_logger.LogInformation("ZIP文件创建完成: {ZipPath}", zipFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "创建ZIP文件时出错: {SourcePath} -> {ZipPath}", sourcePath, zipFilePath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task ExtractRarAsync(string rarFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogWarning("RAR格式暂不支持,请使用第三方库实现");
|
||||
throw new NotSupportedException("RAR格式暂不支持");
|
||||
}
|
||||
|
||||
public Task Extract7ZAsync(string sevenZipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogWarning("7Z格式暂不支持,请使用第三方库实现");
|
||||
throw new NotSupportedException("7Z格式暂不支持");
|
||||
}
|
||||
|
||||
public bool IsFormatSupported(string fileExtension)
|
||||
{
|
||||
var supportedFormats = new[] { ".zip" };
|
||||
return supportedFormats.Contains(fileExtension.ToLowerInvariant());
|
||||
}
|
||||
|
||||
public async Task ExtractAsync(string archiveFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var extension = Path.GetExtension(archiveFilePath).ToLowerInvariant();
|
||||
|
||||
switch (extension)
|
||||
{
|
||||
case ".zip":
|
||||
await ExtractZipAsync(archiveFilePath, extractPath, progress, cancellationToken);
|
||||
break;
|
||||
case ".rar":
|
||||
await ExtractRarAsync(archiveFilePath, extractPath, progress, cancellationToken);
|
||||
break;
|
||||
case ".7z":
|
||||
await Extract7ZAsync(archiveFilePath, extractPath, progress, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"不支持的压缩格式: {extension}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "自动解压文件时出错: {ArchivePath}", archiveFilePath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
200
src/LubanHub.Core/Services/CoreDownloadService.cs
Normal file
200
src/LubanHub.Core/Services/CoreDownloadService.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using LubanHub.Core.Attributes;
|
||||
using LubanHub.Core.Interfaces;
|
||||
using LubanHub.Core.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 下载服务实现
|
||||
/// </summary>
|
||||
[RegistService(ServiceLifetime.Singleton, typeof(ICoreDownloadService))]
|
||||
public class CoreDownloadService : ICoreDownloadService
|
||||
{
|
||||
private readonly ILogger<CoreDownloadService> _logger;
|
||||
private readonly ICoreFileService _fileService;
|
||||
private readonly HttpClient _httpClient;
|
||||
private string _downloadDirectory;
|
||||
|
||||
public CoreDownloadService(ILogger<CoreDownloadService> logger, ICoreFileService fileService, HttpClient httpClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileService = fileService;
|
||||
_httpClient = httpClient;
|
||||
_downloadDirectory = GetDefaultDownloadDirectory();
|
||||
}
|
||||
|
||||
public string GetDefaultDownloadDirectory()
|
||||
{
|
||||
// 使用系统用户数据目录的LubanHub子目录
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
return Path.Combine(appDataPath, "LubanHub", "Downloads");
|
||||
}
|
||||
|
||||
public void SetDownloadDirectory(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("下载目录路径不能为空", nameof(path));
|
||||
|
||||
_downloadDirectory = path;
|
||||
|
||||
// 确保目录存在
|
||||
if (!_fileService.DirectoryExists(_downloadDirectory))
|
||||
{
|
||||
_fileService.CreateDirectory(_downloadDirectory);
|
||||
}
|
||||
|
||||
_logger.LogDebug("设置下载目录: {Path}", _downloadDirectory);
|
||||
}
|
||||
|
||||
public string GetDownloadDirectory()
|
||||
{
|
||||
// 确保目录存在
|
||||
if (!_fileService.DirectoryExists(_downloadDirectory))
|
||||
{
|
||||
_fileService.CreateDirectory(_downloadDirectory);
|
||||
}
|
||||
|
||||
return _downloadDirectory;
|
||||
}
|
||||
|
||||
public Task<string> DownloadFileAsync(string url, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return DownloadFileAsync(url, GetDownloadDirectory(), fileName, progress, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
throw new ArgumentException("URL不能为空", nameof(url));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(targetDirectory))
|
||||
throw new ArgumentException("目标目录不能为空", nameof(targetDirectory));
|
||||
|
||||
// 确保目标目录存在
|
||||
if (!_fileService.DirectoryExists(targetDirectory))
|
||||
{
|
||||
_fileService.CreateDirectory(targetDirectory);
|
||||
}
|
||||
|
||||
// 确定文件名
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
fileName = GetFileNameFromUrl(url);
|
||||
}
|
||||
|
||||
var targetFilePath = Path.Combine(targetDirectory, fileName);
|
||||
|
||||
_logger.LogInformation("开始下载文件: {Url} -> {TargetPath}", url, targetFilePath);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var lastBytesRead = 0L;
|
||||
var lastTime = stopwatch.Elapsed;
|
||||
|
||||
var progressInfo = new DownloadProgressInfo
|
||||
{
|
||||
FileName = fileName
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var totalBytes = response.Content.Headers.ContentLength ?? -1;
|
||||
progressInfo.TotalBytes = totalBytes;
|
||||
|
||||
using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var fileStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
|
||||
|
||||
var buffer = new byte[8192];
|
||||
var totalBytesRead = 0L;
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
// 计算进度和速度
|
||||
var currentTime = stopwatch.Elapsed;
|
||||
var timeDiff = currentTime - lastTime;
|
||||
|
||||
if (timeDiff.TotalMilliseconds >= 200) // 每200ms更新一次进度
|
||||
{
|
||||
var bytesDiff = totalBytesRead - lastBytesRead;
|
||||
var speed = timeDiff.TotalSeconds > 0 ? (long)(bytesDiff / timeDiff.TotalSeconds) : 0;
|
||||
|
||||
progressInfo.DownloadedBytes = totalBytesRead;
|
||||
progressInfo.Speed = speed;
|
||||
progressInfo.Progress = totalBytes > 0 ? (double)totalBytesRead / totalBytes * 100 : 0;
|
||||
|
||||
progress?.Report(progressInfo);
|
||||
|
||||
lastBytesRead = totalBytesRead;
|
||||
lastTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
// 完成
|
||||
progressInfo.DownloadedBytes = totalBytesRead;
|
||||
progressInfo.Progress = 100;
|
||||
progressInfo.IsCompleted = true;
|
||||
progressInfo.Speed = 0;
|
||||
progress?.Report(progressInfo);
|
||||
|
||||
_logger.LogInformation("文件下载完成: {TargetPath}, 大小: {Size} 字节, 耗时: {Duration:F2}s",
|
||||
targetFilePath, totalBytesRead, stopwatch.Elapsed.TotalSeconds);
|
||||
|
||||
return targetFilePath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
progressInfo.ErrorMessage = ex.Message;
|
||||
progress?.Report(progressInfo);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "下载文件时出错: {Url}", url);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFileNameFromUrl(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
var fileName = Path.GetFileName(uri.LocalPath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fileName) || fileName == "/")
|
||||
{
|
||||
fileName = $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
}
|
||||
|
||||
// 移除查询参数
|
||||
var queryIndex = fileName.IndexOf('?');
|
||||
if (queryIndex >= 0)
|
||||
{
|
||||
fileName = fileName.Substring(0, queryIndex);
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/LubanHub.Core/Services/CoreFileService.cs
Normal file
206
src/LubanHub.Core/Services/CoreFileService.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using LubanHub.Core.Attributes;
|
||||
using LubanHub.Core.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 文件管理服务实现
|
||||
/// </summary>
|
||||
[RegistService(ServiceLifetime.Singleton, typeof(ICoreFileService))]
|
||||
public class CoreFileService : ICoreFileService
|
||||
{
|
||||
private readonly ILogger<CoreFileService> _logger;
|
||||
|
||||
public CoreFileService(ILogger<CoreFileService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "检查文件是否存在时出错: {Path}", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DirectoryExists(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "检查目录是否存在时出错: {Path}", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateDirectory(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
_logger.LogDebug("创建目录: {Path}", path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "创建目录时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteFile(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
_logger.LogDebug("删除文件: {Path}", path);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "删除文件时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteDirectory(string path, bool recursive = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
Directory.Delete(path, recursive);
|
||||
_logger.LogDebug("删除目录: {Path}, 递归: {Recursive}", path, recursive);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "删除目录时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyFile(string sourceFile, string destFile, bool overwrite = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Copy(sourceFile, destFile, overwrite);
|
||||
_logger.LogDebug("复制文件: {Source} -> {Dest}", sourceFile, destFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "复制文件时出错: {Source} -> {Dest}", sourceFile, destFile);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void MoveFile(string sourceFile, string destFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Move(sourceFile, destFile);
|
||||
_logger.LogDebug("移动文件: {Source} -> {Dest}", sourceFile, destFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "移动文件时出错: {Source} -> {Dest}", sourceFile, destFile);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> ReadAllTextAsync(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(path);
|
||||
_logger.LogDebug("读取文件: {Path}, 大小: {Size} 字符", path, content.Length);
|
||||
return content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "读取文件时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteAllTextAsync(string path, string content)
|
||||
{
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(path, content);
|
||||
_logger.LogDebug("写入文件: {Path}, 大小: {Size} 字符", path, content.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "写入文件时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public FileInfo GetFileInfo(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new FileInfo(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "获取文件信息时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public DirectoryInfo GetDirectoryInfo(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new DirectoryInfo(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "获取目录信息时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public string[] GetFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.GetFiles(path, searchPattern, searchOption);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "获取文件列表时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public string[] GetDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.GetDirectories(path, searchPattern, searchOption);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "获取目录列表时出错: {Path}", path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
229
src/LubanHub.Core/Services/CoreProcessService.cs
Normal file
229
src/LubanHub.Core/Services/CoreProcessService.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using LubanHub.Core.Attributes;
|
||||
using LubanHub.Core.Interfaces;
|
||||
using LubanHub.Core.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LubanHub.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 进程调用服务实现
|
||||
/// </summary>
|
||||
[RegistService(ServiceLifetime.Singleton, typeof(ICoreProcessService))]
|
||||
public class CoreProcessService : ICoreProcessService
|
||||
{
|
||||
private readonly ILogger<CoreProcessService> _logger;
|
||||
|
||||
public CoreProcessService(ILogger<CoreProcessService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ProcessResult> ExecuteAsync(string fileName, string? arguments = null, string? workingDirectory = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("执行进程: {FileName} {Arguments}", fileName, arguments);
|
||||
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments ?? string.Empty,
|
||||
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
using var process = new Process { StartInfo = processStartInfo };
|
||||
|
||||
var outputBuilder = new StringBuilder();
|
||||
var errorBuilder = new StringBuilder();
|
||||
|
||||
process.OutputDataReceived += (sender, args) =>
|
||||
{
|
||||
if (args.Data != null)
|
||||
{
|
||||
outputBuilder.AppendLine(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, args) =>
|
||||
{
|
||||
if (args.Data != null)
|
||||
{
|
||||
errorBuilder.AppendLine(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
|
||||
var result = new ProcessResult
|
||||
{
|
||||
ExitCode = process.ExitCode,
|
||||
StandardOutput = outputBuilder.ToString(),
|
||||
StandardError = errorBuilder.ToString()
|
||||
};
|
||||
|
||||
_logger.LogDebug("进程执行完成: {FileName}, 退出代码: {ExitCode}", fileName, result.ExitCode);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "执行进程时出错: {FileName} {Arguments}", fileName, arguments);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
|
||||
Action<string>? onOutputReceived, Action<string>? onErrorReceived, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("执行进程(实时输出): {FileName} {Arguments}", fileName, arguments);
|
||||
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments ?? string.Empty,
|
||||
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
using var process = new Process { StartInfo = processStartInfo };
|
||||
|
||||
var outputBuilder = new StringBuilder();
|
||||
var errorBuilder = new StringBuilder();
|
||||
|
||||
process.OutputDataReceived += (sender, args) =>
|
||||
{
|
||||
if (args.Data != null)
|
||||
{
|
||||
outputBuilder.AppendLine(args.Data);
|
||||
onOutputReceived?.Invoke(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, args) =>
|
||||
{
|
||||
if (args.Data != null)
|
||||
{
|
||||
errorBuilder.AppendLine(args.Data);
|
||||
onErrorReceived?.Invoke(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
|
||||
var result = new ProcessResult
|
||||
{
|
||||
ExitCode = process.ExitCode,
|
||||
StandardOutput = outputBuilder.ToString(),
|
||||
StandardError = errorBuilder.ToString()
|
||||
};
|
||||
|
||||
_logger.LogDebug("进程执行完成(实时输出): {FileName}, 退出代码: {ExitCode}", fileName, result.ExitCode);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "执行进程时出错(实时输出): {FileName} {Arguments}", fileName, arguments);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Process StartProcess(string fileName, string? arguments = null, string? workingDirectory = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("启动进程: {FileName} {Arguments}", fileName, arguments);
|
||||
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments ?? string.Empty,
|
||||
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
var process = Process.Start(processStartInfo);
|
||||
if (process == null)
|
||||
{
|
||||
throw new InvalidOperationException($"无法启动进程: {fileName}");
|
||||
}
|
||||
|
||||
return process;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "启动进程时出错: {FileName} {Arguments}", fileName, arguments);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsProcessRunning(string processName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
return processes.Length > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "检查进程是否运行时出错: {ProcessName}", processName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void KillProcess(string processName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
foreach (var process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
_logger.LogDebug("杀死进程: {ProcessName} (PID: {ProcessId})", processName, process.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "杀死进程时出错: {ProcessName} (PID: {ProcessId})", processName, process.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "杀死进程时出错: {ProcessName}", processName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user