From aecf9e443cee266194f32388c3698159b10f8cfb Mon Sep 17 00:00:00 2001 From: doudou0720 <98651603+doudou0720@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:31:17 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat(Upload/Common):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E4=BB=A5=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E8=AE=BE=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增UploadSettings类用于管理上传通用设置 - 重构上传逻辑,将延迟上传功能移至UploadHelper - 在Dlass设置窗口添加通用设置标签页 - 支持多上传提供者管理及取消操作 - 增强文件上传前的验证和错误处理 Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com> --- Ink Canvas/Helpers/DlassNoteUploader.cs | 124 +- Ink Canvas/Helpers/UploadHelper.cs | 54 +- Ink Canvas/Resources/Settings.cs | 22 + Ink Canvas/Windows/DlassSettingsWindow.xaml | 1030 ++++++++++------- .../Windows/DlassSettingsWindow.xaml.cs | 162 ++- 5 files changed, 914 insertions(+), 478 deletions(-) diff --git a/Ink Canvas/Helpers/DlassNoteUploader.cs b/Ink Canvas/Helpers/DlassNoteUploader.cs index 295faf82..d36a00b7 100644 --- a/Ink Canvas/Helpers/DlassNoteUploader.cs +++ b/Ink Canvas/Helpers/DlassNoteUploader.cs @@ -188,20 +188,24 @@ public static void InitializeQueue() /// /// 保存队列到文件 /// - private static async Task SaveQueueToFileAsync() + private static async Task SaveQueueToFileAsync(CancellationToken cancellationToken = default) { - if (!await _queueSaveLock.WaitAsync(1000)) // 最多等待1秒 + if (!await _queueSaveLock.WaitAsync(1000, cancellationToken)) // 最多等待1秒 { return; // 如果无法获取锁,跳过保存(避免阻塞) } try { + cancellationToken.ThrowIfCancellationRequested(); + var queueData = new List(); // 将队列转换为可序列化的格式 foreach (var item in _uploadQueue) { + cancellationToken.ThrowIfCancellationRequested(); + queueData.Add(new UploadQueueItemData { FilePath = item.FilePath, @@ -231,6 +235,10 @@ private static async Task SaveQueueToFileAsync() File.Move(tempFilePath, queueFilePath); }); } + catch (OperationCanceledException) + { + // 取消操作,静默处理 + } catch (Exception ex) { LogHelper.WriteLogToFile($"保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error); @@ -325,11 +333,14 @@ private class AuthWithTokenResponse /// 异步上传笔记文件到Dlass(支持PNG、ICSTK、XML和ZIP格式) /// /// 文件路径(支持PNG、ICSTK、XML和ZIP) + /// 取消令牌 /// 是否成功加入队列(不等待实际上传完成) - public static async Task UploadNoteFileAsync(string filePath) + public static async Task UploadNoteFileAsync(string filePath, CancellationToken cancellationToken = default) { try { + cancellationToken.ThrowIfCancellationRequested(); + // 检查是否启用自动上传 if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true) { @@ -357,42 +368,16 @@ public static async Task UploadNoteFileAsync(string filePath) return false; } - // 获取上传延迟时间(分钟) - var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0; - - // 如果设置了延迟时间,在后台任务中等待后再加入队列 - if (delayMinutes > 0) - { - _ = Task.Run(async () => - { - try - { - await Task.Delay(TimeSpan.FromMinutes(delayMinutes)).ConfigureAwait(false); - if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true) - { - LogHelper.WriteLogToFile($"延迟结束后自动上传已关闭,跳过入队: {filePath}", LogHelper.LogType.Event); - return; - } - if (!File.Exists(filePath)) - { - LogHelper.WriteLogToFile($"延迟结束后文件已不存在,跳过入队: {filePath}", LogHelper.LogType.Event); - return; - } - EnqueueFile(filePath); - } - catch (Exception ex) - { - LogHelper.WriteLogToFile($"延迟加入上传队列时出错: {ex}", LogHelper.LogType.Error); - } - }); - } - else - { - EnqueueFile(filePath); - } + // 直接加入队列,延迟逻辑由UploadHelper处理 + EnqueueFile(filePath, 0, cancellationToken); return true; } + catch (OperationCanceledException) + { + LogHelper.WriteLogToFile($"上传被取消: {Path.GetFileName(filePath)}", LogHelper.LogType.Event); + throw; + } catch (Exception ex) { LogHelper.WriteLogToFile($"加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error); @@ -403,7 +388,7 @@ public static async Task UploadNoteFileAsync(string filePath) /// /// 将文件加入上传队列 /// - private static void EnqueueFile(string filePath, int retryCount = 0) + private static void EnqueueFile(string filePath, int retryCount = 0, CancellationToken cancellationToken = default) { _uploadQueue.Enqueue(new UploadQueueItem { @@ -416,39 +401,48 @@ private static void EnqueueFile(string filePath, int retryCount = 0) { try { + cancellationToken.ThrowIfCancellationRequested(); await SaveQueueToFileAsync().ConfigureAwait(false); } + catch (OperationCanceledException) + { + // 取消操作,静默处理 + } catch (Exception ex) { LogHelper.WriteLogToFile($"保存上传队列时出错(后台任务): {ex}", LogHelper.LogType.Error); } - }); + }, cancellationToken); // 如果队列达到批量大小,触发批量上传 if (_uploadQueue.Count >= BATCH_SIZE) { - _ = ProcessUploadQueueAsync(); + _ = ProcessUploadQueueAsync(cancellationToken); } } /// /// 处理上传队列,批量上传文件 /// - private static async Task ProcessUploadQueueAsync() + private static async Task ProcessUploadQueueAsync(CancellationToken cancellationToken = default) { // 使用信号量防止并发处理 - if (!await _queueProcessingLock.WaitAsync(0)) + if (!await _queueProcessingLock.WaitAsync(0, cancellationToken)) { return; // 已有处理任务在运行 } try { + cancellationToken.ThrowIfCancellationRequested(); + var filesToUpload = new List(); // 从队列中取出最多BATCH_SIZE个文件 while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item)) { + cancellationToken.ThrowIfCancellationRequested(); + // 再次检查文件是否存在 if (File.Exists(item.FilePath)) { @@ -468,6 +462,8 @@ private static async Task ProcessUploadQueueAsync() try { + cancellationToken.ThrowIfCancellationRequested(); + var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName; if (string.IsNullOrEmpty(selectedClassName)) { @@ -475,7 +471,7 @@ private static async Task ProcessUploadQueueAsync() // 将文件重新加入队列 foreach (var item in filesToUpload) { - EnqueueFile(item.FilePath, item.RetryCount); + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); } return; } @@ -487,7 +483,7 @@ private static async Task ProcessUploadQueueAsync() // 将文件重新加入队列 foreach (var item in filesToUpload) { - EnqueueFile(item.FilePath, item.RetryCount); + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); } return; } @@ -512,7 +508,7 @@ private static async Task ProcessUploadQueueAsync() // 将文件重新加入队列 foreach (var item in filesToUpload) { - EnqueueFile(item.FilePath, item.RetryCount); + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); } return; } @@ -526,19 +522,28 @@ private static async Task ProcessUploadQueueAsync() // 将文件重新加入队列 foreach (var item in filesToUpload) { - EnqueueFile(item.FilePath, item.RetryCount); + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); } return; } } } + catch (OperationCanceledException) + { + // 取消操作,将文件重新加入队列 + foreach (var item in filesToUpload) + { + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); + } + throw; + } catch (Exception ex) { LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error); // 将文件重新加入队列 foreach (var item in filesToUpload) { - EnqueueFile(item.FilePath, item.RetryCount); + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); } return; } @@ -548,7 +553,8 @@ private static async Task ProcessUploadQueueAsync() { try { - var success = await UploadFileInternalAsync(item.FilePath, sharedWhiteboard, apiBaseUrl, userToken); + cancellationToken.ThrowIfCancellationRequested(); + var success = await UploadFileInternalAsync(item.FilePath, sharedWhiteboard, apiBaseUrl, userToken, cancellationToken); if (!success) { // 检查是否是可重试的错误 @@ -558,7 +564,7 @@ private static async Task ProcessUploadQueueAsync() if (item.RetryCount < MAX_RETRY_COUNT) { LogHelper.WriteLogToFile($"上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event); - EnqueueFile(item.FilePath, item.RetryCount + 1); + EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken); } else { @@ -568,6 +574,12 @@ private static async Task ProcessUploadQueueAsync() } return success; } + catch (OperationCanceledException) + { + // 取消操作,将文件重新加入队列 + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); + throw; + } catch (Exception ex) { // 检查是否是可重试的错误(超时、网络错误等) @@ -583,7 +595,7 @@ private static async Task ProcessUploadQueueAsync() if (item.RetryCount < MAX_RETRY_COUNT) { LogHelper.WriteLogToFile($"上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event); - EnqueueFile(item.FilePath, item.RetryCount + 1); + EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken); } else { @@ -596,7 +608,7 @@ private static async Task ProcessUploadQueueAsync() await Task.WhenAll(uploadTasks); // 上传完成后保存队列状态 - await SaveQueueToFileAsync(); + await SaveQueueToFileAsync(cancellationToken); // 如果队列达到批量大小,继续处理 if (_uploadQueue.Count >= BATCH_SIZE) @@ -605,13 +617,13 @@ private static async Task ProcessUploadQueueAsync() { try { - await ProcessUploadQueueAsync().ConfigureAwait(false); + await ProcessUploadQueueAsync(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { LogHelper.WriteLogToFile($"继续批量处理上传队列时出错: {ex}", LogHelper.LogType.Error); } - }); + }, cancellationToken); } } finally @@ -627,10 +639,12 @@ private static async Task ProcessUploadQueueAsync() /// 白板信息(如果为null则重新获取) /// API基础URL(如果为null则从设置获取) /// 用户Token(如果为null则从设置获取) - private static async Task UploadFileInternalAsync(string filePath, WhiteboardInfo whiteboard = null, string apiBaseUrl = null, string userToken = null) + private static async Task UploadFileInternalAsync(string filePath, WhiteboardInfo whiteboard = null, string apiBaseUrl = null, string userToken = null, CancellationToken cancellationToken = default) { try { + cancellationToken.ThrowIfCancellationRequested(); + // 再次检查文件是否存在(可能在队列等待时被删除) if (!File.Exists(filePath)) { @@ -656,6 +670,8 @@ private static async Task UploadFileInternalAsync(string filePath, Whitebo // 如果白板信息未提供,则重新获取 if (whiteboard == null) { + cancellationToken.ThrowIfCancellationRequested(); + var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName; if (string.IsNullOrEmpty(selectedClassName)) { @@ -736,6 +752,8 @@ private static async Task UploadFileInternalAsync(string filePath, Whitebo // 创建API客户端并上传文件 using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken)) { + cancellationToken.ThrowIfCancellationRequested(); + var uploadResult = await apiClient.UploadNoteAsync( "/api/whiteboard/upload_note", filePath, diff --git a/Ink Canvas/Helpers/UploadHelper.cs b/Ink Canvas/Helpers/UploadHelper.cs index cef66c61..d815c6c6 100644 --- a/Ink Canvas/Helpers/UploadHelper.cs +++ b/Ink Canvas/Helpers/UploadHelper.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; +using System.Threading; namespace Ink_Canvas.Helpers { @@ -24,8 +26,9 @@ public interface IUploadProvider /// 上传文件 /// /// 文件路径 + /// 取消令牌 /// 是否上传成功 - Task UploadAsync(string filePath); + Task UploadAsync(string filePath, CancellationToken cancellationToken = default); } /// @@ -41,16 +44,17 @@ public class DlassUploadProvider : IUploadProvider /// /// 是否启用 /// - public bool IsEnabled => MainWindow.Settings?.Dlass?.IsAutoUploadNotes ?? false; + public bool IsEnabled => MainWindow.Settings?.Upload?.EnabledProviders?.Contains(Name) ?? false; /// /// 上传文件 /// /// 文件路径 + /// 取消令牌 /// 是否上传成功 - public async Task UploadAsync(string filePath) + public async Task UploadAsync(string filePath, CancellationToken cancellationToken = default) { - return await DlassNoteUploader.UploadNoteFileAsync(filePath); + return await DlassNoteUploader.UploadNoteFileAsync(filePath, cancellationToken); } } @@ -113,8 +117,9 @@ private static void RegisterProviderInternal(IUploadProvider provider) /// 上传文件到所有启用的提供者 /// /// 文件路径 + /// 取消令牌 /// 是否至少有一个提供者上传成功 - public static async Task UploadFileAsync(string filePath) + public static async Task UploadFileAsync(string filePath, CancellationToken cancellationToken = default) { if (!_initialized) { @@ -129,19 +134,56 @@ public static async Task UploadFileAsync(string filePath) bool anySuccess = false; + // 获取上传延迟时间 + int delayMinutes = MainWindow.Settings?.Upload?.UploadDelayMinutes ?? 0; + + // 应用上传延迟 + if (delayMinutes > 0) + { + LogHelper.WriteLogToFile($"上传延迟 {delayMinutes} 分钟", LogHelper.LogType.Event); + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(TimeSpan.FromMinutes(delayMinutes), cancellationToken).ConfigureAwait(false); + } + + // 上传前验证文件是否存在且可访问 + if (!File.Exists(filePath)) + { + LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error); + return false; + } + + try + { + // 检查文件是否可访问 + using (var fileStream = File.OpenRead(filePath)) + { + // 文件可访问 + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"上传失败:文件不可访问 - {filePath}, 原因: {ex.Message}", LogHelper.LogType.Error); + return false; + } + foreach (var provider in providersSnapshot) { try { if (provider.IsEnabled) { - bool success = await provider.UploadAsync(filePath); + bool success = await provider.UploadAsync(filePath, cancellationToken).ConfigureAwait(false); if (success) { anySuccess = true; } } } + catch (OperationCanceledException) + { + LogHelper.WriteLogToFile($"上传被取消: {provider.Name}", LogHelper.LogType.Event); + throw; + } catch (Exception ex) { LogHelper.WriteLogToFile($"使用 {provider.Name} 上传失败: {ex}", LogHelper.LogType.Error); diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index 34372fea..c55f4151 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -32,6 +32,9 @@ public class Settings [JsonProperty("dlass")] public DlassSettings Dlass { get; set; } = new DlassSettings(); + [JsonProperty("upload")] + public UploadSettings Upload { get; set; } = new UploadSettings(); + [JsonProperty("security")] public Security Security { get; set; } = new Security(); } @@ -861,5 +864,24 @@ public int AutoUploadDelayMinutes } } + public class UploadSettings + { + [JsonProperty("uploadDelayMinutes")] + public int UploadDelayMinutes + { + get { return _uploadDelayMinutes; } + set { _uploadDelayMinutes = Math.Max(0, Math.Min(60, value)); } + } + private int _uploadDelayMinutes = 0; + + [JsonProperty("enabledProviders")] + public List EnabledProviders + { + get { return _enabledProviders; } + set { _enabledProviders = value ?? new List(); } + } + private List _enabledProviders = new List(); + } + } diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml b/Ink Canvas/Windows/DlassSettingsWindow.xaml index 5db41bc4..b541637f 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml @@ -16,15 +16,62 @@ - - - - - - - - - + + + + + + + + + + + + + + @@ -85,410 +132,587 @@ Background="{StaticResource WindowBackground}" Padding="20,10,20,20" CornerRadius="0,0,15,15"> - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - + BorderThickness="1" + CornerRadius="6" + Padding="8"> + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + Foreground="{StaticResource TextSecondary}" + TextWrapping="Wrap" + Margin="0,20,0,20"/> - - - - - - - - - - - - + - - - + + + diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs index 966d2b2b..d4aa5c7a 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs @@ -7,17 +7,27 @@ using System.Windows; using System.Windows.Input; using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox; +using ui = iNKORE.UI.WPF.Modern.Controls; namespace Ink_Canvas.Windows { /// - /// DlassSettingsWindow.xaml 的交互逻辑 + /// Dlass设置管理窗口 /// + /// + /// 该窗口包含三个标签页: + /// 1. 通用设置 - 管理所有上传提供者的通用设置,包括上传延迟时间和提供者启用/禁用 + /// 2. Dlass - 管理Dlass服务端连接和设置,包括用户Token、班级选择和自动上传设置 + /// 3. WebDav - 预留的WebDav连接设置页面 + /// public partial class DlassSettingsWindow : Window { private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA"; private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA"; + // 静态 Regex 实例,用于验证数字输入 + private static readonly Regex _nonDigitRegex = new Regex("[^0-9]+", RegexOptions.Compiled | RegexOptions.CultureInvariant); + private DlassApiClient _apiClient; private List _currentWhiteboards = new List(); private UserInfo _currentUser; @@ -38,6 +48,9 @@ public DlassSettingsWindow(MainWindow mainWindow = null) // 加载自动上传设置 LoadAutoUploadSettings(); + // 加载通用设置 + LoadUniversalUploadSettings(); + // 初始化API客户端(优先使用用户token) InitializeApiClient(); @@ -291,7 +304,7 @@ private void LoadAutoUploadSettings() { delayMinutes = 0; } - TxtUploadDelayMinutes.Text = delayMinutes.ToString(); + } } catch (Exception ex) @@ -310,6 +323,31 @@ private void ToggleSwitchAutoUploadNotes_Toggled(object sender, RoutedEventArgs if (MainWindow.Settings?.Dlass != null) { MainWindow.Settings.Dlass.IsAutoUploadNotes = ToggleSwitchAutoUploadNotes.IsOn; + + // 同步更新到EnabledProviders列表 + if (MainWindow.Settings.Upload != null) + { + if (MainWindow.Settings.Upload.EnabledProviders == null) + { + MainWindow.Settings.Upload.EnabledProviders = new List(); + } + + if (ToggleSwitchAutoUploadNotes.IsOn) + { + if (!MainWindow.Settings.Upload.EnabledProviders.Contains("Dlass")) + { + MainWindow.Settings.Upload.EnabledProviders.Add("Dlass"); + } + } + else + { + MainWindow.Settings.Upload.EnabledProviders.Remove("Dlass"); + } + + // 重新加载通用设置,更新UI + LoadUniversalUploadSettings(); + } + MainWindow.SaveSettingsToFile(); } } @@ -319,53 +357,145 @@ private void ToggleSwitchAutoUploadNotes_Toggled(object sender, RoutedEventArgs } } + + + + + /// + /// 加载通用设置 + /// + private void LoadUniversalUploadSettings() + { + try + { + // 加载上传延迟时间 + if (MainWindow.Settings?.Upload != null) + { + var delayMinutes = MainWindow.Settings.Upload.UploadDelayMinutes; + if (delayMinutes < 0 || delayMinutes > 60) + { + delayMinutes = 0; + } + TxtUniversalUploadDelayMinutes.Text = delayMinutes.ToString(); + } + + // 加载上传提供者列表 + LoadUploadProvidersList(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载通用设置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 加载上传提供者列表 + /// + private void LoadUploadProvidersList() + { + try + { + var providers = UploadHelper.GetProviders(); + LstUploadProviders.ItemsSource = providers; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载上传提供者列表时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + /// - /// 上传延迟时间输入框文本改变事件 + /// 通用设置延迟时间输入框文本改变事件 /// - private void TxtUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) + private void TxtUniversalUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) { try { - if (MainWindow.Settings?.Dlass != null && int.TryParse(TxtUploadDelayMinutes.Text, out int delayMinutes)) + if (MainWindow.Settings?.Upload != null && int.TryParse(TxtUniversalUploadDelayMinutes.Text, out int delayMinutes)) { // 限制范围在0-60分钟 if (delayMinutes < 0) { delayMinutes = 0; - TxtUploadDelayMinutes.Text = "0"; + TxtUniversalUploadDelayMinutes.Text = "0"; } else if (delayMinutes > 60) { delayMinutes = 60; - TxtUploadDelayMinutes.Text = "60"; + TxtUniversalUploadDelayMinutes.Text = "60"; } - MainWindow.Settings.Dlass.AutoUploadDelayMinutes = delayMinutes; + MainWindow.Settings.Upload.UploadDelayMinutes = delayMinutes; MainWindow.SaveSettingsToFile(); } - else if (string.IsNullOrWhiteSpace(TxtUploadDelayMinutes.Text)) + else if (string.IsNullOrWhiteSpace(TxtUniversalUploadDelayMinutes.Text)) { // 空文本时设置为0 - if (MainWindow.Settings?.Dlass != null) + if (MainWindow.Settings?.Upload != null) { - MainWindow.Settings.Dlass.AutoUploadDelayMinutes = 0; + MainWindow.Settings.Upload.UploadDelayMinutes = 0; MainWindow.SaveSettingsToFile(); } } } catch (Exception ex) { - LogHelper.WriteLogToFile($"保存上传延迟时间时出错: {ex.Message}", LogHelper.LogType.Error); + LogHelper.WriteLogToFile($"保存通用设置延迟时间时出错: {ex.Message}", LogHelper.LogType.Error); } } /// - /// 上传延迟时间输入框预览文本输入事件(只允许数字) + /// 通用设置延迟时间输入框预览文本输入事件(只允许数字) + /// + private void TxtUniversalUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e) + { + e.Handled = _nonDigitRegex.IsMatch(e.Text); + } + + /// + /// 上传提供者启用/禁用开关切换事件 /// - private void TxtUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e) + private void ToggleProviderEnabled_Toggled(object sender, RoutedEventArgs e) { - Regex regex = new Regex("[^0-9]+"); - e.Handled = regex.IsMatch(e.Text); + try + { + if (sender is iNKORE.UI.WPF.Modern.Controls.ToggleSwitch toggleSwitch && toggleSwitch.DataContext is IUploadProvider provider) + { + if (MainWindow.Settings?.Upload != null) + { + if (MainWindow.Settings.Upload.EnabledProviders == null) + { + MainWindow.Settings.Upload.EnabledProviders = new List(); + } + + if (toggleSwitch.IsOn) + { + if (!MainWindow.Settings.Upload.EnabledProviders.Contains(provider.Name)) + { + MainWindow.Settings.Upload.EnabledProviders.Add(provider.Name); + } + } + else + { + MainWindow.Settings.Upload.EnabledProviders.Remove(provider.Name); + } + + // 同步更新Dlass的IsAutoUploadNotes设置(如果是Dlass提供者) + if (provider.Name == "Dlass" && MainWindow.Settings.Dlass != null) + { + MainWindow.Settings.Dlass.IsAutoUploadNotes = toggleSwitch.IsOn; + // 同步更新Dlass标签页中的开关状态 + ToggleSwitchAutoUploadNotes.IsOn = toggleSwitch.IsOn; + } + + MainWindow.SaveSettingsToFile(); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存上传提供者启用状态时出错: {ex.Message}", LogHelper.LogType.Error); + } } /// From ad00b30612a8f01f6944c08c8dabdc0b44a6c4db Mon Sep 17 00:00:00 2001 From: doudou0720 <98651603+doudou0720@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:43:09 +0800 Subject: [PATCH 02/12] =?UTF-8?q?feat(upload):=20=E6=B7=BB=E5=8A=A0WebDav?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增WebDavUploader工具类实现文件上传功能 - 添加WebDavUploadProvider作为上传提供者 - 在设置界面增加WebDav配置选项 - 添加WebDav.Client NuGet包依赖 Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com> --- Ink Canvas/Helpers/UploadHelper.cs | 28 +++ Ink Canvas/Helpers/WebDavUploader.cs | 146 +++++++++++++ Ink Canvas/InkCanvasForClass.csproj | 1 + Ink Canvas/Resources/Settings.cs | 13 ++ Ink Canvas/Windows/DlassSettingsWindow.xaml | 196 +++++++++++++++++- .../Windows/DlassSettingsWindow.xaml.cs | 58 ++++++ Ink Canvas/packages.lock.json | 6 + 7 files changed, 444 insertions(+), 4 deletions(-) create mode 100644 Ink Canvas/Helpers/WebDavUploader.cs diff --git a/Ink Canvas/Helpers/UploadHelper.cs b/Ink Canvas/Helpers/UploadHelper.cs index d815c6c6..dfcf7fc5 100644 --- a/Ink Canvas/Helpers/UploadHelper.cs +++ b/Ink Canvas/Helpers/UploadHelper.cs @@ -58,6 +58,33 @@ public async Task UploadAsync(string filePath, CancellationToken cancellat } } + /// + /// WebDav上传提供者 + /// + public class WebDavUploadProvider : IUploadProvider + { + /// + /// 提供者名称 + /// + public string Name => "WebDav"; + + /// + /// 是否启用 + /// + public bool IsEnabled => MainWindow.Settings?.Upload?.EnabledProviders?.Contains(Name) ?? false; + + /// + /// 上传文件 + /// + /// 文件路径 + /// 取消令牌 + /// 是否上传成功 + public async Task UploadAsync(string filePath, CancellationToken cancellationToken = default) + { + return await WebDavUploader.UploadFileAsync(filePath, cancellationToken); + } + } + /// @@ -81,6 +108,7 @@ public static void Initialize() // 注册默认上传提供者 RegisterProviderInternal(new DlassUploadProvider()); + RegisterProviderInternal(new WebDavUploadProvider()); _initialized = true; } diff --git a/Ink Canvas/Helpers/WebDavUploader.cs b/Ink Canvas/Helpers/WebDavUploader.cs new file mode 100644 index 00000000..61f64f8d --- /dev/null +++ b/Ink Canvas/Helpers/WebDavUploader.cs @@ -0,0 +1,146 @@ +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using WebDav; + +namespace Ink_Canvas.Helpers +{ + /// + /// WebDav上传工具类 + /// + public static class WebDavUploader + { + /// + /// 上传文件到WebDav服务器 + /// + /// 文件路径 + /// 取消令牌 + /// 是否上传成功 + public static async Task UploadFileAsync(string filePath, CancellationToken cancellationToken = default) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + // 检查文件是否存在 + if (!File.Exists(filePath)) + { + LogHelper.WriteLogToFile($"WebDav上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error); + return false; + } + + // 获取WebDav设置 + var webDavUrl = MainWindow.Settings?.Dlass?.WebDavUrl; + var username = MainWindow.Settings?.Dlass?.WebDavUsername; + var password = MainWindow.Settings?.Dlass?.WebDavPassword; + var rootDirectory = MainWindow.Settings?.Dlass?.WebDavRootDirectory; + + // 验证设置 + if (string.IsNullOrEmpty(webDavUrl)) + { + LogHelper.WriteLogToFile("WebDav上传失败:未设置WebDav地址", LogHelper.LogType.Error); + return false; + } + + // 构建完整的目标路径 + var fileName = Path.GetFileName(filePath); + var targetPath = Path.Combine(rootDirectory ?? string.Empty, fileName).Replace("\\", "/"); + if (targetPath.StartsWith("/")) + { + targetPath = targetPath.Substring(1); + } + + // 创建WebDav客户端 + var clientParams = new WebDavClientParams + { + BaseAddress = new Uri(webDavUrl), + Credentials = new NetworkCredential(username ?? string.Empty, password ?? string.Empty) + }; + + using (var client = new WebDavClient(clientParams)) + { + cancellationToken.ThrowIfCancellationRequested(); + + // 确保目录存在 + var directoryPath = Path.GetDirectoryName(targetPath); + if (!string.IsNullOrEmpty(directoryPath)) + { + await EnsureDirectoryExistsAsync(client, directoryPath, cancellationToken); + } + + // 上传文件 + using (var fileStream = File.OpenRead(filePath)) + { + // 检查取消令牌 + cancellationToken.ThrowIfCancellationRequested(); + + var result = await client.PutFile(targetPath, fileStream); + if (result.IsSuccessful) + { + LogHelper.WriteLogToFile($"WebDav上传成功:{filePath} -> {targetPath}", LogHelper.LogType.Event); + return true; + } + else + { + LogHelper.WriteLogToFile($"WebDav上传失败:{filePath}, 状态码: {result.StatusCode}, 原因: {result.Description}", LogHelper.LogType.Error); + return false; + } + } + } + } + catch (OperationCanceledException) + { + LogHelper.WriteLogToFile("WebDav上传被取消", LogHelper.LogType.Event); + throw; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"WebDav上传异常:{ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 确保WebDav目录存在 + /// + /// WebDav客户端 + /// 目录路径 + /// 取消令牌 + private static async Task EnsureDirectoryExistsAsync(IWebDavClient client, string directoryPath, CancellationToken cancellationToken) + { + try + { + // 分割路径并逐级创建目录 + var pathParts = directoryPath.Split('/'); + var currentPath = string.Empty; + + foreach (var part in pathParts) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (string.IsNullOrEmpty(part)) + continue; + + currentPath = Path.Combine(currentPath, part).Replace("\\", "/"); + + // 检查取消令牌 + cancellationToken.ThrowIfCancellationRequested(); + + // 尝试创建目录 + var result = await client.Mkcol(currentPath); + // 如果目录已存在,忽略错误(409 Conflict) + if (!result.IsSuccessful && result.StatusCode != 409) + { + LogHelper.WriteLogToFile($"创建WebDav目录失败:{currentPath}, 状态码: {result.StatusCode}", LogHelper.LogType.Warning); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"确保WebDav目录存在时出错:{ex.Message}", LogHelper.LogType.Error); + } + } + } +} diff --git a/Ink Canvas/InkCanvasForClass.csproj b/Ink Canvas/InkCanvasForClass.csproj index a8bf0332..814181a8 100644 --- a/Ink Canvas/InkCanvasForClass.csproj +++ b/Ink Canvas/InkCanvasForClass.csproj @@ -166,6 +166,7 @@ + diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index c55f4151..9ce5d268 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -862,6 +862,19 @@ public int AutoUploadDelayMinutes get { return _autoUploadDelayMinutes; } set { _autoUploadDelayMinutes = Math.Max(0, value); } } + + // WebDav设置 + [JsonProperty("webDavUrl")] + public string WebDavUrl { get; set; } = string.Empty; + + [JsonProperty("webDavUsername")] + public string WebDavUsername { get; set; } = string.Empty; + + [JsonProperty("webDavPassword")] + public string WebDavPassword { get; set; } = string.Empty; + + [JsonProperty("webDavRootDirectory")] + public string WebDavRootDirectory { get; set; } = string.Empty; } public class UploadSettings diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml b/Ink Canvas/Windows/DlassSettingsWindow.xaml index b541637f..f8cac659 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml @@ -701,12 +701,200 @@ HorizontalAlignment="Stretch" Margin="0,2,0,2"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Margin="0,0,0,2"/> + + + + + + + + + + + + + diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs index d4aa5c7a..358c523f 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs @@ -51,6 +51,9 @@ public DlassSettingsWindow(MainWindow mainWindow = null) // 加载通用设置 LoadUniversalUploadSettings(); + // 加载WebDav设置 + LoadWebDavSettings(); + // 初始化API客户端(优先使用用户token) InitializeApiClient(); @@ -662,6 +665,61 @@ private void BtnCancel_Click(object sender, RoutedEventArgs e) Close(); } + /// + /// 加载WebDav设置 + /// + private void LoadWebDavSettings() + { + try + { + if (MainWindow.Settings?.Dlass != null) + { + TxtWebDavUrl.Text = MainWindow.Settings.Dlass.WebDavUrl; + TxtWebDavUsername.Text = MainWindow.Settings.Dlass.WebDavUsername; + TxtWebDavPassword.Password = MainWindow.Settings.Dlass.WebDavPassword; + TxtWebDavRootDirectory.Text = MainWindow.Settings.Dlass.WebDavRootDirectory; + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载WebDav设置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 保存WebDav设置按钮点击事件 + /// + private void BtnSaveWebDav_Click(object sender, RoutedEventArgs e) + { + try + { + if (MainWindow.Settings?.Dlass != null) + { + MainWindow.Settings.Dlass.WebDavUrl = TxtWebDavUrl.Text; + MainWindow.Settings.Dlass.WebDavUsername = TxtWebDavUsername.Text; + MainWindow.Settings.Dlass.WebDavPassword = TxtWebDavPassword.Password; + MainWindow.Settings.Dlass.WebDavRootDirectory = TxtWebDavRootDirectory.Text; + MainWindow.SaveSettingsToFile(); + + MessageBox.Show("WebDav设置已保存", "成功", MessageBoxButton.OK, MessageBoxImage.Information); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存WebDav设置时出错: {ex.Message}", LogHelper.LogType.Error); + MessageBox.Show($"保存WebDav设置时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + /// + /// 取消WebDav设置按钮点击事件 + /// + private void BtnCancelWebDav_Click(object sender, RoutedEventArgs e) + { + // 重新加载设置,恢复原值 + LoadWebDavSettings(); + } + /// /// 测试API连接 /// diff --git a/Ink Canvas/packages.lock.json b/Ink Canvas/packages.lock.json index 7d2e7578..a50648ea 100644 --- a/Ink Canvas/packages.lock.json +++ b/Ink Canvas/packages.lock.json @@ -137,6 +137,12 @@ "System.Text.Json": "8.0.5" } }, + "WebDav.Client": { + "type": "Direct", + "requested": "[2.9.0, )", + "resolved": "2.9.0", + "contentHash": "GLhd1tQAJeuVO1sj3Wm/dkg0GEVWxk+XGl6rdegMSMHenZuOaWQw4PifWDsjNEC1dtV1/C8JJfK0qfdkM+VIgA==" + }, "AForge": { "type": "Transitive", "resolved": "2.2.5", From 2d7f2923597928fa7f7706875ad4b42bb99de1a2 Mon Sep 17 00:00:00 2001 From: doudou0720 <98651603+doudou0720@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:27:23 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat(WebDAV):=20=E5=AE=9E=E7=8E=B0WebDAV?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=98=9F=E5=88=97=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com> --- Ink Canvas/Helpers/DlassNoteUploader.cs | 2 +- Ink Canvas/Helpers/UploadHelper.cs | 2 +- Ink Canvas/Helpers/WebDavUploadQueue.cs | 553 ++++++++++++++++++ Ink Canvas/Helpers/WebDavUploader.cs | 41 +- Ink Canvas/MainWindow.xaml.cs | 3 +- .../MainWindow_cs/MW_Save&OpenStrokes.cs | 41 +- Ink Canvas/Windows/DlassSettingsWindow.xaml | 12 +- 7 files changed, 622 insertions(+), 32 deletions(-) create mode 100644 Ink Canvas/Helpers/WebDavUploadQueue.cs diff --git a/Ink Canvas/Helpers/DlassNoteUploader.cs b/Ink Canvas/Helpers/DlassNoteUploader.cs index d36a00b7..d4bc6bc4 100644 --- a/Ink Canvas/Helpers/DlassNoteUploader.cs +++ b/Ink Canvas/Helpers/DlassNoteUploader.cs @@ -765,7 +765,7 @@ private static async Task UploadFileInternalAsync(string filePath, Whitebo if (uploadResult != null && uploadResult.Success) { - LogHelper.WriteLogToFile($"笔记上传成功:{fileName} -> {uploadResult.FileUrl}", LogHelper.LogType.Event); + LogHelper.WriteLogToFile($"[Dlass] 笔记上传成功:{fileName} -> {uploadResult.FileUrl}", LogHelper.LogType.Event); return true; } else diff --git a/Ink Canvas/Helpers/UploadHelper.cs b/Ink Canvas/Helpers/UploadHelper.cs index dfcf7fc5..303e3ce6 100644 --- a/Ink Canvas/Helpers/UploadHelper.cs +++ b/Ink Canvas/Helpers/UploadHelper.cs @@ -81,7 +81,7 @@ public class WebDavUploadProvider : IUploadProvider /// 是否上传成功 public async Task UploadAsync(string filePath, CancellationToken cancellationToken = default) { - return await WebDavUploader.UploadFileAsync(filePath, cancellationToken); + return await WebDavUploadQueue.UploadFileAsync(filePath, cancellationToken); } } diff --git a/Ink Canvas/Helpers/WebDavUploadQueue.cs b/Ink Canvas/Helpers/WebDavUploadQueue.cs new file mode 100644 index 00000000..7b76222b --- /dev/null +++ b/Ink Canvas/Helpers/WebDavUploadQueue.cs @@ -0,0 +1,553 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Ink_Canvas.Helpers +{ + /// + /// WebDAV上传队列辅助类 + /// + public class WebDavUploadQueue + { + private const int BATCH_SIZE = 10; // 批量上传大小 + private const int MAX_RETRY_COUNT = 3; // 最大重试次数 + private const string QUEUE_FILE_NAME = "WebDavUploadQueue.json"; + + /// + /// 上传队列项 + /// + private class UploadQueueItemData + { + [JsonProperty("file_path")] + public string FilePath { get; set; } + + [JsonProperty("retry_count")] + public int RetryCount { get; set; } + + [JsonProperty("added_time")] + public DateTime AddedTime { get; set; } + } + + /// + /// 上传队列项 + /// + private class UploadQueueItem + { + public string FilePath { get; set; } + public int RetryCount { get; set; } + } + + /// + /// 上传队列 + /// + private static readonly ConcurrentQueue _uploadQueue = new ConcurrentQueue(); + + /// + /// 队列处理锁,防止并发处理 + /// + private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1); + + /// + /// 队列保存锁,防止并发保存 + /// + private static readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1); + + /// + /// 是否已初始化队列 + /// + private static bool _isQueueInitialized = false; + + /// + /// 获取队列文件路径 + /// + private static string GetQueueFilePath() + { + var configsDir = Path.Combine(App.RootPath, "Configs"); + if (!Directory.Exists(configsDir)) + { + Directory.CreateDirectory(configsDir); + } + return Path.Combine(configsDir, QUEUE_FILE_NAME); + } + + /// + /// 初始化上传队列 + /// + public static void InitializeQueue() + { + if (_isQueueInitialized) + { + return; + } + + try + { + var queueFilePath = GetQueueFilePath(); + if (!File.Exists(queueFilePath)) + { + _isQueueInitialized = true; + return; + } + + var jsonContent = File.ReadAllText(queueFilePath); + if (string.IsNullOrWhiteSpace(jsonContent)) + { + _isQueueInitialized = true; + return; + } + + var queueData = JsonConvert.DeserializeObject>(jsonContent); + if (queueData == null || queueData.Count == 0) + { + _isQueueInitialized = true; + return; + } + + int restoredCount = 0; + int skippedCount = 0; + + foreach (var item in queueData) + { + // 验证文件是否存在 + if (!File.Exists(item.FilePath)) + { + skippedCount++; + continue; + } + + // 验证文件格式和大小 + var fileExtension = Path.GetExtension(item.FilePath).ToLower(); + if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip") + { + skippedCount++; + continue; + } + + try + { + var fileInfo = new FileInfo(item.FilePath); + long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024; + if (fileInfo.Length > maxSize) + { + skippedCount++; + continue; + } + } + catch + { + skippedCount++; + continue; + } + + // 恢复队列项 + _uploadQueue.Enqueue(new UploadQueueItem + { + FilePath = item.FilePath, + RetryCount = item.RetryCount + }); + restoredCount++; + } + + _isQueueInitialized = true; + + if (restoredCount > 0) + { + LogHelper.WriteLogToFile($"[WebDAV] 已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event); + // 如果恢复了队列,触发处理 + _ = Task.Run(async () => + { + try + { + await ProcessUploadQueueAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"恢复WebDAV上传队列后处理时出错: {ex}", LogHelper.LogType.Error); + } + }); + } + else if (skippedCount > 0) + { + LogHelper.WriteLogToFile($"[WebDAV] 队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"[WebDAV] 恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error); + _isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试 + } + } + + /// + /// 保存队列到文件 + /// + private static async Task SaveQueueToFileAsync(CancellationToken cancellationToken = default) + { + if (!await _queueSaveLock.WaitAsync(1000, cancellationToken)) // 最多等待1秒 + { + return; // 如果无法获取锁,跳过保存(避免阻塞) + } + + try + { + cancellationToken.ThrowIfCancellationRequested(); + + var queueData = new List(); + + // 将队列转换为可序列化的格式 + foreach (var item in _uploadQueue) + { + cancellationToken.ThrowIfCancellationRequested(); + + queueData.Add(new UploadQueueItemData + { + FilePath = item.FilePath, + RetryCount = item.RetryCount, + AddedTime = DateTime.Now + }); + } + + var queueFilePath = GetQueueFilePath(); + + // 如果队列为空,清空文件 + if (queueData.Count == 0) + { + ClearQueueFile(); + return; + } + + var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented); + + // 使用进程保护的写入门控,避免安全面板中"进程文件保护"占用导致无法写入 + var tempFilePath = queueFilePath + ".tmp"; + ProcessProtectionManager.WithWriteAccess(queueFilePath, () => + { + File.WriteAllText(tempFilePath, jsonContent); + if (File.Exists(queueFilePath)) + File.Delete(queueFilePath); + File.Move(tempFilePath, queueFilePath); + }); + } + catch (OperationCanceledException) + { + // 取消操作,静默处理 + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"[WebDAV] 保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error); + } + finally + { + _queueSaveLock.Release(); + } + } + + /// + /// 清空队列文件 + /// + private static void ClearQueueFile() + { + try + { + var queueFilePath = GetQueueFilePath(); + ProcessProtectionManager.WithWriteAccess(queueFilePath, () => + { + if (File.Exists(queueFilePath)) + File.WriteAllText(queueFilePath, "[]"); + }); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"[WebDAV] 清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 异步上传文件到WebDAV + /// + /// 文件路径 + /// 取消令牌 + /// 是否成功加入队列(不等待实际上传完成) + public static async Task UploadFileAsync(string filePath, CancellationToken cancellationToken = default) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + // 检查是否启用WebDAV上传 + if (!WebDavUploader.IsWebDavEnabled()) + { + return false; + } + + // 基本验证 + if (!File.Exists(filePath)) + { + LogHelper.WriteLogToFile($"[WebDAV] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error); + return false; + } + + var fileExtension = Path.GetExtension(filePath).ToLower(); + if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip") + { + return false; + } + + var fileInfo = new FileInfo(filePath); + long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024; + if (fileInfo.Length > maxSize) + { + LogHelper.WriteLogToFile($"[WebDAV] 上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error); + return false; + } + + // 确保队列已初始化 + if (!_isQueueInitialized) + { + InitializeQueue(); + } + + // 加入队列 + EnqueueFile(filePath, 0, cancellationToken); + + return true; + } + catch (OperationCanceledException) + { + LogHelper.WriteLogToFile($"[WebDAV] 上传被取消: {Path.GetFileName(filePath)}", LogHelper.LogType.Event); + throw; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"[WebDAV] 加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 将文件加入上传队列 + /// + private static void EnqueueFile(string filePath, int retryCount = 0, CancellationToken cancellationToken = default) + { + _uploadQueue.Enqueue(new UploadQueueItem + { + FilePath = filePath, + RetryCount = retryCount + }); + + // 异步保存队列到文件 + _ = Task.Run(async () => + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + await SaveQueueToFileAsync().ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // 取消操作,静默处理 + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"[WebDAV] 保存上传队列时出错(后台任务): {ex}", LogHelper.LogType.Error); + } + }, cancellationToken); + + // 只要有文件加入队列就触发处理 + _ = ProcessUploadQueueAsync(cancellationToken); + } + + /// + /// 处理上传队列,批量上传文件 + /// + private static async Task ProcessUploadQueueAsync(CancellationToken cancellationToken = default) + { + // 使用信号量防止并发处理 + if (!await _queueProcessingLock.WaitAsync(0, cancellationToken)) + { + return; // 已有处理任务在运行 + } + + try + { + cancellationToken.ThrowIfCancellationRequested(); + + var filesToUpload = new List(); + + // 从队列中取出所有文件 + while (_uploadQueue.TryDequeue(out UploadQueueItem item)) + { + cancellationToken.ThrowIfCancellationRequested(); + + // 再次检查文件是否存在 + if (File.Exists(item.FilePath)) + { + filesToUpload.Add(item); + } + } + + if (filesToUpload.Count == 0) + { + return; + } + + // 检查WebDAV设置 + if (!WebDavUploader.IsWebDavEnabled()) + { + LogHelper.WriteLogToFile("[WebDAV] 上传失败:WebDAV未启用", LogHelper.LogType.Error); + // 将文件重新加入队列 + foreach (var item in filesToUpload) + { + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); + } + return; + } + + // 并发上传所有文件,并处理失败重试 + var uploadTasks = filesToUpload.Select(async item => + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + var success = await WebDavUploader.UploadFileAsync(item.FilePath, cancellationToken); + if (!success) + { + // 检查是否是可重试的错误 + if (IsRetryableError(item.FilePath)) + { + // 检查重试次数 + if (item.RetryCount < MAX_RETRY_COUNT) + { + LogHelper.WriteLogToFile($"[WebDAV] 上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event); + EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken); + } + else + { + LogHelper.WriteLogToFile($"[WebDAV] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error); + } + } + } + else + { + LogHelper.WriteLogToFile($"[WebDAV] 上传成功: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event); + } + return success; + } + catch (OperationCanceledException) + { + // 取消操作,将文件重新加入队列 + EnqueueFile(item.FilePath, item.RetryCount, cancellationToken); + throw; + } + catch (Exception ex) + { + // 检查是否是可重试的错误(超时、网络错误等) + var errorMessage = ex.Message.ToLower(); + bool isRetryable = errorMessage.Contains("超时") || + errorMessage.Contains("timeout") || + errorMessage.Contains("网络错误") || + errorMessage.Contains("network") || + errorMessage.Contains("408") || // 请求超时 + errorMessage.Contains("423") || // 资源锁定 + errorMessage.Contains("429") || // 请求过多 + errorMessage.Contains("500") || // 服务器错误 + errorMessage.Contains("502") || // 网关错误 + errorMessage.Contains("503") || // 服务不可用 + errorMessage.Contains("504"); // 网关超时 + + if (isRetryable && IsRetryableError(item.FilePath)) + { + // 检查重试次数 + if (item.RetryCount < MAX_RETRY_COUNT) + { + LogHelper.WriteLogToFile($"[WebDAV] 上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event); + EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken); + } + else + { + LogHelper.WriteLogToFile($"[WebDAV] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error); + } + } + else + { + LogHelper.WriteLogToFile($"[WebDAV] 上传失败(不可重试): {Path.GetFileName(item.FilePath)} - {ex.Message}", LogHelper.LogType.Error); + } + return false; + } + }); + await Task.WhenAll(uploadTasks); + + // 上传完成后保存队列状态 + await SaveQueueToFileAsync(cancellationToken); + + // 检查队列中是否还有文件,如果有就继续处理 + if (_uploadQueue.Count > 0) + { + _ = Task.Run(async () => + { + try + { + await ProcessUploadQueueAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"[WebDAV] 继续处理上传队列时出错: {ex}", LogHelper.LogType.Error); + } + }, cancellationToken); + } + } + finally + { + _queueProcessingLock.Release(); + } + } + + /// + /// 判断错误是否可重试 + /// + private static bool IsRetryableError(string filePath) + { + // 检查文件是否存在 + if (!File.Exists(filePath)) + { + return false; // 文件不存在,不可重试 + } + + // 检查文件扩展名 + var fileExtension = Path.GetExtension(filePath).ToLower(); + if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip") + { + return false; // 文件格式错误,不可重试 + } + + // 检查文件大小 + try + { + var fileInfo = new FileInfo(filePath); + long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024; + if (fileInfo.Length > maxSize) + { + return false; // 文件过大,不可重试 + } + } + catch + { + return false; // 无法读取文件信息,不可重试 + } + + // 检查WebDAV设置是否仍然有效 + if (!WebDavUploader.IsWebDavEnabled()) + { + return false; // WebDAV未启用,不可重试 + } + + // 其他错误(超时、网络错误等)可以重试 + return true; + } + } +} diff --git a/Ink Canvas/Helpers/WebDavUploader.cs b/Ink Canvas/Helpers/WebDavUploader.cs index 61f64f8d..587e33f7 100644 --- a/Ink Canvas/Helpers/WebDavUploader.cs +++ b/Ink Canvas/Helpers/WebDavUploader.cs @@ -27,7 +27,7 @@ public static async Task UploadFileAsync(string filePath, CancellationToke // 检查文件是否存在 if (!File.Exists(filePath)) { - LogHelper.WriteLogToFile($"WebDav上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error); + LogHelper.WriteLogToFile($"[WebDAV] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error); return false; } @@ -40,7 +40,7 @@ public static async Task UploadFileAsync(string filePath, CancellationToke // 验证设置 if (string.IsNullOrEmpty(webDavUrl)) { - LogHelper.WriteLogToFile("WebDav上传失败:未设置WebDav地址", LogHelper.LogType.Error); + LogHelper.WriteLogToFile("[WebDAV] 上传失败:未设置WebDav地址", LogHelper.LogType.Error); return false; } @@ -79,12 +79,12 @@ public static async Task UploadFileAsync(string filePath, CancellationToke var result = await client.PutFile(targetPath, fileStream); if (result.IsSuccessful) { - LogHelper.WriteLogToFile($"WebDav上传成功:{filePath} -> {targetPath}", LogHelper.LogType.Event); + LogHelper.WriteLogToFile($"[WebDAV] 上传成功:{filePath} -> {targetPath}", LogHelper.LogType.Event); return true; } else { - LogHelper.WriteLogToFile($"WebDav上传失败:{filePath}, 状态码: {result.StatusCode}, 原因: {result.Description}", LogHelper.LogType.Error); + LogHelper.WriteLogToFile($"[WebDAV] 上传失败:{filePath}, 状态码: {result.StatusCode}, 原因: {result.Description}", LogHelper.LogType.Error); return false; } } @@ -92,12 +92,12 @@ public static async Task UploadFileAsync(string filePath, CancellationToke } catch (OperationCanceledException) { - LogHelper.WriteLogToFile("WebDav上传被取消", LogHelper.LogType.Event); + LogHelper.WriteLogToFile("[WebDAV] 上传被取消", LogHelper.LogType.Event); throw; } catch (Exception ex) { - LogHelper.WriteLogToFile($"WebDav上传异常:{ex.Message}", LogHelper.LogType.Error); + LogHelper.WriteLogToFile($"[WebDAV] 上传异常:{ex.Message}", LogHelper.LogType.Error); return false; } } @@ -133,13 +133,38 @@ private static async Task EnsureDirectoryExistsAsync(IWebDavClient client, strin // 如果目录已存在,忽略错误(409 Conflict) if (!result.IsSuccessful && result.StatusCode != 409) { - LogHelper.WriteLogToFile($"创建WebDav目录失败:{currentPath}, 状态码: {result.StatusCode}", LogHelper.LogType.Warning); + LogHelper.WriteLogToFile($"[WebDAV] 创建目录失败:{currentPath}, 状态码: {result.StatusCode}", LogHelper.LogType.Warning); } } } catch (Exception ex) { - LogHelper.WriteLogToFile($"确保WebDav目录存在时出错:{ex.Message}", LogHelper.LogType.Error); + LogHelper.WriteLogToFile($"[WebDAV] 确保目录存在时出错:{ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 检查WebDAV是否已启用 + /// + /// 是否启用 + public static bool IsWebDavEnabled() + { + // 检查WebDav设置是否有效 + var webDavUrl = MainWindow.Settings?.Dlass?.WebDavUrl; + if (string.IsNullOrEmpty(webDavUrl)) + { + return false; + } + + // 尝试解析URL + try + { + new Uri(webDavUrl); + return true; + } + catch + { + return false; } } } diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs index 85f5857b..7eee652e 100644 --- a/Ink Canvas/MainWindow.xaml.cs +++ b/Ink Canvas/MainWindow.xaml.cs @@ -1159,8 +1159,9 @@ private void Window_Loaded(object sender, RoutedEventArgs e) AutoBackupManager.Initialize(Settings); CheckUpdateChannelAndTelemetryConsistency(); - // 初始化Dlass上传队列(恢复上次的上传队列) + // 初始化上传队列(恢复上次的上传队列) DlassNoteUploader.InitializeQueue(); + WebDavUploadQueue.InitializeQueue(); _ = TelemetryUploader.UploadTelemetryIfNeededAsync(); diff --git a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs index 32e55e1a..fde4054a 100644 --- a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs +++ b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs @@ -276,7 +276,7 @@ private void SaveInkCanvasStrokes(bool newNotice = true, bool saveByUser = false await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); } - await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePathWithName); + await Helpers.UploadHelper.UploadFileAsync(savePathWithName); } catch (Exception) { @@ -314,7 +314,7 @@ private void SaveInkCanvasStrokes(bool newNotice = true, bool saveByUser = false /// /// 将StrokeCollection保存为XML格式 /// - private void SaveStrokesAsXML(StrokeCollection strokes, string xmlPath) + private void SaveStrokesAsXML(StrokeCollection strokes, string xmlPath, bool triggerUpload = true) { try { @@ -368,22 +368,25 @@ from point in stroke.StylusPoints File.WriteAllText(Path.ChangeExtension(xmlPath, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented)); // 异步上传到Dlass - _ = Task.Run(async () => + if (triggerUpload) { - try + _ = Task.Run(async () => { - var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0; - if (delayMinutes > 0) + try { - await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); - } + var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0; + if (delayMinutes > 0) + { + await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); + } - await Helpers.DlassNoteUploader.UploadNoteFileAsync(xmlPath); - } - catch (Exception) - { - } - }); + await Helpers.UploadHelper.UploadFileAsync(xmlPath); + } + catch (Exception) + { + } + }); + } } catch (Exception ex) { @@ -427,9 +430,9 @@ private void SaveMultiPageStrokesAsXMLZip(List allPageStrokes, var strokes = allPageStrokes[i]; if (strokes.Count > 0) { - // 保存XML文件 + // 保存XML文件(临时文件,不触发上传) string xmlFileName = Path.Combine(tempDir, $"page_{i + 1:D4}.xml"); - SaveStrokesAsXML(strokes, xmlFileName); + SaveStrokesAsXML(strokes, xmlFileName, false); } } @@ -476,7 +479,7 @@ private void SaveMultiPageStrokesAsXMLZip(List allPageStrokes, await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); } - await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName); + await Helpers.UploadHelper.UploadFileAsync(zipFileName); } catch (Exception) { @@ -593,7 +596,7 @@ private void SaveMultiPageStrokesAsZip(List allPageStrokes, st await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); } - await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName); + await Helpers.UploadHelper.UploadFileAsync(zipFileName); } catch (Exception) { @@ -702,7 +705,7 @@ private void SaveSinglePageStrokesAsImage(string savePathWithName, bool newNotic await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); } - await Helpers.DlassNoteUploader.UploadNoteFileAsync(imagePathWithName); + await Helpers.UploadHelper.UploadFileAsync(imagePathWithName); } catch (Exception) { diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml b/Ink Canvas/Windows/DlassSettingsWindow.xaml index f8cac659..82496aa1 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml @@ -617,7 +617,7 @@ @@ -675,7 +675,15 @@ - + + + + + + + + + From 1d9cbe33d3c7a6dbb429427a735cb3dcce057002 Mon Sep 17 00:00:00 2001 From: doudou0720 <98651603+doudou0720@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:51:55 +0800 Subject: [PATCH 04/12] =?UTF-8?q?feat(Upload):=20=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8DDlass=E8=AE=BE=E7=BD=AE=E9=A1=B9=E4=B8=BA=E4=BA=91?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E4=BB=A5=E6=94=AF=E6=8C=81WebDav=E4=BF=9D?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com> --- Ink Canvas/MainWindow.xaml | 2 +- .../MainWindow_cs/MW_Save&OpenStrokes.cs | 279 ++++++++++++++---- Ink Canvas/MainWindow_cs/MW_Settings.cs | 4 +- Ink Canvas/Windows/DlassSettingsWindow.xaml | 2 +- .../SettingsViews/AdvancedPanel.xaml | 6 +- 5 files changed, 222 insertions(+), 71 deletions(-) diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 8bfcafac..6ea32014 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -3655,7 +3655,7 @@ StrokeThickness="1" Margin="0,8,0,8" /> -