From a7237e7ba62f3c07b9d15b9f7ee331c8dde6b642 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:21:38 +0800 Subject: [PATCH 01/90] Update dotnet-desktop.yml --- .github/workflows/dotnet-desktop.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml index db0fd37..912acca 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-desktop.yml @@ -34,11 +34,10 @@ jobs: echo "发布目录内容:" dir src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish - - name: 删除 PDB 和 XML 文件 + - name: 删除 PDB 和 部分XML 文件 run: | # 删除目录中的 .pdb 和 .xml 文件(如果存在) Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue - Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/*.xml -Force -ErrorAction SilentlyContinue - name: 压缩构建产物 run: | From 737bcbdc861862385d698a2c0e3752f40bb48afc Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:25:07 +0800 Subject: [PATCH 02/90] Update and rename dotnet-desktop.yml to dotnet-win-x64.yml --- .../{dotnet-desktop.yml => dotnet-win-x64.yml} | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) rename .github/workflows/{dotnet-desktop.yml => dotnet-win-x64.yml} (85%) diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-win-x64.yml similarity index 85% rename from .github/workflows/dotnet-desktop.yml rename to .github/workflows/dotnet-win-x64.yml index 912acca..e1b7e59 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-win-x64.yml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: write # 确保有写入发布的权限 + jobs: build_and_release: name: 构建并发布 .NET 应用程序 @@ -34,9 +37,9 @@ jobs: echo "发布目录内容:" dir src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish - - name: 删除 PDB 和 部分XML 文件 + - name: 删除 PDB 和部分 XML 文件 run: | - # 删除目录中的 .pdb 和 .xml 文件(如果存在) + # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue - name: 压缩构建产物 @@ -44,6 +47,11 @@ jobs: # 将发布目录中的文件压缩为 zip 文件 Compress-Archive -Path src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/* -DestinationPath "MDrive-win-x64-v${{ github.event.release.tag_name }}.zip" + - name: 检查 ZIP 文件 + run: | + echo "生成的 ZIP 文件:" + dir MDrive-win-x64-v${{ github.event.release.tag_name }}.zip + - name: 上传 ZIP 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 env: From 450f4bd96f0bda530e7de0da28d14e7cfb84ff8f Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:28:42 +0800 Subject: [PATCH 03/90] Update dotnet-win-x64.yml --- .github/workflows/dotnet-win-x64.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet-win-x64.yml b/.github/workflows/dotnet-win-x64.yml index e1b7e59..d49dab8 100644 --- a/.github/workflows/dotnet-win-x64.yml +++ b/.github/workflows/dotnet-win-x64.yml @@ -1,4 +1,4 @@ -name: Build and Release .NET Application +name: Release Windows x64 .NET Application on: release: @@ -58,6 +58,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: "MDrive-win-x64-v${{ github.event.release.tag_name }}.zip" - asset_name: "MDrive-win-x64-v${{ github.event.release.tag_name }}.zip" + asset_path: "MDrive-win-x64-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-win-x64-${{ github.event.release.tag_name }}.zip" asset_content_type: application/zip From 2152ff38dc68433843fd1dd6994a7f67cc389608 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:31:10 +0800 Subject: [PATCH 04/90] Create dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/dotnet-linux-x64.yml diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml new file mode 100644 index 0000000..7b762b0 --- /dev/null +++ b/.github/workflows/dotnet-linux-x64.yml @@ -0,0 +1,63 @@ +name: Release Linux x64 .NET Application + +on: + release: + types: [published] + +permissions: + contents: write # 确保有写入发布的权限 + +jobs: + build_and_release: + name: 构建并发布 .NET 应用程序 + runs-on: ubuntu-latest + + steps: + - name: 检出仓库代码 + uses: actions/checkout@v3 # 使用最新稳定版本 + + - name: 设置 .NET 环境 + uses: actions/setup-dotnet@v3 # 使用最新稳定版本 + with: + dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 + + - name: 还原 NuGet 包 + run: dotnet restore src/MDriveSync.Client.API + + - name: 列出目录内容 (调试步骤) + run: | + echo "PublishProfiles 目录内容:" + dir src/MDriveSync.Client.API/Properties/PublishProfiles + + - name: 构建并发布 .NET 应用程序 + run: dotnet publish src/MDriveSync.Client.API -c Release /p:PublishProfile=Client.Publish.SelfContained.linux.x64.pubxml + + - name: 列出发布目录内容 (调试步骤) + run: | + echo "发布目录内容:" + dir src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish + + - name: 删除 PDB 和部分 XML 文件 + run: | + # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) + Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + + - name: 压缩构建产物 + run: | + # 将发布目录中的文件压缩为 zip 文件 + Compress-Archive -Path src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/* -DestinationPath "MDrive-linux-x64-v${{ github.event.release.tag_name }}.zip" + + - name: 检查 ZIP 文件 + run: | + echo "生成的 ZIP 文件:" + dir MDrive-linux-x64-v${{ github.event.release.tag_name }}.zip + + - name: 上传 ZIP 文件到 release + uses: actions/upload-release-asset@v1 # 使用最新稳定版本 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: "MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" + asset_content_type: application/zip From b99331d14ab01f69610ba2d3a76514017c017587 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:33:55 +0800 Subject: [PATCH 05/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index 7b762b0..e2fc4b4 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -27,7 +27,7 @@ jobs: - name: 列出目录内容 (调试步骤) run: | echo "PublishProfiles 目录内容:" - dir src/MDriveSync.Client.API/Properties/PublishProfiles + ls src/MDriveSync.Client.API/Properties/PublishProfiles - name: 构建并发布 .NET 应用程序 run: dotnet publish src/MDriveSync.Client.API -c Release /p:PublishProfile=Client.Publish.SelfContained.linux.x64.pubxml @@ -35,22 +35,22 @@ jobs: - name: 列出发布目录内容 (调试步骤) run: | echo "发布目录内容:" - dir src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish + ls src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish - name: 删除 PDB 和部分 XML 文件 run: | - # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + # 删除目录中的 .pdb 文件(如果存在) + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/* -DestinationPath "MDrive-linux-x64-v${{ github.event.release.tag_name }}.zip" + zip -r "MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/* - name: 检查 ZIP 文件 run: | echo "生成的 ZIP 文件:" - dir MDrive-linux-x64-v${{ github.event.release.tag_name }}.zip + ls "MDrive-linux-x64-v${{ github.event.release.tag_name }}.zip" - name: 上传 ZIP 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 From 971f8ea83b523bcb457c4d6df62d1a5b56516604 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:34:19 +0800 Subject: [PATCH 06/90] Update dotnet-win-x64.yml --- .github/workflows/dotnet-win-x64.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-win-x64.yml b/.github/workflows/dotnet-win-x64.yml index d49dab8..d772447 100644 --- a/.github/workflows/dotnet-win-x64.yml +++ b/.github/workflows/dotnet-win-x64.yml @@ -50,7 +50,7 @@ jobs: - name: 检查 ZIP 文件 run: | echo "生成的 ZIP 文件:" - dir MDrive-win-x64-v${{ github.event.release.tag_name }}.zip + dir MDrive-win-x64-${{ github.event.release.tag_name }}.zip - name: 上传 ZIP 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 From 6b6cea27bf29e64cde05cf8ca2c4c98c27fe25bf Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:37:05 +0800 Subject: [PATCH 07/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index e2fc4b4..0178a48 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -50,7 +50,7 @@ jobs: - name: 检查 ZIP 文件 run: | echo "生成的 ZIP 文件:" - ls "MDrive-linux-x64-v${{ github.event.release.tag_name }}.zip" + ls "MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" - name: 上传 ZIP 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 From 41d473f58716428bdbcb9f74054587d1f9f3632f Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:38:23 +0800 Subject: [PATCH 08/90] Update dotnet-win-x64.yml --- .github/workflows/dotnet-win-x64.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-win-x64.yml b/.github/workflows/dotnet-win-x64.yml index d772447..6f3cab8 100644 --- a/.github/workflows/dotnet-win-x64.yml +++ b/.github/workflows/dotnet-win-x64.yml @@ -45,7 +45,7 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/* -DestinationPath "MDrive-win-x64-v${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/* -DestinationPath "MDrive-win-x64-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | From 8f597bd101be1c89d91fe627c25fb6f24b016fb5 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:39:50 +0800 Subject: [PATCH 09/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index 0178a48..f381d89 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -41,6 +41,9 @@ jobs: run: | # 删除目录中的 .pdb 文件(如果存在) rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.xml + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.bat + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/WinSW-x64.exe - name: 压缩构建产物 run: | From 5d1090abec9e744126cdd1f22d34df718d55b50e Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:11:31 +0800 Subject: [PATCH 10/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index f381d89..046a31e 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -44,6 +44,7 @@ jobs: rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.xml rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.bat rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/WinSW-x64.exe + rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/wwwroot/driver/ - name: 压缩构建产物 run: | From 94c3c8e1ad48ae9adec12aadf00ebc6f20ddf0c5 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:13:24 +0800 Subject: [PATCH 11/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index 046a31e..4133668 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -36,15 +36,16 @@ jobs: run: | echo "发布目录内容:" ls src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish + ls src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/ - name: 删除 PDB 和部分 XML 文件 run: | # 删除目录中的 .pdb 文件(如果存在) - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.xml - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.bat - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/WinSW-x64.exe - rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/wwwroot/driver/ + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/*.pdb + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/*.xml + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/*.bat + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/WinSW-x64.exe + rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/wwwroot/driver/ - name: 压缩构建产物 run: | From 2a734b5a966cb4d64073349813e42c4715825991 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:26:46 +0800 Subject: [PATCH 12/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index 4133668..046a31e 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -36,16 +36,15 @@ jobs: run: | echo "发布目录内容:" ls src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish - ls src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/ - name: 删除 PDB 和部分 XML 文件 run: | # 删除目录中的 .pdb 文件(如果存在) - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/*.pdb - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/*.xml - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/*.bat - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/WinSW-x64.exe - rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/wwwroot/driver/ + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.xml + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.bat + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/WinSW-x64.exe + rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/wwwroot/driver/ - name: 压缩构建产物 run: | From e6a4e5403613038444693fd9232b370ce792afd4 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:29:27 +0800 Subject: [PATCH 13/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index 046a31e..9298516 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -46,10 +46,17 @@ jobs: rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/WinSW-x64.exe rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/wwwroot/driver/ + - name: 创建临时目录并复制发布文件 + run: | + mkdir -p temp_publish + cp -r src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/* temp_publish/ + - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - zip -r "MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/* + cd temp_publish + zip -r "../MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" ./* + cd .. - name: 检查 ZIP 文件 run: | From 56ece15739c138d52eec8dbdc6e2259c6304d602 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:38:17 +0800 Subject: [PATCH 14/90] Create run_app.sh --- scripts/run_app.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts/run_app.sh diff --git a/scripts/run_app.sh b/scripts/run_app.sh new file mode 100644 index 0000000..0a2b995 --- /dev/null +++ b/scripts/run_app.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# 获取脚本所在目录 +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 应用程序名称 +APP_NAME="MDriveSync.Client.API" + +# 检查应用程序文件是否存在 +if [ ! -f "$DIR/$APP_NAME" ]; then + echo "错误:应用程序文件 $DIR/$APP_NAME 不存在。" + exit 1 +fi + +# 赋予应用程序执行权限 +chmod +x "$DIR/$APP_NAME" + +# 执行应用程序 +"$DIR/$APP_NAME" + +# 检查应用程序退出状态 +if [ $? -ne 0 ]; then + echo "错误:应用程序执行失败。" + exit 1 +else + echo "应用程序执行成功。" +fi From fd09f5f80db77962cebab753937f650fd762e82b Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:40:54 +0800 Subject: [PATCH 15/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index 9298516..cbc1b44 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -51,6 +51,10 @@ jobs: mkdir -p temp_publish cp -r src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/* temp_publish/ + - name: 复制 run_app.sh 脚本 + run: | + cp src/scripts/run_app.sh temp_publish/ + - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 From 12b1e61c8d690d5d4e1847f394a879e95ef71854 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:45:16 +0800 Subject: [PATCH 16/90] Update README.md --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d6da5d3..38a87eb 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,14 @@ e. 部署到 IIS(可选),在 IIS 添加网站,将文件夹部署到 IIS f. 使用系统自带的 `任务计划程序`(可选),创建基本任务,选择 `.exe` 程序即可,请选择`请勿启动多个实例`,保证只有一个任务执行即可。 g. 磁盘挂载支持(可选),下载并安装驱动(http://localhost:8080/driver/Dokan_x64.msi),将云盘挂载到本地,像管理本地文件一样管理远程文件。 ``` +> Linux 版本 + +```bash +a. 通过 https://github.com/trueai-org/mdrive/releases 下载 linux 最新免安装版,例如:MDrive-linux-x64.zip +b. 解压并执行 run_app.sh (不需要安装 dotnet 环境) +c. 启动方式1: sh run_app.sh +d. 启动方式2: chmod +x run_app.sh && ./run_app.sh +``` ### 在线预览 @@ -618,4 +626,4 @@ docker run --name mdrive -d --restart=always \ - 感谢阿里云盘共创团对 MDrive 的支持。 - 感谢 Duplicati 对本产品的支持。 -- [WinSW](https://github.com/winsw/winsw) 系统服务,将本程序安装作为 Windows 服务运行。 \ No newline at end of file +- [WinSW](https://github.com/winsw/winsw) 系统服务,将本程序安装作为 Windows 服务运行。 From fc90c6b02fa32b549ec6d12cae8d98b25d31df97 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:49:17 +0800 Subject: [PATCH 17/90] Update dotnet-linux-x64.yml --- .github/workflows/dotnet-linux-x64.yml | 27 +++++++++++--------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index cbc1b44..806f8fd 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -50,29 +50,24 @@ jobs: run: | mkdir -p temp_publish cp -r src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/* temp_publish/ + cp scripts/run_app.sh temp_publish/ - - name: 复制 run_app.sh 脚本 + - name: 压缩构建产物为 tar.gz run: | - cp src/scripts/run_app.sh temp_publish/ + # 将发布目录中的文件压缩为 tar.gz 文件 + tar -czvf "MDrive-linux-x64-${{ github.event.release.tag_name }}.tar.gz" -C temp_publish . - - name: 压缩构建产物 + - name: 检查 tar.gz 文件 run: | - # 将发布目录中的文件压缩为 zip 文件 - cd temp_publish - zip -r "../MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" ./* - cd .. + echo "生成的 tar.gz 文件:" + ls "MDrive-linux-x64-${{ github.event.release.tag_name }}.tar.gz" - - name: 检查 ZIP 文件 - run: | - echo "生成的 ZIP 文件:" - ls "MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" - - - name: 上传 ZIP 文件到 release + - name: 上传 tar.gz 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: "MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" - asset_name: "MDrive-linux-x64-${{ github.event.release.tag_name }}.zip" - asset_content_type: application/zip + asset_path: "MDrive-linux-x64-${{ github.event.release.tag_name }}.tar.gz" + asset_name: "MDrive-linux-x64-${{ github.event.release.tag_name }}.tar.gz" + asset_content_type: application/gzip From 5b0b3c7a4c632623ad5e33e92ab575f804a28b6f Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:55:00 +0800 Subject: [PATCH 18/90] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 38a87eb..859b78e 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ docker run --name mdrive -d --restart=always \ registry.cn-guangzhou.aliyuncs.com/trueai-org/mdrive ``` -> Windows 版本 +> Windows 版本(不需要安装 dotnet 环境) ```bash a. 通过 https://github.com/trueai-org/mdrive/releases 下载 windows 最新免安装版,例如:MDrvie-win-x64.zip @@ -67,11 +67,13 @@ e. 部署到 IIS(可选),在 IIS 添加网站,将文件夹部署到 IIS f. 使用系统自带的 `任务计划程序`(可选),创建基本任务,选择 `.exe` 程序即可,请选择`请勿启动多个实例`,保证只有一个任务执行即可。 g. 磁盘挂载支持(可选),下载并安装驱动(http://localhost:8080/driver/Dokan_x64.msi),将云盘挂载到本地,像管理本地文件一样管理远程文件。 ``` -> Linux 版本 + +> Linux 版本(不需要安装 dotnet 环境) ```bash a. 通过 https://github.com/trueai-org/mdrive/releases 下载 linux 最新免安装版,例如:MDrive-linux-x64.zip -b. 解压并执行 run_app.sh (不需要安装 dotnet 环境) +b. 解压到当前目录: tar -xzf MDrive-linux-x64-v3.2.11-alpha.tar.gz +c. 执行: run_app.sh c. 启动方式1: sh run_app.sh d. 启动方式2: chmod +x run_app.sh && ./run_app.sh ``` From f50dcbbbaebcb6118e753974189138dc6c34d550 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:55:23 +0800 Subject: [PATCH 19/90] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 859b78e..005c5c2 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ docker run --name mdrive -d --restart=always \ registry.cn-guangzhou.aliyuncs.com/trueai-org/mdrive ``` -> Windows 版本(不需要安装 dotnet 环境) +> Windows 版本 ```bash a. 通过 https://github.com/trueai-org/mdrive/releases 下载 windows 最新免安装版,例如:MDrvie-win-x64.zip @@ -68,7 +68,7 @@ f. 使用系统自带的 `任务计划程序`(可选),创建基本任务 g. 磁盘挂载支持(可选),下载并安装驱动(http://localhost:8080/driver/Dokan_x64.msi),将云盘挂载到本地,像管理本地文件一样管理远程文件。 ``` -> Linux 版本(不需要安装 dotnet 环境) +> Linux 版本 ```bash a. 通过 https://github.com/trueai-org/mdrive/releases 下载 linux 最新免安装版,例如:MDrive-linux-x64.zip From f76656d88518f312edee6a6a38aba32e4637e3a3 Mon Sep 17 00:00:00 2001 From: trueai-org <37255565+trueai-org@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:57:16 +0800 Subject: [PATCH 20/90] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 005c5c2..9e810ac 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ g. 磁盘挂载支持(可选),下载并安装驱动(http://localhost:808 ```bash a. 通过 https://github.com/trueai-org/mdrive/releases 下载 linux 最新免安装版,例如:MDrive-linux-x64.zip -b. 解压到当前目录: tar -xzf MDrive-linux-x64-v3.2.11-alpha.tar.gz +b. 解压到当前目录: tar -xzf MDrive-linux-x64-.tar.gz c. 执行: run_app.sh c. 启动方式1: sh run_app.sh d. 启动方式2: chmod +x run_app.sh && ./run_app.sh From 7e434cc02e952caae126f2ee5a941f2dd1940b4b Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 15:40:26 +0800 Subject: [PATCH 21/90] osx and arm64 --- ...t.Publish.SelfContained.linux.arm64.pubxml | 23 +++++++++++++++++ ...ent.Publish.SelfContained.osx.arm64.pubxml | 23 +++++++++++++++++ ...lient.Publish.SelfContained.osx.x64.pubxml | 23 +++++++++++++++++ ...ent.Publish.SelfContained.win.arm64.pubxml | 25 +++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.linux.arm64.pubxml create mode 100644 src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.osx.arm64.pubxml create mode 100644 src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.osx.x64.pubxml create mode 100644 src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.win.arm64.pubxml diff --git a/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.linux.arm64.pubxml b/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.linux.arm64.pubxml new file mode 100644 index 0000000..c98984e --- /dev/null +++ b/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.linux.arm64.pubxml @@ -0,0 +1,23 @@ + + + + + false + false + true + Release + Any CPU + FileSystem + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + net8.0 + linux-arm64 + true + db5108c3-4fce-4ea0-aff5-d152b95abf7a + true + + \ No newline at end of file diff --git a/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.osx.arm64.pubxml b/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.osx.arm64.pubxml new file mode 100644 index 0000000..8e76556 --- /dev/null +++ b/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.osx.arm64.pubxml @@ -0,0 +1,23 @@ + + + + + false + false + true + Release + Any CPU + FileSystem + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + net8.0 + osx-arm64 + true + db5108c3-4fce-4ea0-aff5-d152b95abf7a + true + + \ No newline at end of file diff --git a/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.osx.x64.pubxml b/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.osx.x64.pubxml new file mode 100644 index 0000000..b1849fb --- /dev/null +++ b/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.osx.x64.pubxml @@ -0,0 +1,23 @@ + + + + + false + false + true + Release + Any CPU + FileSystem + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + net8.0 + osx-x64 + true + db5108c3-4fce-4ea0-aff5-d152b95abf7a + true + + \ No newline at end of file diff --git a/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.win.arm64.pubxml b/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.win.arm64.pubxml new file mode 100644 index 0000000..5332984 --- /dev/null +++ b/src/MDriveSync.Client.API/Properties/PublishProfiles/Client.Publish.SelfContained.win.arm64.pubxml @@ -0,0 +1,25 @@ + + + + + false + false + true + Release + Any CPU + FileSystem + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + net8.0 + win-arm64 + true + false + false + db5108c3-4fce-4ea0-aff5-d152b95abf7a + true + + \ No newline at end of file From ceeca0956b420d14dd6ba2bb39ffacb4f2d81a46 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 15:49:57 +0800 Subject: [PATCH 22/90] add ci arm64 --- .github/workflows/dotnet-linux-arm64.yml | 73 ++++++++++++++++++++++++ .github/workflows/dotnet-osx-arm64.yml | 73 ++++++++++++++++++++++++ .github/workflows/dotnet-osx-x64.yml | 73 ++++++++++++++++++++++++ .github/workflows/dotnet-win-arm64.yml | 63 ++++++++++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 .github/workflows/dotnet-linux-arm64.yml create mode 100644 .github/workflows/dotnet-osx-arm64.yml create mode 100644 .github/workflows/dotnet-osx-x64.yml create mode 100644 .github/workflows/dotnet-win-arm64.yml diff --git a/.github/workflows/dotnet-linux-arm64.yml b/.github/workflows/dotnet-linux-arm64.yml new file mode 100644 index 0000000..43e3f55 --- /dev/null +++ b/.github/workflows/dotnet-linux-arm64.yml @@ -0,0 +1,73 @@ +name: Release Linux arm64 .NET Application + +on: + release: + types: [published] + +permissions: + contents: write # 确保有写入发布的权限 + +jobs: + build_and_release: + name: 构建并发布 .NET 应用程序 + runs-on: ubuntu-latest + + steps: + - name: 检出仓库代码 + uses: actions/checkout@v3 # 使用最新稳定版本 + + - name: 设置 .NET 环境 + uses: actions/setup-dotnet@v3 # 使用最新稳定版本 + with: + dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 + + - name: 还原 NuGet 包 + run: dotnet restore src/MDriveSync.Client.API + + - name: 列出目录内容 (调试步骤) + run: | + echo "PublishProfiles 目录内容:" + ls src/MDriveSync.Client.API/Properties/PublishProfiles + + - name: 构建并发布 .NET 应用程序 + run: dotnet publish src/MDriveSync.Client.API -c Release /p:PublishProfile=Client.Publish.SelfContained.linux.arm64.pubxml + + - name: 列出发布目录内容 (调试步骤) + run: | + echo "发布目录内容:" + ls src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish + + - name: 删除 PDB 和部分 XML 文件 + run: | + # 删除目录中的 .pdb 文件(如果存在) + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.pdb + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.xml + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.bat + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/WinSW-x64.exe + rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/wwwroot/driver/ + + - name: 创建临时目录并复制发布文件 + run: | + mkdir -p temp_publish + cp -r src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/* temp_publish/ + cp scripts/run_app.sh temp_publish/ + + - name: 压缩构建产物为 tar.gz + run: | + # 将发布目录中的文件压缩为 tar.gz 文件 + tar -czvf "MDrive-linux-arm64-${{ github.event.release.tag_name }}.tar.gz" -C temp_publish . + + - name: 检查 tar.gz 文件 + run: | + echo "生成的 tar.gz 文件:" + ls "MDrive-linux-arm64-${{ github.event.release.tag_name }}.tar.gz" + + - name: 上传 tar.gz 文件到 release + uses: actions/upload-release-asset@v1 # 使用最新稳定版本 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: "MDrive-linux-arm64-${{ github.event.release.tag_name }}.tar.gz" + asset_name: "MDrive-linux-arm64-${{ github.event.release.tag_name }}.tar.gz" + asset_content_type: application/gzip diff --git a/.github/workflows/dotnet-osx-arm64.yml b/.github/workflows/dotnet-osx-arm64.yml new file mode 100644 index 0000000..afe917b --- /dev/null +++ b/.github/workflows/dotnet-osx-arm64.yml @@ -0,0 +1,73 @@ +name: Release macOS arm64 .NET Application + +on: + release: + types: [published] + +permissions: + contents: write # 确保有写入发布的权限 + +jobs: + build_and_release: + name: 构建并发布 .NET 应用程序 + runs-on: macos-latest + + steps: + - name: 检出仓库代码 + uses: actions/checkout@v3 # 使用最新稳定版本 + + - name: 设置 .NET 环境 + uses: actions/setup-dotnet@v3 # 使用最新稳定版本 + with: + dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 + + - name: 还原 NuGet 包 + run: dotnet restore src/MDriveSync.Client.API + + - name: 列出目录内容 (调试步骤) + run: | + echo "PublishProfiles 目录内容:" + ls src/MDriveSync.Client.API/Properties/PublishProfiles + + - name: 构建并发布 .NET 应用程序 + run: dotnet publish src/MDriveSync.Client.API -c Release /p:PublishProfile=Client.Publish.SelfContained.osx.arm64.pubxml + + - name: 列出发布目录内容 (调试步骤) + run: | + echo "发布目录内容:" + ls src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish + + - name: 删除 PDB 和部分 XML 文件 + run: | + # 删除目录中的 .pdb 文件(如果存在) + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.pdb + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.xml + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.bat + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/WinSW-x64.exe + rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/wwwroot/driver/ + + - name: 创建临时目录并复制发布文件 + run: | + mkdir -p temp_publish + cp -r src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/* temp_publish/ + cp scripts/run_app.sh temp_publish/ + + - name: 压缩构建产物为 tar.gz + run: | + # 将发布目录中的文件压缩为 tar.gz 文件 + tar -czvf "MDrive-osx-arm64-${{ github.event.release.tag_name }}.tar.gz" -C temp_publish . + + - name: 检查 tar.gz 文件 + run: | + echo "生成的 tar.gz 文件:" + ls "MDrive-osx-arm64-${{ github.event.release.tag_name }}.tar.gz" + + - name: 上传 tar.gz 文件到 release + uses: actions/upload-release-asset@v1 # 使用最新稳定版本 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: "MDrive-osx-arm64-${{ github.event.release.tag_name }}.tar.gz" + asset_name: "MDrive-osx-arm64-${{ github.event.release.tag_name }}.tar.gz" + asset_content_type: application/gzip \ No newline at end of file diff --git a/.github/workflows/dotnet-osx-x64.yml b/.github/workflows/dotnet-osx-x64.yml new file mode 100644 index 0000000..76b7e9b --- /dev/null +++ b/.github/workflows/dotnet-osx-x64.yml @@ -0,0 +1,73 @@ +name: Release macOS x64 .NET Application + +on: + release: + types: [published] + +permissions: + contents: write # 确保有写入发布的权限 + +jobs: + build_and_release: + name: 构建并发布 .NET 应用程序 + runs-on: macos-latest + + steps: + - name: 检出仓库代码 + uses: actions/checkout@v3 # 使用最新稳定版本 + + - name: 设置 .NET 环境 + uses: actions/setup-dotnet@v3 # 使用最新稳定版本 + with: + dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 + + - name: 还原 NuGet 包 + run: dotnet restore src/MDriveSync.Client.API + + - name: 列出目录内容 (调试步骤) + run: | + echo "PublishProfiles 目录内容:" + ls src/MDriveSync.Client.API/Properties/PublishProfiles + + - name: 构建并发布 .NET 应用程序 + run: dotnet publish src/MDriveSync.Client.API -c Release /p:PublishProfile=Client.Publish.SelfContained.osx.x64.pubxml + + - name: 列出发布目录内容 (调试步骤) + run: | + echo "发布目录内容:" + ls src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish + + - name: 删除 PDB 和部分 XML 文件 + run: | + # 删除目录中的 .pdb 文件(如果存在) + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.pdb + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.xml + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.bat + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/WinSW-x64.exe + rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/wwwroot/driver/ + + - name: 创建临时目录并复制发布文件 + run: | + mkdir -p temp_publish + cp -r src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/* temp_publish/ + cp scripts/run_app.sh temp_publish/ + + - name: 压缩构建产物为 tar.gz + run: | + # 将发布目录中的文件压缩为 tar.gz 文件 + tar -czvf "MDrive-osx-x64-${{ github.event.release.tag_name }}.tar.gz" -C temp_publish . + + - name: 检查 tar.gz 文件 + run: | + echo "生成的 tar.gz 文件:" + ls "MDrive-osx-x64-${{ github.event.release.tag_name }}.tar.gz" + + - name: 上传 tar.gz 文件到 release + uses: actions/upload-release-asset@v1 # 使用最新稳定版本 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: "MDrive-osx-x64-${{ github.event.release.tag_name }}.tar.gz" + asset_name: "MDrive-osx-x64-${{ github.event.release.tag_name }}.tar.gz" + asset_content_type: application/gzip \ No newline at end of file diff --git a/.github/workflows/dotnet-win-arm64.yml b/.github/workflows/dotnet-win-arm64.yml new file mode 100644 index 0000000..8acaf72 --- /dev/null +++ b/.github/workflows/dotnet-win-arm64.yml @@ -0,0 +1,63 @@ +name: Release Windows arm64 .NET Application + +on: + release: + types: [published] + +permissions: + contents: write # 确保有写入发布的权限 + +jobs: + build_and_release: + name: 构建并发布 .NET 应用程序 + runs-on: windows-latest + + steps: + - name: 检出仓库代码 + uses: actions/checkout@v3 # 使用最新稳定版本 + + - name: 设置 .NET 环境 + uses: actions/setup-dotnet@v3 # 使用最新稳定版本 + with: + dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 + + - name: 还原 NuGet 包 + run: dotnet restore src/MDriveSync.Client.API + + - name: 列出目录内容 (调试步骤) + run: | + echo "PublishProfiles 目录内容:" + dir src/MDriveSync.Client.API/Properties/PublishProfiles + + - name: 构建并发布 .NET 应用程序 + run: dotnet publish src/MDriveSync.Client.API -c Release /p:PublishProfile=Client.Publish.SelfContained.win.arm64.pubxml + + - name: 列出发布目录内容 (调试步骤) + run: | + echo "发布目录内容:" + dir src/MDriveSync.Client.API/bin/Release/net8.0/win-arm64/publish + + - name: 删除 PDB 和部分 XML 文件 + run: | + # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) + Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-arm64/publish/*.pdb -Force -ErrorAction SilentlyContinue + + - name: 压缩构建产物 + run: | + # 将发布目录中的文件压缩为 zip 文件 + Compress-Archive -Path src/MDriveSync.Client.API/bin/Release/net8.0/win-arm64/publish/* -DestinationPath "MDrive-win-arm64-${{ github.event.release.tag_name }}.zip" + + - name: 检查 ZIP 文件 + run: | + echo "生成的 ZIP 文件:" + dir MDrive-win-arm64-${{ github.event.release.tag_name }}.zip + + - name: 上传 ZIP 文件到 release + uses: actions/upload-release-asset@v1 # 使用最新稳定版本 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: "MDrive-win-arm64-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-win-arm64-${{ github.event.release.tag_name }}.zip" + asset_content_type: application/zip From 0216ebc9d5ccf1e148428f5a2941d99e37007b1d Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 15:51:29 +0800 Subject: [PATCH 23/90] readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 9e810ac..4f5eb53 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,15 @@ c. 执行: run_app.sh c. 启动方式1: sh run_app.sh d. 启动方式2: chmod +x run_app.sh && ./run_app.sh ``` +> macOS 版本 + +```bash +a. 通过 https://github.com/trueai-org/mdrive/releases 下载 macOS 最新免安装版,例如:MDrive-osx-x64.zip +b. 解压到当前目录: tar -xzf MDrive-osx-x64-.tar.gz +c. 执行: run_app.sh +c. 启动方式1: sh run_app.sh +d. 启动方式2: chmod +x run_app.sh && ./run_app.sh +``` ### 在线预览 From 619e9c10debfb0ffa0afe54769c74ce1e028934c Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 16:04:42 +0800 Subject: [PATCH 24/90] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f5eb53..580d8b8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 多平台、模块化、可挂载、安全、加密的云盘自动同步、备份工具,支持本地存储、阿里云盘等,支持镜像、备份等同步模式,完全免费开源。 -提供 Docker、Windows、Linux、Web 等多平台版本。 +提供 Docker、Windows、Linux、macOS、Web 等多平台版本。 支持 **AES256-GCM、ChaCha20-Poly1305** 加密,支持 **Zstd、LZ4、Snappy** 压缩,支持 **SHA256、BLAKE3** 等哈希算法,**任何第三方、服务商都无法查看数据,保护您的数据安全和隐私**。 From 3f62a38f1b0e8469ad9ddd5cd23437954076016f Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 21:33:43 +0800 Subject: [PATCH 25/90] fix: osx script --- .github/workflows/dotnet-osx-arm64.yml | 2 +- .github/workflows/dotnet-osx-x64.yml | 2 +- README.md | 6 +++--- scripts/run_app.sh | 2 +- scripts/run_app_osx.sh | 30 ++++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 scripts/run_app_osx.sh diff --git a/.github/workflows/dotnet-osx-arm64.yml b/.github/workflows/dotnet-osx-arm64.yml index afe917b..e86edcb 100644 --- a/.github/workflows/dotnet-osx-arm64.yml +++ b/.github/workflows/dotnet-osx-arm64.yml @@ -50,7 +50,7 @@ jobs: run: | mkdir -p temp_publish cp -r src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/* temp_publish/ - cp scripts/run_app.sh temp_publish/ + cp scripts/run_app_osx.sh temp_publish/ - name: 压缩构建产物为 tar.gz run: | diff --git a/.github/workflows/dotnet-osx-x64.yml b/.github/workflows/dotnet-osx-x64.yml index 76b7e9b..01b4cec 100644 --- a/.github/workflows/dotnet-osx-x64.yml +++ b/.github/workflows/dotnet-osx-x64.yml @@ -50,7 +50,7 @@ jobs: run: | mkdir -p temp_publish cp -r src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/* temp_publish/ - cp scripts/run_app.sh temp_publish/ + cp scripts/run_app_osx.sh temp_publish/ - name: 压缩构建产物为 tar.gz run: | diff --git a/README.md b/README.md index 580d8b8..fc09f54 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,9 @@ d. 启动方式2: chmod +x run_app.sh && ./run_app.sh ```bash a. 通过 https://github.com/trueai-org/mdrive/releases 下载 macOS 最新免安装版,例如:MDrive-osx-x64.zip b. 解压到当前目录: tar -xzf MDrive-osx-x64-.tar.gz -c. 执行: run_app.sh -c. 启动方式1: sh run_app.sh -d. 启动方式2: chmod +x run_app.sh && ./run_app.sh +c. 执行: run_app_osx.sh +c. 启动方式1: sh run_app_osx.sh +d. 启动方式2: chmod +x run_app_osx.sh && ./run_app_osx.sh ``` ### 在线预览 diff --git a/scripts/run_app.sh b/scripts/run_app.sh index 0a2b995..11eead1 100644 --- a/scripts/run_app.sh +++ b/scripts/run_app.sh @@ -24,4 +24,4 @@ if [ $? -ne 0 ]; then exit 1 else echo "应用程序执行成功。" -fi +fi \ No newline at end of file diff --git a/scripts/run_app_osx.sh b/scripts/run_app_osx.sh new file mode 100644 index 0000000..3b279a5 --- /dev/null +++ b/scripts/run_app_osx.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# 获取脚本所在目录 +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 应用程序名称 +APP_NAME="MDriveSync.Client.API" + +# 检查应用程序文件是否存在 +if [ ! -f "$DIR/$APP_NAME" ]; then + echo "错误:应用程序文件 $DIR/$APP_NAME 不存在。" + exit 1 +fi + +# 移除应用程序的隔离属性 +xattr -d com.apple.quarantine "$DIR/$APP_NAME" + +# 赋予应用程序执行权限 +chmod +x "$DIR/$APP_NAME" + +# 执行应用程序 +"$DIR/$APP_NAME" + +# 检查应用程序退出状态 +if [ $? -ne 0 ]; then + echo "错误:应用程序执行失败。" + exit 1 +else + echo "应用程序执行成功。" +fi \ No newline at end of file From ce949ac0bade3fb21194264e4fabb5fe8b5150d1 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 21:52:05 +0800 Subject: [PATCH 26/90] fix: mac path error --- src/MDriveSync.Core/Services/AliyunJob.cs | 11 ++++-- .../Services/LocalStorageJob.cs | 38 ++++++------------- .../GlobalConfiguration.cs | 33 +++++++++++++++- 3 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/MDriveSync.Core/Services/AliyunJob.cs b/src/MDriveSync.Core/Services/AliyunJob.cs index 41b8f0e..704c63f 100644 --- a/src/MDriveSync.Core/Services/AliyunJob.cs +++ b/src/MDriveSync.Core/Services/AliyunJob.cs @@ -1862,12 +1862,15 @@ private void AliyunDriveInitialize() var sw = new Stopwatch(); sw.Start(); - var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - _log.LogInformation($"Linux: {isLinux}"); + var isLinux = GlobalConfiguration.IsLinux(); + var isMacOS = GlobalConfiguration.IsMacOS(); + var isWindows = GlobalConfiguration.IsWindows(); + + _log.LogInformation($"Linux: {isLinux}, macOS: {isMacOS}, Windows: {isWindows}"); // 处理 RestoreRootPath - if (isLinux && (_jobConfig.Restore?.StartsWith("/") ?? false)) + if ((isLinux || isMacOS) && (_jobConfig.Restore?.StartsWith("/") ?? false)) { _localRestorePath = "/" + _jobConfig.Restore.TrimPath(); } @@ -1883,7 +1886,7 @@ private void AliyunDriveInitialize() _jobConfig.Sources.Clear(); foreach (var item in sources) { - if (isLinux && item.StartsWith('/')) + if ((isLinux || isMacOS) && item.StartsWith('/')) { var dir = new DirectoryInfo(item); if (!dir.Exists) diff --git a/src/MDriveSync.Core/Services/LocalStorageJob.cs b/src/MDriveSync.Core/Services/LocalStorageJob.cs index 208ebc4..2fd56a0 100644 --- a/src/MDriveSync.Core/Services/LocalStorageJob.cs +++ b/src/MDriveSync.Core/Services/LocalStorageJob.cs @@ -1633,24 +1633,6 @@ private LocalStorageFileInfo GetLocalDirectory(string dirFullPath, string rootDi return ld; } - /// - /// 判断是否是 Windows 系统 - /// - /// - private static bool IsWindows() - { - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - } - - /// - /// 判断是否是 Linux 系统 - /// - /// - private static bool IsLinux() - { - return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - } - #endregion 私有方法 #region 本地存储 @@ -1674,12 +1656,14 @@ private void Initialize() var sw = new Stopwatch(); sw.Start(); - var isLinux = IsLinux(); + var isLinux = GlobalConfiguration.IsLinux(); + var isMacOS = GlobalConfiguration.IsMacOS(); + var isWindows = GlobalConfiguration.IsWindows(); - _log.LogInformation($"Linux: {isLinux}"); + _log.LogInformation($"Linux: {isLinux}, macOS: {isMacOS}, Windows: {isWindows}"); // 处理 RestoreRootPath - if (isLinux && (_jobConfig.Restore?.StartsWith("/") ?? false)) + if ((isLinux || isMacOS) && (_jobConfig.Restore?.StartsWith("/") ?? false)) { _tartgetRestoreRootPath = "/" + _jobConfig.Restore.TrimPath(); } @@ -1689,7 +1673,7 @@ private void Initialize() } // 处理 TargetRootPath - if (isLinux && (_jobConfig.Target?.StartsWith("/") ?? false)) + if ((isLinux || isMacOS) && (_jobConfig.Target?.StartsWith("/") ?? false)) { _targetSaveRootPath = "/" + _jobConfig.Target.TrimPath(); } @@ -1703,7 +1687,7 @@ private void Initialize() _jobConfig.Sources.Clear(); foreach (var item in sources) { - if (isLinux && item.StartsWith('/')) + if ((isLinux || isMacOS) && item.StartsWith('/')) { var dir = new DirectoryInfo(item); if (!dir.Exists) @@ -1790,7 +1774,7 @@ private void SyncVerify() try { // 如果是 windows 平台并且启动回收站 - if (IsWindows() && _jobConfig.IsRecycleBin) + if (GlobalConfiguration.IsWindows() && _jobConfig.IsRecycleBin) { // 删除文件夹到系统回收站 // 将文件移动到回收站 @@ -1840,7 +1824,7 @@ private void SyncVerify() { try { - if (IsWindows() && _jobConfig.IsRecycleBin) + if (GlobalConfiguration.IsWindows() && _jobConfig.IsRecycleBin) { // 删除文件到系统回收站 // 将文件移动到回收站 @@ -1889,7 +1873,7 @@ private void SyncVerify() { try { - if (IsWindows() && _jobConfig.IsRecycleBin) + if (GlobalConfiguration.IsWindows() && _jobConfig.IsRecycleBin) { // 删除文件到系统回收站 // 将文件移动到回收站 @@ -1934,7 +1918,7 @@ private void SyncVerify() { try { - if (IsWindows() && _jobConfig.IsRecycleBin) + if (GlobalConfiguration.IsWindows() && _jobConfig.IsRecycleBin) { // 删除文件到系统回收站 // 将文件移动到回收站 diff --git a/src/MDriveSync.Infrastructure/GlobalConfiguration.cs b/src/MDriveSync.Infrastructure/GlobalConfiguration.cs index 7f760f8..262fbde 100644 --- a/src/MDriveSync.Infrastructure/GlobalConfiguration.cs +++ b/src/MDriveSync.Infrastructure/GlobalConfiguration.cs @@ -1,4 +1,6 @@ -namespace MDriveSync.Infrastructure +using System.Runtime.InteropServices; + +namespace MDriveSync.Infrastructure { /// /// 全局配置 @@ -9,5 +11,34 @@ public class GlobalConfiguration /// 网站配置为演示模式 /// public static bool? IsDemoMode { get; set; } + + /// + /// 判断是否是 Windows 系统 + /// + /// + public static bool IsWindows() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + + /// + /// 判断是否是 Linux 系统 + /// + /// + public static bool IsLinux() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } + + /// + /// 判断是否是 macOS 系统 + /// + /// + public static bool IsMacOS() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + || Environment.OSVersion.Platform == PlatformID.Unix + || Environment.OSVersion.Platform == PlatformID.MacOSX; + } } } From fa6bf10df40859bf9a6e91dceecea65f766466ad Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 21:59:45 +0800 Subject: [PATCH 27/90] open borwser --- src/MDriveSync.Client.API/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MDriveSync.Client.API/Program.cs b/src/MDriveSync.Client.API/Program.cs index 9e43e00..dcda7fd 100644 --- a/src/MDriveSync.Client.API/Program.cs +++ b/src/MDriveSync.Client.API/Program.cs @@ -286,15 +286,15 @@ private static void OpenBrowser(string url) } // ݲͬIJϵͳʹòͬ - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (GlobalConfiguration.IsWindows()) { Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")); // Windows } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + else if (GlobalConfiguration.IsLinux()) { // Process.Start("xdg-open", url); // Linux } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + else if (GlobalConfiguration.IsMacOS()) { Process.Start("open", url); // MacOS } From b80c792797ebc7ccab297f2d0076b4bc733a11db Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 22:03:46 +0800 Subject: [PATCH 28/90] os system if --- src/MDriveSync.Core/DownloadManager.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/MDriveSync.Core/DownloadManager.cs b/src/MDriveSync.Core/DownloadManager.cs index 3a180c3..1354c99 100644 --- a/src/MDriveSync.Core/DownloadManager.cs +++ b/src/MDriveSync.Core/DownloadManager.cs @@ -235,16 +235,27 @@ public string LastDownloadPath() } // 根据平台类型返回特定的文件夹 - if (Platform.IsClientWindows) + if (GlobalConfiguration.IsWindows()) { // Windows平台的特殊文件夹 return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); } + else if (GlobalConfiguration.IsMacOS()) + { + // MacOS平台的特殊文件夹 + return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + } + else if (GlobalConfiguration.IsLinux()) + { + // Linux平台的特殊文件夹 + return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + } else { // 非Windows平台的特殊文件夹 return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); } + } /// @@ -258,7 +269,7 @@ public void OpenDownloadFolder(string taskId) if (downloadTask.Status == DownloadStatus.Completed) { // 根据平台类型打开文件夹 - if (Platform.IsClientWindows) + if (GlobalConfiguration.IsWindows()) { // 如果下载完成,则打开文件夹并选中文件 Process.Start("explorer.exe", "/select," + downloadTask.FilePath); @@ -271,7 +282,7 @@ public void OpenDownloadFolder(string taskId) else { // 打开对应的文件夹,不选中文件 - if (Platform.IsClientWindows) + if (GlobalConfiguration.IsWindows()) { Process.Start("explorer.exe", "/select," + Path.GetDirectoryName(downloadTask.FilePath)); } From 60dddb725765d08b3e9f8131360c5b6980e008bc Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 20 Jun 2024 22:12:58 +0800 Subject: [PATCH 29/90] macos readme --- README.md | 2 +- docs/screenshots/macOS.png | Bin 0 -> 1153559 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/screenshots/macOS.png diff --git a/README.md b/README.md index fc09f54..640506c 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ d. 启动方式2: chmod +x run_app_osx.sh && ./run_app_osx.sh ![作业](/docs/screenshots/job.gif) ![挂载](/docs/screenshots/mount.png) - +![macOS](/docs/screenshots/macOS.png) ### 压缩与加密 diff --git a/docs/screenshots/macOS.png b/docs/screenshots/macOS.png new file mode 100644 index 0000000000000000000000000000000000000000..035caa5c988f79a7e4c6353035b163988466ef01 GIT binary patch literal 1153559 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

`Vu9Lle#d(7A8X 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 z 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

cf&|Dg`F%xSP8uwkgi-J@A0j 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 Yrby-nCr!I?5G+B;trf|TE zvv^y7*|ul7>-ULz0Bfy&aH1mrxR5}rva}cLf0zmx{a9-wvB{5IZVSg|vq}vE1~}h2 zI{H=#ZNz377@kclbpifcn}Npj%{O;U;Ow$DsyX`@j+8#WCrt{)f5| z4;#Q-HqO@Q5CmY@-e6I=*$6wL1d>`n`U!h>=a=#?MX#6kq=8XDESMBc!K@6Wr3N8h zj0#+VLV_oY$64pQQ-*jcy6?; zv>vyv=8s19-ewCd-h5`OYT)QmrigN9&U71JZ97pKoa1mahZ2~2>RTet0hsUb4lIEs z-J7D%{4pS6(Xae;iN$n*ci(%D4d!{`0ygY_&}vFiyRJ(gumB(Hpc=|DYq-RZ4T&9W})d{Wnc#lPEs9}?T^3C%ioj$6iuUz}BQ7q|#=63#Q ztqXo4>`6$+rBUk~#Mr-1;o>c5vt2Tqt8-{BO{kmPnH8K0h`a@~19uFTY{ zXIQA?Jo)w(^}Q!&#n2phXhk3$R9v3K)^xfSQNKB`RLQ^kJ?f3v5g-8EP}w`&Fx9i9 zzHXj1h3*!2tQ+x6UV2hll^%=$hyd7LZ2 z5=Zf|HKZv{oEwjOcwoG}qE>$4SeS1KhuwD>$0=M+c)9Y?iz4gFQIzb|P8d4eSXqFy zN}KoZvCT=;Q#B!jHp0ZO#(Xjs_c`#~K!jH&-1-EBC4%yQouZ?@D3iJ2%v#)!~-;MaMOZc7p{3%8C))Iyg!79x0HwQ*Dc;F%`_weuT-?yh+ zQ$AJY`X~@HsUS3&OaTnNWHxKHsiZ*6rD&^IF@@G^F7jZJ)lV$vHyszh?xKlk9oDs0 zE;q=)m(%|N?s-OKZU-Os{e7kAqfFmy;&sn@P|naSaefItV)@|ff7 zup!@2`)YJ4a3&r0s$LE@TKo*}zOMPh4j_P!m27aH7mYV(a|4=R-IuM8tvak_2`ijM9XP~qEvUUg=QDVIKX(oIIqsd`fcC6ixTk?mDs)?U! zLcC}JNZ$kB*TlqYacgXI=I|76+Tr^@D7)Fkk`78Ad@Bvy5YP zzNlPZRPvGu;@pS5<9P+5h?_q_%R{v+qmbSi zUIn2#Pu_?OqNVlu&aeOmQ!*9f)ep8`DNB}2pVE4+`dSp~Qv2N=dlCg8lti>APmAm* zlI-EH?cx?k+>de09~%&mJ?{azWtv=}=pEmMEXC~JlO11GU5X^?KY$zlC{aouJGra?|HCQJ})OQym2fRxTv$@Lc2Q zeC*&*sZ=0xSGHDidc!S&Va&Xndl7Z47I=PHW~~2YwkW_OackDZ!Ex|lU8pA|U|mEH zrPv6C91y?7e&o~v>234hcfbo`C#nbNcRa43t`8&M3SKmiOjMLNF z2OBFgnjS$t0m&#=X4CNE%4b@nF-^ee_()3ly-2j#BI02N92`hUX;P<;%^~Z9J=;Lx z?!0-NJl0f*k0;E{hbp~o;^l6n+At^dSF(i%-k_b7VENi`S>*=FaMRi0YGAfk6T8!J z|Mi33HnXMGxTU4SMyXhFNuQI{)ul&XRSkz_CW-kbXYWJ#sy6p71GdIA9f%J4yd0;3 zOf|8?%&{d@DR^Id-C<|Zf&+S60m3O?z#4xx;1MLr)3# zKvb^2srIRQL;q=eW-?~iO_9cL-yRMoN7K7_1 zfWNCC^Hg(69_34gy=jlYz5%RaBp8B`eMBiwGDRSQ$bkw4IYAYPQ~q@zw#73Ix}EdA ztMszmd_oa0b#|^-ujbtSwmm!IMvpoP3Jl3YOn0Hpq4;pB3jZ_|lwmDok;T%Kxib69 z?pSBIp3yJv!mADp3H2&xEyyr|D9BsqAE(u~;0H1P`Zg)Y=eM!>k}9?ob%& z4D+NoT*AcZ!Do`S)GPlt@a-6_dJeZ`y}})lPRp2QA7VQwshsk@4~n4zc=oH}A6r?a zJ)%y_9I3nrO+$x)k|5M#D3=vGzoXg>#iYxn=M-$J3V!PFp z+5Cx3ny6Q{SGM;iG-(PVZnY<;BS}9nKWbWC<`K;Tv*~$y`@Cc3i%bE*T&dkrV8N>Y zmuMZj1NBalAvrN?66ZFS;y{S=;Z%0`goE{%fHaC;ya3(+3sO%r; z2Fb}Jo$H&0tBpKk-O@GmRLNx*rLxxZofKwKFKpx#9r9~rnZUfpM&So$!3sl@oD*&O z*R-&+i}mQ8B4y|3f|}a#w$h}7zmtMx@gjH+xo*=hU{t%ZYmFjYzo(HrwC+te)!DQW zaEuOf{8^PR1fry@%p@_4Eqjkhy{B>G8(xE@R*wav;wG9~+8eb2^aQa{-**-Y1Dfq> z?zfX3=XFcqvPtl{wSIy0(yto5U=)T9i=F=Dz21bz6qbp*1#Y#~H|MFQUzG6|qqpF4 zHKKa`_!hhNuo$;+cSfEe0g`}GqLo4Dp7T1V!7GN{?}sqmRaUkFTL;P70>h6twqp^; zLPiXTxkZ;GaN}y#Ie0Ss{QmffI0vc4>LXRin=#c}%CaDD6~u=7{K~^^6Z1KOY*q=c zF5(-usK=LEDx8n;#jOR@-A^mxV}{JWY+{>N9Ewtwvm_ArX8veeTu+H)w?^5&cyw?H z*3e?OSZIXr(AK@OmF(U=kI4Ocz3996_4UbsH$dQO>=gGHqr~2jYv0rs_#&`@m?H%E zOfQ^!sdUgcHFtzxgJ1qoowWx!Rr0Jsj)?&JUbnQN|fM;{yr*5K%A!%HU^htHpApUGzRSYCE5EdhD?I1EaHwKL@uk zm+8^y3v$@n-ms$ZWuyCp{qjrfDEBVLxD%R|a!Y)TNLWpa3Mvh9+z7i$GV)EKgMYqTjCQUZi9rqM;7bErBxvRZT?X zYByRI?*cUEN^Xr=ftoecX6_p@yNk#E%zpWQhBlsnplAE7Yj#h~+k8~w4Q;5>g?_|g zvysoa&r31QHpo!_$GSj^>9EUV_q5@9xjC9}89n zPr{;&t;{LpeunwY**~DM7vk<}Zc>VQrGh0q&@(dipH`aIw66qOkp z`>=}WZTQSs(&p&QeBPX1`Iu&%_K9b$f6tW(DKP7PvG3V#PY&{TXqjad1oWPL$dMya*J}=wH>?NGs&dpwpo~=A0Sm(-)y(SU@KvAY z=F+CFU86EO%nCTsZ)-np9MVBP5_!2Tt;A9Y`!q2z>HGhpG9lbEuA&hAueqsQQM88@P(~x{LK1 zDIHat?6Tipvf*?r-}$(VPh3rOfv0e<+1u_;3{{+fDFsz`{6S4FEPX@9jHCcpi4aO3 z#K-m3oKS=5Q8SiaesUCe-RHhBii5uJi zqofD^o?ML-Z4=YjaWoFpuRN^Ku(YLTH7EPZYEd~Mv{Z{pa+)R))Wtq*pvfC%hw{|) zan;N~4CL*_cygPjU6>~%hh84fbeZbFtJiEAM4w)b$8?FXdPQMInGIAjQfeecOt;^j zu1k0{o-Dw}I$To}YhRA(>YZW!cH^3SN#Eo~do7FHGuQyjkl&rI12KYvTl6ekm1^D= z@F&0{EKy3)>dkOX@~MI7e(+wU#%=LsEc2v(J5t3v)(#hpw_W%bXLC zbNvW(Mvrhjm1hBJ9TyVShsCDFNtSv2s|W)@6hCQ){2yN##jDWW`0kyDDU*5DoTb_;HeL2$@!UMObH4vbx)y{aQmB5R@`T^9>V&BFj zTmWqas$BTOpK07*Whp4Z5iFw=Ecn_V;J~%HEqKeJ=o(!8k1&dU!g_n8g8FFxJct)* zj#!xIhuqsFrOgULa%)k;aqc-C{5BwK4yvm{7FbFo@GwcROiZ#D)2UH&SUCuCurtUH zTyC`_R0MqOQAoPSO$4)4Cu3{WCJ&Ub-~#wZHrUFHJ-Q;80y=Y3oPY_Me}RPFoCdc< zDrt8bkrZ}^o=?;%!!PQdM{8##Joz`4s*I*#0D~^4jO9we%gEnrPzO00RlSPM_WTCP zD`?JK(_`=!T%YD;_)hl}W?@o;QhycDZOPty21-sczrpU(ce1n$g#uE8=ET}n%Wp5TQX7ZlI4 zUpFIfvnu~}n`LQ%j?gRHuXe8#)BE*HIW08v<|j#WyR($DrHe#9@%(;TapAWZxvVFT zuCfawsU@#YlrtrEyW2HdcDqMXGfsu~KJ1JHMLa<9>a9jcee;nObq7j<$BsKLOFZlu z;{!7q-j=5oLId(p}nu!AG@lX%5;`>D`+v3;IgwSX;7G=;yZKz{!O4`0v#ns zIF&fr>@e`Saa#R@CMiH)AdIl$nDBe0G;n439p-VMBaF zoTeG2rVy$D=Uy9p%6o8jdyU~}AV6hZcXwNDF`GZ`@2le679c)}o|7~Yt3FPBsyJh{ zAknQC=-0zAIO1&Dslgos4TR9fkmC%g%t*jof@7k_v^;(YF=0@lKY}RRvqc`;qXw0^ z)AdE}p@oEHHnQ4N;STLKm_D-T$^hM#>ob8}c!D3;Cv3z$9$mkyP?=$yu{y3Bu69DS2B!t)la^reKMi-~6OcF>H3`c{{FoxOnBW&5xI`j<#_Ow9r0 z<52eO#wZxi$iaiH&{k;z*8UxfxE~Zy-7s^)8>LBsho(5Xo!80>OfaXP7pzyzn53rI zgq^LVt2Yi$!W2qyC;_)1{}*u9f?Yxya|cWhh?5<$DQ!vjhJ&Bb3iX=CN7rR5bE%f4 z+|n}dQkRX1V~ZU5yr+T3fg1lq8J`&>;* z7mjGe`GCGVU_O*^@%D{TV{vKOjmE6w#`_JEjaN1#lKxyeXl>ELb~^1}u9H`9Q=mX; z_Ac?J9|!|ZGCcsQ$eJ(jxTl)V;txOhybho+xSWr!FN(IzK1_;s$dddZu5+VV&>&kq zqijg-@TqS26y|_=H^crc@>sSmyTpeZ?UFu zuRRB#zJIPzO(`vACDxkUhn#0;E6N>3ZSG2lUe8o~EZ#D{w$SDw;7#hQ8r8WvYj*VY zwQJN>iw4S43VW;&j5iCL%|14b)`eF;wF>M4Yg@V7lh?@1uM&5_SlmcZ^^!dR#uKkGz)fhr^OPX0lhu@nshDkj#?R|#w$|CeBMXD>XMR3m(gH+*vK*c#^P+kM8M#)x}gyz60>C}4} zLS`EE*A1BqUv6YyVVfxAfWm_B3*fOKQAPpJ7wjx)1_^kT_hXh0F{u4UZ!SjyJ^sRc zd97k3>%qpartI63LW><#WDa;91Z(c?Y=M-+2^aVGU}|R=8Ih3yb-S(8(I8pKF`-wd z?dU;0h&#Eov$M5jO=O&>nFqn%{(|N`xPd7m18Mj1H zdJD4R9=sUA9>{edLMWoXpT#uVdZ;<|9aC5I0%>_c(rYQW>6RSFk=~wk_u5HqS5U9& zsc?XB$6PW3!hde~XaCncDX}<>LL9ams zpgWSsejr);>*}(!;NY~MIomBteAKG>u!q8P#!HUTBKqxm{S8RQi_N{^2Tf9fFTbMR z;mUNm_ue3#(bLhU6`+%+34(&I>6ThkPcG*LqH=C@gZ%@Sb@Lv$$3847>lVe!T}|85uctrJT?7GCqM;)M6{kl=!4 zy2D0w3kV%lGFQ6KUF*9yQ4w;jR~JG0w%YAew$01&VY!Kc>LnuYUo6S1;V>O$*uwLq zSm1Q6Kp}w%tSU=Nnqa?MXMj&h(?Dx!VdfQ6Nl9|E-rq^_i1)32;!jlk1_iyon?&~G z!r`<6LvuxOwVG}cNM4bIkkqI?rge4FvlST{&Y&)Q(ZQRWgFsJ^zZ!83{^!&cnZRf) z_9%p6+R!z+860CbFfU(mfx2pZ=SxukvW0SaALWKz~dG!@OBek8QHr zkT@_-ziaY6Z}q;N&q+*Mutkw=c1JH?7occ!RtXu=#zg1lY*>a$rbawvvU8ldEXfAr zKF96#7*uX?b1nlM$&s?+D_}*_XN&AjQ;4)!qUDdV_KR-LrfHKvac%?%@?A%g_!5Wd zzhr^r>+7-DfL~|e4bS+Y)0+srA1Gw6HFo+*B*63XChl)?h!e)qK#U=pE3#bH~9mCEzkGOsjpLv6bnW`v|59vnuVD{eggJxn-=SQ z49OEak!+B#>%XMQ7eI7Pec2~B57MR8pTE!>Fnaz8k4zVeu$S6H_v3Txr&H$#TY`@1 zzpdn;j$U3~g4SRiEcLNxG(oV%&dZQrW(0-n@g7++xu7|ieeUH$5@RT0W=?DJ=`nPn z+0#+b_Q&V6h#VrxwK2$Hev71z`@sE1RaCEl!@6(`pKrsMk%VNa?&uJ{MKdrCr*?6) zf(7CqZ|klRa+2HPavE*mfq4)o|2NRV{tM*h4myD7JrAs_{{x|Z90xe0AeCmm;!-E} zY|mZ|1z&hQsltjRDYqKQLei++C>(0ysgANk+7X{3=ZWMfS3b`%lP^9#EOT29!mvDg z^qq#m-Fy4~f5|1S`Ji6O^WZ4@V{v?~7m-pprn}|y-~9KEki5jHR9*fZFz@gf@@ieb zhy9Y>Dk_4@qpOgdc$Cd(EN>BP2z_lFUk{R5n?Jmh(5uBUXPj0X+BcBH$|*9hR#fF+ zCusjv)=n}#kvRSV(1#+*PFlW{7)65Ih0B9vu%)`q&zr(H=+z~tI?RksH;aHhM9Pap zi#PoAAK7g7i#}wUAE}}$c)0?qL^sNqdMzS^4G!VMsFo1gw9g)?B>NP>o8HG8i6Yb~ z?)C$2%*parGC8h9m{v;~JT3XiYSE%=Q(_G-W)2I^A_P2w9wfF+Q0^?Xx0_!Y@t%!< z5T$yMGdfF)%sdNVoKaWFD(&GSbU*YamCZ_$cT2_!wzay_fST5(Why z6LZFsUrxx!#3I9|+>mV{s@(Mz^l|p>MXP)hd)k}Dj>r*{Bt?DE?_3ieSt|A2fl%d=;)ayrCUEMV=A@OiqCFTkZuMydHP#mvKVrm=`rfswzn%W# z@W0p=tWlvzG-xx3x}R4S-TR=VJdPRV7X42L81mf|Rarhw4sft(0WZ~!F^5u=NTkW4 ze|v*&Exq90w_CX2cLz>t#inHcbbvBMcro_4q8`~f5H>(KRmAsC=fGJMq6PC8@(~!j z42!md-9M6&)S#J|6~fwt$v_BNd%tc;;r|_$QZk?cU9YZ-kG%zTG<1IPo6K)ha7Sf0 zJv+PoS%&QV8zu|$Z<--Tq+yG4brZz=7knGFAwsmEBJ6nyP%`U3uq8zu@GEt!+yIVP z|20JZL%;clf%E@Z(F+iDGqzL6b$LnP^tl&>Zg?<)6yDYcSxt+e_^X zXhf$XbfL@Js2v`6#p}95t%Jn%#Yv(gwNpGXf8E+gmoH|ckpkO4=h;^;CP0Ml?8Z=f zvScE9ENC%%BMCJ9NhA8K{UI&i6uI4FjPP(ex(p{PUe$2*)q-^BhR47gl>g2pZ@>ynb#xZdZ zsF(e}ee}kRbpIRh{(mCyX`rD*T(m`?^V$2wlijzkl>7D#YQL_38t>QL{Tl2(=|YO0 zlM@0m7PIEYJoy9#n;(h$uqUIi$){WVWU?n4UX_)c_{xweryF6esa?Gf?r8abzB@}e z`uvy(jRZoZ8wAcWY&89T@N=gLu+??Wi+kV*>_QQt7VN$k#0OPkpMV_Egq3egmsus_U*qSK3J}Rr7(obY2{8q8UoLNQ} z^6QF4aDDPr$Mf!u+(%kLklX*t^X}%nxiC9mHdVJ$r`-7U*AT3I-t5aL3O?(&&3gfY zRrbUzxTK-ipj2w26*PI|cY(zEba3_xv{@4P%9q5hxBAHY7tuK=)o2uU-MFB^td)x* zTj`0W*c?cgJ0RJ3dAY3tQZ9XM4-f!K|v?nqMPfugDG(}Sm)r%0p3VykcS;aaaw zz}V9$&{+2qN0LbWY-@s9Xu?~NltcfQvG@9e!1gI{cKm`pi19AX7#`=jqnhM#ZqjtIG-+(0 z;w(0&Qs=Xrebkaqx&y&^$fdsO*}vwL+Lj$fgMUr^|HJI}4}SiicUES2oEIpzF!mGj z`n~*}__H+bcTvYhF*$Fd*E;6BmV>`-N~^xJ;zKNSD(V+~28Oc4=a#CU=gMkn{gQ3J z{j%)-rVo^m9ui}LhAP2B(AVSRd;>DGwl+qx55S|?4!#J2rXJi>CYK4^$7OaP780|H zPqlaMXH0GYuQsO(ZL%^3KnB$xKcq@gfxyde5zOZ=pi|hZzq7>K?9vnr#U~SfxiAc7~0xTKwul^3|;f0_4snq z71@@ImA60iSfG9KHL!*0*yyV=ZdAvk*a6HWa6{^U-Z$ylcx|`^3g|tG8 zy_u;5h>?)E3#+(PGP!zxH|*S*(KUC3Y?w}g1tDR``_j}`aZ7c9NlAO{;4m@%e|UTA zuqxYadzg}t7HI(m0g)D@ODQQ)lx~pj5Ec#64IqD}rlxOBNWAey<*YZrwMfzkgOy_UcA~e^!J2_o~cK$Q5BQQ`UaT5N8gPvJsRl zdIY#jy%o;Vt?{_en3M|Y13#e!8;CTAi4qw_pio-w)#G?H!9BCBX$k*?*}#Ti);vRW zjBGLWDcI?4@YAzH#il&oX8xrZ-AUjwO9GZI*xLi3;C#CcT0hP)Zb;?Uew0`aI*t%s@-ggw5RoTR=xn4K53su=e%T znIaJE8m$S!5XF?A5jU~5q~UP<(|c zK3pH^C3`>gI;58YVTE_Ayovv0)^qy&ps-xP$C%)vlC}$4$4~-^ z^S>HFz(c~l9^h4EjjhAO3d|ZsyVH*Nm{yb|1V|Bn$>&F#s)n!21*-4IGpdyQKo^+P znhxJo;}@uKt>&|t0zYuD`sT41E_o&C<(U)xNCd)mI&&*XDPquH7;6fnG@?blqdo8$+$);BY!7N|}=ypfyBFJI0@NTBVIm)#R zFw+?=Pt3`)_55U$=Rz>dS}kZB$}3cO2lt-P@R{>3bZ@?tup){^g+jeiIb9TLI_`>J zt2R{MSNQxEC_J01eZb@8zZRqac|XnzAdz?{s7enQu7F9Z+1KhzmYT z`}CrK)%60FEP#jjn3dju+Ul+X@C3XuZx8zT9D|5Oh&&}?_8mVkfKrgVO^XnT3l6wm z;bZhf2rsb?^g%l=HuW`9f}y3*SB2gN>-hd{{`lv2`&f~9Jj<4I6a%!1<-!eA#)Dli zjhrjjPubxQz^%5vRd?;mtbf(YSvw~v++aDtQOyN;&Q(IrLst&`fOMTqdFCnk2)r+- z{qyBK0`?vq{ChvBrg_i7VAp1A2-!Nokhb)@V&)(tNpTe-%60IYt0-mr=ANF29LR!_ z3I__}wiSLM*(AP-+>8urx5t>|pfy2ugR?z-N~NVU8>iQo^hbN9`df90i68qXedeFs zao{QxGI-dB&-Z|E^_1HYo8tD*nTscQzCupPc_r2o(d|LnWLrn~TTbKwk z+c(mkI%K+!AajKyIHHm>28zIKr>XwaOtch>s!fh#pr5V+plSI+sNggqvomr;DcXxk zaSE3qOfsG!GOK3Xt5eK;=0LbJ;LuSa{36(_u!X=p-mDZYLdhI@oQKmXbn9re31MSW zjxfwl>MSyl(!W7?0|n@GPRAm3B;nMbr()th`XnUI9yYM|g17gU_-}=uw}2#ck^S3x z?_a)ajrRgiNYm}|5G!>LoV7`?K7Ml_?N*b-1O#aV!}g4kqzzD@Kma=ZCsy#{fys(3 z!08XIfNt(SX145c)k4A6lb4Z~82PEDLXgj6mLGUz_;V^u1%yJf z;9fPmx;X7j?0abk&ZU7tMi}KQGyoAO-pL#&xT#C(e!D^{2YA2k{FO)a&z}L+i4npI zWI$2_9Ba%kPIk>fRet2n(!in;2C5Rs1QLebZAkywRy8( z(%-}OIS4r&@YWQAa_&M+Or8Y7%AHU(QV@iVUtUm#NrJNCD||L;7<9Q5h5UB8WS4&p zc>I?ymz(2y>kbz%LYe~oyV@Osc%m5D=D;^!q^+c)nchR>z!ZV1>6}d|OC9jbf*k5s z9FMm)YlIgHhk{@Z_o6932mmV<24Z_4f3Jd=g6#zl$CbX+44eL7sMos$^L7Wm*ub`s z6WlO{VWTcpNuv=wk3e4%51wg7Kle6#<)!F2hsAXD#aDJ1s1Qgm;QJOwg53Q*OroAS zH9`+G)AS#l4>LUb+jswbRR43t{qtvDxgkJzLOhq1P7d4|gzJN6mCgJMGUZiJe=lDb ztOaZTNWFEj-)sK`aSUX$DKK30rZNRWCcl@Id^%d-@T)THr*!+KS?3yJiz@`Jip-cT z*6+4X_uld{L+Oz%ppbqpp)HbP4L!=9E*%(lAMnpKmvRbM zL2}7^0U!|k*cCt2yU)Pm6itu(BnG8F3C@4CrMdHvZ7D6jdfPv7%D)zvJnr>buhS+y zP{>!1F-d;eu(7f@lA2OfYP=&{rZWXB?^vEv`QGj%>XOh@Yb>UUQN$r?(Kc|VA$GN9hm^z&~p za}ejNhJU$Y{vThav?4+E3a}BoX>-~*bY^d%6L3}2K_%)o6%9EsFa7#3RjVBI*}sgq z`CiVhxYBuC&-{4op2TU9R0R;^dpeTMy}kA;0Np zfuY@>y)i^&3`-fX63;kZ(9LzsohMcrJl1y{JX8ojA*|u4QF@CERO7Dxe#siyzg+&$ z9%+g$Y7S_jH7Ev(4P-f;Pb}M(0aI2z8gmCSt${d!QEQ#0S1bu7GLt*^&R=&vcBh#J zJ};ZVjY7W+9NlE;WC54z9}Y`hvF57-8COIf8W6F#QYd%Xa0fe9@kRteo(O=R7Hg3l z60>_0%Kz$x1IE{;0GYBm4Sekv<;={@?QX6bsJ0CCrJQ4!5V_sN7=3cR{g%?5rem_g zrY3XQXz)vJ*gf*pT^)XaaNzf3kiDQcP;pKqgva%x1$mq=nnBzFg;NHsSjQwhDRPGT z*fyC$k^8~mCK+s$w-JES_%E{Gs4GPqMf~3Z?!ORR0aTVtu?CJVs2a(jwb<-?Ka1}; zl=V6f09qMA&%-UF%qV^4B+*1U1{#|alLg&soA%ijTcm(-bKY9#JqezDb2ai>@m&g?aO=VamHN!D~BsJ8E+Vz18g#>Dg#{thVf z)=ub3Q6EAPJ8()Fa`Td&{Ek42PO4}8o$LDNwfk?c=AU=~n~Z-|ISfBxKkV~YEN5>&M5`=a+` zq0rUy`+I^t*`jn$;lk1`Edk?{I*V7JvG32nj+?@aJp@Cu9mC-pZtmWSY$C_(Bwbh|D8VM`JsAnfe`$h8M* z^i%#L83SL)rK`vP>3fa3Ic|#Q7<9EDfWVRWmpn zBX6SJ+$H}7d43iCJ3sy}ar7r&3SEK�YPDnfyF5rRom&h_4f&gdzi#TK~t#Y1j!+ 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≤zF)Bt3bW7?2;2p^$dfx1COt}#CL_0j14wDN{2 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%0D@yNVH!LO;Ly^=@6 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�u0#`wD62t9ZQ1qGd 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%0Dn2jIxX%j`RF=wv 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%0DWiAB^Je#J2j~odMwhMH0s6+a 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 T;{so~N3Qvt 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 Dd Date: Fri, 21 Jun 2024 18:48:08 +0800 Subject: [PATCH 30/90] fix: macOS if --- src/MDriveSync.Infrastructure/GlobalConfiguration.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/MDriveSync.Infrastructure/GlobalConfiguration.cs b/src/MDriveSync.Infrastructure/GlobalConfiguration.cs index 262fbde..eec2d10 100644 --- a/src/MDriveSync.Infrastructure/GlobalConfiguration.cs +++ b/src/MDriveSync.Infrastructure/GlobalConfiguration.cs @@ -37,7 +37,6 @@ public static bool IsLinux() public static bool IsMacOS() { return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) - || Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; } } From 7811a357a4a4d93da8d5c73e11226a494564c31b Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 09:41:16 +0800 Subject: [PATCH 31/90] remove winform and wfp, by proj --- .gitignore | 1 + MDriveSync.sln | 23 +---------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index fe46fbb..2835219 100644 --- a/.gitignore +++ b/.gitignore @@ -367,3 +367,4 @@ FodyWeavers.xsd *.db *.d *.cache +.vscode/ diff --git a/MDriveSync.sln b/MDriveSync.sln index f056153..4e92ae4 100644 --- a/MDriveSync.sln +++ b/MDriveSync.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.API", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Server.API", "src\MDriveSync.Server.API\MDriveSync.Server.API.csproj", "{DB493EFB-5438-4088-BA32-2A84E008DB01}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.WPF", "src\MDriveSync.WPF\MDriveSync.WPF.csproj", "{3379ABA8-7F55-40A8-ABBE-B8BE2E4A6D19}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0FF6CD64-0E94-4AD4-80C2-369B7019359D}" ProjectSection(SolutionItems) = preProject CHANGELOG.md = CHANGELOG.md @@ -18,12 +16,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0FF6CD64-0 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Core", "src\MDriveSync.Core\MDriveSync.Core.csproj", "{279736DD-7A47-4F6D-8795-A8ED3278DD33}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "libraries", "libraries", "{687C2212-CB3A-4267-9F20-C4ACA34B9570}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SeleniumService", "src\Libraries\SeleniumService\SeleniumService.csproj", "{894C26A4-BD81-49A5-9D73-AA68E5BEBDD7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.WinForm", "src\MDriveSync.Client.WinForm\MDriveSync.Client.WinForm.csproj", "{FE8B7D48-5BC8-4DEE-95F5-B7B9DD3F2FDC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{EE45B944-6991-42CB-8EA5-E9F7FE33DF58}" ProjectSection(SolutionItems) = preProject scripts\build.ps1 = scripts\build.ps1 @@ -35,7 +27,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Security", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Security.Test", "src\MDriveSync.Security.Test\MDriveSync.Security.Test.csproj", "{B036CE46-D6BD-45B0-A967-7F866D06FD2B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDriveSync.Infrastructure", "src\MDriveSync.Infrastructure\MDriveSync.Infrastructure.csproj", "{4D5DF593-BC91-4803-99AA-7872DFA98987}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Infrastructure", "src\MDriveSync.Infrastructure\MDriveSync.Infrastructure.csproj", "{4D5DF593-BC91-4803-99AA-7872DFA98987}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3CE27DBE-FA45-41B6-A170-A721CD66562B}" EndProject @@ -53,22 +45,10 @@ Global {DB493EFB-5438-4088-BA32-2A84E008DB01}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB493EFB-5438-4088-BA32-2A84E008DB01}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB493EFB-5438-4088-BA32-2A84E008DB01}.Release|Any CPU.Build.0 = Release|Any CPU - {3379ABA8-7F55-40A8-ABBE-B8BE2E4A6D19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3379ABA8-7F55-40A8-ABBE-B8BE2E4A6D19}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3379ABA8-7F55-40A8-ABBE-B8BE2E4A6D19}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3379ABA8-7F55-40A8-ABBE-B8BE2E4A6D19}.Release|Any CPU.Build.0 = Release|Any CPU {279736DD-7A47-4F6D-8795-A8ED3278DD33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {279736DD-7A47-4F6D-8795-A8ED3278DD33}.Debug|Any CPU.Build.0 = Debug|Any CPU {279736DD-7A47-4F6D-8795-A8ED3278DD33}.Release|Any CPU.ActiveCfg = Release|Any CPU {279736DD-7A47-4F6D-8795-A8ED3278DD33}.Release|Any CPU.Build.0 = Release|Any CPU - {894C26A4-BD81-49A5-9D73-AA68E5BEBDD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {894C26A4-BD81-49A5-9D73-AA68E5BEBDD7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {894C26A4-BD81-49A5-9D73-AA68E5BEBDD7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {894C26A4-BD81-49A5-9D73-AA68E5BEBDD7}.Release|Any CPU.Build.0 = Release|Any CPU - {FE8B7D48-5BC8-4DEE-95F5-B7B9DD3F2FDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE8B7D48-5BC8-4DEE-95F5-B7B9DD3F2FDC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE8B7D48-5BC8-4DEE-95F5-B7B9DD3F2FDC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE8B7D48-5BC8-4DEE-95F5-B7B9DD3F2FDC}.Release|Any CPU.Build.0 = Release|Any CPU {25258960-ED11-4C31-8119-E91D7BB33C7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {25258960-ED11-4C31-8119-E91D7BB33C7C}.Debug|Any CPU.Build.0 = Debug|Any CPU {25258960-ED11-4C31-8119-E91D7BB33C7C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -86,7 +66,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {894C26A4-BD81-49A5-9D73-AA68E5BEBDD7} = {687C2212-CB3A-4267-9F20-C4ACA34B9570} {B036CE46-D6BD-45B0-A967-7F866D06FD2B} = {3CE27DBE-FA45-41B6-A170-A721CD66562B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution From ee6e825e8785c0fdc80ac64e07972a083355ff5b Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 10:00:48 +0800 Subject: [PATCH 32/90] condition delete by pre relasese --- .github/workflows/dotnet-linux-arm64.yml | 11 ++++++++++- .github/workflows/dotnet-linux-x64.yml | 11 ++++++++++- .github/workflows/dotnet-osx-arm64.yml | 11 ++++++++++- .github/workflows/dotnet-osx-x64.yml | 11 ++++++++++- .github/workflows/dotnet-win-arm64.yml | 18 ++++++++++++++---- .github/workflows/dotnet-win-x64.yml | 16 +++++++++++++--- CHANGELOG.md | 21 ++++++++++++++++++++- 7 files changed, 87 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dotnet-linux-arm64.yml b/.github/workflows/dotnet-linux-arm64.yml index 43e3f55..fe42b3c 100644 --- a/.github/workflows/dotnet-linux-arm64.yml +++ b/.github/workflows/dotnet-linux-arm64.yml @@ -40,12 +40,21 @@ jobs: - name: 删除 PDB 和部分 XML 文件 run: | # 删除目录中的 .pdb 文件(如果存在) - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.pdb + # rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.pdb rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.xml rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.bat rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/WinSW-x64.exe rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/wwwroot/driver/ + - name: 条件性删除 PDB 文件 + run: | + # 根据版本标签包含 beta, rc, alpha 的条件来决定是否删除 .pdb 文件 + if [[ ! "${{ github.event.release.tag_name }}" =~ (beta|rc|alpha) ]]; then + echo "Deleting .pdb files..." + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.pdb + else + echo "Preserving .pdb files for pre-release versions..." + - name: 创建临时目录并复制发布文件 run: | mkdir -p temp_publish diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index 806f8fd..c1b6872 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -40,12 +40,21 @@ jobs: - name: 删除 PDB 和部分 XML 文件 run: | # 删除目录中的 .pdb 文件(如果存在) - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb + # rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.xml rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.bat rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/WinSW-x64.exe rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/wwwroot/driver/ + - name: 条件性删除 PDB 文件 + run: | + # 根据版本标签包含 beta, rc, alpha 的条件来决定是否删除 .pdb 文件 + if [[ ! "${{ github.event.release.tag_name }}" =~ (beta|rc|alpha) ]]; then + echo "Deleting .pdb files..." + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb + else + echo "Preserving .pdb files for pre-release versions..." + - name: 创建临时目录并复制发布文件 run: | mkdir -p temp_publish diff --git a/.github/workflows/dotnet-osx-arm64.yml b/.github/workflows/dotnet-osx-arm64.yml index e86edcb..cd24fa5 100644 --- a/.github/workflows/dotnet-osx-arm64.yml +++ b/.github/workflows/dotnet-osx-arm64.yml @@ -40,11 +40,20 @@ jobs: - name: 删除 PDB 和部分 XML 文件 run: | # 删除目录中的 .pdb 文件(如果存在) - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.pdb + # rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.pdb rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.xml rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.bat rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/WinSW-x64.exe rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/wwwroot/driver/ + + - name: 条件性删除 PDB 文件 + run: | + # 根据版本标签包含 beta, rc, alpha 的条件来决定是否删除 .pdb 文件 + if [[ ! "${{ github.event.release.tag_name }}" =~ (beta|rc|alpha) ]]; then + echo "Deleting .pdb files..." + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.pdb + else + echo "Preserving .pdb files for pre-release versions..." - name: 创建临时目录并复制发布文件 run: | diff --git a/.github/workflows/dotnet-osx-x64.yml b/.github/workflows/dotnet-osx-x64.yml index 01b4cec..864b608 100644 --- a/.github/workflows/dotnet-osx-x64.yml +++ b/.github/workflows/dotnet-osx-x64.yml @@ -40,11 +40,20 @@ jobs: - name: 删除 PDB 和部分 XML 文件 run: | # 删除目录中的 .pdb 文件(如果存在) - rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.pdb + # rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.pdb rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.xml rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.bat rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/WinSW-x64.exe rm -rf src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/wwwroot/driver/ + + - name: 条件性删除 PDB 文件 + run: | + # 根据版本标签包含 beta, rc, alpha 的条件来决定是否删除 .pdb 文件 + if [[ ! "${{ github.event.release.tag_name }}" =~ (beta|rc|alpha) ]]; then + echo "Deleting .pdb files..." + rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.pdb + else + echo "Preserving .pdb files for pre-release versions..." - name: 创建临时目录并复制发布文件 run: | diff --git a/.github/workflows/dotnet-win-arm64.yml b/.github/workflows/dotnet-win-arm64.yml index 8acaf72..4ff3310 100644 --- a/.github/workflows/dotnet-win-arm64.yml +++ b/.github/workflows/dotnet-win-arm64.yml @@ -37,11 +37,21 @@ jobs: echo "发布目录内容:" dir src/MDriveSync.Client.API/bin/Release/net8.0/win-arm64/publish - - name: 删除 PDB 和部分 XML 文件 - run: | - # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-arm64/publish/*.pdb -Force -ErrorAction SilentlyContinue + # - name: 删除 PDB 和部分 XML 文件 + # run: | + # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) + # Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-arm64/publish/*.pdb -Force -ErrorAction SilentlyContinue + - name: 条件性删除 PDB 文件 + run: | + $tagName = "${{ github.event.release.tag_name }}" + if (-not ($tagName -match "beta|rc|alpha")) { + echo "Deleting .pdb files..." + Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-arm64/publish/*.pdb -Force -ErrorAction SilentlyContinue + } else { + echo "Preserving .pdb files for pre-release versions..." + } + - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 diff --git a/.github/workflows/dotnet-win-x64.yml b/.github/workflows/dotnet-win-x64.yml index 6f3cab8..5d5580a 100644 --- a/.github/workflows/dotnet-win-x64.yml +++ b/.github/workflows/dotnet-win-x64.yml @@ -37,10 +37,20 @@ jobs: echo "发布目录内容:" dir src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish - - name: 删除 PDB 和部分 XML 文件 + # - name: 删除 PDB 和部分 XML 文件 + # run: | + # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) + # Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + + - name: 条件性删除 PDB 文件 run: | - # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + $tagName = "${{ github.event.release.tag_name }}" + if (-not ($tagName -match "beta|rc|alpha")) { + echo "Deleting .pdb files..." + Remove-Item src/MDriveSync.Client.API/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + } else { + echo "Preserving .pdb files for pre-release versions..." + } - name: 压缩构建产物 run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 766aa2f..6dd0733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,23 @@ -# MDrive 汾ʷ +# MDrive 汾 + +> 汾˵ + +1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 + +- alphaͨζŰ汾ڵģܲȶδɵĹܣҪڲԡ +- beta alpha һܸߣԿ֪ܰδ֪Ĵ +- rcѡ汾ӽղƷҪڲȻڵϸ΢ȱݣͨΪѾ㹻ȶʺϷ + +汾ź alpha.1beta.2 ȣʾý׶εĵ汾Խ󣬱˸޸ĻŻ + +- 1.0.0-alphaʾһڵ alpha ԰汾 +- 1.0.0-alpha.1ʾ alpha 汾ϵĵһ޶ +- 1.0.0-alpha.betaʾ alpha 汾󣬼 beta ԵĸĽ +- 1.0.0-betaʾ beta Խ׶Ρ +- 1.0.0-beta.2ʾ beta ׶εĵڶ޶ +- 1.0.0-beta.11ʾ beta ׶εĵʮһ޶ +- 1.0.0-rc.1ʾѡ׶εĵһ޶ +- 1.0.0ʾʽİ汾ԤIJԺ޶ɣΪȶɿġ ## v1.5.5 From f407985738dfd70c399ceb98bb0fcd31069e4ee7 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 10:08:45 +0800 Subject: [PATCH 33/90] fix: published fi --- .github/workflows/dotnet-linux-arm64.yml | 1 + .github/workflows/dotnet-linux-x64.yml | 1 + .github/workflows/dotnet-osx-arm64.yml | 1 + .github/workflows/dotnet-osx-x64.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/dotnet-linux-arm64.yml b/.github/workflows/dotnet-linux-arm64.yml index fe42b3c..23d92f3 100644 --- a/.github/workflows/dotnet-linux-arm64.yml +++ b/.github/workflows/dotnet-linux-arm64.yml @@ -54,6 +54,7 @@ jobs: rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-arm64/publish/*.pdb else echo "Preserving .pdb files for pre-release versions..." + fi - name: 创建临时目录并复制发布文件 run: | diff --git a/.github/workflows/dotnet-linux-x64.yml b/.github/workflows/dotnet-linux-x64.yml index c1b6872..6dd3411 100644 --- a/.github/workflows/dotnet-linux-x64.yml +++ b/.github/workflows/dotnet-linux-x64.yml @@ -54,6 +54,7 @@ jobs: rm -f src/MDriveSync.Client.API/bin/Release/net8.0/linux-x64/publish/*.pdb else echo "Preserving .pdb files for pre-release versions..." + fi - name: 创建临时目录并复制发布文件 run: | diff --git a/.github/workflows/dotnet-osx-arm64.yml b/.github/workflows/dotnet-osx-arm64.yml index cd24fa5..943343f 100644 --- a/.github/workflows/dotnet-osx-arm64.yml +++ b/.github/workflows/dotnet-osx-arm64.yml @@ -54,6 +54,7 @@ jobs: rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-arm64/publish/*.pdb else echo "Preserving .pdb files for pre-release versions..." + fi - name: 创建临时目录并复制发布文件 run: | diff --git a/.github/workflows/dotnet-osx-x64.yml b/.github/workflows/dotnet-osx-x64.yml index 864b608..600b477 100644 --- a/.github/workflows/dotnet-osx-x64.yml +++ b/.github/workflows/dotnet-osx-x64.yml @@ -54,6 +54,7 @@ jobs: rm -f src/MDriveSync.Client.API/bin/Release/net8.0/osx-x64/publish/*.pdb else echo "Preserving .pdb files for pre-release versions..." + fi - name: 创建临时目录并复制发布文件 run: | From b7a16c55539bd223fdcd92947439221794f65795 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 10:11:35 +0800 Subject: [PATCH 34/90] readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 640506c..9fc79fe 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,7 @@ docker run --name mdrive -d --restart=always \ - 只读模式:WebUI 下如果开启只读模式,则允许编辑和修改,只能查看,默认 `ReadOnly: false`。使用方式,可以通过修改 `appsettings.json` 或 docker 使用环境变量 `-e READ_ONLY=true`。 - 基础认证:WebUI 账号和密码,如果开启则打开网站管理后台时需要输入账号和密码,默认启用 `BasicAuth`。使用方式,可以通过修改 `appsettings.json` 或 docker 使用环境变量 ` -e BASIC_AUTH_USER=admin -e BASIC_AUTH_PASSWORD=123456`。 - 演示模式:网站配置为演示模式,只能查看和下载,功能受限,默认 `Demo: null`。使用方式,可以通过修改 `appsettings.json` 或 docker 使用环境变量 `-e DEMO=true`。 +- 调试模式:非正式版(alpha、beta、rc)默认输出调试错误代码行号,正式版不包含 `.pdb` 文件。 ## 系统配置 From ac55bd18b714545bc2a347114aa1e4f0804372dc Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 12:21:16 +0800 Subject: [PATCH 35/90] feat: winform api --- .../MDriveSync.Client.WinFormAPI.csproj | 42 +++++ .../MainForm.Designer.cs | 47 ++++++ src/MDriveSync.Client.WinFormAPI/MainForm.cs | 124 +++++++++++++++ .../MainForm.resx | 145 ++++++++++++++++++ src/MDriveSync.Client.WinFormAPI/Program.cs | 57 +++++++ .../Resources/logo.png | Bin 0 -> 3364 bytes .../appsettings.json | 44 ++++++ src/MDriveSync.Client.WinFormAPI/favicon.ico | Bin 0 -> 1150 bytes 8 files changed, 459 insertions(+) create mode 100644 src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj create mode 100644 src/MDriveSync.Client.WinFormAPI/MainForm.Designer.cs create mode 100644 src/MDriveSync.Client.WinFormAPI/MainForm.cs create mode 100644 src/MDriveSync.Client.WinFormAPI/MainForm.resx create mode 100644 src/MDriveSync.Client.WinFormAPI/Program.cs create mode 100644 src/MDriveSync.Client.WinFormAPI/Resources/logo.png create mode 100644 src/MDriveSync.Client.WinFormAPI/appsettings.json create mode 100644 src/MDriveSync.Client.WinFormAPI/favicon.ico diff --git a/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj b/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj new file mode 100644 index 0000000..14739b8 --- /dev/null +++ b/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj @@ -0,0 +1,42 @@ + + + + WinExe + net8.0-windows + true + enable + + + + + + + + + + + + + Always + + + Always + + + + + + wwwroot\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MDriveSync.Client.WinFormAPI/MainForm.Designer.cs b/src/MDriveSync.Client.WinFormAPI/MainForm.Designer.cs new file mode 100644 index 0000000..7cf2e73 --- /dev/null +++ b/src/MDriveSync.Client.WinFormAPI/MainForm.Designer.cs @@ -0,0 +1,47 @@ +namespace MDriveSync.Client.WinFormAPI +{ + partial class MainForm + { + ///

+ /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); + SuspendLayout(); + // + // MainForm + // + AutoScaleDimensions = new SizeF(7F, 17F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1264, 681); + Icon = (Icon)resources.GetObject("$this.Icon"); + Name = "MainForm"; + Text = "MDriveUI v3.3.2"; + ResumeLayout(false); + } + + #endregion + } +} diff --git a/src/MDriveSync.Client.WinFormAPI/MainForm.cs b/src/MDriveSync.Client.WinFormAPI/MainForm.cs new file mode 100644 index 0000000..f1162c7 --- /dev/null +++ b/src/MDriveSync.Client.WinFormAPI/MainForm.cs @@ -0,0 +1,124 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Web.WebView2.WinForms; +using System.Reflection; + +namespace MDriveSync.Client.WinFormAPI +{ + public partial class MainForm : Form + { + private WebView2 webView; + private NotifyIcon notifyIcon; + private string apiUrl; + private ContextMenuStrip contextMenuStrip; + private ToolStripMenuItem exitMenuItem; + + public MainForm() + { + InitializeComponent(); + + // ļжȡ URLûʹĬֵ + var configuration = Program.Configuration; + apiUrl = configuration.GetValue("urls")?.Replace("*", "localhost") ?? "http://localhost:8080"; + + // Initialize WebView2 + webView = new WebView2 + { + Dock = DockStyle.Fill, + Source = new Uri($"{apiUrl}") // ָ Web API URL + }; + this.Controls.Add(webView); + + // Initialize NotifyIcon + notifyIcon = new NotifyIcon + { + Icon = this.Icon, + Text = "MDrive", + Visible = true + }; + + // ʹԴе PNG ͼ + this.Icon = LoadIconFromResource("MDriveSync.Client.WinFormAPI.Resources.logo.png", 64, 64); + + notifyIcon.DoubleClick += NotifyIcon_DoubleClick; + + // Initialize ContextMenuStrip + contextMenuStrip = new ContextMenuStrip(); + exitMenuItem = new ToolStripMenuItem("˳", null, ExitMenuItem_Click); + contextMenuStrip.Items.Add(exitMenuItem); + notifyIcon.ContextMenuStrip = contextMenuStrip; + + this.Resize += MainForm_Resize; + } + + private void MainForm_Resize(object sender, EventArgs e) + { + if (this.WindowState == FormWindowState.Minimized) + { + this.Hide(); + notifyIcon.Visible = true; + } + } + + private void NotifyIcon_DoubleClick(object sender, EventArgs e) + { + this.Show(); + this.WindowState = FormWindowState.Normal; + notifyIcon.Visible = false; + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + if (e.CloseReason == CloseReason.UserClosing) + { + e.Cancel = true; // ȡرղ + this.WindowState = FormWindowState.Minimized; // С + this.Hide(); // ش + notifyIcon.Visible = true; // ʾ NotifyIcon + } + } + + private void ExitMenuItem_Click(object sender, EventArgs e) + { + notifyIcon.Visible = false; + notifyIcon.Dispose(); // ȷͼ걻ͷ + Application.Exit(); + } + + private Icon LoadIconFromResource(string resourceName, int width, int height) + { + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) + { + if (stream != null) + { + using (var bitmap = new Bitmap(stream)) + { + var resizedBitmap = new Bitmap(bitmap, new Size(width, height)); + return Icon.FromHandle(resizedBitmap.GetHicon()); + } + } + else + { + throw new ArgumentException("Resource not found: " + resourceName); + } + } + } + + private Icon LoadIconFromResource(string resourceName) + { + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) + { + if (stream != null) + { + using (var bitmap = new Bitmap(stream)) + { + return Icon.FromHandle(bitmap.GetHicon()); + } + } + else + { + throw new ArgumentException("Resource not found: " + resourceName); + } + } + } + } +} diff --git a/src/MDriveSync.Client.WinFormAPI/MainForm.resx b/src/MDriveSync.Client.WinFormAPI/MainForm.resx new file mode 100644 index 0000000..119e16a --- /dev/null +++ b/src/MDriveSync.Client.WinFormAPI/MainForm.resx @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAA + AAD///8f////df///5L///+R////kf///5H///+S////dv///xz///8A////AAAAAAAAAAAAAAAAAAAA + AAAAAAAA/fz7tt7Pv/3Ls5r/zLSa/8y0mv/MtJr/y7Oa/97Pvv39/Py6////fv///3////97////Qf// + /wP///8AAAAAAPPt5/unfVL/lWIu/5ZkMP+WZDD/lmQw/5ViLv+mfFH/8uvh//Hevv/u2LL/8Ny5//r0 + 6un///9B////AAAAAADx6uP/pHlN/5ZlMf+XZjP/l2Yz/5ZlMv+VZDD/o3hL/+7j0//hunb/3K9g/9yv + Yf/v27j/////e////wAAAAAA8erj/6R5Tf+WZTH/l2Yz/5dmM/+fckP/pHlM/7CKZP/x59n/5sWL/+K7 + eP/iu3f/8d69/////4////8Q////AfHq4/+jeEv/lWQw/5ZlMf+7mnn/5NjM/+LXy//j2c7/7OXf/+vh + 1P/q39H/6t/R/+zk2//t6OLw9fLvzf///2P49fHqvJx7/6N4Sv+rhFr/5drP/6aIbf+FXTj/hl46/4Zd + Of+GXjn/hl45/4ZeOf+GXTn/hVw3/6aIbP/28vDq////Y/j08c7x6uPh9O7p5O3m4PuFXDf/c0QZ/3RG + G/90Rhv/dEYb/3RGG/90Rhv/dEYb/3NEGf+FXTj/7Obg/////wH///8U////Hf///zrs5uHkhl46/3RG + G/91Rx3/dUcd/3VHHf91Rx3/dUcd/3VHHf90Rhv/h146/+zm4P8AAAAAAAAAAP///wD///8f7Obg4Ide + Ov90Rhv/dUcd/3VHHf91Rx3/dUcd/3VHHf91Rx3/dEYb/4deOv/s5uD/AAAAAAAAAAD///8A////H+zm + 4OCHXjr/dEYb/3VHHf91Rx3/dUcd/3VHHf91Rx3/dUcd/3RGG/+HXjr/7Obg/wAAAAAAAAAA////AP// + /x/s5uDgh146/3RGG/91Rx3/dUcd/3VHHf91Rx3/dUcd/3VHHf90Rhv/h146/+zm4P8AAAAAAAAAAP// + /wD///8f7Obg4IVdOP9zRBn/dEYb/3RGG/90Rhv/dEYb/3RGG/90Rhv/c0QZ/4VdOP/s5uD/AAAAAAAA + AAD///8A////FPby8M6miGz/hV04/4deOv+HXjr/h146/4deOv+HXjr/h146/4VdOP+miGz/9vLw6gAA + AAAAAAAA////AP38+wD///9W9fLvzuzm4N/s5uDf7Obg3+zm4N/s5uDf7Obg3+zm4N/s5uDf9fLvzP// + /2IAAAAAAAAAAAAAAAD///8A////Af///xb///8j////I////yP///8j////I////yP///8j////I/// + /xb///8CAH8AAAADAAAAAwAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAOAAAADgAAAA4AAAAOAA + AADwAAAA8AAAAA== + + + \ No newline at end of file diff --git a/src/MDriveSync.Client.WinFormAPI/Program.cs b/src/MDriveSync.Client.WinFormAPI/Program.cs new file mode 100644 index 0000000..e907a04 --- /dev/null +++ b/src/MDriveSync.Client.WinFormAPI/Program.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Debugging; + +namespace MDriveSync.Client.WinFormAPI +{ + internal static class Program + { + public static IConfiguration Configuration { get; private set; } + + [STAThread] + private static void Main() + { + var host = CreateHostBuilder().Build(); + Configuration = host.Services.GetRequiredService(); + + Task.Run(() => host.Run()); + + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm()); + } + + public static IHostBuilder CreateHostBuilder() => + Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, config) => + { + var env = context.HostingEnvironment; + + // appsettings.json ļ + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + + // ӻ + config.AddEnvironmentVariables(); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .UseSerilog((context, services, configuration) => + { + configuration.ReadFrom.Configuration(context.Configuration); + + if (context.HostingEnvironment.IsDevelopment()) + { + configuration.MinimumLevel.Debug() + .Enrich.FromLogContext(); + + SelfLog.Enable(Console.Error); + } + }); + } +} \ No newline at end of file diff --git a/src/MDriveSync.Client.WinFormAPI/Resources/logo.png b/src/MDriveSync.Client.WinFormAPI/Resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9af25289c8683fcdef333844a11cbfbee1709b0c GIT binary patch literal 3364 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 p^*=myvd@H3WN(qJrP+3J!}Wdz5!GNkO#b} literal 0 HcmV?d00001 diff --git a/src/MDriveSync.Client.WinFormAPI/appsettings.json b/src/MDriveSync.Client.WinFormAPI/appsettings.json new file mode 100644 index 0000000..81cf365 --- /dev/null +++ b/src/MDriveSync.Client.WinFormAPI/appsettings.json @@ -0,0 +1,44 @@ +{ + "ReadOnly": null, + "BasicAuth": { + "User": "", + "Password": "" + }, + "Client": { + "AliyunDrives": [ + ] + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Default": "Warning", + "System": "Warning", + "Microsoft": "Warning" + } + }, + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "logs/log.txt", + "rollingInterval": "Day", + "fileSizeLimitBytes": null, + "rollOnFileSizeLimit": false, + "retainedFileCountLimit": 31 + } + }, + { + "Name": "Console" + } + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "urls": "http://*:8080" +} diff --git a/src/MDriveSync.Client.WinFormAPI/favicon.ico b/src/MDriveSync.Client.WinFormAPI/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c088b0100ab5625c8ae89516a408b6e7a94eb1f3 GIT binary patch literal 1150 zcmdUuKTK0m6vl7G&7~oP#3<2(g^3^zDx1-0f-uOyK)}d96QR}o#;V7TTG zUU-Sf@-%j0`r2L%Q*Y=#jJH?9)_e*hH%^<_noBXvT2I@qAnf}LZKf5=&##kuY%IN@ z+&ssD`X_fTux15b^f$p+T}0r+0KBtp?8Zh8D>Y@;a{-B`*j|^ zMF)Z_Zmerxpu0br1pP}0efLH-qZ=QoZ`|HA^tWRjtWl0<-8ESG=EOwbZS>TgMensT z^wyo@ng;Ej+B3|#g{^2rH*3TU2hX9!!fXCez!R7F-HS(Y@6xfXNKEPn10Kp@A$g>L zlm!EtU-lJk%jJTsNbC`FM4!40)S7pRK99uyXT9|1U;CHw{dt^;LuMm2qz~DUSZaRh zdj6NBht<3r4))RgCzxYrZbM2h>)s4xp82HkFG$WI)kR@W8d50Z@zBmBLZ9#xj+%@o literal 0 HcmV?d00001 From e2b7374887f3ebe8f6824cbfeab84bed00b1bf8b Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 12:30:15 +0800 Subject: [PATCH 36/90] feat: winform webapi --- MDriveSync.sln | 6 + src/MDriveSync.Client.API/Program.cs | 546 ++++++++++++++++----------- src/MDriveSync.Client.API/Startup.cs | 126 +++++++ 3 files changed, 459 insertions(+), 219 deletions(-) create mode 100644 src/MDriveSync.Client.API/Startup.cs diff --git a/MDriveSync.sln b/MDriveSync.sln index 4e92ae4..9e0047b 100644 --- a/MDriveSync.sln +++ b/MDriveSync.sln @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Infrastructure", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3CE27DBE-FA45-41B6-A170-A721CD66562B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.WinFormAPI", "src\MDriveSync.Client.WinFormAPI\MDriveSync.Client.WinFormAPI.csproj", "{585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,6 +63,10 @@ Global {4D5DF593-BC91-4803-99AA-7872DFA98987}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D5DF593-BC91-4803-99AA-7872DFA98987}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D5DF593-BC91-4803-99AA-7872DFA98987}.Release|Any CPU.Build.0 = Release|Any CPU + {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MDriveSync.Client.API/Program.cs b/src/MDriveSync.Client.API/Program.cs index dcda7fd..27c7521 100644 --- a/src/MDriveSync.Client.API/Program.cs +++ b/src/MDriveSync.Client.API/Program.cs @@ -1,15 +1,8 @@ -using MDriveSync.Core; -using MDriveSync.Core.BaseAuth; -using MDriveSync.Core.Dashboard; -using MDriveSync.Core.Filters; -using MDriveSync.Core.Middlewares; using MDriveSync.Infrastructure; -using Microsoft.AspNetCore.Mvc; using Quartz.Logging; using Serilog; using Serilog.Debugging; using System.Diagnostics; -using System.Runtime.InteropServices; namespace MDriveSync.Client.API { @@ -17,44 +10,18 @@ public class Program { public static void Main(string[] args) { - var builder = WebApplication.CreateBuilder(args); - - var env = builder.Environment; - - //// ļ - //var configuration = builder.Configuration - // .AddJsonFile($"appsettings.json", optional: true, reloadOnChange: true) - - // // е - // .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - - // //// Ĭϵ - // //.AddJsonFile($"{ClientSettings.ClientSettingsPath}", optional: true, reloadOnChange: true); - - //// Զļ - //builder.Configuration.AddJsonFile($"{ClientSettings.ClientSettingsPath}", optional: true, reloadOnChange: true); - - //// Ϊ exe ʱʹôʽ Serilogȫļ - //// ܵ Serilog ڳԶֺͼչ򼯣 Sinksʱ - //// ʽ Serilogڳ Main Уʹôʽ Serilogȫļ - //// Ҫôֶг Sinks ַͨʽ - //var logOptions = new ConfigurationReaderOptions( - // typeof(ConsoleLoggerConfigurationExtensions).Assembly); + var builder = CreateHostBuilder(args).Build(); + var env = builder.Services.GetService(); // Serilog var logger = new LoggerConfiguration() - .ReadFrom.Configuration(builder.Configuration); - - //// Ϊ exe ļ޷д־д - //.WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day); + .ReadFrom.Configuration(builder.Services.GetService()); if (env.IsDevelopment()) { logger.MinimumLevel.Debug() .Enrich.FromLogContext(); - //.WriteTo.Console(); - // ʹ Serilog.Debugging.SelfLog.Enable(Console.Error) Serilog ϣ⽫⡣ SelfLog.Enable(Console.Error); } @@ -72,194 +39,17 @@ public static void Main(string[] args) { Log.Information($"Current: {Directory.GetCurrentDirectory()}"); - // ʹ Serilog - builder.Host.UseSerilog(); - - // ҵͻ - builder.Services.Configure(builder.Configuration.GetSection("Client")); - - //// API ͼģ֤ 400 - //builder.Services.Configure(options => - //{ - // options.SuppressModelStateInvalidFilter = true; - //}); + //// ʹ Serilog + //builder.Host.UseSerilog(); - // API 쳣 - // API /ģ͹ - builder.Services.AddControllers(options => - { - options.Filters.Add(); - options.Filters.Add(); - }); - - // Զ API Ϊѡ - // api ͼģ֤ 400 Ҫ AddControllers ֮ - builder.Services.Configure(options => - { - // 1 ModelState Զ 400 ʹԶ巽֤ - // Ҫڿֶ ModelState.IsValidڿʹ [ApiController] ԣʹ ActionFilterAttribute - //options.SuppressModelStateInvalidFilter = true; - - // 2Զģ֤ - options.InvalidModelStateResponseFactory = (context) => - { - var error = context.ModelState.Values.FirstOrDefault()?.Errors?.FirstOrDefault()?.ErrorMessage ?? "쳣"; - Log.Logger.Warning("쳣 {@0} - {@1}", context.HttpContext?.Request?.GetUrl() ?? "", error); - return new JsonResult(Result.Fail(error)); - }; - }); - - // ̨ - //builder.Services.AddHostedService(); - - // ʹõģʽ - - // ̨̺ - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => provider.GetRequiredService()); - - // ش洢̨ - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => provider.GetRequiredService()); - - var app = builder.Build(); - - //// API ;̬/ͻ· - //app.Use(async (context, next) => - //{ - // if (context.Request.Path.StartsWithSegments("/api")) - // { - // // API 󣬼ִкм - // await next(); - // } - // //else - // //{ - // // app.MapFallbackToFile("index.html"); - - // // //// API 󣬳Ϊ̬ļ - // // //await next(); - - // // //// API д index.html - // // //if (!Path.HasExtension(context.Request.Path.Value)) - // // //{ - // // // context.Request.Path = "/index.html"; - // // // await next(); - // // //} - // //} - //}); - - app.UseStaticFiles(); - - app.UseCors(builder => - { - builder.AllowAnyMethod().AllowAnyHeader().SetIsOriginAllowed(origin => true).AllowCredentials(); - }); - - // ӻ֤ - // ӻȡã appsettings.json еΪ - var userFromConfig = builder.Configuration["BasicAuth:User"]; - var passwordFromConfig = builder.Configuration["BasicAuth:Password"]; - var user = string.IsNullOrEmpty(userFromConfig) ? - Environment.GetEnvironmentVariable("BASIC_AUTH_USER") : userFromConfig; - - var password = string.IsNullOrEmpty(passwordFromConfig) ? - Environment.GetEnvironmentVariable("BASIC_AUTH_PASSWORD") : passwordFromConfig; - - // ˺ź - if (!string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(password)) - { - var basicAuth = new BasicAuthAuthorizationUser() - { - Login = user, - PasswordClear = password - }; - var filter = new BasicAuthAuthorizationFilterOptions - { - RequireSsl = false, - SslRedirect = false, - LoginCaseSensitive = true, - Users = new[] { basicAuth } - }; - var options = new DashboardOptions - { - Authorization = new[] { new BasicAuthAuthorizationFilter(filter) } - }; - - // ȫ - app.UseMiddleware(options); - - //// - //app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder => - //{ - // appBuilder.UseMiddleware(options); - //}); - } - - // û򻷾лȡǷֻģʽ - var isReadOnlyMode = builder.Configuration.GetSection("ReadOnly").Get(); - if (isReadOnlyMode != true) - { - if (bool.TryParse(Environment.GetEnvironmentVariable("READ_ONLY"), out var ro) && ro) - { - isReadOnlyMode = ro; - } - } - - if (isReadOnlyMode == true) - { - app.UseMiddleware(isReadOnlyMode); - } - - // ʾģʽ - var isDemoMode = builder.Configuration.GetSection("Demo").Get(); - if (isDemoMode != true) - { - if (bool.TryParse(Environment.GetEnvironmentVariable("DEMO"), out var demo) && demo) - { - isDemoMode = demo; - } - } - GlobalConfiguration.IsDemoMode = isDemoMode; - - app.MapControllers(); - - //app.MapGet("/", () => - //{ - // return "ok"; - //}); - - //app.UseEndpoints(endpoints => - //{ - // //endpoints.MapControllers(); - - // // û· - // endpoints.MapFallbackToFile("index.html"); - //}); - - // û· - app.MapFallbackToFile("/", "index.html"); - - //app.Use(async (context, next) => - //{ - // await next(); - - // if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value)) - // { - // context.Request.Path = "/index.html"; // ضҳ - // await next(); - // } - //}); + var app = builder; // ڴ˴ // ļжȡ URL // 滻 * Ա - var url = builder.Configuration.GetSection("urls")?.Get()?.Replace("*", "localhost"); + var url = app.Services.GetService().GetSection("urls")?.Get()?.Replace("*", "localhost"); OpenBrowser(url); - // Windows Service ֧ - // install Microsoft.Extensions.Hosting.WindowsServices - //builder.Host.UseWindowsService(); - app.Run(); } catch (Exception ex) @@ -272,6 +62,14 @@ public static void Main(string[] args) } } + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .UseSerilog(); + /// /// Ĭķ /// @@ -303,9 +101,319 @@ private static void OpenBrowser(string url) { // ʱֵܳ쳣 // ¼־ - Log.Error(ex, "Ĭ쳣 {@0}", url); } } } -} \ No newline at end of file +} + +//using MDriveSync.Core; +//using MDriveSync.Core.BaseAuth; +//using MDriveSync.Core.Dashboard; +//using MDriveSync.Core.Filters; +//using MDriveSync.Core.Middlewares; +//using MDriveSync.Infrastructure; +//using Microsoft.AspNetCore.Mvc; +//using Quartz.Logging; +//using Serilog; +//using Serilog.Debugging; +//using System.Diagnostics; + +//namespace MDriveSync.Client.API +//{ +// public class Program +// { +// public static void Main(string[] args) +// { +// var builder = WebApplication.CreateBuilder(args); + +// var env = builder.Environment; + +// //// ļ +// //var configuration = builder.Configuration +// // .AddJsonFile($"appsettings.json", optional: true, reloadOnChange: true) + +// // // е +// // .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + +// // //// Ĭϵ +// // //.AddJsonFile($"{ClientSettings.ClientSettingsPath}", optional: true, reloadOnChange: true); + +// //// Զļ +// //builder.Configuration.AddJsonFile($"{ClientSettings.ClientSettingsPath}", optional: true, reloadOnChange: true); + +// //// Ϊ exe ʱʹôʽ Serilogȫļ +// //// ܵ Serilog ڳԶֺͼչ򼯣 Sinksʱ +// //// ʽ Serilogڳ Main Уʹôʽ Serilogȫļ +// //// Ҫôֶг Sinks ַͨʽ +// //var logOptions = new ConfigurationReaderOptions( +// // typeof(ConsoleLoggerConfigurationExtensions).Assembly); + +// // Serilog +// var logger = new LoggerConfiguration() +// .ReadFrom.Configuration(builder.Configuration); + +// //// Ϊ exe ļ޷д־д +// //.WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day); + +// if (env.IsDevelopment()) +// { +// logger.MinimumLevel.Debug() +// .Enrich.FromLogContext(); + +// //.WriteTo.Console(); + +// // ʹ Serilog.Debugging.SelfLog.Enable(Console.Error) Serilog ϣ⽫⡣ +// SelfLog.Enable(Console.Error); +// } + +// Log.Logger = logger.CreateLogger(); + +// // Quartz Log +// var loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); +// LogProvider.SetLogProvider(loggerFactory); + +// // ȷӦóʱرղˢ־ +// AppDomain.CurrentDomain.ProcessExit += (s, e) => Log.CloseAndFlush(); + +// try +// { +// Log.Information($"Current: {Directory.GetCurrentDirectory()}"); + +// // ʹ Serilog +// builder.Host.UseSerilog(); + +// // ҵͻ +// builder.Services.Configure(builder.Configuration.GetSection("Client")); + +// //// API ͼģ֤ 400 +// //builder.Services.Configure(options => +// //{ +// // options.SuppressModelStateInvalidFilter = true; +// //}); + +// // API 쳣 +// // API /ģ͹ +// builder.Services.AddControllers(options => +// { +// options.Filters.Add(); +// options.Filters.Add(); +// }); + +// // Զ API Ϊѡ +// // api ͼģ֤ 400 Ҫ AddControllers ֮ +// builder.Services.Configure(options => +// { +// // 1 ModelState Զ 400 ʹԶ巽֤ +// // Ҫڿֶ ModelState.IsValidڿʹ [ApiController] ԣʹ ActionFilterAttribute +// //options.SuppressModelStateInvalidFilter = true; + +// // 2Զģ֤ +// options.InvalidModelStateResponseFactory = (context) => +// { +// var error = context.ModelState.Values.FirstOrDefault()?.Errors?.FirstOrDefault()?.ErrorMessage ?? "쳣"; +// Log.Logger.Warning("쳣 {@0} - {@1}", context.HttpContext?.Request?.GetUrl() ?? "", error); +// return new JsonResult(Result.Fail(error)); +// }; +// }); + +// // ̨ +// //builder.Services.AddHostedService(); + +// // ʹõģʽ + +// // ̨̺ +// builder.Services.AddSingleton(); +// builder.Services.AddHostedService(provider => provider.GetRequiredService()); + +// // ش洢̨ +// builder.Services.AddSingleton(); +// builder.Services.AddHostedService(provider => provider.GetRequiredService()); + +// var app = builder.Build(); + +// //// API ;̬/ͻ· +// //app.Use(async (context, next) => +// //{ +// // if (context.Request.Path.StartsWithSegments("/api")) +// // { +// // // API 󣬼ִкм +// // await next(); +// // } +// // //else +// // //{ +// // // app.MapFallbackToFile("index.html"); + +// // // //// API 󣬳Ϊ̬ļ +// // // //await next(); + +// // // //// API д index.html +// // // //if (!Path.HasExtension(context.Request.Path.Value)) +// // // //{ +// // // // context.Request.Path = "/index.html"; +// // // // await next(); +// // // //} +// // //} +// //}); + +// app.UseStaticFiles(); + +// app.UseCors(builder => +// { +// builder.AllowAnyMethod().AllowAnyHeader().SetIsOriginAllowed(origin => true).AllowCredentials(); +// }); + +// // ӻ֤ +// // ӻȡã appsettings.json еΪ +// var userFromConfig = builder.Configuration["BasicAuth:User"]; +// var passwordFromConfig = builder.Configuration["BasicAuth:Password"]; +// var user = string.IsNullOrEmpty(userFromConfig) ? +// Environment.GetEnvironmentVariable("BASIC_AUTH_USER") : userFromConfig; + +// var password = string.IsNullOrEmpty(passwordFromConfig) ? +// Environment.GetEnvironmentVariable("BASIC_AUTH_PASSWORD") : passwordFromConfig; + +// // ˺ź +// if (!string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(password)) +// { +// var basicAuth = new BasicAuthAuthorizationUser() +// { +// Login = user, +// PasswordClear = password +// }; +// var filter = new BasicAuthAuthorizationFilterOptions +// { +// RequireSsl = false, +// SslRedirect = false, +// LoginCaseSensitive = true, +// Users = new[] { basicAuth } +// }; +// var options = new DashboardOptions +// { +// Authorization = new[] { new BasicAuthAuthorizationFilter(filter) } +// }; + +// // ȫ +// app.UseMiddleware(options); + +// //// +// //app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder => +// //{ +// // appBuilder.UseMiddleware(options); +// //}); +// } + +// // û򻷾лȡǷֻģʽ +// var isReadOnlyMode = builder.Configuration.GetSection("ReadOnly").Get(); +// if (isReadOnlyMode != true) +// { +// if (bool.TryParse(Environment.GetEnvironmentVariable("READ_ONLY"), out var ro) && ro) +// { +// isReadOnlyMode = ro; +// } +// } + +// if (isReadOnlyMode == true) +// { +// app.UseMiddleware(isReadOnlyMode); +// } + +// // ʾģʽ +// var isDemoMode = builder.Configuration.GetSection("Demo").Get(); +// if (isDemoMode != true) +// { +// if (bool.TryParse(Environment.GetEnvironmentVariable("DEMO"), out var demo) && demo) +// { +// isDemoMode = demo; +// } +// } +// GlobalConfiguration.IsDemoMode = isDemoMode; + +// app.MapControllers(); + +// //app.MapGet("/", () => +// //{ +// // return "ok"; +// //}); + +// //app.UseEndpoints(endpoints => +// //{ +// // //endpoints.MapControllers(); + +// // // û· +// // endpoints.MapFallbackToFile("index.html"); +// //}); + +// // û· +// app.MapFallbackToFile("/", "index.html"); + +// //app.Use(async (context, next) => +// //{ +// // await next(); + +// // if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value)) +// // { +// // context.Request.Path = "/index.html"; // ضҳ +// // await next(); +// // } +// //}); + +// // ڴ˴ +// // ļжȡ URL +// // 滻 * Ա +// var url = builder.Configuration.GetSection("urls")?.Get()?.Replace("*", "localhost"); +// OpenBrowser(url); + +// // Windows Service ֧ +// // install Microsoft.Extensions.Hosting.WindowsServices +// //builder.Host.UseWindowsService(); + +// app.Run(); +// } +// catch (Exception ex) +// { +// Log.Fatal(ex, "Ӧʧ"); +// } +// finally +// { +// Log.CloseAndFlush(); +// } +// } + +// /// +// /// Ĭķ +// /// +// /// +// private static void OpenBrowser(string url) +// { +// try +// { +// if (string.IsNullOrWhiteSpace(url)) +// { +// return; +// } + +// // ݲͬIJϵͳʹòͬ +// if (GlobalConfiguration.IsWindows()) +// { +// Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")); // Windows +// } +// else if (GlobalConfiguration.IsLinux()) +// { +// // Process.Start("xdg-open", url); // Linux +// } +// else if (GlobalConfiguration.IsMacOS()) +// { +// Process.Start("open", url); // MacOS +// } +// } +// catch (Exception ex) +// { +// // ʱֵܳ쳣 +// // ¼־ + +// Log.Error(ex, "Ĭ쳣 {@0}", url); +// } +// } +// } +//} \ No newline at end of file diff --git a/src/MDriveSync.Client.API/Startup.cs b/src/MDriveSync.Client.API/Startup.cs new file mode 100644 index 0000000..8cd0f16 --- /dev/null +++ b/src/MDriveSync.Client.API/Startup.cs @@ -0,0 +1,126 @@ +using MDriveSync.Core; +using MDriveSync.Core.BaseAuth; +using MDriveSync.Core.Dashboard; +using MDriveSync.Core.Filters; +using MDriveSync.Core.Middlewares; +using MDriveSync.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Serilog; + +namespace MDriveSync.Client.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.Configure(Configuration.GetSection("Client")); + + // API 异常过滤器 + // API 方法/模型过滤器 + services.AddControllers(options => + { + options.Filters.Add(); + options.Filters.Add(); + }); + + // 自定义配置 API 行为选项 + // 配置 api 视图模型验证 400 错误处理,需要在 AddControllers 之后配置 + services.Configure(options => + { + options.InvalidModelStateResponseFactory = (context) => + { + var error = context.ModelState.Values.FirstOrDefault()?.Errors?.FirstOrDefault()?.ErrorMessage ?? "参数异常"; + Log.Logger.Warning("参数异常 {@0} - {@1}", context.HttpContext?.Request?.GetUrl() ?? "", error); + return new JsonResult(Result.Fail(error)); + }; + }); + + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); + + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseStaticFiles(); + + app.UseCors(builder => + { + builder.AllowAnyMethod().AllowAnyHeader().SetIsOriginAllowed(origin => true).AllowCredentials(); + }); + + var userFromConfig = Configuration["BasicAuth:User"]; + var passwordFromConfig = Configuration["BasicAuth:Password"]; + var user = string.IsNullOrEmpty(userFromConfig) ? Environment.GetEnvironmentVariable("BASIC_AUTH_USER") : userFromConfig; + var password = string.IsNullOrEmpty(passwordFromConfig) ? Environment.GetEnvironmentVariable("BASIC_AUTH_PASSWORD") : passwordFromConfig; + + if (!string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(password)) + { + var basicAuth = new BasicAuthAuthorizationUser() + { + Login = user, + PasswordClear = password + }; + var filter = new BasicAuthAuthorizationFilterOptions + { + RequireSsl = false, + SslRedirect = false, + LoginCaseSensitive = true, + Users = new[] { basicAuth } + }; + var options = new DashboardOptions + { + Authorization = new[] { new BasicAuthAuthorizationFilter(filter) } + }; + + app.UseMiddleware(options); + } + + var isReadOnlyMode = Configuration.GetSection("ReadOnly").Get(); + if (isReadOnlyMode != true) + { + if (bool.TryParse(Environment.GetEnvironmentVariable("READ_ONLY"), out var ro) && ro) + { + isReadOnlyMode = ro; + } + } + + if (isReadOnlyMode == true) + { + app.UseMiddleware(isReadOnlyMode); + } + + var isDemoMode = Configuration.GetSection("Demo").Get(); + if (isDemoMode != true) + { + if (bool.TryParse(Environment.GetEnvironmentVariable("DEMO"), out var demo) && demo) + { + isDemoMode = demo; + } + } + GlobalConfiguration.IsDemoMode = isDemoMode; + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapFallbackToFile("index.html"); + }); + } + } +} \ No newline at end of file From fa6e7f6aa5ea50ffcb8dc56f01ab8c1cc3c07515 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 14:45:46 +0800 Subject: [PATCH 37/90] feat: win gui, pub xml --- MDriveSync.sln | 6 +++++ .../MDriveSync.Client.API.csproj | 1 + src/MDriveSync.Client.API/Program.cs | 1 + .../Controllers/AliyunStorageController.cs | 3 ++- .../Controllers/LocalStorageController.cs | 2 +- .../MDriveSync.Client.App.csproj | 12 +++++++++ .../Startup.cs | 8 ++++-- .../MDriveSync.Client.WinFormAPI.csproj | 25 ++++++++++++------- src/MDriveSync.Client.WinFormAPI/Program.cs | 2 +- ...t.Publish.SelfContained.win.gui.x64.pubxml | 18 +++++++++++++ ...t.Publish.SelfContained.win.gui.x86.pubxml | 18 +++++++++++++ 11 files changed, 82 insertions(+), 14 deletions(-) rename src/{MDriveSync.Client.API => MDriveSync.Client.App}/Controllers/AliyunStorageController.cs (99%) rename src/{MDriveSync.Client.API => MDriveSync.Client.App}/Controllers/LocalStorageController.cs (99%) create mode 100644 src/MDriveSync.Client.App/MDriveSync.Client.App.csproj rename src/{MDriveSync.Client.API => MDriveSync.Client.App}/Startup.cs (94%) create mode 100644 src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml create mode 100644 src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml diff --git a/MDriveSync.sln b/MDriveSync.sln index 9e0047b..be40cef 100644 --- a/MDriveSync.sln +++ b/MDriveSync.sln @@ -33,6 +33,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3CE27DBE EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.WinFormAPI", "src\MDriveSync.Client.WinFormAPI\MDriveSync.Client.WinFormAPI.csproj", "{585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDriveSync.Client.App", "src\MDriveSync.Client.App\MDriveSync.Client.App.csproj", "{F648E4A2-E525-426F-A12C-F9ADAF8E211D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +69,10 @@ Global {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Debug|Any CPU.Build.0 = Debug|Any CPU {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Release|Any CPU.ActiveCfg = Release|Any CPU {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Release|Any CPU.Build.0 = Release|Any CPU + {F648E4A2-E525-426F-A12C-F9ADAF8E211D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F648E4A2-E525-426F-A12C-F9ADAF8E211D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F648E4A2-E525-426F-A12C-F9ADAF8E211D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F648E4A2-E525-426F-A12C-F9ADAF8E211D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MDriveSync.Client.API/MDriveSync.Client.API.csproj b/src/MDriveSync.Client.API/MDriveSync.Client.API.csproj index 1498eba..49554d2 100644 --- a/src/MDriveSync.Client.API/MDriveSync.Client.API.csproj +++ b/src/MDriveSync.Client.API/MDriveSync.Client.API.csproj @@ -15,6 +15,7 @@ + diff --git a/src/MDriveSync.Client.API/Program.cs b/src/MDriveSync.Client.API/Program.cs index 27c7521..58f3f23 100644 --- a/src/MDriveSync.Client.API/Program.cs +++ b/src/MDriveSync.Client.API/Program.cs @@ -1,3 +1,4 @@ +using MDriveSync.Client.App; using MDriveSync.Infrastructure; using Quartz.Logging; using Serilog; diff --git a/src/MDriveSync.Client.API/Controllers/AliyunStorageController.cs b/src/MDriveSync.Client.App/Controllers/AliyunStorageController.cs similarity index 99% rename from src/MDriveSync.Client.API/Controllers/AliyunStorageController.cs rename to src/MDriveSync.Client.App/Controllers/AliyunStorageController.cs index 40997e8..5d1b9bc 100644 --- a/src/MDriveSync.Client.API/Controllers/AliyunStorageController.cs +++ b/src/MDriveSync.Client.App/Controllers/AliyunStorageController.cs @@ -6,12 +6,13 @@ using MDriveSync.Core.ViewModels; using MDriveSync.Infrastructure; using MDriveSync.Security; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using System.Text; -namespace MDriveSync.Client.API.Controllers +namespace MDriveSync.Client.App.Controllers { /// /// 阿里云盘存储控制器 diff --git a/src/MDriveSync.Client.API/Controllers/LocalStorageController.cs b/src/MDriveSync.Client.App/Controllers/LocalStorageController.cs similarity index 99% rename from src/MDriveSync.Client.API/Controllers/LocalStorageController.cs rename to src/MDriveSync.Client.App/Controllers/LocalStorageController.cs index b3983ee..2bd26e0 100644 --- a/src/MDriveSync.Client.API/Controllers/LocalStorageController.cs +++ b/src/MDriveSync.Client.App/Controllers/LocalStorageController.cs @@ -5,7 +5,7 @@ using MDriveSync.Infrastructure; using Microsoft.AspNetCore.Mvc; -namespace MDriveSync.Client.API.Controllers +namespace MDriveSync.Client.App.Controllers { /// /// 本地存储控制器 diff --git a/src/MDriveSync.Client.App/MDriveSync.Client.App.csproj b/src/MDriveSync.Client.App/MDriveSync.Client.App.csproj new file mode 100644 index 0000000..646a3ca --- /dev/null +++ b/src/MDriveSync.Client.App/MDriveSync.Client.App.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + enable + + + + + + + diff --git a/src/MDriveSync.Client.API/Startup.cs b/src/MDriveSync.Client.App/Startup.cs similarity index 94% rename from src/MDriveSync.Client.API/Startup.cs rename to src/MDriveSync.Client.App/Startup.cs index 8cd0f16..2273cff 100644 --- a/src/MDriveSync.Client.API/Startup.cs +++ b/src/MDriveSync.Client.App/Startup.cs @@ -4,10 +4,14 @@ using MDriveSync.Core.Filters; using MDriveSync.Core.Middlewares; using MDriveSync.Infrastructure; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Serilog; -namespace MDriveSync.Client.API +namespace MDriveSync.Client.App { public class Startup { @@ -49,7 +53,7 @@ public void ConfigureServices(IServiceCollection services) services.AddHostedService(provider => provider.GetRequiredService()); } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IHostEnvironment env) { if (env.IsDevelopment()) { diff --git a/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj b/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj index 14739b8..3a27eae 100644 --- a/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj +++ b/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj @@ -5,6 +5,9 @@ net8.0-windows true enable + zh + true + true @@ -12,15 +15,11 @@ - - - - - - Always - Always + + + true @@ -28,15 +27,23 @@ wwwroot\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest + PreserveNewest + + + true - + + + + + - + \ No newline at end of file diff --git a/src/MDriveSync.Client.WinFormAPI/Program.cs b/src/MDriveSync.Client.WinFormAPI/Program.cs index e907a04..fd29cb3 100644 --- a/src/MDriveSync.Client.WinFormAPI/Program.cs +++ b/src/MDriveSync.Client.WinFormAPI/Program.cs @@ -39,7 +39,7 @@ public static IHostBuilder CreateHostBuilder() => }) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); + webBuilder.UseStartup(); }) .UseSerilog((context, services, configuration) => { diff --git a/src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml b/src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml new file mode 100644 index 0000000..a8b90ce --- /dev/null +++ b/src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + bin\Release\net8.0-windows\publish\win-x64\ + FileSystem + <_TargetId>Folder + net8.0-windows + true + win-x64 + true + false + + diff --git a/src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml b/src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml new file mode 100644 index 0000000..dd610f4 --- /dev/null +++ b/src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + bin\Release\net8.0-windows\publish\win-x86\ + FileSystem + <_TargetId>Folder + net8.0-windows + true + win-x86 + true + false + + \ No newline at end of file From 8895b2f01276accae532b3d7eb7a45bc711cef19 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 14:51:06 +0800 Subject: [PATCH 38/90] feat: github action, by win gui --- .github/workflows/dotnet-win-gui-x64.yml | 73 ++++++++++++++++++++++++ .github/workflows/dotnet-win-gui-x86.yml | 73 ++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 .github/workflows/dotnet-win-gui-x64.yml create mode 100644 .github/workflows/dotnet-win-gui-x86.yml diff --git a/.github/workflows/dotnet-win-gui-x64.yml b/.github/workflows/dotnet-win-gui-x64.yml new file mode 100644 index 0000000..8199aed --- /dev/null +++ b/.github/workflows/dotnet-win-gui-x64.yml @@ -0,0 +1,73 @@ +name: Release Windows GUI x64 .NET Application + +on: + release: + types: [published] + +permissions: + contents: write # 确保有写入发布的权限 + +jobs: + build_and_release: + name: 构建并发布 .NET 应用程序 + runs-on: windows-latest + + steps: + - name: 检出仓库代码 + uses: actions/checkout@v3 # 使用最新稳定版本 + + - name: 设置 .NET 环境 + uses: actions/setup-dotnet@v3 # 使用最新稳定版本 + with: + dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 + + - name: 还原 NuGet 包 + run: dotnet restore src/MDriveSync.Client.WinFormAPI + + - name: 列出目录内容 (调试步骤) + run: | + echo "PublishProfiles 目录内容:" + dir src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles + + - name: 构建并发布 .NET 应用程序 + run: dotnet publish src/MDriveSync.Client.WinFormAPI -c Release /p:PublishProfile=Client.Publish.SelfContained.win.gui.x64.pubxml + + - name: 列出发布目录内容 (调试步骤) + run: | + echo "发布目录内容:" + dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x64/publish + + # - name: 删除 PDB 和部分 XML 文件 + # run: | + # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) + # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + + - name: 条件性删除 PDB 文件 + run: | + $tagName = "${{ github.event.release.tag_name }}" + if (-not ($tagName -match "beta|rc|alpha")) { + echo "Deleting .pdb files..." + Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + } else { + echo "Preserving .pdb files for pre-release versions..." + } + + - name: 压缩构建产物 + run: | + # 将发布目录中的文件压缩为 zip 文件 + Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x64/publish/* -DestinationPath "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" + + - name: 检查 ZIP 文件 + run: | + echo "生成的 ZIP 文件:" + dir MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip + + - name: 上传 ZIP 文件到 release + uses: actions/upload-release-asset@v1 # 使用最新稳定版本 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" + asset_content_type: application/zip diff --git a/.github/workflows/dotnet-win-gui-x86.yml b/.github/workflows/dotnet-win-gui-x86.yml new file mode 100644 index 0000000..be2f82b --- /dev/null +++ b/.github/workflows/dotnet-win-gui-x86.yml @@ -0,0 +1,73 @@ +name: Release Windows GUI x86 .NET Application + +on: + release: + types: [published] + +permissions: + contents: write # 确保有写入发布的权限 + +jobs: + build_and_release: + name: 构建并发布 .NET 应用程序 + runs-on: windows-latest + + steps: + - name: 检出仓库代码 + uses: actions/checkout@v3 # 使用最新稳定版本 + + - name: 设置 .NET 环境 + uses: actions/setup-dotnet@v3 # 使用最新稳定版本 + with: + dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 + + - name: 还原 NuGet 包 + run: dotnet restore src/MDriveSync.Client.WinFormAPI + + - name: 列出目录内容 (调试步骤) + run: | + echo "PublishProfiles 目录内容:" + dir src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles + + - name: 构建并发布 .NET 应用程序 + run: dotnet publish src/MDriveSync.Client.WinFormAPI -c Release /p:PublishProfile=Client.Publish.SelfContained.win.gui.x86.pubxml + + - name: 列出发布目录内容 (调试步骤) + run: | + echo "发布目录内容:" + dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x86/publish + + # - name: 删除 PDB 和部分 XML 文件 + # run: | + # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) + # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x86/publish/*.pdb -Force -ErrorAction SilentlyContinue + + - name: 条件性删除 PDB 文件 + run: | + $tagName = "${{ github.event.release.tag_name }}" + if (-not ($tagName -match "beta|rc|alpha")) { + echo "Deleting .pdb files..." + Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x86/publish/*.pdb -Force -ErrorAction SilentlyContinue + } else { + echo "Preserving .pdb files for pre-release versions..." + } + + - name: 压缩构建产物 + run: | + # 将发布目录中的文件压缩为 zip 文件 + Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x86/publish/* -DestinationPath "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" + + - name: 检查 ZIP 文件 + run: | + echo "生成的 ZIP 文件:" + dir MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip + + - name: 上传 ZIP 文件到 release + uses: actions/upload-release-asset@v1 # 使用最新稳定版本 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" + asset_content_type: application/zip From f191a5d9181587b6d9d0ba771d59d6b102fda14e Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 15:01:20 +0800 Subject: [PATCH 39/90] gui --- .github/workflows/dotnet-win-gui-x64.yml | 8 ++++---- .github/workflows/dotnet-win-gui-x86.yml | 13 +++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dotnet-win-gui-x64.yml b/.github/workflows/dotnet-win-gui-x64.yml index 8199aed..bda10e9 100644 --- a/.github/workflows/dotnet-win-gui-x64.yml +++ b/.github/workflows/dotnet-win-gui-x64.yml @@ -35,19 +35,19 @@ jobs: - name: 列出发布目录内容 (调试步骤) run: | echo "发布目录内容:" - dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x64/publish + dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x64/publish # - name: 删除 PDB 和部分 XML 文件 # run: | # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue - name: 条件性删除 PDB 文件 run: | $tagName = "${{ github.event.release.tag_name }}" if (-not ($tagName -match "beta|rc|alpha")) { echo "Deleting .pdb files..." - Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue } else { echo "Preserving .pdb files for pre-release versions..." } @@ -55,7 +55,7 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x64/publish/* -DestinationPath "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x64/publish/* -DestinationPath "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | diff --git a/.github/workflows/dotnet-win-gui-x86.yml b/.github/workflows/dotnet-win-gui-x86.yml index be2f82b..e084c03 100644 --- a/.github/workflows/dotnet-win-gui-x86.yml +++ b/.github/workflows/dotnet-win-gui-x86.yml @@ -35,19 +35,24 @@ jobs: - name: 列出发布目录内容 (调试步骤) run: | echo "发布目录内容:" - dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x86/publish + dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x86/publish + + - name: 列出发布目录内容 (调试步骤2) + run: | + echo "发布目录内容:" + dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86 # - name: 删除 PDB 和部分 XML 文件 # run: | # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x86/publish/*.pdb -Force -ErrorAction SilentlyContinue + # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x86/publish/*.pdb -Force -ErrorAction SilentlyContinue - name: 条件性删除 PDB 文件 run: | $tagName = "${{ github.event.release.tag_name }}" if (-not ($tagName -match "beta|rc|alpha")) { echo "Deleting .pdb files..." - Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x86/publish/*.pdb -Force -ErrorAction SilentlyContinue + Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x86/publish/*.pdb -Force -ErrorAction SilentlyContinue } else { echo "Preserving .pdb files for pre-release versions..." } @@ -55,7 +60,7 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0/win-x86/publish/* -DestinationPath "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x86/publish/* -DestinationPath "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | From 2dd09a5b61a284fc87d0a0b07276ad6533c5d432 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 15:04:42 +0800 Subject: [PATCH 40/90] fix: gui --- .github/workflows/dotnet-win-gui-x64.yml | 8 ++++---- .github/workflows/dotnet-win-gui-x86.yml | 11 +++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dotnet-win-gui-x64.yml b/.github/workflows/dotnet-win-gui-x64.yml index bda10e9..0892fb2 100644 --- a/.github/workflows/dotnet-win-gui-x64.yml +++ b/.github/workflows/dotnet-win-gui-x64.yml @@ -35,19 +35,19 @@ jobs: - name: 列出发布目录内容 (调试步骤) run: | echo "发布目录内容:" - dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x64/publish + dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64 # - name: 删除 PDB 和部分 XML 文件 # run: | # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64/*.pdb -Force -ErrorAction SilentlyContinue - name: 条件性删除 PDB 文件 run: | $tagName = "${{ github.event.release.tag_name }}" if (-not ($tagName -match "beta|rc|alpha")) { echo "Deleting .pdb files..." - Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x64/publish/*.pdb -Force -ErrorAction SilentlyContinue + Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64/*.pdb -Force -ErrorAction SilentlyContinue } else { echo "Preserving .pdb files for pre-release versions..." } @@ -55,7 +55,7 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x64/publish/* -DestinationPath "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64/* -DestinationPath "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | diff --git a/.github/workflows/dotnet-win-gui-x86.yml b/.github/workflows/dotnet-win-gui-x86.yml index e084c03..f19a266 100644 --- a/.github/workflows/dotnet-win-gui-x86.yml +++ b/.github/workflows/dotnet-win-gui-x86.yml @@ -33,11 +33,6 @@ jobs: run: dotnet publish src/MDriveSync.Client.WinFormAPI -c Release /p:PublishProfile=Client.Publish.SelfContained.win.gui.x86.pubxml - name: 列出发布目录内容 (调试步骤) - run: | - echo "发布目录内容:" - dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x86/publish - - - name: 列出发布目录内容 (调试步骤2) run: | echo "发布目录内容:" dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86 @@ -45,14 +40,14 @@ jobs: # - name: 删除 PDB 和部分 XML 文件 # run: | # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x86/publish/*.pdb -Force -ErrorAction SilentlyContinue + # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86/*.pdb -Force -ErrorAction SilentlyContinue - name: 条件性删除 PDB 文件 run: | $tagName = "${{ github.event.release.tag_name }}" if (-not ($tagName -match "beta|rc|alpha")) { echo "Deleting .pdb files..." - Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x86/publish/*.pdb -Force -ErrorAction SilentlyContinue + Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86/*.pdb -Force -ErrorAction SilentlyContinue } else { echo "Preserving .pdb files for pre-release versions..." } @@ -60,7 +55,7 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/win-x86/publish/* -DestinationPath "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86/* -DestinationPath "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | From 52efcbbb5e8a69228538ff393e6ae6af97ee8ae3 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 15:22:53 +0800 Subject: [PATCH 41/90] fix: builed chinese bug, transfrom to utf-8 --- .../MDriveSync.Client.WinFormAPI.csproj | 6 +++++ src/MDriveSync.Client.WinFormAPI/MainForm.cs | 22 +++++++++---------- src/MDriveSync.Client.WinFormAPI/Program.cs | 8 +++---- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj b/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj index 3a27eae..3e8f523 100644 --- a/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj +++ b/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj @@ -42,6 +42,12 @@ + + + true + + + diff --git a/src/MDriveSync.Client.WinFormAPI/MainForm.cs b/src/MDriveSync.Client.WinFormAPI/MainForm.cs index f1162c7..2b98464 100644 --- a/src/MDriveSync.Client.WinFormAPI/MainForm.cs +++ b/src/MDriveSync.Client.WinFormAPI/MainForm.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Web.WebView2.WinForms; using System.Reflection; @@ -16,7 +16,7 @@ public MainForm() { InitializeComponent(); - // ļжȡ URLûʹĬֵ + // 从配置文件中读取 URL,如果没有则使用默认值 var configuration = Program.Configuration; apiUrl = configuration.GetValue("urls")?.Replace("*", "localhost") ?? "http://localhost:8080"; @@ -24,7 +24,7 @@ public MainForm() webView = new WebView2 { Dock = DockStyle.Fill, - Source = new Uri($"{apiUrl}") // ָ Web API URL + Source = new Uri($"{apiUrl}") // 指向 Web API 的 URL }; this.Controls.Add(webView); @@ -36,14 +36,14 @@ public MainForm() Visible = true }; - // ʹԴе PNG ͼ + // 使用资源中的 PNG 图像 this.Icon = LoadIconFromResource("MDriveSync.Client.WinFormAPI.Resources.logo.png", 64, 64); notifyIcon.DoubleClick += NotifyIcon_DoubleClick; // Initialize ContextMenuStrip contextMenuStrip = new ContextMenuStrip(); - exitMenuItem = new ToolStripMenuItem("˳", null, ExitMenuItem_Click); + exitMenuItem = new ToolStripMenuItem("退出", null, ExitMenuItem_Click); contextMenuStrip.Items.Add(exitMenuItem); notifyIcon.ContextMenuStrip = contextMenuStrip; @@ -70,17 +70,17 @@ protected override void OnFormClosing(FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing) { - e.Cancel = true; // ȡرղ - this.WindowState = FormWindowState.Minimized; // С - this.Hide(); // ش - notifyIcon.Visible = true; // ʾ NotifyIcon + e.Cancel = true; // 取消关闭操作 + this.WindowState = FormWindowState.Minimized; // 最小化窗口 + this.Hide(); // 隐藏窗口 + notifyIcon.Visible = true; // 显示 NotifyIcon } } private void ExitMenuItem_Click(object sender, EventArgs e) { notifyIcon.Visible = false; - notifyIcon.Dispose(); // ȷͼ걻ͷ + notifyIcon.Dispose(); // 确保图标被释放 Application.Exit(); } @@ -121,4 +121,4 @@ private Icon LoadIconFromResource(string resourceName) } } } -} +} \ No newline at end of file diff --git a/src/MDriveSync.Client.WinFormAPI/Program.cs b/src/MDriveSync.Client.WinFormAPI/Program.cs index fd29cb3..a1833f8 100644 --- a/src/MDriveSync.Client.WinFormAPI/Program.cs +++ b/src/MDriveSync.Client.WinFormAPI/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -30,11 +30,11 @@ public static IHostBuilder CreateHostBuilder() => { var env = context.HostingEnvironment; - // appsettings.json ļ + // 添加 appsettings.json 配置文件 config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - // ӻ + // 添加环境变量 config.AddEnvironmentVariables(); }) .ConfigureWebHostDefaults(webBuilder => @@ -54,4 +54,4 @@ public static IHostBuilder CreateHostBuilder() => } }); } -} \ No newline at end of file +} From c1da9fa20b8bc4dc37049184359b98b84487750e Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 16:38:54 +0800 Subject: [PATCH 42/90] github action rename gui name --- .../{dotnet-win-gui-x64.yml => dotnet-win-x64-gui.yml} | 8 ++++---- .../{dotnet-win-gui-x86.yml => dotnet-win-x86-gui.yml} | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) rename .github/workflows/{dotnet-win-gui-x64.yml => dotnet-win-x64-gui.yml} (91%) rename .github/workflows/{dotnet-win-gui-x86.yml => dotnet-win-x86-gui.yml} (91%) diff --git a/.github/workflows/dotnet-win-gui-x64.yml b/.github/workflows/dotnet-win-x64-gui.yml similarity index 91% rename from .github/workflows/dotnet-win-gui-x64.yml rename to .github/workflows/dotnet-win-x64-gui.yml index 0892fb2..28b5451 100644 --- a/.github/workflows/dotnet-win-gui-x64.yml +++ b/.github/workflows/dotnet-win-x64-gui.yml @@ -55,12 +55,12 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64/* -DestinationPath "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64/* -DestinationPath "MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | echo "生成的 ZIP 文件:" - dir MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip + dir MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip - name: 上传 ZIP 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 @@ -68,6 +68,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" - asset_name: "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" + asset_path: "MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip" asset_content_type: application/zip diff --git a/.github/workflows/dotnet-win-gui-x86.yml b/.github/workflows/dotnet-win-x86-gui.yml similarity index 91% rename from .github/workflows/dotnet-win-gui-x86.yml rename to .github/workflows/dotnet-win-x86-gui.yml index f19a266..b526d83 100644 --- a/.github/workflows/dotnet-win-gui-x86.yml +++ b/.github/workflows/dotnet-win-x86-gui.yml @@ -55,12 +55,12 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86/* -DestinationPath "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86/* -DestinationPath "MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | echo "生成的 ZIP 文件:" - dir MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip + dir MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip - name: 上传 ZIP 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 @@ -68,6 +68,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" - asset_name: "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" + asset_path: "MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip" asset_content_type: application/zip From 2dbbafedc6a63f742aa31e52111a6fc1f40fb9aa Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 16:44:04 +0800 Subject: [PATCH 43/90] rename app.winform --- .github/workflows/dotnet-win-x64-gui.yml | 14 +++++++------- .github/workflows/dotnet-win-x86-gui.yml | 14 +++++++------- MDriveSync.sln | 12 ++++++------ .../MDriveSync.Client.App.WinForm.csproj} | 0 .../MainForm.Designer.cs | 2 +- .../MainForm.cs | 2 +- .../MainForm.resx | 0 .../Program.cs | 2 +- ...lient.Publish.SelfContained.win.gui.x64.pubxml | 0 ...lient.Publish.SelfContained.win.gui.x86.pubxml | 0 .../Resources/logo.png | Bin .../appsettings.json | 0 .../favicon.ico | Bin 13 files changed, 23 insertions(+), 23 deletions(-) rename src/{MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj => MDriveSync.Client.App.WinForm/MDriveSync.Client.App.WinForm.csproj} (100%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/MainForm.Designer.cs (97%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/MainForm.cs (99%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/MainForm.resx (100%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/Program.cs (98%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml (100%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml (100%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/Resources/logo.png (100%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/appsettings.json (100%) rename src/{MDriveSync.Client.WinFormAPI => MDriveSync.Client.App.WinForm}/favicon.ico (100%) diff --git a/.github/workflows/dotnet-win-x64-gui.yml b/.github/workflows/dotnet-win-x64-gui.yml index 28b5451..8aad3c7 100644 --- a/.github/workflows/dotnet-win-x64-gui.yml +++ b/.github/workflows/dotnet-win-x64-gui.yml @@ -22,32 +22,32 @@ jobs: dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 - name: 还原 NuGet 包 - run: dotnet restore src/MDriveSync.Client.WinFormAPI + run: dotnet restore src/MDriveSync.Client.App.WinForm - name: 列出目录内容 (调试步骤) run: | echo "PublishProfiles 目录内容:" - dir src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles + dir src/MDriveSync.Client.App.WinForm/Properties/PublishProfiles - name: 构建并发布 .NET 应用程序 - run: dotnet publish src/MDriveSync.Client.WinFormAPI -c Release /p:PublishProfile=Client.Publish.SelfContained.win.gui.x64.pubxml + run: dotnet publish src/MDriveSync.Client.App.WinForm -c Release /p:PublishProfile=Client.Publish.SelfContained.win.gui.x64.pubxml - name: 列出发布目录内容 (调试步骤) run: | echo "发布目录内容:" - dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64 + dir src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x64 # - name: 删除 PDB 和部分 XML 文件 # run: | # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64/*.pdb -Force -ErrorAction SilentlyContinue + # Remove-Item src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x64/*.pdb -Force -ErrorAction SilentlyContinue - name: 条件性删除 PDB 文件 run: | $tagName = "${{ github.event.release.tag_name }}" if (-not ($tagName -match "beta|rc|alpha")) { echo "Deleting .pdb files..." - Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64/*.pdb -Force -ErrorAction SilentlyContinue + Remove-Item src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x64/*.pdb -Force -ErrorAction SilentlyContinue } else { echo "Preserving .pdb files for pre-release versions..." } @@ -55,7 +55,7 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x64/* -DestinationPath "MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x64/* -DestinationPath "MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | diff --git a/.github/workflows/dotnet-win-x86-gui.yml b/.github/workflows/dotnet-win-x86-gui.yml index b526d83..f2fc588 100644 --- a/.github/workflows/dotnet-win-x86-gui.yml +++ b/.github/workflows/dotnet-win-x86-gui.yml @@ -22,32 +22,32 @@ jobs: dotnet-version: '8.0.x' # 确保使用的 .NET 版本与你的项目版本匹配 - name: 还原 NuGet 包 - run: dotnet restore src/MDriveSync.Client.WinFormAPI + run: dotnet restore src/MDriveSync.Client.App.WinForm - name: 列出目录内容 (调试步骤) run: | echo "PublishProfiles 目录内容:" - dir src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles + dir src/MDriveSync.Client.App.WinForm/Properties/PublishProfiles - name: 构建并发布 .NET 应用程序 - run: dotnet publish src/MDriveSync.Client.WinFormAPI -c Release /p:PublishProfile=Client.Publish.SelfContained.win.gui.x86.pubxml + run: dotnet publish src/MDriveSync.Client.App.WinForm -c Release /p:PublishProfile=Client.Publish.SelfContained.win.gui.x86.pubxml - name: 列出发布目录内容 (调试步骤) run: | echo "发布目录内容:" - dir src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86 + dir src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x86 # - name: 删除 PDB 和部分 XML 文件 # run: | # # 删除目录中的 .pdb 和部分 .xml 文件(如果存在) - # Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86/*.pdb -Force -ErrorAction SilentlyContinue + # Remove-Item src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x86/*.pdb -Force -ErrorAction SilentlyContinue - name: 条件性删除 PDB 文件 run: | $tagName = "${{ github.event.release.tag_name }}" if (-not ($tagName -match "beta|rc|alpha")) { echo "Deleting .pdb files..." - Remove-Item src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86/*.pdb -Force -ErrorAction SilentlyContinue + Remove-Item src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x86/*.pdb -Force -ErrorAction SilentlyContinue } else { echo "Preserving .pdb files for pre-release versions..." } @@ -55,7 +55,7 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.WinFormAPI/bin/Release/net8.0-windows/publish/win-x86/* -DestinationPath "MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x86/* -DestinationPath "MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | diff --git a/MDriveSync.sln b/MDriveSync.sln index be40cef..67874c6 100644 --- a/MDriveSync.sln +++ b/MDriveSync.sln @@ -31,9 +31,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Infrastructure", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3CE27DBE-FA45-41B6-A170-A721CD66562B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.WinFormAPI", "src\MDriveSync.Client.WinFormAPI\MDriveSync.Client.WinFormAPI.csproj", "{585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.App", "src\MDriveSync.Client.App\MDriveSync.Client.App.csproj", "{F648E4A2-E525-426F-A12C-F9ADAF8E211D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDriveSync.Client.App", "src\MDriveSync.Client.App\MDriveSync.Client.App.csproj", "{F648E4A2-E525-426F-A12C-F9ADAF8E211D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.App.WinForm", "src\MDriveSync.Client.App.WinForm\MDriveSync.Client.App.WinForm.csproj", "{664D3037-D417-43A4-B9A8-2909C5D9D874}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -65,14 +65,14 @@ Global {4D5DF593-BC91-4803-99AA-7872DFA98987}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D5DF593-BC91-4803-99AA-7872DFA98987}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D5DF593-BC91-4803-99AA-7872DFA98987}.Release|Any CPU.Build.0 = Release|Any CPU - {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {585E1ED7-B1D9-4EEE-AF65-32853A05FA9E}.Release|Any CPU.Build.0 = Release|Any CPU {F648E4A2-E525-426F-A12C-F9ADAF8E211D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F648E4A2-E525-426F-A12C-F9ADAF8E211D}.Debug|Any CPU.Build.0 = Debug|Any CPU {F648E4A2-E525-426F-A12C-F9ADAF8E211D}.Release|Any CPU.ActiveCfg = Release|Any CPU {F648E4A2-E525-426F-A12C-F9ADAF8E211D}.Release|Any CPU.Build.0 = Release|Any CPU + {664D3037-D417-43A4-B9A8-2909C5D9D874}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {664D3037-D417-43A4-B9A8-2909C5D9D874}.Debug|Any CPU.Build.0 = Debug|Any CPU + {664D3037-D417-43A4-B9A8-2909C5D9D874}.Release|Any CPU.ActiveCfg = Release|Any CPU + {664D3037-D417-43A4-B9A8-2909C5D9D874}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj b/src/MDriveSync.Client.App.WinForm/MDriveSync.Client.App.WinForm.csproj similarity index 100% rename from src/MDriveSync.Client.WinFormAPI/MDriveSync.Client.WinFormAPI.csproj rename to src/MDriveSync.Client.App.WinForm/MDriveSync.Client.App.WinForm.csproj diff --git a/src/MDriveSync.Client.WinFormAPI/MainForm.Designer.cs b/src/MDriveSync.Client.App.WinForm/MainForm.Designer.cs similarity index 97% rename from src/MDriveSync.Client.WinFormAPI/MainForm.Designer.cs rename to src/MDriveSync.Client.App.WinForm/MainForm.Designer.cs index 7cf2e73..bd91fad 100644 --- a/src/MDriveSync.Client.WinFormAPI/MainForm.Designer.cs +++ b/src/MDriveSync.Client.App.WinForm/MainForm.Designer.cs @@ -1,4 +1,4 @@ -namespace MDriveSync.Client.WinFormAPI +namespace MDriveSync.Client.App.WinForm { partial class MainForm { diff --git a/src/MDriveSync.Client.WinFormAPI/MainForm.cs b/src/MDriveSync.Client.App.WinForm/MainForm.cs similarity index 99% rename from src/MDriveSync.Client.WinFormAPI/MainForm.cs rename to src/MDriveSync.Client.App.WinForm/MainForm.cs index 2b98464..7a7df53 100644 --- a/src/MDriveSync.Client.WinFormAPI/MainForm.cs +++ b/src/MDriveSync.Client.App.WinForm/MainForm.cs @@ -2,7 +2,7 @@ using Microsoft.Web.WebView2.WinForms; using System.Reflection; -namespace MDriveSync.Client.WinFormAPI +namespace MDriveSync.Client.App.WinForm { public partial class MainForm : Form { diff --git a/src/MDriveSync.Client.WinFormAPI/MainForm.resx b/src/MDriveSync.Client.App.WinForm/MainForm.resx similarity index 100% rename from src/MDriveSync.Client.WinFormAPI/MainForm.resx rename to src/MDriveSync.Client.App.WinForm/MainForm.resx diff --git a/src/MDriveSync.Client.WinFormAPI/Program.cs b/src/MDriveSync.Client.App.WinForm/Program.cs similarity index 98% rename from src/MDriveSync.Client.WinFormAPI/Program.cs rename to src/MDriveSync.Client.App.WinForm/Program.cs index a1833f8..9187703 100644 --- a/src/MDriveSync.Client.WinFormAPI/Program.cs +++ b/src/MDriveSync.Client.App.WinForm/Program.cs @@ -5,7 +5,7 @@ using Serilog; using Serilog.Debugging; -namespace MDriveSync.Client.WinFormAPI +namespace MDriveSync.Client.App.WinForm { internal static class Program { diff --git a/src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml b/src/MDriveSync.Client.App.WinForm/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml similarity index 100% rename from src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml rename to src/MDriveSync.Client.App.WinForm/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x64.pubxml diff --git a/src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml b/src/MDriveSync.Client.App.WinForm/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml similarity index 100% rename from src/MDriveSync.Client.WinFormAPI/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml rename to src/MDriveSync.Client.App.WinForm/Properties/PublishProfiles/Client.Publish.SelfContained.win.gui.x86.pubxml diff --git a/src/MDriveSync.Client.WinFormAPI/Resources/logo.png b/src/MDriveSync.Client.App.WinForm/Resources/logo.png similarity index 100% rename from src/MDriveSync.Client.WinFormAPI/Resources/logo.png rename to src/MDriveSync.Client.App.WinForm/Resources/logo.png diff --git a/src/MDriveSync.Client.WinFormAPI/appsettings.json b/src/MDriveSync.Client.App.WinForm/appsettings.json similarity index 100% rename from src/MDriveSync.Client.WinFormAPI/appsettings.json rename to src/MDriveSync.Client.App.WinForm/appsettings.json diff --git a/src/MDriveSync.Client.WinFormAPI/favicon.ico b/src/MDriveSync.Client.App.WinForm/favicon.ico similarity index 100% rename from src/MDriveSync.Client.WinFormAPI/favicon.ico rename to src/MDriveSync.Client.App.WinForm/favicon.ico From 62e9a0ab7fab62fb42133b263084ad26abe544f6 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 16:46:13 +0800 Subject: [PATCH 44/90] rename --- .../{dotnet-win-x64-gui.yml => dotnet-win-gui-x64.yml} | 8 ++++---- .../{dotnet-win-x86-gui.yml => dotnet-win-gui-x86.yml} | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) rename .github/workflows/{dotnet-win-x64-gui.yml => dotnet-win-gui-x64.yml} (91%) rename .github/workflows/{dotnet-win-x86-gui.yml => dotnet-win-gui-x86.yml} (91%) diff --git a/.github/workflows/dotnet-win-x64-gui.yml b/.github/workflows/dotnet-win-gui-x64.yml similarity index 91% rename from .github/workflows/dotnet-win-x64-gui.yml rename to .github/workflows/dotnet-win-gui-x64.yml index 8aad3c7..d01c64a 100644 --- a/.github/workflows/dotnet-win-x64-gui.yml +++ b/.github/workflows/dotnet-win-gui-x64.yml @@ -55,12 +55,12 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x64/* -DestinationPath "MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x64/* -DestinationPath "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | echo "生成的 ZIP 文件:" - dir MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip + dir MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip - name: 上传 ZIP 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 @@ -68,6 +68,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: "MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip" - asset_name: "MDrive-win-x64-gui-${{ github.event.release.tag_name }}.zip" + asset_path: "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-win-gui-x64-${{ github.event.release.tag_name }}.zip" asset_content_type: application/zip diff --git a/.github/workflows/dotnet-win-x86-gui.yml b/.github/workflows/dotnet-win-gui-x86.yml similarity index 91% rename from .github/workflows/dotnet-win-x86-gui.yml rename to .github/workflows/dotnet-win-gui-x86.yml index f2fc588..9c894d8 100644 --- a/.github/workflows/dotnet-win-x86-gui.yml +++ b/.github/workflows/dotnet-win-gui-x86.yml @@ -55,12 +55,12 @@ jobs: - name: 压缩构建产物 run: | # 将发布目录中的文件压缩为 zip 文件 - Compress-Archive -Path src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x86/* -DestinationPath "MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip" + Compress-Archive -Path src/MDriveSync.Client.App.WinForm/bin/Release/net8.0-windows/publish/win-x86/* -DestinationPath "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" - name: 检查 ZIP 文件 run: | echo "生成的 ZIP 文件:" - dir MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip + dir MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip - name: 上传 ZIP 文件到 release uses: actions/upload-release-asset@v1 # 使用最新稳定版本 @@ -68,6 +68,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: "MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip" - asset_name: "MDrive-win-x86-gui-${{ github.event.release.tag_name }}.zip" + asset_path: "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" + asset_name: "MDrive-win-gui-x86-${{ github.event.release.tag_name }}.zip" asset_content_type: application/zip From 638e17bf60ea2b59e683d222b64b007ff204419f Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 17:05:08 +0800 Subject: [PATCH 45/90] load png.logo error --- src/MDriveSync.Client.App.WinForm/MainForm.cs | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/MDriveSync.Client.App.WinForm/MainForm.cs b/src/MDriveSync.Client.App.WinForm/MainForm.cs index 7a7df53..2c75790 100644 --- a/src/MDriveSync.Client.App.WinForm/MainForm.cs +++ b/src/MDriveSync.Client.App.WinForm/MainForm.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Web.WebView2.WinForms; +using Serilog; using System.Reflection; namespace MDriveSync.Client.App.WinForm @@ -16,38 +17,49 @@ public MainForm() { InitializeComponent(); - // 从配置文件中读取 URL,如果没有则使用默认值 - var configuration = Program.Configuration; - apiUrl = configuration.GetValue("urls")?.Replace("*", "localhost") ?? "http://localhost:8080"; - - // Initialize WebView2 - webView = new WebView2 + try { - Dock = DockStyle.Fill, - Source = new Uri($"{apiUrl}") // 指向 Web API 的 URL - }; - this.Controls.Add(webView); + // 从配置文件中读取 URL,如果没有则使用默认值 + var configuration = Program.Configuration; + apiUrl = configuration.GetValue("urls")?.Replace("*", "localhost") ?? "http://localhost:8080"; - // Initialize NotifyIcon - notifyIcon = new NotifyIcon - { - Icon = this.Icon, - Text = "MDrive", - Visible = true - }; + // Initialize WebView2 + webView = new WebView2 + { + Dock = DockStyle.Fill, + Source = new Uri($"{apiUrl}") // 指向 Web API 的 URL + }; + this.Controls.Add(webView); + + // Initialize NotifyIcon + notifyIcon = new NotifyIcon + { + Icon = this.Icon, + Text = "MDrive", + Visible = true + }; - // 使用资源中的 PNG 图像 - this.Icon = LoadIconFromResource("MDriveSync.Client.WinFormAPI.Resources.logo.png", 64, 64); + // 使用资源中的 PNG 图像 + // 获取当前程序集名称,而不是写死 + var assemblyName = Assembly.GetExecutingAssembly().GetName().Name; + this.Icon = LoadIconFromResource($"{assemblyName}.Resources.logo.png", 64, 64); - notifyIcon.DoubleClick += NotifyIcon_DoubleClick; + notifyIcon.DoubleClick += NotifyIcon_DoubleClick; - // Initialize ContextMenuStrip - contextMenuStrip = new ContextMenuStrip(); - exitMenuItem = new ToolStripMenuItem("退出", null, ExitMenuItem_Click); - contextMenuStrip.Items.Add(exitMenuItem); - notifyIcon.ContextMenuStrip = contextMenuStrip; + // Initialize ContextMenuStrip + contextMenuStrip = new ContextMenuStrip(); + exitMenuItem = new ToolStripMenuItem("退出", null, ExitMenuItem_Click); + contextMenuStrip.Items.Add(exitMenuItem); + notifyIcon.ContextMenuStrip = contextMenuStrip; - this.Resize += MainForm_Resize; + this.Resize += MainForm_Resize; + } + catch (Exception ex) + { + Log.Logger.Error(ex, "初始化失败,请重试"); + + MessageBox.Show("初始化失败,请重试"); + } } private void MainForm_Resize(object sender, EventArgs e) From 3cc0aca8a36d0a2f345b77818ba78a70d25363bc Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 26 Jun 2024 19:06:15 +0800 Subject: [PATCH 46/90] gui show icon --- src/MDriveSync.Client.App.WinForm/MainForm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MDriveSync.Client.App.WinForm/MainForm.cs b/src/MDriveSync.Client.App.WinForm/MainForm.cs index 2c75790..665cc0b 100644 --- a/src/MDriveSync.Client.App.WinForm/MainForm.cs +++ b/src/MDriveSync.Client.App.WinForm/MainForm.cs @@ -75,7 +75,7 @@ private void NotifyIcon_DoubleClick(object sender, EventArgs e) { this.Show(); this.WindowState = FormWindowState.Normal; - notifyIcon.Visible = false; + notifyIcon.Visible = true; // 始终显示 } protected override void OnFormClosing(FormClosingEventArgs e) From 70b240b13360d6c300b4418acdaefca5a3ae30ab Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 27 Jun 2024 12:26:12 +0800 Subject: [PATCH 47/90] fix: repair failed to load cloud disk file, no longer continue --- src/MDriveSync.Core/Services/AliyunJob.cs | 29 ++++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/MDriveSync.Core/Services/AliyunJob.cs b/src/MDriveSync.Core/Services/AliyunJob.cs index 704c63f..e1480bb 100644 --- a/src/MDriveSync.Core/Services/AliyunJob.cs +++ b/src/MDriveSync.Core/Services/AliyunJob.cs @@ -11,7 +11,6 @@ using System.Data; using System.Diagnostics; using System.Net; -using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -682,7 +681,7 @@ public async Task StartSyncJob(CancellationToken cancellationToken) } // 加载所有文件列表 - AliyunDriveSearchFiles(_driveId); + AliyunDriveSearchFiles(); // 加载备份文件夹下的所有文件夹 //await FetchAllFilesAsync(_driveId, saveParentFileId, 100); @@ -696,22 +695,24 @@ public async Task StartSyncJob(CancellationToken cancellationToken) sw.Stop(); _log.LogInformation($"同步作业完成,用时:{sw.ElapsedMilliseconds}ms"); + + // 开始校验 + sw.Restart(); + _log.LogInformation($"同步作业结束:{DateTime.Now:G}"); + ChangeState(JobState.Verifying); + + await AliyunDriveVerify(); + + sw.Stop(); + _log.LogInformation($"同步作业校验完成,用时:{sw.ElapsedMilliseconds}ms"); + } catch (Exception ex) { _log.LogError(ex, "同步作业完成执行异常"); + throw ex; } - // 开始校验 - sw.Restart(); - _log.LogInformation($"同步作业结束:{DateTime.Now:G}"); - ChangeState(JobState.Verifying); - - await AliyunDriveVerify(); - - sw.Stop(); - _log.LogInformation($"同步作业校验完成,用时:{sw.ElapsedMilliseconds}ms"); - swAll.Stop(); ProcessMessage = $"执行完成,总用时 {swAll.ElapsedMilliseconds / 1000} 秒"; @@ -875,7 +876,7 @@ public async Task StartRestore() _log.LogInformation("加载云盘存储文件列表..."); // 所有文件列表 - AliyunDriveSearchFiles(_driveId); + AliyunDriveSearchFiles(); //await FetchAllFilesAsync(_driveId, saveParentFileId, 100); @@ -2154,7 +2155,7 @@ await Parallel.ForEachAsync(addFileKeys, options, async (item, cancellationToken /// /// /// - private void AliyunDriveSearchFiles(string driveId, int limit = 100) + private void AliyunDriveSearchFiles(int limit = 100) { try { From c03b1d7810238c0bde54ab6d6cd727347dab1e63 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 27 Jun 2024 12:33:46 +0800 Subject: [PATCH 48/90] local job start --- .../Services/LocalStorageJob.cs | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/MDriveSync.Core/Services/LocalStorageJob.cs b/src/MDriveSync.Core/Services/LocalStorageJob.cs index 2fd56a0..4f79ace 100644 --- a/src/MDriveSync.Core/Services/LocalStorageJob.cs +++ b/src/MDriveSync.Core/Services/LocalStorageJob.cs @@ -478,7 +478,7 @@ public void StartSync() //await Task.Delay(20000, cancellationToken); // 实现文件作业逻辑 - await StartSyncJob(cancellationToken); + StartSyncJob(cancellationToken); //ChangeState(JobState.Idle); @@ -496,10 +496,16 @@ public void StartSync() ChangeState(JobState.Error); } + + await Task.CompletedTask; }); } - public async Task StartSyncJob(CancellationToken cancellationToken) + /// + /// 开始作业 + /// + /// + public void StartSyncJob(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -553,25 +559,27 @@ public async Task StartSyncJob(CancellationToken cancellationToken) sw.Stop(); _log.LogInformation($"同步作业完成,用时:{sw.ElapsedMilliseconds}ms"); + + // 开始校验 + sw.Restart(); + _log.LogInformation($"同步作业结束:{DateTime.Now:G}"); + + ChangeState(JobState.Verifying); + + SyncVerify(); + + sw.Stop(); + _log.LogInformation($"同步作业校验完成,用时:{sw.ElapsedMilliseconds}ms"); + } catch (Exception ex) { _log.LogError(ex, "同步作业完成执行异常"); - } - - // 开始校验 - sw.Restart(); - _log.LogInformation($"同步作业结束:{DateTime.Now:G}"); - - ChangeState(JobState.Verifying); - SyncVerify(); - - sw.Stop(); - _log.LogInformation($"同步作业校验完成,用时:{sw.ElapsedMilliseconds}ms"); + throw ex; + } swAll.Stop(); - ProcessMessage = $"执行完成,总用时 {swAll.ElapsedMilliseconds / 1000} 秒"; } From ed254d7e129e2141f60d383a55f0aa5b3f2b28d7 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Sun, 27 Apr 2025 14:55:23 +0800 Subject: [PATCH 49/90] readme --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9fc79fe..39af26b 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,16 @@ 支持 **AES256-GCM、ChaCha20-Poly1305** 加密,支持 **Zstd、LZ4、Snappy** 压缩,支持 **SHA256、BLAKE3** 等哈希算法,**任何第三方、服务商都无法查看数据,保护您的数据安全和隐私**。 +## 预览 + +> 账号:admin,密码:123456 + + + +![作业](/docs/screenshots/job.gif) +![挂载](/docs/screenshots/mount.png) +![macOS](/docs/screenshots/macOS.png) + ## 安装与使用 ### 快速启动 @@ -87,16 +97,6 @@ c. 启动方式1: sh run_app_osx.sh d. 启动方式2: chmod +x run_app_osx.sh && ./run_app_osx.sh ``` -### 在线预览 - -> 账号:admin,密码:123456 - - - -![作业](/docs/screenshots/job.gif) -![挂载](/docs/screenshots/mount.png) -![macOS](/docs/screenshots/macOS.png) - ### 压缩与加密 - [对称加密方法](https://soatok.blog/2020/07/12/comparison-of-symmetric-encryption-methods/) https://soatok.blog/2020/07/12/comparison-of-symmetric-encryption-methods/ From 19b3bccc712eeeb24bf0bede8e225546922ea3ca Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 8 May 2025 18:03:28 +0800 Subject: [PATCH 50/90] readme --- README.md | 6 +- src/MDriveSync.Security/HashHelper.cs | 180 +++++++++++++++--- .../MDriveSync.Security.csproj | 1 + 3 files changed, 161 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 39af26b..275ff93 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ 提供 Docker、Windows、Linux、macOS、Web 等多平台版本。 -支持 **AES256-GCM、ChaCha20-Poly1305** 加密,支持 **Zstd、LZ4、Snappy** 压缩,支持 **SHA256、BLAKE3** 等哈希算法,**任何第三方、服务商都无法查看数据,保护您的数据安全和隐私**。 +- 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305** +- 压缩算法:**Zstd(默认)、LZ4、Snappy** +- 哈希算法:**SHA256(默认)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3** + +**任何第三方、服务商都无法查看数据,保护您的数据安全和隐私。** ## 预览 diff --git a/src/MDriveSync.Security/HashHelper.cs b/src/MDriveSync.Security/HashHelper.cs index 1895556..1250ab7 100644 --- a/src/MDriveSync.Security/HashHelper.cs +++ b/src/MDriveSync.Security/HashHelper.cs @@ -1,10 +1,12 @@ -using Blake3; +using System.Buffers.Binary; +using System.IO.Hashing; using System.Security.Cryptography; +using Blake3; namespace MDriveSync.Security { /// - /// 哈希算法(MD5、SHA1、SHA256、BLAKE3) + /// 哈希算法(MD5、SHA1、SHA256、SHA3/SHA384、BLAKE3、XXH3、XXH128) /// 用于生成数据块或文件的哈希值,以验证数据的完整性和唯一性 /// 默认:SHA256 /// @@ -40,6 +42,20 @@ public static byte[] ComputeHash(byte[] data, string algorithm = "SHA256") { return md5.ComputeHash(data); } + case "SHA3": + case "SHA384": + using (SHA384 sha3 = SHA384.Create()) + { + return sha3.ComputeHash(data); + } + case "XXH3": + { + return XxHash3.Hash(data); + } + case "XXH128": + { + return XxHash128.Hash(data); + } default: throw new ArgumentException("Unsupported hash algorithm", nameof(algorithm)); } @@ -54,30 +70,96 @@ public static byte[] ComputeHash(byte[] data, string algorithm = "SHA256") /// public static byte[] ComputeHash(Stream stream, string algorithm = "SHA256") { - switch (algorithm.ToUpper()) + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + // 保存原始位置,以便在操作后恢复 + long originalPosition = stream.Position; + + try { - case "SHA256": - using (SHA256 sha256 = SHA256.Create()) - { - return sha256.ComputeHash(stream); - } - case "BLAKE3": - { - using var blake3Stream = new Blake3Stream(stream); - return blake3Stream.ComputeHash().AsSpan().ToArray(); - } - case "MD5": - using (MD5 md5 = MD5.Create()) - { - return md5.ComputeHash(stream); - } - case "SHA1": - using (SHA1 sha1 = SHA1.Create()) - { - return sha1.ComputeHash(stream); - } - default: - throw new ArgumentException("Unsupported hash algorithm", nameof(algorithm)); + switch (algorithm.ToUpper()) + { + case "SHA256": + using (SHA256 sha256 = SHA256.Create()) + { + return sha256.ComputeHash(stream); + } + case "BLAKE3": + { + using var blake3Stream = new Blake3Stream(stream); + return blake3Stream.ComputeHash().AsSpan().ToArray(); + } + case "MD5": + using (MD5 md5 = MD5.Create()) + { + return md5.ComputeHash(stream); + } + case "SHA1": + using (SHA1 sha1 = SHA1.Create()) + { + return sha1.ComputeHash(stream); + } + + case "SHA3": + case "SHA384": + using (SHA384 sha384 = SHA384.Create()) + { + return sha384.ComputeHash(stream); + } + case "XXH3": + { + var hasher = new XxHash3(); + byte[] buffer = new byte[81920]; // 80KB buffer for good performance + int bytesRead; + + // Reset stream position to beginning + stream.Position = originalPosition; + + while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) + { + hasher.Append(buffer.AsSpan(0, bytesRead)); + } + + // Get the hash as ulong + ulong hashValue = hasher.GetCurrentHashAsUInt64(); + + // Convert to byte array (8 bytes) + byte[] result = new byte[8]; + BinaryPrimitives.WriteUInt64BigEndian(result, hashValue); + return result; + } + case "XXH128": + { + var hasher = new XxHash128(); + byte[] buffer = new byte[81920]; // 80KB buffer for good performance + int bytesRead; + + // Reset stream position to beginning + stream.Position = originalPosition; + + while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) + { + hasher.Append(buffer.AsSpan(0, bytesRead)); + } + + // Get the hash as ulong + var hashValue = hasher.GetCurrentHashAsUInt128(); + + // Convert to byte array (16 bytes) + byte[] result = new byte[16]; + BinaryPrimitives.WriteUInt128BigEndian(result, hashValue); + return result; + } + + default: + throw new ArgumentException("Unsupported hash algorithm", nameof(algorithm)); + } + } + finally + { + // 恢复流的原始位置 + stream.Position = originalPosition; } } @@ -101,6 +183,8 @@ public static string ComputeHashHex(byte[] data, string algorithm = "SHA256") /// public static string ComputeHashHex(string filePath, string algorithm = "SHA256") { + algorithm = algorithm.ToUpper(); + if (algorithm == "SHA1") { using (SHA1 sha1 = SHA1.Create()) @@ -134,6 +218,17 @@ public static string ComputeHashHex(string filePath, string algorithm = "SHA256" } } } + else if (algorithm == "SHA3" || algorithm == "SHA384") + { + using (SHA384 sha3 = SHA384.Create()) + { + using (FileStream fileStream = File.OpenRead(filePath)) + { + var hashBytes = sha3.ComputeHash(fileStream); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); + } + } + } else if (algorithm == "BLAKE3") { // 当大文件时,要读取文件 byte[] 会导致内存溢出,应该使用流 @@ -144,6 +239,41 @@ public static string ComputeHashHex(string filePath, string algorithm = "SHA256" var hash = blake3Stream.ComputeHash().AsSpan().ToArray(); return BitConverter.ToString(hash).Replace("-", "").ToLower(); } + else if (algorithm == "XXH3") + { + using FileStream fileStream = File.OpenRead(filePath); + var hasher = new XxHash3(); + byte[] buffer = new byte[81920]; // 80KB buffer for good performance + int bytesRead; + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0) + { + hasher.Append(buffer.AsSpan(0, bytesRead)); + } + // Get the hash as ulong + ulong hashValue = hasher.GetCurrentHashAsUInt64(); + // Convert to byte array (8 bytes) + byte[] result = new byte[8]; + BinaryPrimitives.WriteUInt64BigEndian(result, hashValue); + return BitConverter.ToString(result).Replace("-", "").ToLower(); + + } + else if (algorithm == "XXH128") + { + using FileStream fileStream = File.OpenRead(filePath); + var hasher = new XxHash128(); + byte[] buffer = new byte[81920]; // 80KB buffer for good performance + int bytesRead; + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0) + { + hasher.Append(buffer.AsSpan(0, bytesRead)); + } + // Get the hash as ulong + var hashValue = hasher.GetCurrentHashAsUInt128(); + // Convert to byte array (16 bytes) + byte[] result = new byte[16]; + BinaryPrimitives.WriteUInt128BigEndian(result, hashValue); + return BitConverter.ToString(result).Replace("-", "").ToLower(); + } else { throw new ArgumentException("Unsupported hash algorithm", nameof(algorithm)); diff --git a/src/MDriveSync.Security/MDriveSync.Security.csproj b/src/MDriveSync.Security/MDriveSync.Security.csproj index 1a1d66f..8d5f3e2 100644 --- a/src/MDriveSync.Security/MDriveSync.Security.csproj +++ b/src/MDriveSync.Security/MDriveSync.Security.csproj @@ -12,6 +12,7 @@ + From dd58c434db399ec86a3e539b45e18f60cb619ae6 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 8 May 2025 18:04:25 +0800 Subject: [PATCH 51/90] readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 275ff93..d9969d6 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ 提供 Docker、Windows、Linux、macOS、Web 等多平台版本。 +> 任何第三方、服务商都无法查看数据,保护您的数据安全和隐私。 + - 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305** - 压缩算法:**Zstd(默认)、LZ4、Snappy** - 哈希算法:**SHA256(默认)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3** -**任何第三方、服务商都无法查看数据,保护您的数据安全和隐私。** ## 预览 From 06a238e3c8febbb69c0ad56aabde5c73d691248f Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 8 May 2025 18:25:19 +0800 Subject: [PATCH 52/90] readme --- README.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d9969d6..d155adb 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ > 任何第三方、服务商都无法查看数据,保护您的数据安全和隐私。 -- 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305** -- 压缩算法:**Zstd(默认)、LZ4、Snappy** -- 哈希算法:**SHA256(默认)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3** +- 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305。** +- 压缩算法:**Zstd(默认)、LZ4、Snappy。** +- 哈希算法:**SHA256(默认)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3。** ## 预览 @@ -116,6 +116,24 @@ d. 启动方式2: chmod +x run_app_osx.sh && ./run_app_osx.sh ![文件打包加密模式](/docs/screenshots/package-encrypt.png) +### 哈希校验 + +> 测试环境 + +- 操作系统: Windows 10.0.22621.0 +- 处理器: i7 8700K +- 测试数据大小: 100 MB +- 测试时间: 2025/5/8 18:19:50 + +| 算法名称 | 哈希宽度 (位) | 平均耗时 (ms) | 吞吐量 (GB/s) | +|----------|---------------|---------------|---------------| +| XXH3 | 64 | 6.80 | 15.66 | +| XXH128 | 128 | 6.30 | 15.50 | +| BLAKE3 | 256 | 19.90 | 4.91 | +| SHA-256 | 256 | 41.50 | 2.35 | +| SHA1 | 160 | 103.40 | 0.94 | +| SHA3 | 384 | 133.80 | 0.73 | + ### Docker 部署 近期 Docker Hub 国内无法使用,可以使用 https://docker.m.daocloud.io 加速,或使用阿里云镜像。 From 9ab9941dcb3b242440c7b5b55323ba31e86e157e Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 8 May 2025 18:26:38 +0800 Subject: [PATCH 53/90] README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d155adb..afb230a 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # MDrive -多平台、模块化、可挂载、安全、加密的云盘自动同步、备份工具,支持本地存储、阿里云盘等,支持镜像、备份等同步模式,完全免费开源。 +任何第三方、服务商都无法查看数据,保护您的数据安全和隐私。 -提供 Docker、Windows、Linux、macOS、Web 等多平台版本。 +多平台、模块化、可挂载、安全、加密的云盘自动同步、备份工具,支持本地存储、阿里云盘等,支持镜像、备份等同步模式,完全免费开源。 -> 任何第三方、服务商都无法查看数据,保护您的数据安全和隐私。 - 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305。** - 压缩算法:**Zstd(默认)、LZ4、Snappy。** - 哈希算法:**SHA256(默认)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3。** +> 提供 Docker、Windows、Linux、macOS、Web 等多平台版本。 ## 预览 From 2ed9bd6e00432fa6e69f24e27f7f7117e4300325 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 8 May 2025 18:31:37 +0800 Subject: [PATCH 54/90] readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index afb230a..3196927 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,12 @@ 多平台、模块化、可挂载、安全、加密的云盘自动同步、备份工具,支持本地存储、阿里云盘等,支持镜像、备份等同步模式,完全免费开源。 +提供 Docker、Windows、Linux、macOS、Web 等多平台版本。 - 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305。** - 压缩算法:**Zstd(默认)、LZ4、Snappy。** - 哈希算法:**SHA256(默认)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3。** -> 提供 Docker、Windows、Linux、macOS、Web 等多平台版本。 - ## 预览 > 账号:admin,密码:123456 From c15e0e26b4b99f565a60a63b42ff1e7d8425d6b1 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 9 May 2025 10:29:12 +0800 Subject: [PATCH 55/90] readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3196927..245cc3d 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,13 @@ 多平台、模块化、可挂载、安全、加密的云盘自动同步、备份工具,支持本地存储、阿里云盘等,支持镜像、备份等同步模式,完全免费开源。 -提供 Docker、Windows、Linux、macOS、Web 等多平台版本。 +提供 Docker、Windows、Linux、macOS、Web、Cli 等多平台版本。 - 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305。** - 压缩算法:**Zstd(默认)、LZ4、Snappy。** - 哈希算法:**SHA256(默认)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3。** +- 分块算法:**Buzhash+(默认)、FastCDC+、FastCDC** +- 分块策略:**Balanced(默认)、Dynamic、Fixed** ## 预览 From d822693060de38e76b0b72828592b81a472c2e12 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 9 May 2025 10:33:15 +0800 Subject: [PATCH 56/90] readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 245cc3d..9369a9c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ 多平台、模块化、可挂载、安全、加密的云盘自动同步、备份工具,支持本地存储、阿里云盘等,支持镜像、备份等同步模式,完全免费开源。 +支持数据同步、差异备份、增量备份、定时备份、版本历史、在线下载、文件管理等功能,支持多账号、多作业计划、多线程上传、下载、同步等功能,支持秒传、加密、压缩等功能。 + 提供 Docker、Windows、Linux、macOS、Web、Cli 等多平台版本。 - 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305。** From 615de3ea9aa09d6afd3eb632d680afabfe7f07c4 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 9 May 2025 10:33:34 +0800 Subject: [PATCH 57/90] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9369a9c..c8204c4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MDrive -任何第三方、服务商都无法查看数据,保护您的数据安全和隐私。 +任何第三方、服务商都无法查看备份数据,保护您的数据安全和隐私。 多平台、模块化、可挂载、安全、加密的云盘自动同步、备份工具,支持本地存储、阿里云盘等,支持镜像、备份等同步模式,完全免费开源。 From 91ba491ae89de8bf97c1fe216b7c0ca30af5e5d6 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 9 May 2025 10:34:13 +0800 Subject: [PATCH 58/90] readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8204c4..2fad69c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # MDrive -任何第三方、服务商都无法查看备份数据,保护您的数据安全和隐私。 +任何第三方、服务商都无法查阅备份数据,保护您的数据安全和隐私。 多平台、模块化、可挂载、安全、加密的云盘自动同步、备份工具,支持本地存储、阿里云盘等,支持镜像、备份等同步模式,完全免费开源。 -支持数据同步、差异备份、增量备份、定时备份、版本历史、在线下载、文件管理等功能,支持多账号、多作业计划、多线程上传、下载、同步等功能,支持秒传、加密、压缩等功能。 +支持数据同步、文件加密、文件名加密、差异备份、增量备份、定时备份、版本历史、在线下载、文件管理等功能,支持多账号、多作业计划、多线程上传、下载、同步等功能,支持秒传、加密、压缩等功能。 提供 Docker、Windows、Linux、macOS、Web、Cli 等多平台版本。 From bf9e174a0404712a91722f855a8cdc7b1b55b90a Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 9 May 2025 10:35:29 +0800 Subject: [PATCH 59/90] readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2fad69c..f708e02 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ 提供 Docker、Windows、Linux、macOS、Web、Cli 等多平台版本。 -- 加密算法:**AES256-GCM(默认)、ChaCha20-Poly1305。** -- 压缩算法:**Zstd(默认)、LZ4、Snappy。** -- 哈希算法:**SHA256(默认)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3。** -- 分块算法:**Buzhash+(默认)、FastCDC+、FastCDC** -- 分块策略:**Balanced(默认)、Dynamic、Fixed** +- 加密算法:**AES256-GCM(Default)、ChaCha20-Poly1305。** +- 压缩算法:**Zstd(Default)、LZ4、Snappy。** +- 哈希算法:**SHA256(Default)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3。** +- 分块算法:**Buzhash+(Default)、FastCDC+、FastCDC** +- 分块策略:**Balanced(Default)、Dynamic、Fixed** ## 预览 From 093e46595d0ec1090ebc3a290f002947a3e6a16d Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 9 May 2025 10:37:48 +0800 Subject: [PATCH 60/90] readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f708e02..8385fd7 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ - 加密算法:**AES256-GCM(Default)、ChaCha20-Poly1305。** - 压缩算法:**Zstd(Default)、LZ4、Snappy。** - 哈希算法:**SHA256(Default)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3。** -- 分块算法:**Buzhash+(Default)、FastCDC+、FastCDC** -- 分块策略:**Balanced(Default)、Dynamic、Fixed** +- 分块算法:**Buzhash+(Default)、FastCDC+、FastCDC。** +- 分块策略:**Balanced(Default)、Dynamic、Fixed。** ## 预览 From 6dfca3d34dc219122a029281691922647fbb8e3d Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 9 May 2025 11:21:52 +0800 Subject: [PATCH 61/90] ConfigureAwait(false) --- .../Services/AliyunDriveMounterByJob.cs | 6 ++++-- .../Services/AliyunDriveMounter_Dokan.cs | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/MDriveSync.Core/Services/AliyunDriveMounterByJob.cs b/src/MDriveSync.Core/Services/AliyunDriveMounterByJob.cs index 6283801..d3d29aa 100644 --- a/src/MDriveSync.Core/Services/AliyunDriveMounterByJob.cs +++ b/src/MDriveSync.Core/Services/AliyunDriveMounterByJob.cs @@ -192,7 +192,8 @@ public NtStatus ReadFile(string fileName, byte[] buffer, out int bytesRead, long }; // 使用 Range 请求下载文件的特定部分 int endOffset = (int)offset + buffer.Length - 1; - var content = DownloadFileSegment(url, (int)offset, endOffset).GetAwaiter().GetResult(); + var content = DownloadFileSegment(url, (int)offset, endOffset) + .ConfigureAwait(false).GetAwaiter().GetResult(); return content; }); isCached = true; @@ -229,7 +230,8 @@ public NtStatus ReadFile(string fileName, byte[] buffer, out int bytesRead, long } int endOffset = (int)offset + buffer.Length - 1; - partialContent = DownloadFileSegment(url, (int)offset, endOffset).GetAwaiter().GetResult(); + partialContent = DownloadFileSegment(url, (int)offset, endOffset) + .ConfigureAwait(false).GetAwaiter().GetResult(); } // 确保不会复制超出 buffer 大小的数据 diff --git a/src/MDriveSync.Core/Services/AliyunDriveMounter_Dokan.cs b/src/MDriveSync.Core/Services/AliyunDriveMounter_Dokan.cs index 6d6cc6d..094cfbc 100644 --- a/src/MDriveSync.Core/Services/AliyunDriveMounter_Dokan.cs +++ b/src/MDriveSync.Core/Services/AliyunDriveMounter_Dokan.cs @@ -423,7 +423,8 @@ public NtStatus ReadFile(string fileName, byte[] buffer, out int bytesRead, long // 使用 Range 请求下载文件的特定部分 //int endOffset = (int)offset + buffer.Length - 1; int endOffset = (int)Math.Min(offset + buffer.Length - 1, (int)f.Size - 1); - var content = DownloadFile(url, (int)offset, endOffset).GetAwaiter().GetResult(); + var content = DownloadFile(url, (int)offset, endOffset) + .ConfigureAwait(false).GetAwaiter().GetResult(); return content; }); isCached = true; @@ -461,7 +462,8 @@ public NtStatus ReadFile(string fileName, byte[] buffer, out int bytesRead, long //int endOffset = (int)offset + buffer.Length - 1; int endOffset = (int)Math.Min(offset + buffer.Length - 1, (int)f.Size - 1); - partialContent = DownloadFile(url, (int)offset, endOffset).GetAwaiter().GetResult(); + partialContent = DownloadFile(url, (int)offset, endOffset) + .ConfigureAwait(false).GetAwaiter().GetResult(); } //if (fileName.Contains("jpg")) @@ -740,7 +742,8 @@ public void CloseFile(string fileName, IDokanFileInfo info) if (item.PartNumber < ps.Count && item.CurrentSize >= _uploadPartSize) { // 非最后一个分块 - AliyunDrivePartUpload(item.LocalFilePath, item.UploadUrl).GetAwaiter().GetResult(); + AliyunDrivePartUpload(item.LocalFilePath, item.UploadUrl) + .ConfigureAwait(false).GetAwaiter().GetResult(); item.IsUploaded = true; } else if (item.PartNumber == ps.Count) @@ -756,7 +759,8 @@ public void CloseFile(string fileName, IDokanFileInfo info) { // 分块上传 // 最后一块上传 - AliyunDrivePartUpload(item.LocalFilePath, item.UploadUrl).GetAwaiter().GetResult(); + AliyunDrivePartUpload(item.LocalFilePath, item.UploadUrl) + .ConfigureAwait(false).GetAwaiter().GetResult(); item.IsUploaded = true; } } @@ -923,7 +927,8 @@ public NtStatus WriteFile(string fileName, byte[] buffer, out int bytesWritten, if (currentPart.CurrentSize >= _uploadPartSize && !currentPart.IsUploaded) { // 分块上传 - AliyunDrivePartUpload(currentPart.LocalFilePath, currentPart.UploadUrl).GetAwaiter().GetResult(); + AliyunDrivePartUpload(currentPart.LocalFilePath, currentPart.UploadUrl) + .ConfigureAwait(false).GetAwaiter().GetResult(); currentPart.IsUploaded = true; } @@ -940,7 +945,8 @@ public NtStatus WriteFile(string fileName, byte[] buffer, out int bytesWritten, { // 分块上传 // 最后一块上传 - AliyunDrivePartUpload(currentPart.LocalFilePath, currentPart.UploadUrl).GetAwaiter().GetResult(); + AliyunDrivePartUpload(currentPart.LocalFilePath, currentPart.UploadUrl) + .ConfigureAwait(false).GetAwaiter().GetResult(); currentPart.IsUploaded = true; } } From d9ee333684b5f7715d1e9e17f6d866ab6f4126b6 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 9 May 2025 14:19:25 +0800 Subject: [PATCH 62/90] tests --- MDriveSync.sln | 20 +- .../MDriveSync.Client.csproj} | 0 src/MDriveSync.Client/Program.cs | 33 + src/MDriveSync.Security.Test/Program.cs | 229 ------ src/MDriveSync.Security/CompressionHelper.cs | 8 + src/MDriveSync.Test/BaseTests.cs | 24 + .../CompressionPerformanceTests.cs | 684 ++++++++++++++++++ .../HashAlgorithmPerformanceTests.cs | 174 +++++ src/MDriveSync.Test/HashHelperTests.cs | 44 ++ src/MDriveSync.Test/HashPerformanceTests.cs | 532 ++++++++++++++ src/MDriveSync.Test/MDriveSync.Test.csproj | 27 + .../Properties/launchSettings.json | 10 + 12 files changed, 1549 insertions(+), 236 deletions(-) rename src/{MDriveSync.Security.Test/MDriveSync.Security.Test.csproj => MDriveSync.Client/MDriveSync.Client.csproj} (100%) create mode 100644 src/MDriveSync.Client/Program.cs delete mode 100644 src/MDriveSync.Security.Test/Program.cs create mode 100644 src/MDriveSync.Test/BaseTests.cs create mode 100644 src/MDriveSync.Test/CompressionPerformanceTests.cs create mode 100644 src/MDriveSync.Test/HashAlgorithmPerformanceTests.cs create mode 100644 src/MDriveSync.Test/HashHelperTests.cs create mode 100644 src/MDriveSync.Test/HashPerformanceTests.cs create mode 100644 src/MDriveSync.Test/MDriveSync.Test.csproj create mode 100644 src/MDriveSync.Test/Properties/launchSettings.json diff --git a/MDriveSync.sln b/MDriveSync.sln index 67874c6..dbdbf0a 100644 --- a/MDriveSync.sln +++ b/MDriveSync.sln @@ -25,8 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{EE45 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Security", "src\MDriveSync.Security\MDriveSync.Security.csproj", "{25258960-ED11-4C31-8119-E91D7BB33C7C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Security.Test", "src\MDriveSync.Security.Test\MDriveSync.Security.Test.csproj", "{B036CE46-D6BD-45B0-A967-7F866D06FD2B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Infrastructure", "src\MDriveSync.Infrastructure\MDriveSync.Infrastructure.csproj", "{4D5DF593-BC91-4803-99AA-7872DFA98987}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3CE27DBE-FA45-41B6-A170-A721CD66562B}" @@ -35,6 +33,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.App", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MDriveSync.Client.App.WinForm", "src\MDriveSync.Client.App.WinForm\MDriveSync.Client.App.WinForm.csproj", "{664D3037-D417-43A4-B9A8-2909C5D9D874}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDriveSync.Test", "src\MDriveSync.Test\MDriveSync.Test.csproj", "{CDA2FC3F-D3CC-4D25-93EA-926DE2CBAFBD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDriveSync.Client", "src\MDriveSync.Client\MDriveSync.Client.csproj", "{6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,10 +59,6 @@ Global {25258960-ED11-4C31-8119-E91D7BB33C7C}.Debug|Any CPU.Build.0 = Debug|Any CPU {25258960-ED11-4C31-8119-E91D7BB33C7C}.Release|Any CPU.ActiveCfg = Release|Any CPU {25258960-ED11-4C31-8119-E91D7BB33C7C}.Release|Any CPU.Build.0 = Release|Any CPU - {B036CE46-D6BD-45B0-A967-7F866D06FD2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B036CE46-D6BD-45B0-A967-7F866D06FD2B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B036CE46-D6BD-45B0-A967-7F866D06FD2B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B036CE46-D6BD-45B0-A967-7F866D06FD2B}.Release|Any CPU.Build.0 = Release|Any CPU {4D5DF593-BC91-4803-99AA-7872DFA98987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4D5DF593-BC91-4803-99AA-7872DFA98987}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D5DF593-BC91-4803-99AA-7872DFA98987}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -73,12 +71,20 @@ Global {664D3037-D417-43A4-B9A8-2909C5D9D874}.Debug|Any CPU.Build.0 = Debug|Any CPU {664D3037-D417-43A4-B9A8-2909C5D9D874}.Release|Any CPU.ActiveCfg = Release|Any CPU {664D3037-D417-43A4-B9A8-2909C5D9D874}.Release|Any CPU.Build.0 = Release|Any CPU + {CDA2FC3F-D3CC-4D25-93EA-926DE2CBAFBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDA2FC3F-D3CC-4D25-93EA-926DE2CBAFBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDA2FC3F-D3CC-4D25-93EA-926DE2CBAFBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDA2FC3F-D3CC-4D25-93EA-926DE2CBAFBD}.Release|Any CPU.Build.0 = Release|Any CPU + {6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {B036CE46-D6BD-45B0-A967-7F866D06FD2B} = {3CE27DBE-FA45-41B6-A170-A721CD66562B} + {CDA2FC3F-D3CC-4D25-93EA-926DE2CBAFBD} = {3CE27DBE-FA45-41B6-A170-A721CD66562B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A3F34F36-7CED-4A53-A203-D8F89661D42C} diff --git a/src/MDriveSync.Security.Test/MDriveSync.Security.Test.csproj b/src/MDriveSync.Client/MDriveSync.Client.csproj similarity index 100% rename from src/MDriveSync.Security.Test/MDriveSync.Security.Test.csproj rename to src/MDriveSync.Client/MDriveSync.Client.csproj diff --git a/src/MDriveSync.Client/Program.cs b/src/MDriveSync.Client/Program.cs new file mode 100644 index 0000000..5ede57e --- /dev/null +++ b/src/MDriveSync.Client/Program.cs @@ -0,0 +1,33 @@ +using MDriveSync.Security; +using System.Diagnostics; + +namespace MDriveSync.Client +{ + internal class Program + { + static void Main(string[] args) + { + + var sw = new Stopwatch(); + + //sw.Restart(); + //LocalStorage.RunRestore(); + //sw.Stop(); + + //Console.WriteLine($"还原用时:{sw.ElapsedMilliseconds}ms"); + //Console.WriteLine("Hello, World!"); + //Console.ReadKey(); + //return; + + sw.Restart(); + LocalStorage.RunBackup(); + sw.Stop(); + + Console.WriteLine($"备份用时:{sw.ElapsedMilliseconds}ms"); + Console.WriteLine("Hello, World!"); + Console.ReadKey(); + + Console.WriteLine("Hello, World!"); + } + } +} diff --git a/src/MDriveSync.Security.Test/Program.cs b/src/MDriveSync.Security.Test/Program.cs deleted file mode 100644 index f5a8d72..0000000 --- a/src/MDriveSync.Security.Test/Program.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System.Diagnostics; -using System.Security.Cryptography; -using System.Text; - -namespace MDriveSync.Security.Test -{ - internal class Program - { - private static void Main(string[] args) - { - //var txt = "hello"; - //var txtbytes = Encoding.UTF8.GetBytes(txt); - - //var t1 = EncryptionHelper.EncryptWithAES256GCM(txtbytes, "123"); - //var t2 = EncryptionHelper.EncryptWithChaCha20Poly1305(txtbytes, "123"); - - /* - // 文件流压缩解压测试 - string inputFilePath = "E:\\_test\\imgs\\5555.txt"; - string outputFilePath = "E:\\_test\\imgs\\5555.txt.enc"; - string outputFilePathJm = "E:\\_test\\imgs\\5555-2.txt"; - string compressionType = "Zstd"; - string encryptionType = "AES256-GCM"; - string encryptionKey = "your-encryption"; - - //using FileStream inputFileStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read); - //using FileStream outputFileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write); - - //CompressionHelper.CompressAndEncryptStream(inputFileStream, - // outputFileStream, - // compressionType, - // encryptionType, - // encryptionKey, "BLAKE3"); - - using FileStream jmFileStream = new FileStream(outputFilePath, FileMode.Open, FileAccess.Read); - using FileStream outputFileStreamJm = new FileStream(outputFilePathJm, FileMode.Create, FileAccess.ReadWrite); - CompressionHelper.DecompressStream(jmFileStream, - outputFileStreamJm, - compressionType, - encryptionType, - encryptionKey, "BLAKE3"); - - return; - */ - - var sw = new Stopwatch(); - - /* - // 文件加密解密测试 - - for (int i = 0; i < 120; i++) - { - - // 文件加密模式,文件名或单个路径不应该超过 120 个字符,否则会导致加密失败 - // 拆解为2个文件?xxx.1, pxxx ??? - - // 生成随机的最大长度文件名(255字符)和扩展名(例如:.txt) - // 最大200长度 - string randomFileName = GenerateRandomString(i + 1); // GenerateRandomString(i + 1) + ".xfdsdf"; // 保留5字符用于扩展名 - - // 生成随机的最大长度路径(例如Windows的4096字符或更长) - string randomPath = GenerateRandomPath(4096); // 路径最大长度,可以调整为更长以测试长路径支持 - //Directory.CreateDirectory(randomPath); - - var x1 = Encoding.UTF8.GetBytes(randomFileName); - - //var x2 = DeflateCompressor.Shared.Compress(x1); - - var x2 = CompressionHelper.Compress(x1, null, "AES256-GCM", "123"); - var bytes = CompressionHelper.Compress(x1, "LZ4", "AES256-GCM", "123"); - - var b2 = CompressionHelper.Compress(bytes, "LZ4"); - var b3 = CompressionHelper.Decompress(b2, "LZ4"); - var b4 = Convert.ToBase64String(b2); - - // bytes 转为转为最小长度的字符串 - // 转为 base64 字符串 - var base64 = bytes.ToSafeBase64(); - Directory.CreateDirectory(base64); - - var decodedData = base64.FromSafeBase64(); - - //var bbb = Base16384.ConvertFromUtf16BEBytesToString(bytes).ToString(); - //var resul333t = Base85.Ascii85.Encode(bytes); - - // 适合压缩小数据,其他算法更不合适 - // BrotliCompressor - // DeflateCompressor - - //var b5 = ZstdSharpCompressor.Shared.Compress(bytes); - //var b6 = SnappierCompressor.Shared.Compress(bytes); - //var b7 = LZ4Compressor.Shared.Compress(bytes); // 最小,但仍然比base64大+1~2 - //var b8 = LZMACompressor.Shared.Compress(bytes); - //var b9 = DeflateCompressor.Shared.Compress(bytes); - //var b10 = BrotliCompressor.Shared.Compress(bytes); - //var b10base64 = Convert.ToBase64String(b10); - - //var hex = BitConverter.ToString(bytes).Replace("-", ""); - - //var result = CompressionHelper.Decompress(decodedData, "LZ4", "ChaCha20-Poly1305", "12342342SDFSDFAS"); - //var str = Encoding.UTF8.GetString(result); - - //Console.WriteLine($"压缩后: {bytes.Length}, base64: {base64.Length}, cbase64: {b10base64.Length}"); - - // 控制台显示b5~b10 长度 - //Console.WriteLine($"byte: {bytes.Length}, b5: {b5.Length}, b6: {b6.Length}, b7: {b7.Length}, b8: {b8.Length}, b9: {b9.Length}, b10: {b10.Length}, base64: {base64.Length}, {str}"); - - Console.WriteLine($"i: {i + 1}, x1: {x1.Length}, {x2.Length}, byte: {bytes.Length}, base64: {base64.Length}"); - - Thread.Sleep(1); - } - - Console.WriteLine("Hello, World!"); - Console.ReadKey(); - return; - */ - - //sw.Restart(); - //LocalStorage.RunRestore(); - //sw.Stop(); - - //Console.WriteLine($"还原用时:{sw.ElapsedMilliseconds}ms"); - //Console.WriteLine("Hello, World!"); - //Console.ReadKey(); - //return; - - sw.Restart(); - LocalStorage.RunBackup(); - sw.Stop(); - - Console.WriteLine($"备份用时:{sw.ElapsedMilliseconds}ms"); - Console.WriteLine("Hello, World!"); - Console.ReadKey(); - - //Test2(); - - //Console.WriteLine("Hello, World!"); - //Console.ReadKey(); - } - - private static string GenerateRandomPath(int maxLength) - { - StringBuilder pathBuilder = new StringBuilder(); - pathBuilder.Append(Directory.GetCurrentDirectory()); // 从当前工作目录开始 - - Random random = new Random(); - try - { - while (pathBuilder.Length < maxLength - 260) // 确保留有空间添加文件名 - { - pathBuilder.Append(Path.DirectorySeparatorChar); - pathBuilder.Append(GenerateRandomString(10)); // 每个目录名最多10个字符 - } - } - catch (Exception) - { - Console.WriteLine("生成路径时出现异常"); - } - - return pathBuilder.ToString(); - } - - private static Random random = new Random(); - - private static string GenerateRandomString(int length) - { - StringBuilder stringBuilder = new StringBuilder(length); - for (int i = 0; i < length; i++) - { - char c = (char)random.Next('a', 'z' + 1); // 生成随机小写字母 - stringBuilder.Append(c); - } - return stringBuilder.ToString(); - } - - /// - /// 测试加密时间 - /// - private static void Test2() - { - int[] sizes = { 10 * 1024 * 1024, 20 * 1024 * 1024, 30 * 1024 * 1024, 100 * 1024 * 1024 }; - int iterations = 10; - - foreach (int size in sizes) - { - byte[] data = new byte[size]; - new Random().NextBytes(data); - - TimeSpan sha256Time = ComputeSha256(data, iterations); - TimeSpan sha1Time = ComputeSha1(data, iterations); - - Console.WriteLine($"Data size: {size / (1024 * 1024)} MB"); - Console.WriteLine($"SHA-256 Time: {sha256Time.TotalMilliseconds} ms"); - Console.WriteLine($"SHA-1 Time: {sha1Time.TotalMilliseconds} ms"); - Console.WriteLine(); - } - } - - public static TimeSpan ComputeSha1(byte[] data, int iterations) - { - Stopwatch stopwatch = new Stopwatch(); - using (var sha = SHA1.Create()) - { - stopwatch.Start(); - for (int i = 0; i < iterations; i++) - { - sha.ComputeHash(data); - } - stopwatch.Stop(); - } - return stopwatch.Elapsed; - } - - public static TimeSpan ComputeSha256(byte[] data, int iterations) - { - Stopwatch stopwatch = new Stopwatch(); - using (var sha = SHA256.Create()) - { - stopwatch.Start(); - for (int i = 0; i < iterations; i++) - { - sha.ComputeHash(data); - } - stopwatch.Stop(); - } - return stopwatch.Elapsed; - } - } -} \ No newline at end of file diff --git a/src/MDriveSync.Security/CompressionHelper.cs b/src/MDriveSync.Security/CompressionHelper.cs index d27fda9..6a09ad8 100644 --- a/src/MDriveSync.Security/CompressionHelper.cs +++ b/src/MDriveSync.Security/CompressionHelper.cs @@ -5,6 +5,8 @@ namespace MDriveSync.Security { /// /// 压缩解压函数 + /// 推荐算法:LZ4/Zstd/Snappy + /// 支持算法:LZ4/Zstd/Snappy/LZMA/Deflate/Brotli /// public static class CompressionHelper { @@ -37,6 +39,9 @@ public static byte[] Compress(byte[] buffer, string compressionType, "LZ4" => LZ4Compressor.Shared.Compress(buffer), "Zstd" => ZstdSharpCompressor.Shared.Compress(buffer), "Snappy" => SnappierCompressor.Shared.Compress(buffer), + "LZMA" => LZMACompressor.Shared.Compress(buffer), + "Deflate" => DeflateCompressor.Shared.Compress(buffer), + "Brotli" => BrotliCompressor.Shared.Compress(buffer), _ => buffer }; @@ -81,6 +86,9 @@ public static byte[] Decompress(byte[] buffer, string compressionType, string en "LZ4" => LZ4Compressor.Shared.Decompress(buffer), "Zstd" => ZstdSharpCompressor.Shared.Decompress(buffer), "Snappy" => SnappierCompressor.Shared.Decompress(buffer), + "LZMA" => LZMACompressor.Shared.Compress(buffer), + "Deflate" => DeflateCompressor.Shared.Compress(buffer), + "Brotli" => BrotliCompressor.Shared.Compress(buffer), _ => buffer }; diff --git a/src/MDriveSync.Test/BaseTests.cs b/src/MDriveSync.Test/BaseTests.cs new file mode 100644 index 0000000..05dbdbb --- /dev/null +++ b/src/MDriveSync.Test/BaseTests.cs @@ -0,0 +1,24 @@ +using System.Text; + +namespace MDriveSync.Test +{ + /// + /// 测试基类 + /// + public class BaseTests + { + public BaseTests() + { + // 避免中文输出乱码问题 + Console.OutputEncoding = Encoding.UTF8; + } + + /// + /// 避免中文输出乱码问题 - 用于方法 + /// + public virtual void SetOutputUTF8() + { + Console.OutputEncoding = Encoding.UTF8; + } + } +} diff --git a/src/MDriveSync.Test/CompressionPerformanceTests.cs b/src/MDriveSync.Test/CompressionPerformanceTests.cs new file mode 100644 index 0000000..1e333f5 --- /dev/null +++ b/src/MDriveSync.Test/CompressionPerformanceTests.cs @@ -0,0 +1,684 @@ +using MDriveSync.Security; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace MDriveSync.Test +{ + /// + /// 测试压缩算法性能 + /// + public class CompressionPerformanceTests : BaseTests + { + [Fact] + public void TestCompressorPerformance() + { + // 配置测试参数 + int[] sizes = { 1 * 1024 * 1024, 5 * 1024 * 1024, 10 * 1024 * 1024, 20 * 1024 * 1024 }; + int iterations = 3; + string[] algorithms = { "LZ4", "Zstd", "Snappy", "LZMA", "Deflate", "Brotli" }; + + var results = new Dictionary>>(); + foreach (var algorithm in algorithms) + { + results[algorithm] = new Dictionary>(); + foreach (var size in sizes) + { + results[algorithm][size] = new List<(double, double, double)>(); + } + } + + // 执行测试 + foreach (var size in sizes) + { + Console.WriteLine($"测试数据大小: {size / (1024 * 1024)} MB"); + + // 创建模拟可压缩数据 (有一些重复) + byte[] data = GenerateCompressibleData(size); + Console.WriteLine($"生成测试数据, 长度: {data.Length} 字节"); + + foreach (var algorithm in algorithms) + { + for (int i = 0; i < iterations; i++) + { + // 压缩计时 + var stopwatchCompress = new Stopwatch(); + stopwatchCompress.Start(); + byte[] compressed = CompressionHelper.Compress(data, algorithm); + stopwatchCompress.Stop(); + double compressTime = stopwatchCompress.Elapsed.TotalMilliseconds; + + // 解压缩计时 + var stopwatchDecompress = new Stopwatch(); + stopwatchDecompress.Start(); + byte[] decompressed = CompressionHelper.Decompress(compressed, algorithm); + stopwatchDecompress.Stop(); + double decompressTime = stopwatchDecompress.Elapsed.TotalMilliseconds; + + // 计算压缩率 + double compressionRatio = (double)compressed.Length / data.Length; + + // 验证数据完整性 + bool isValid = data.SequenceEqual(decompressed); + if (!isValid) + { + Console.WriteLine($"警告: {algorithm} 算法解压缩结果与原始数据不匹配!"); + } + + results[algorithm][size].Add((compressTime, decompressTime, compressionRatio)); + + Console.WriteLine($"算法: {algorithm,-7}, 轮次: {i + 1}, 压缩: {compressTime:F2} ms, 解压: {decompressTime:F2} ms, 压缩率: {compressionRatio:P2}"); + } + } + Console.WriteLine(); + } + + // 生成报告 + StringBuilder report = new StringBuilder(); + report.AppendLine("# 压缩算法性能测试报告"); + report.AppendLine(); + report.AppendLine("## 测试环境"); + report.AppendLine($"- 操作系统: {RuntimeInformation.OSDescription}"); + report.AppendLine($"- 处理器: {Environment.ProcessorCount} 核心"); + report.AppendLine($"- .NET 版本: {Environment.Version}"); + report.AppendLine($"- 测试时间: {DateTime.Now}"); + report.AppendLine(); + + foreach (var size in sizes) + { + report.AppendLine($"## 数据大小: {size / (1024 * 1024)} MB"); + report.AppendLine(); + report.AppendLine("| 算法 | 压缩时间 (ms) | 解压时间 (ms) | 压缩率 | 压缩速度 (MB/s) | 解压速度 (MB/s) |"); + report.AppendLine("|------|-------------|-------------|--------|----------------|----------------|"); + + var sizeResults = new List<(string Algorithm, double AvgCompressTime, double AvgDecompressTime, double AvgRatio, double CompressSpeed, double DecompressSpeed)>(); + + foreach (var algorithm in algorithms) + { + // 跳过第一轮结果(预热) + var validResults = results[algorithm][size].Skip(1).ToList(); + if (validResults.Count > 0) + { + double avgCompressTime = validResults.Average(r => r.CompressTime); + double avgDecompressTime = validResults.Average(r => r.DecompressTime); + double avgRatio = validResults.Average(r => r.CompressRatio); + + // 计算速度 (MB/s) + double compressSpeed = size / 1024.0 / 1024.0 / (avgCompressTime / 1000.0); + double decompressSpeed = size / 1024.0 / 1024.0 / (avgDecompressTime / 1000.0); + + sizeResults.Add((algorithm, avgCompressTime, avgDecompressTime, avgRatio, compressSpeed, decompressSpeed)); + } + } + + // 按压缩速度排序 + foreach (var result in sizeResults.OrderByDescending(r => r.CompressSpeed)) + { + report.AppendLine($"| {result.Algorithm,-6} | {result.AvgCompressTime,-13:F2} | {result.AvgDecompressTime,-13:F2} | {result.AvgRatio,-6:P2} | {result.CompressSpeed,-16:F2} | {result.DecompressSpeed,-16:F2} |"); + } + + report.AppendLine(); + } + + // 保存报告 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "压缩算法性能测试报告.md"); + File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); + + Console.WriteLine($"压缩性能报告已保存至: {reportPath}"); + Assert.True(File.Exists(reportPath)); + } + + /// + /// 测试压缩并加密流程的性能 + /// + [Fact] + public void TestCompressAndEncryptPerformance() + { + // 测试配置 + int dataSize = 10 * 1024 * 1024; // 10MB + string[] compressionAlgorithms = { "LZ4", "Zstd", "Snappy", "LZMA", "Deflate", "Brotli" }; + string[] encryptionAlgorithms = { "AES256-GCM", "ChaCha20-Poly1305" }; + int iterations = 3; + + // 生成测试数据 + byte[] data = GenerateCompressibleData(dataSize); + + // 准备结果收集 + var results = new Dictionary>(); + + // 为每种组合创建一个键 + foreach (var compAlgo in compressionAlgorithms) + { + foreach (var encAlgo in encryptionAlgorithms) + { + string key = $"{compAlgo}/{encAlgo}"; + results[key] = new List<(double, double, double)>(); + } + } + + Console.WriteLine($"测试压缩+加密性能 (数据大小: {dataSize / (1024 * 1024)} MB)"); + + // 执行测试 + foreach (var compAlgo in compressionAlgorithms) + { + foreach (var encAlgo in encryptionAlgorithms) + { + string key = $"{compAlgo}/{encAlgo}"; + string encryptionKey = "test-encryption-key-123456"; + + for (int i = 0; i < iterations; i++) + { + // 压缩并加密 + var compressEncryptSw = new Stopwatch(); + compressEncryptSw.Start(); + byte[] processed = CompressionHelper.Compress(data, compAlgo, encAlgo, encryptionKey); + compressEncryptSw.Stop(); + double compressEncryptTime = compressEncryptSw.Elapsed.TotalMilliseconds; + + // 解密并解压缩 + var decryptDecompressSw = new Stopwatch(); + decryptDecompressSw.Start(); + byte[] decompressed = CompressionHelper.Decompress(processed, compAlgo, encAlgo, encryptionKey); + decryptDecompressSw.Stop(); + double decryptDecompressTime = decryptDecompressSw.Elapsed.TotalMilliseconds; + + // 计算压缩率 + double compressionRatio = (double)processed.Length / data.Length; + + // 验证数据 + bool isValid = data.SequenceEqual(decompressed); + if (!isValid) + { + Console.WriteLine($"警告: {key} 处理后的数据与原始数据不匹配!"); + } + + results[key].Add((compressEncryptTime, decryptDecompressTime, compressionRatio)); + + Console.WriteLine($"组合: {key,-20}, 轮次: {i + 1}, 压缩+加密: {compressEncryptTime:F2} ms, 解密+解压: {decryptDecompressTime:F2} ms, 压缩率: {compressionRatio:P2}"); + } + } + } + + // 生成报告 + StringBuilder report = new StringBuilder(); + report.AppendLine("# 压缩和加密组合性能测试报告"); + report.AppendLine(); + report.AppendLine("## 测试环境"); + report.AppendLine($"- 操作系统: {RuntimeInformation.OSDescription}"); + report.AppendLine($"- 处理器: {Environment.ProcessorCount} 核心"); + report.AppendLine($"- .NET 版本: {Environment.Version}"); + report.AppendLine($"- 测试数据大小: {dataSize / (1024 * 1024)} MB"); + report.AppendLine($"- 测试时间: {DateTime.Now}"); + report.AppendLine(); + + report.AppendLine("| 算法组合 | 压缩+加密 (ms) | 解密+解压 (ms) | 压缩率 | 压缩+加密速度 (MB/s) | 解密+解压速度 (MB/s) |"); + report.AppendLine("|----------|--------------|--------------|--------|---------------------|---------------------|"); + + var summaryResults = new List<(string Combo, double EncryptTime, double DecryptTime, double Ratio, double EncSpeed, double DecSpeed)>(); + + foreach (var entry in results) + { + // 跳过第一轮结果(预热) + var validResults = entry.Value.Skip(1).ToList(); + if (validResults.Count > 0) + { + double avgEncryptTime = validResults.Average(r => r.CompressEncryptTime); + double avgDecryptTime = validResults.Average(r => r.DecryptDecompressTime); + double avgRatio = validResults.Average(r => r.CompressRatio); + + // 计算速度 (MB/s) + double encSpeed = dataSize / 1024.0 / 1024.0 / (avgEncryptTime / 1000.0); + double decSpeed = dataSize / 1024.0 / 1024.0 / (avgDecryptTime / 1000.0); + + summaryResults.Add((entry.Key, avgEncryptTime, avgDecryptTime, avgRatio, encSpeed, decSpeed)); + } + } + + // 按加密+压缩速度排序 + foreach (var result in summaryResults.OrderByDescending(r => r.EncSpeed)) + { + report.AppendLine($"| {result.Combo,-10} | {result.EncryptTime,-14:F2} | {result.DecryptTime,-14:F2} | {result.Ratio,-6:P2} | {result.EncSpeed,-21:F2} | {result.DecSpeed,-21:F2} |"); + } + + // 保存报告 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "压缩加密组合性能测试报告.md"); + File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); + + Console.WriteLine($"压缩加密组合性能报告已保存至: {reportPath}"); + Assert.True(File.Exists(reportPath)); + } + + /// + /// 生成具有一定可压缩性的随机数据 + /// + /// 数据大小(字节) + /// 随机生成的数据 + private byte[] GenerateCompressibleData(int size) + { + byte[] data = new byte[size]; + Random random = new Random(42); // 使用固定种子以确保结果可复现 + + // 创建一些重复模式以增加压缩率 + byte[] pattern = new byte[1024]; + random.NextBytes(pattern); + + for (int i = 0; i < size; i++) + { + // 70%的数据使用重复模式,30%完全随机 + if (random.NextDouble() < 0.7) + { + data[i] = pattern[i % pattern.Length]; + } + else + { + data[i] = (byte)random.Next(256); + } + } + + return data; + } + } + + /// + /// 测试加密算法性能 + /// + public class EncryptionPerformanceTests : BaseTests + { + [Fact] + public void TestEncryptionPerformance() + { + // 配置测试参数 + int[] sizes = { 1 * 1024 * 1024, 5 * 1024 * 1024, 10 * 1024 * 1024, 20 * 1024 * 1024 }; + int iterations = 3; + var algorithms = new Dictionary> + { + ["AES256-GCM"] = (data, key) => EncryptionHelper.EncryptWithAES256GCM(data, key), + ["ChaCha20-Poly1305"] = (data, key) => EncryptionHelper.EncryptWithChaCha20Poly1305(data, key) + }; + + var decryptionAlgorithms = new Dictionary> + { + ["AES256-GCM"] = (data, key) => EncryptionHelper.DecryptWithAES256GCM(data, key), + ["ChaCha20-Poly1305"] = (data, key) => EncryptionHelper.DecryptWithChaCha20Poly1305(data, key) + }; + + string encryptionKey = "test-encryption-key-for-performance-benchmarks"; + + var results = new Dictionary>>(); + foreach (var algorithm in algorithms.Keys) + { + results[algorithm] = new Dictionary>(); + foreach (var size in sizes) + { + results[algorithm][size] = new List<(double, double)>(); + } + } + + // 执行测试 + foreach (var size in sizes) + { + Console.WriteLine($"测试数据大小: {size / (1024 * 1024)} MB"); + + // 创建随机数据 + byte[] data = new byte[size]; + new Random(42).NextBytes(data); + + foreach (var algorithm in algorithms.Keys) + { + for (int i = 0; i < iterations; i++) + { + // 加密测试 + var encryptSw = new Stopwatch(); + encryptSw.Start(); + byte[] encrypted = algorithms[algorithm](data, encryptionKey); + encryptSw.Stop(); + double encryptTime = encryptSw.Elapsed.TotalMilliseconds; + + // 解密测试 + var decryptSw = new Stopwatch(); + decryptSw.Start(); + byte[] decrypted = decryptionAlgorithms[algorithm](encrypted, encryptionKey); + decryptSw.Stop(); + double decryptTime = decryptSw.Elapsed.TotalMilliseconds; + + // 验证数据 + bool isValid = data.SequenceEqual(decrypted); + if (!isValid) + { + Console.WriteLine($"警告: {algorithm} 解密后的数据与原始数据不匹配!"); + } + + results[algorithm][size].Add((encryptTime, decryptTime)); + + Console.WriteLine($"算法: {algorithm,-16}, 轮次: {i + 1}, 加密: {encryptTime:F2} ms, 解密: {decryptTime:F2} ms"); + } + } + Console.WriteLine(); + } + + // 生成报告 + StringBuilder report = new StringBuilder(); + report.AppendLine("# 加密算法性能测试报告"); + report.AppendLine(); + report.AppendLine("## 测试环境"); + report.AppendLine($"- 操作系统: {RuntimeInformation.OSDescription}"); + report.AppendLine($"- 处理器: {Environment.ProcessorCount} 核心"); + report.AppendLine($"- .NET 版本: {Environment.Version}"); + report.AppendLine($"- 测试时间: {DateTime.Now}"); + report.AppendLine(); + + foreach (var size in sizes) + { + report.AppendLine($"## 数据大小: {size / (1024 * 1024)} MB"); + report.AppendLine(); + report.AppendLine("| 算法 | 加密时间 (ms) | 解密时间 (ms) | 加密速度 (MB/s) | 解密速度 (MB/s) |"); + report.AppendLine("|------|-------------|-------------|----------------|----------------|"); + + var sizeResults = new List<(string Algorithm, double AvgEncryptTime, double AvgDecryptTime, double EncryptSpeed, double DecryptSpeed)>(); + + foreach (var algorithm in algorithms.Keys) + { + // 跳过第一轮结果(预热) + var validResults = results[algorithm][size].Skip(1).ToList(); + if (validResults.Count > 0) + { + double avgEncryptTime = validResults.Average(r => r.EncryptTime); + double avgDecryptTime = validResults.Average(r => r.DecryptTime); + + // 计算速度 (MB/s) + double encryptSpeed = size / 1024.0 / 1024.0 / (avgEncryptTime / 1000.0); + double decryptSpeed = size / 1024.0 / 1024.0 / (avgDecryptTime / 1000.0); + + sizeResults.Add((algorithm, avgEncryptTime, avgDecryptTime, encryptSpeed, decryptSpeed)); + } + } + + // 按加密速度排序 + foreach (var result in sizeResults.OrderByDescending(r => r.EncryptSpeed)) + { + report.AppendLine($"| {result.Algorithm,-6} | {result.AvgEncryptTime,-13:F2} | {result.AvgDecryptTime,-13:F2} | {result.EncryptSpeed,-16:F2} | {result.DecryptSpeed,-16:F2} |"); + } + + report.AppendLine(); + } + + // 保存报告 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "加密算法性能测试报告.md"); + File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); + + Console.WriteLine($"加密性能报告已保存至: {reportPath}"); + Assert.True(File.Exists(reportPath)); + } + + /// + /// 测试不同大小文件名的加密性能 + /// + [Fact] + public void TestFileNameEncryptionPerformance() + { + // 配置 + int iterations = 10; + int maxFileNameLength = 200; + string[] encryptionAlgorithms = { "AES256-GCM", "ChaCha20-Poly1305" }; + string encryptionKey = "test-key-for-filename-encryption"; + + // 结果收集 + var results = new Dictionary>>(); + foreach (var algorithm in encryptionAlgorithms) + { + results[algorithm] = new Dictionary>(); + for (int i = 1; i <= maxFileNameLength; i++) + { + results[algorithm][i] = new List<(double, double, int)>(); + } + } + + // 执行测试 + for (int length = 1; length <= maxFileNameLength; length += 10) // 增量10以减少测试时间 + { + // 生成随机文件名 + string randomFileName = GenerateRandomString(length); + byte[] fileNameBytes = Encoding.UTF8.GetBytes(randomFileName); + + foreach (var algorithm in encryptionAlgorithms) + { + for (int i = 0; i < iterations; i++) + { + // 加密文件名 + var encryptSw = new Stopwatch(); + encryptSw.Start(); + var encrypted = CompressionHelper.Compress(fileNameBytes, null, algorithm, encryptionKey); + encryptSw.Stop(); + + // 解密文件名 + var decryptSw = new Stopwatch(); + decryptSw.Start(); + var decrypted = CompressionHelper.Decompress(encrypted, null, algorithm, encryptionKey); + decryptSw.Stop(); + + // 验证 + bool isValid = fileNameBytes.SequenceEqual(decrypted); + if (!isValid) + { + Console.WriteLine($"警告: {algorithm} 文件名解密失败! 长度: {length}"); + } + + results[algorithm][length].Add((encryptSw.Elapsed.TotalMilliseconds, + decryptSw.Elapsed.TotalMilliseconds, + encrypted.Length)); + } + + // 输出当前测试的平均结果 + var currentResults = results[algorithm][length]; + double avgEncryptTime = currentResults.Average(r => r.EncryptTime); + double avgDecryptTime = currentResults.Average(r => r.DecryptTime); + double avgEncryptedSize = currentResults.Average(r => r.EncryptedSize); + + Console.WriteLine($"文件名长度: {length,-3}, 算法: {algorithm,-16}, " + + $"加密: {avgEncryptTime:F2} ms, 解密: {avgDecryptTime:F2} ms, " + + $"加密后大小: {avgEncryptedSize:F0} 字节"); + } + } + + // 生成报告 + StringBuilder report = new StringBuilder(); + report.AppendLine("# 文件名加密性能测试报告"); + report.AppendLine(); + report.AppendLine("## 测试环境"); + report.AppendLine($"- 操作系统: {RuntimeInformation.OSDescription}"); + report.AppendLine($"- 处理器: {Environment.ProcessorCount} 核心"); + report.AppendLine($"- .NET 版本: {Environment.Version}"); + report.AppendLine($"- 测试时间: {DateTime.Now}"); + report.AppendLine(); + + report.AppendLine("## 文件名加密性能比较"); + report.AppendLine(); + report.AppendLine("| 文件名长度 | 算法 | 加密时间 (ms) | 解密时间 (ms) | 加密后大小 (字节) | 膨胀比率 |"); + report.AppendLine("|------------|------|--------------|--------------|-----------------|----------|"); + + for (int length = 1; length <= maxFileNameLength; length += 10) + { + foreach (var algorithm in encryptionAlgorithms) + { + if (results[algorithm].ContainsKey(length) && results[algorithm][length].Count > 0) + { + double avgEncryptTime = results[algorithm][length].Average(r => r.EncryptTime); + double avgDecryptTime = results[algorithm][length].Average(r => r.DecryptTime); + double avgEncryptedSize = results[algorithm][length].Average(r => r.EncryptedSize); + double inflationRatio = avgEncryptedSize / length; + + report.AppendLine($"| {length,-12} | {algorithm,-6} | {avgEncryptTime,-14:F2} | {avgDecryptTime,-14:F2} | {avgEncryptedSize,-17:F0} | {inflationRatio,-8:F2} |"); + } + } + } + + // 保存报告 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "文件名加密性能测试报告.md"); + File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); + + Console.WriteLine($"文件名加密性能报告已保存至: {reportPath}"); + Assert.True(File.Exists(reportPath)); + } + + private static string GenerateRandomString(int length) + { + Random random = new Random(42); // 使用固定种子以确保结果可复现 + StringBuilder stringBuilder = new StringBuilder(length); + for (int i = 0; i < length; i++) + { + char c = (char)random.Next('a', 'z' + 1); // 生成随机小写字母 + stringBuilder.Append(c); + } + return stringBuilder.ToString(); + } + } + + /// + /// 比较不同编码方式对加密和压缩数据的表示效率 + /// + public class EncodingEfficiencyTests + { + [Fact] + public void TestDataEncodingEfficiency() + { + // 测试不同长度的字符串 + int[] lengths = { 10, 20, 50, 100, 200 }; + int iterations = 5; + + // 定义编码方式 + var encodings = new Dictionary> + { + ["Base64"] = data => Convert.ToBase64String(data), + ["Hex"] = data => BitConverter.ToString(data).Replace("-", "").ToLowerInvariant(), + //["Base85"] = data => Base85.Ascii85.Encode(data), // 需要添加相应的库 + ["SafeBase64"] = data => ToSafeBase64(data) + }; + + // 收集结果 + var results = new Dictionary>(); + + foreach (int length in lengths) + { + results[length] = new Dictionary(); + + for (int i = 0; i < iterations; i++) + { + // 生成随机文件名 + string randomText = GenerateRandomString(length); + byte[] originalBytes = Encoding.UTF8.GetBytes(randomText); + + // 压缩文件名 + byte[] compressedBytes = CompressionHelper.Compress(originalBytes, "LZ4"); + + // 加密文件名 + byte[] encryptedBytes = CompressionHelper.Compress(originalBytes, null, "AES256-GCM", "test-key"); + + // 压缩并加密 + byte[] compressedEncryptedBytes = CompressionHelper.Compress(originalBytes, "LZ4", "AES256-GCM", "test-key"); + + // 测试各种编码的效率 + TestEncodingEfficiency(encodings, "原始", originalBytes, length, results); + TestEncodingEfficiency(encodings, "压缩", compressedBytes, length, results); + TestEncodingEfficiency(encodings, "加密", encryptedBytes, length, results); + TestEncodingEfficiency(encodings, "压缩加密", compressedEncryptedBytes, length, results); + } + } + + // 生成报告 + StringBuilder report = new StringBuilder(); + report.AppendLine("# 数据编码效率测试报告"); + report.AppendLine(); + report.AppendLine("## 测试环境"); + report.AppendLine($"- 操作系统: {RuntimeInformation.OSDescription}"); + report.AppendLine($"- .NET 版本: {Environment.Version}"); + report.AppendLine($"- 测试时间: {DateTime.Now}"); + report.AppendLine(); + + foreach (int length in lengths) + { + report.AppendLine($"## 原始长度: {length} 字符"); + report.AppendLine(); + report.AppendLine("| 数据类型 | 编码方式 | 编码前大小 (字节) | 编码后大小 (字符) | 膨胀比率 |"); + report.AppendLine("|----------|----------|-------------------|-------------------|----------|"); + + foreach (var dataType in new[] { "原始", "压缩", "加密", "压缩加密" }) + { + foreach (var encoding in encodings.Keys) + { + string key = $"{dataType}_{encoding}"; + if (results[length].ContainsKey(key)) + { + var (originalSize, encodedSize, ratio) = results[length][key]; + report.AppendLine($"| {dataType,-8} | {encoding,-8} | {originalSize,-19:F0} | {encodedSize,-19:F0} | {ratio,-8:F2} |"); + } + } + } + + report.AppendLine(); + } + + // 保存报告 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "数据编码效率测试报告.md"); + File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); + + Console.WriteLine($"数据编码效率报告已保存至: {reportPath}"); + Assert.True(File.Exists(reportPath)); + } + + private void TestEncodingEfficiency( + Dictionary> encodings, + string dataType, + byte[] data, + int originalLength, + Dictionary> results) + { + foreach (var encoding in encodings) + { + string encodedString = encoding.Value(data); + string key = $"{dataType}_{encoding.Key}"; + + double originalSize = data.Length; + double encodedSize = encodedString.Length; + double ratio = encodedSize / originalSize; + + // 更新或添加结果 + if (!results[originalLength].ContainsKey(key)) + { + results[originalLength][key] = (originalSize, encodedSize, ratio); + } + else + { + var (oldOriginalSize, oldEncodedSize, oldRatio) = results[originalLength][key]; + // 取平均值 + results[originalLength][key] = ( + (oldOriginalSize + originalSize) / 2, + (oldEncodedSize + encodedSize) / 2, + (oldRatio + ratio) / 2 + ); + } + + Console.WriteLine($"长度: {originalLength}, 类型: {dataType}, 编码: {encoding.Key}, " + + $"原始大小: {originalSize} 字节, 编码后: {encodedSize} 字符, 比率: {ratio:F2}"); + } + } + + private static string ToSafeBase64(byte[] data) + { + return Convert.ToBase64String(data) + .Replace('+', '-') + .Replace('/', '_') + .TrimEnd('='); + } + + private static string GenerateRandomString(int length) + { + Random random = new Random(42); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) + { + sb.Append((char)random.Next('a', 'z' + 1)); + } + return sb.ToString(); + } + } +} diff --git a/src/MDriveSync.Test/HashAlgorithmPerformanceTests.cs b/src/MDriveSync.Test/HashAlgorithmPerformanceTests.cs new file mode 100644 index 0000000..c773df2 --- /dev/null +++ b/src/MDriveSync.Test/HashAlgorithmPerformanceTests.cs @@ -0,0 +1,174 @@ +using MDriveSync.Security; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Xunit.Abstractions; + +namespace MDriveSync.Test +{ + /// + /// 测试多种哈希算法的耗时,并输出结果 + /// + public class HashAlgorithmPerformanceTests : BaseTests + { + private readonly ITestOutputHelper _output; + + public HashAlgorithmPerformanceTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void TestHashAlgorithmPerformance() + { + // 测试配置 + int dataSizeMB = 100; + int numRounds = 10; + string[] algorithms = { "SHA1", "SHA256", "SHA384", "MD5", "XXH3", "XXH128", "BLAKE3" }; + + // 准备测试数据 (100MB) + int dataSize = dataSizeMB * 1024 * 1024; + byte[] data = new byte[dataSize]; + new Random(42).NextBytes(data); // 使用固定种子以确保结果可重现 + + // 收集测试结果 + var results = new Dictionary>(); + foreach (var algorithm in algorithms) + { + results[algorithm] = new List(); + } + + _output.WriteLine($"比较各种哈希算法性能 ({dataSizeMB}MB 数据):\n"); + + // 执行多轮测试 + for (int round = 1; round <= numRounds; round++) + { + _output.WriteLine($"测试轮次 {round}:"); + + foreach (var algorithm in algorithms) + { + // 测量性能 + var stopwatch = new Stopwatch(); + stopwatch.Start(); + byte[] hash = HashHelper.ComputeHash(data, algorithm); + stopwatch.Stop(); + + double elapsedMs = stopwatch.Elapsed.TotalMilliseconds; + results[algorithm].Add(elapsedMs); + + _output.WriteLine($"{algorithm,-10}: {elapsedMs,6:F0} ms"); + } + + _output.WriteLine(""); + + // 在每轮之间稍微暂停,让系统稳定 + if (round < numRounds) + { + Thread.Sleep(100); + } + } + + // 计算平均结果并生成报告 + string report = GenerateReport(results, dataSizeMB, numRounds); + + // 输出到控制台 + _output.WriteLine(report); + + // 保存报告到文件 + // 保存文件到当前项目目录 + var currentDirectory = Directory.GetCurrentDirectory(); + var baseDir = AppDomain.CurrentDomain.BaseDirectory; + var baseDir2 = AppContext.BaseDirectory; + + string reportPath = Path.Combine(AppContext.BaseDirectory, "哈希算法性能测试报告.md"); + + File.WriteAllText(reportPath, report, Encoding.UTF8); + + _output.WriteLine($"\n性能报告已保存至:{reportPath}"); + } + + private string GenerateReport(Dictionary> results, int dataSizeMB, int numRounds) + { + var sb = new StringBuilder(); + sb.AppendLine("# 哈希算法性能测试报告"); + sb.AppendLine(); + + // 添加测试环境信息 + sb.AppendLine("## 测试环境"); + sb.AppendLine($"- 操作系统: {RuntimeInformation.OSDescription}"); + sb.AppendLine($"- 处理器: {Environment.ProcessorCount} 核心"); + sb.AppendLine($"- .NET 版本: {Environment.Version}"); + sb.AppendLine($"- 测试数据大小: {dataSizeMB} MB"); + sb.AppendLine($"- 测试时间: {DateTime.Now}"); + sb.AppendLine(); + + // 计算每个算法的平均耗时 + var averages = new Dictionary(); + var hashWidths = new Dictionary + { + ["SHA1"] = 160, + ["SHA256"] = 256, + ["SHA384"] = 384, + ["MD5"] = 128, + ["XXH3"] = 64, + ["XXH128"] = 128, + ["BLAKE3"] = 256 + }; + + foreach (var algorithm in results.Keys) + { + // 去掉第一轮的结果(可能受JIT编译影响) + var validResults = results[algorithm].Skip(1).ToList(); + averages[algorithm] = validResults.Average(); + } + + // 添加平均执行时间表格 + sb.AppendLine("## 测试结果"); + sb.AppendLine(); + sb.AppendLine("### 平均执行时间"); + sb.AppendLine(); + sb.AppendLine("| 算法名称 | 哈希宽度 (位) | 平均耗时 (ms) | 吞吐量 (GB/s) |"); + sb.AppendLine("|---------|------------|------------|------------|"); + + foreach (var entry in averages.OrderBy(a => a.Value)) + { + string algorithm = entry.Key; + double avgMs = entry.Value; + int width = hashWidths.ContainsKey(algorithm) ? hashWidths[algorithm] : 0; + + // 计算吞吐量 (GB/s) + double throughput = (dataSizeMB / 1024.0) / (avgMs / 1000.0); + + sb.AppendLine($"| {algorithm,-8} | {width,-3} | {avgMs,-6:F2} | {throughput,-4:F2} |"); + } + + sb.AppendLine(); + + // 添加速度比较表格 + sb.AppendLine("### 算法速度比较"); + sb.AppendLine(); + sb.AppendLine("| 比较项 | 速度比率 |"); + sb.AppendLine("|-------|--------|"); + + // 添加一些有意义的速度比较 + AddSpeedComparison(sb, averages, "SHA1", "SHA256"); + AddSpeedComparison(sb, averages, "SHA1", "SHA384"); + AddSpeedComparison(sb, averages, "XXH3", "SHA1"); + AddSpeedComparison(sb, averages, "XXH3", "SHA256"); + AddSpeedComparison(sb, averages, "BLAKE3", "SHA1"); + AddSpeedComparison(sb, averages, "BLAKE3", "SHA256"); + AddSpeedComparison(sb, averages, "BLAKE3", "XXH3"); + + return sb.ToString(); + } + + private void AddSpeedComparison(StringBuilder sb, Dictionary averages, string algo1, string algo2) + { + if (averages.ContainsKey(algo1) && averages.ContainsKey(algo2)) + { + double ratio = averages[algo2] / averages[algo1]; + sb.AppendLine($"| {algo1} 比 {algo2} 快 | {ratio,-4:F2} 倍 |"); + } + } + } +} diff --git a/src/MDriveSync.Test/HashHelperTests.cs b/src/MDriveSync.Test/HashHelperTests.cs new file mode 100644 index 0000000..02c4ec9 --- /dev/null +++ b/src/MDriveSync.Test/HashHelperTests.cs @@ -0,0 +1,44 @@ +using MDriveSync.Security; +using System.Diagnostics; + +namespace MDriveSync.Test +{ + public class HashHelperTests : BaseTests + { + [Theory] + [InlineData(10 * 1024 * 1024, 10)] // 测试 10MB 数据,迭代 10 次 + [InlineData(50 * 1024 * 1024, 5)] // 测试 50MB 数据,迭代 5 次 + public void TestHashPerformance(int dataSize, int iterations) + { + // 准备测试数据 + byte[] data = new byte[dataSize]; + new Random().NextBytes(data); + + // 定义要测试的哈希算法 + string[] algorithms = { "SHA256", "SHA1", "MD5", "BLAKE3", "XXH3", "XXH128" }; + + foreach (var algorithm in algorithms) + { + // 测试哈希算法耗时 + TimeSpan elapsedTime = MeasureHashTime(data, algorithm, iterations); + + // 输出结果 + Console.WriteLine($"Algorithm: {algorithm}, Data Size: {dataSize / (1024 * 1024)} MB, Iterations: {iterations}, Time: {elapsedTime.TotalMilliseconds} ms"); + } + } + + private TimeSpan MeasureHashTime(byte[] data, string algorithm, int iterations) + { + Stopwatch stopwatch = new Stopwatch(); + + stopwatch.Start(); + for (int i = 0; i < iterations; i++) + { + HashHelper.ComputeHash(data, algorithm); + } + stopwatch.Stop(); + + return stopwatch.Elapsed; + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Test/HashPerformanceTests.cs b/src/MDriveSync.Test/HashPerformanceTests.cs new file mode 100644 index 0000000..bffdb81 --- /dev/null +++ b/src/MDriveSync.Test/HashPerformanceTests.cs @@ -0,0 +1,532 @@ +using MDriveSync.Core; +using MDriveSync.Security; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace MDriveSync.Test +{ + /// + /// 测试哈希算法性能 + /// + public class HashPerformanceTests : BaseTests + { + [Fact] + public void TestHashAlgorithmComparison() + { + // 配置测试参数 + int[] sizes = { 10 * 1024 * 1024, 20 * 1024 * 1024, 30 * 1024 * 1024, 100 * 1024 * 1024 }; + int iterations = 3; // 减少迭代次数以加快测试运行 + string[] algorithms = { "SHA1", "SHA256", "SHA384", "MD5", "XXH3", "XXH128", "BLAKE3" }; + + var results = new Dictionary>>(); + foreach (var algorithm in algorithms) + { + results[algorithm] = new Dictionary>(); + foreach (var size in sizes) + { + results[algorithm][size] = new List(); + } + } + + // 执行测试 + foreach (var size in sizes) + { + Console.WriteLine($"测试数据大小: {size / (1024 * 1024)} MB"); + + // 创建随机测试数据 + byte[] data = new byte[size]; + new Random(42).NextBytes(data); // 使用固定种子以确保结果可重现 + + foreach (var algorithm in algorithms) + { + for (int i = 0; i < iterations; i++) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + byte[] hash = HashHelper.ComputeHash(data, algorithm); + stopwatch.Stop(); + + double elapsedMs = stopwatch.Elapsed.TotalMilliseconds; + results[algorithm][size].Add(elapsedMs); + + Console.WriteLine($"算法: {algorithm}, 轮次: {i + 1}, 耗时: {elapsedMs:F2} ms"); + } + } + Console.WriteLine(); + } + + // 生成摘要报告 + StringBuilder report = new StringBuilder(); + report.AppendLine("# 哈希算法性能测试报告"); + report.AppendLine(); + report.AppendLine("## 测试环境"); + report.AppendLine($"- 操作系统: {RuntimeInformation.OSDescription}"); + report.AppendLine($"- 处理器: {Environment.ProcessorCount} 核心"); + report.AppendLine($"- .NET 版本: {Environment.Version}"); + report.AppendLine($"- 测试时间: {DateTime.Now}"); + report.AppendLine(); + + // 添加各个大小的平均执行时间表格 + foreach (var size in sizes) + { + report.AppendLine($"## 数据大小: {size / (1024 * 1024)} MB"); + report.AppendLine(); + report.AppendLine("| 算法名称 | 平均耗时 (ms) | 吞吐量 (GB/s) |"); + report.AppendLine("|---------|------------|------------|"); + + var hashWidths = new Dictionary + { + ["SHA1"] = 160, + ["SHA256"] = 256, + ["SHA384"] = 384, + ["MD5"] = 128, + ["XXH3"] = 64, + ["XXH128"] = 128, + ["BLAKE3"] = 256 + }; + + // 计算并排序结果 + var sortedResults = new List<(string Algorithm, double AvgTime, double Throughput)>(); + foreach (var algorithm in algorithms) + { + // 跳过第一次测量结果(预热),取后续几次的平均值 + var validResults = results[algorithm][size].Skip(1).ToList(); + double avgTime = validResults.Count > 0 ? validResults.Average() : 0; + double throughput = (size / 1024.0 / 1024.0 / 1024.0) / (avgTime / 1000.0); // GB/s + + sortedResults.Add((algorithm, avgTime, throughput)); + } + + // 按平均时间排序并输出 + foreach (var result in sortedResults.OrderBy(r => r.AvgTime)) + { + report.AppendLine($"| {result.Algorithm,-8} | {result.AvgTime,-6:F2} | {result.Throughput,-4:F2} |"); + } + + report.AppendLine(); + } + + // 保存报告 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "哈希算法性能测试报告.md"); + File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); + + Console.WriteLine($"性能报告已保存至:{reportPath}"); + + // 测试断言,确保测试正常运行 + Assert.True(File.Exists(reportPath)); + } + + [Theory] + [InlineData(10 * 1024 * 1024, 3)] // 10MB, 3次迭代 + public void TestSpecificHashPerformance(int dataSize, int iterations) + { + // 准备测试数据 + byte[] data = new byte[dataSize]; + new Random().NextBytes(data); + + // 定义要测试的哈希算法 + string[] algorithms = { "SHA256", "SHA1", "MD5", "BLAKE3", "XXH3", "XXH128" }; + + foreach (var algorithm in algorithms) + { + // 测试哈希算法耗时 + TimeSpan elapsedTime = MeasureHashTime(data, algorithm, iterations); + + // 输出结果 + Console.WriteLine($"Algorithm: {algorithm}, Data Size: {dataSize / (1024 * 1024)} MB, Iterations: {iterations}, Time: {elapsedTime.TotalMilliseconds} ms"); + + // 简单断言确保算法能正常工作 + Assert.True(elapsedTime.TotalMilliseconds > 0); + } + } + + private TimeSpan MeasureHashTime(byte[] data, string algorithm, int iterations) + { + Stopwatch stopwatch = new Stopwatch(); + + stopwatch.Start(); + for (int i = 0; i < iterations; i++) + { + HashHelper.ComputeHash(data, algorithm); + } + stopwatch.Stop(); + + return stopwatch.Elapsed; + } + } + + /// + /// 测试加密和解密功能 + /// + public class EncryptionTests : BaseTests + { + [Fact] + public void TestAES256GCMEncryptionDecryption() + { + // 测试数据 + string originalText = "hello world"; + byte[] originalData = Encoding.UTF8.GetBytes(originalText); + string password = "test-password"; + + // 加密 + byte[] encryptedData = EncryptionHelper.EncryptWithAES256GCM(originalData, password); + Assert.NotNull(encryptedData); + Assert.NotEmpty(encryptedData); + Assert.NotEqual(originalData, encryptedData); + + // 解密 + byte[] decryptedData = EncryptionHelper.DecryptWithAES256GCM(encryptedData, password); + Assert.NotNull(decryptedData); + Assert.Equal(originalData, decryptedData); + + string decryptedText = Encoding.UTF8.GetString(decryptedData); + Assert.Equal(originalText, decryptedText); + } + + [Fact] + public void TestChaCha20Poly1305EncryptionDecryption() + { + // 测试数据 + string originalText = "hello world"; + byte[] originalData = Encoding.UTF8.GetBytes(originalText); + string password = "test-password"; + + // 加密 + byte[] encryptedData = EncryptionHelper.EncryptWithChaCha20Poly1305(originalData, password); + Assert.NotNull(encryptedData); + Assert.NotEmpty(encryptedData); + Assert.NotEqual(originalData, encryptedData); + + // 解密 + byte[] decryptedData = EncryptionHelper.DecryptWithChaCha20Poly1305(encryptedData, password); + Assert.NotNull(decryptedData); + Assert.Equal(originalData, decryptedData); + + string decryptedText = Encoding.UTF8.GetString(decryptedData); + Assert.Equal(originalText, decryptedText); + } + + [Fact] + public void TestWrongPasswordDecryption() + { + // 测试数据 + string originalText = "hello world"; + byte[] originalData = Encoding.UTF8.GetBytes(originalText); + string correctPassword = "correct-password"; + string wrongPassword = "wrong-password"; + + // 使用正确密码加密 + byte[] encryptedDataAES = EncryptionHelper.EncryptWithAES256GCM(originalData, correctPassword); + byte[] encryptedDataChaCha = EncryptionHelper.EncryptWithChaCha20Poly1305(originalData, correctPassword); + + // 使用错误密码解密,应该抛出异常 + Assert.Throws(() => EncryptionHelper.DecryptWithAES256GCM(encryptedDataAES, wrongPassword)); + Assert.Throws(() => EncryptionHelper.DecryptWithChaCha20Poly1305(encryptedDataChaCha, wrongPassword)); + } + } + + /// + /// 测试压缩和解压缩功能 + /// + public class CompressionTests : BaseTests + { + [Theory] + [InlineData("LZ4")] + [InlineData("Zstd")] + [InlineData("Snappy")] + public void TestCompressDecompress(string algorithm) + { + // 测试数据 + string originalText = "hello world repeated many times to ensure compression is effective. " + + string.Join(" ", Enumerable.Repeat("hello world", 100)); + byte[] originalData = Encoding.UTF8.GetBytes(originalText); + + // 压缩 + byte[] compressedData = CompressionHelper.Compress(originalData, algorithm); + Assert.NotNull(compressedData); + Assert.NotEmpty(compressedData); + + // 对于可压缩的数据,压缩后应该变小 + Console.WriteLine($"原始大小: {originalData.Length}, 压缩后大小: {compressedData.Length}, 压缩率: {(double)compressedData.Length / originalData.Length:P2}"); + + // 解压缩 + byte[] decompressedData = CompressionHelper.Decompress(compressedData, algorithm); + Assert.NotNull(decompressedData); + Assert.Equal(originalData, decompressedData); + + string decompressedText = Encoding.UTF8.GetString(decompressedData); + Assert.Equal(originalText, decompressedText); + } + + [Fact] + public void TestCompressEncryptDecompress() + { + // 测试数据 + string originalText = "hello world with encryption and compression"; + byte[] originalData = Encoding.UTF8.GetBytes(originalText); + string compressionAlgorithm = "LZ4"; + string encryptionAlgorithm = "AES256-GCM"; + string encryptionKey = "test-key"; + + // 压缩并加密 + byte[] processed = CompressionHelper.Compress(originalData, compressionAlgorithm, encryptionAlgorithm, encryptionKey); + Assert.NotNull(processed); + Assert.NotEmpty(processed); + + // 解密并解压缩 + byte[] decompressed = CompressionHelper.Decompress(processed, compressionAlgorithm, encryptionAlgorithm, encryptionKey); + Assert.NotNull(decompressed); + Assert.Equal(originalData, decompressed); + + string resultText = Encoding.UTF8.GetString(decompressed); + Assert.Equal(originalText, resultText); + } + + [Fact] + public void TestStreamCompression() + { + // 准备测试文件 + string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDir); + string inputFilePath = Path.Combine(tempDir, "test-input.txt"); + string outputFilePath = Path.Combine(tempDir, "test-output.enc"); + string decompressedFilePath = Path.Combine(tempDir, "test-decompressed.txt"); + + try + { + //// 创建测试文件 + //string testContent = "This is a test content for file compression and encryption. " + + // string.Join(" ", Enumerable.Repeat("More content for compression.", 100)); + //File.WriteAllText(inputFilePath, testContent); + + //// 配置 + //string compressionType = "Zstd"; + //string encryptionType = "AES256-GCM"; + //string encryptionKey = "test-encryption-key"; + //string hashAlgorithm = "BLAKE3"; + + //// 压缩并加密 + //using (FileStream inputFileStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read)) + //using (FileStream outputFileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) + //{ + // CompressionHelper.CompressAndEncryptStream( + // inputFileStream, + // outputFileStream, + // compressionType, + // encryptionType, + // encryptionKey, + // hashAlgorithm); + //} + + //// 检查输出文件是否存在且不为空 + //FileInfo outputFile = new FileInfo(outputFilePath); + //Assert.True(outputFile.Exists); + //Assert.True(outputFile.Length > 0); + + //// 解密并解压缩 + //using (FileStream inputStream = new FileStream(outputFilePath, FileMode.Open, FileAccess.Read)) + //using (FileStream outputStream = new FileStream(decompressedFilePath, FileMode.Create, FileAccess.Write)) + //{ + // CompressionHelper.DecompressStream( + // inputStream, + // outputStream, + // compressionType, + // encryptionType, + // encryptionKey, + // hashAlgorithm); + //} + + //// 检查解压缩后的文件内容是否一致 + //string decompressedContent = File.ReadAllText(decompressedFilePath); + //Assert.Equal(testContent, decompressedContent); + } + finally + { + // 清理测试文件 + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } + } + + /// + /// 测试文件操作工具类 + /// + public class FileHelperTests : BaseTests + { + [Fact] + public void TestCreateRandomFile() + { + // 创建临时路径 + string tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + try + { + // 指定文件大小 (1 MB) + long fileSize = 1 * 1024 * 1024; + + // 创建随机文件 + FileHelper.CreateRandomFile(tempFilePath, fileSize); + + // 验证文件是否创建成功 + FileInfo fileInfo = new FileInfo(tempFilePath); + Assert.True(fileInfo.Exists); + Assert.Equal(fileSize, fileInfo.Length); + } + finally + { + // 清理 + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + + [Fact] + public void TestModifyFileAndResetLastWriteTime() + { + // 创建临时路径 + string tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + try + { + // 创建随机文件 (10 KB) + long fileSize = 10 * 1024; + FileHelper.CreateRandomFile(tempFilePath, fileSize); + + // 记录原始的最后写入时间和内容 + DateTime originalWriteTime = File.GetLastWriteTime(tempFilePath); + byte[] originalContent = File.ReadAllBytes(tempFilePath); + + // 让系统时间略微流逝,确保时间戳会变化 + Thread.Sleep(100); + + // 修改文件但保持原始时间戳 + int modifyPosition = 1025; // 修改位置 + FileHelper.ModifyFileAndResetLastWriteTime(tempFilePath, modifyPosition); + + // 验证内容已变化 + byte[] modifiedContent = File.ReadAllBytes(tempFilePath); + Assert.NotEqual(originalContent[modifyPosition], modifiedContent[modifyPosition]); + + // 验证时间戳没有变化 + DateTime currentWriteTime = File.GetLastWriteTime(tempFilePath); + Assert.Equal(originalWriteTime, currentWriteTime); + } + finally + { + // 清理 + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + } + + /// + /// 测试生成随机字符串和路径 + /// + public class RandomDataTests : BaseTests + { + private static string GenerateRandomString(int length) + { + Random random = new Random(); + StringBuilder stringBuilder = new StringBuilder(length); + for (int i = 0; i < length; i++) + { + char c = (char)random.Next('a', 'z' + 1); // 生成随机小写字母 + stringBuilder.Append(c); + } + return stringBuilder.ToString(); + } + + private static string GenerateRandomPath(int maxLength) + { + StringBuilder pathBuilder = new StringBuilder(); + pathBuilder.Append(Directory.GetCurrentDirectory()); // 从当前工作目录开始 + + Random random = new Random(); + try + { + while (pathBuilder.Length < maxLength - 260) // 确保留有空间添加文件名 + { + pathBuilder.Append(Path.DirectorySeparatorChar); + pathBuilder.Append(GenerateRandomString(10)); // 每个目录名最多10个字符 + } + } + catch (Exception) + { + Console.WriteLine("生成路径时出现异常"); + } + + return pathBuilder.ToString(); + } + + [Theory] + [InlineData(10)] + [InlineData(50)] + [InlineData(100)] + public void TestGenerateRandomString(int length) + { + string randomString = GenerateRandomString(length); + + // 验证字符串长度 + Assert.Equal(length, randomString.Length); + + // 验证字符串仅包含小写字母 + foreach (char c in randomString) + { + Assert.True(c >= 'a' && c <= 'z'); + } + } + + [Fact] + public void TestGenerateRandomPath() + { + int maxLength = 4096; // 使用较小的长度进行测试 + string randomPath = GenerateRandomPath(maxLength); + + // 验证路径以当前目录开头 + Assert.StartsWith(Directory.GetCurrentDirectory(), randomPath); + + // 验证路径长度不超过指定值 + Assert.True(randomPath.Length <= maxLength); + } + + [Fact] + public void TestFileNameCompression() + { + for (int i = 1; i <= 30; i++) + { + string randomFileName = GenerateRandomString(i); + byte[] fileNameBytes = Encoding.UTF8.GetBytes(randomFileName); + + // 测试压缩 + byte[] compressedBytes = CompressionHelper.Compress(fileNameBytes, "LZ4"); + + // 测试对压缩后的字节再次压缩 + byte[] doubleCompressedBytes = CompressionHelper.Compress(compressedBytes, "LZ4"); + + // 测试解压 + byte[] decompressedBytes = CompressionHelper.Decompress(doubleCompressedBytes, "LZ4"); + byte[] originalBytes = CompressionHelper.Decompress(decompressedBytes, "LZ4"); + + // 验证完整性 + Assert.Equal(fileNameBytes, originalBytes); + + // 转为Base64并验证 + string base64 = Convert.ToBase64String(compressedBytes); + byte[] decodedData = Convert.FromBase64String(base64); + Assert.Equal(compressedBytes, decodedData); + + Console.WriteLine($"长度: {i}, 原始: {fileNameBytes.Length}, 压缩: {compressedBytes.Length}, Base64: {base64.Length}"); + } + } + } +} diff --git a/src/MDriveSync.Test/MDriveSync.Test.csproj b/src/MDriveSync.Test/MDriveSync.Test.csproj new file mode 100644 index 0000000..09dc83f --- /dev/null +++ b/src/MDriveSync.Test/MDriveSync.Test.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/src/MDriveSync.Test/Properties/launchSettings.json b/src/MDriveSync.Test/Properties/launchSettings.json new file mode 100644 index 0000000..3e0580e --- /dev/null +++ b/src/MDriveSync.Test/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "MDriveSync.Test": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION": "True" + } + } + } +} \ No newline at end of file From b02323568307d22a9c0001205092f69568f36531 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Sat, 10 May 2025 15:00:23 +0800 Subject: [PATCH 63/90] file ignore --- FileIgnore.md | 98 +++ MDriveSync.sln | 1 + .../MDriveSync.Client.csproj | 5 + src/MDriveSync.Client/Program.cs | 200 ++++- src/MDriveSync.Core/MDriveSync.Core.csproj | 9 - .../Services/FastFileScanner1.cs | 816 ++++++++++++++++++ .../Services/FileFastScanner.cs | 190 ++++ .../Services/FileIgnoreHelper.cs | 228 +++++ src/MDriveSync.Core/Services/FileSearcher.cs | 303 +++++-- .../Services/FileUltraScanner.cs | 518 +++++++++++ .../MDriveSync.Infrastructure.csproj | 20 +- .../FileSearcherPerformanceTests.cs | 439 ++++++++++ src/MDriveSync.Test/HashHelperTests.cs | 2 +- src/MDriveSync.Test/HashPerformanceTests.cs | 2 +- 14 files changed, 2738 insertions(+), 93 deletions(-) create mode 100644 FileIgnore.md create mode 100644 src/MDriveSync.Core/Services/FastFileScanner1.cs create mode 100644 src/MDriveSync.Core/Services/FileFastScanner.cs create mode 100644 src/MDriveSync.Core/Services/FileIgnoreHelper.cs create mode 100644 src/MDriveSync.Core/Services/FileUltraScanner.cs create mode 100644 src/MDriveSync.Test/FileSearcherPerformanceTests.cs diff --git a/FileIgnore.md b/FileIgnore.md new file mode 100644 index 0000000..c5735cc --- /dev/null +++ b/FileIgnore.md @@ -0,0 +1,98 @@ +# ļϵͳԹϸĵ + + + Kopia ĺԹ﷨ָ֧ӵͨģʽĿ¼ضƥ䡢񶨹ȡ + +## Թ﷨ + +### ͨ + +| ͨ | | ʾ | ƥ | ƥ | +|--------|------|------|------|--------| +| `*` | ƥ䵥еַ | `*.txt` | `file.txt`, `name.txt` | `file.log`, `dir/file.txt` | +| `**` | ƥ· | `**/logs` | `logs`, `app/logs`, `app/sub/logs` | - | +| `?` | ƥ䵥ַ | `file?.log` | `file1.log`, `fileA.log` | `file.log`, `file12.log` | + +### · + +| ﷨ | | ʾ | ƥ | ƥ | +|------|------|------|------|--------| +| `/pattern` | ֻƥĿ¼µ | `/bin` | `/bin`, `/bin/file.exe` | `app/bin`, `lib/bin` | +| `pattern` | ƥκλõ | `bin` | `bin`, `app/bin`, `bin/file.exe` | - | +| `dir/` | ƥĿ¼ĩβбܣ | `temp/` | `temp/`, `temp/file.txt` | `temp.txt` | + +### ƥ + +| ﷨ | | ʾ | ƥ | ƥ | +|------|------|------|------|--------| +| `!pattern` | 񶨹򣨲ųƥݣ | `!*.pdf` | `report.pdf` | - | +| `[abc]` | ַϣƥ伯һַ | `file[123].txt` | `file1.txt`, `file2.txt`, `file3.txt` | `file4.txt` | +| `[a-z]` | ַΧƥ䷶Χһַ | `file[a-c].txt` | `filea.txt`, `fileb.txt`, `filec.txt` | `filed.txt` | + +## ͺȼ + +### + +1. **ų**׼򣩣ĬϹͣƥļĿ¼ų +2. **** `!` ͷ񶨹ƥļĿ¼ʹƥų + +### ȼ + +1. бе˳ +2. ĹԸǰЧ +3. һƥĹļĿ¼Ƿ񱻺ԡ +4. ûйƥ䣬ĬϰļĿ¼ + +### ߼ + +ļ/Ŀ¼Ƿ񱻺Եж̣ + +``` +ʼ״̬ļӦԣ + +ÿ: + ļƥ: + ų: + ļӦ + ǰ: + ļӦ + +״̬ļDZԻǰ +``` + +## ʹʾ + +### ʹ + +```csharp +// ĬϵĺԹ +var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns(); +``` + +### ԶԹ + +```csharp +// ԶԹ +var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "*.obj", // .objļ + "*.tmp", // .tmpļ + "/bin", // ԸĿ¼µbinļ + "node_modules", // node_modulesĿ¼ + "**/debug/**", // debugĿ¼ + "**/.git/**", // .gitĿ¼ + "!*.pdf", // PDFļ + "!important/**" // importantĿ¼µκ +); +``` + +### ļϵͳɨ + +```csharp +// ΪļϵͳŻʹ÷ʽ +var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "*.bak", + "*.tmp", + "**/temp/**", + "**/cache/**" +); +``` diff --git a/MDriveSync.sln b/MDriveSync.sln index dbdbf0a..039bcb1 100644 --- a/MDriveSync.sln +++ b/MDriveSync.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0FF6CD64-0 ProjectSection(SolutionItems) = preProject CHANGELOG.md = CHANGELOG.md docs\Compression.md = docs\Compression.md + FileIgnore.md = FileIgnore.md README.md = README.md EndProjectSection EndProject diff --git a/src/MDriveSync.Client/MDriveSync.Client.csproj b/src/MDriveSync.Client/MDriveSync.Client.csproj index 2d6280a..f2ee9b3 100644 --- a/src/MDriveSync.Client/MDriveSync.Client.csproj +++ b/src/MDriveSync.Client/MDriveSync.Client.csproj @@ -8,7 +8,12 @@ + + + + + diff --git a/src/MDriveSync.Client/Program.cs b/src/MDriveSync.Client/Program.cs index 5ede57e..62edc20 100644 --- a/src/MDriveSync.Client/Program.cs +++ b/src/MDriveSync.Client/Program.cs @@ -1,14 +1,191 @@ -using MDriveSync.Security; +using MDriveSync.Core.Services; +using MDriveSync.Security; using System.Diagnostics; namespace MDriveSync.Client { internal class Program { - static void Main(string[] args) + private static async Task Main(string[] args) { - var sw = new Stopwatch(); + var rootPath = "E:\\guanpeng"; // args.Length > 0 ? args[0] : Environment.CurrentDirectory; + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns("**/node_modules/*", "**/bin/*", "**/obj/*"); + Console.WriteLine($"开始扫描目录: {rootPath}"); + + // 示例使用方法 + + sw.Start(); + var files = FileFastScanner.EnumerateFiles( + rootPath, + "*.*", + ignorePatterns, + maxDegreeOfParallelism: 4, + errorHandler: (path, ex) => Console.WriteLine($"Error processing {path}: {ex.Message}") + ).ToList(); + sw.Stop(); + + Console.WriteLine($"扫描完成! 耗时: {sw.Elapsed.TotalSeconds:F2} 秒, count: {files.Count}"); + + var cts = new CancellationTokenSource(); + + // 注册取消处理 + Console.CancelKeyPress += (s, e) => + { + Console.WriteLine("\n取消扫描..."); + cts.Cancel(); + e.Cancel = true; + }; + + try + { + var scanner = new FileUltraScanner( + rootPath, + maxConcurrency: Environment.ProcessorCount * 2, + batchSize: 8192, + followSymlinks: false, + ignorePatterns + ); + + var result = await scanner.ScanAsync( + reportProgress: true, + progressIntervalMs: 100, + cancellationToken: cts.Token + ); + + Console.WriteLine("\n\n扫描完成!"); + Console.WriteLine($"总文件数: {result.FileCount:N0}"); + Console.WriteLine($"总目录数: {result.DirectoryCount:N0}"); + Console.WriteLine($"总项目数: {result.FileCount + result.DirectoryCount:N0}"); + Console.WriteLine($"扫描耗时: {result.ElapsedTime.TotalSeconds:F2} 秒"); + Console.WriteLine($"处理速度: {result.ItemsPerSecond:N0} 项/秒"); + + if (result.Errors.Count > 0) + { + Console.WriteLine($"扫描过程中发生了 {result.Errors.Count} 个错误"); + + // 显示前5个错误 + int errorsToShow = Math.Min(5, result.Errors.Count); + for (int i = 0; i < errorsToShow; i++) + { + Console.WriteLine($"- {result.Errors[i]}"); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine("\n扫描已取消"); + } + catch (Exception ex) + { + Console.WriteLine($"\n扫描过程中发生错误: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + + //var cts = new CancellationTokenSource(); + + //// 捕获Ctrl+C以取消扫描 + //Console.CancelKeyPress += (s, e) => { + // e.Cancel = true; + // cts.Cancel(); + // Console.WriteLine("\n扫描已被用户取消..."); + //}; + + //try + //{ + // var result = await FastDirectoryScanner.ScanDirectoryFastAsync( + // path, + // reportProgress: true, + // cancellationToken: cts.Token + // ); + + // Console.WriteLine("\n\n扫描结果摘要:"); + // Console.WriteLine($"目录数量: {result.DirectoryCount:N0}"); + // Console.WriteLine($"文件数量: {result.FileCount:N0}"); + // Console.WriteLine($"总数量: {result.DirectoryCount + result.FileCount:N0}"); + // Console.WriteLine($"扫描耗时: {result.ElapsedTime.TotalSeconds:F2} 秒"); + // Console.WriteLine($"处理速度: {result.ItemsPerSecond:F2} 项/秒"); + + // if (result.Errors.Count > 0) + // { + // Console.WriteLine($"发生 {result.Errors.Count} 个错误"); + // // 可选: 输出前几个错误 + // int errorsToPrint = Math.Min(5, result.Errors.Count); + // for (int i = 0; i < errorsToPrint; i++) + // { + // Console.WriteLine($"- {result.Errors[i]}"); + // } + // } + + // // 如果需要,可以访问完整的文件和目录列表 + // // foreach (var dir in result.Directories) { ... } + // // foreach (var file in result.Files) { ... } + //} + //catch (OperationCanceledException) + //{ + // Console.WriteLine("\n扫描已取消"); + //} + //catch (Exception ex) + //{ + // Console.WriteLine($"\n扫描时发生错误: {ex.Message}"); + //} + + //string path = "E:\\guanpeng\\docs"; // args.Length > 0 ? args[0] : Environment.CurrentDirectory; + //Console.WriteLine($"开始扫描目录: {path}"); + + //var progress = new Progress(p => + //{ + // Console.Write($"\r文件: {p.FilesProcessed:N0}, 目录: {p.DirectoriesProcessed:N0}, " + + // $"大小: {FormatSize(p.BytesProcessed)}, 速度: {p.FormattedBytesPerSecond}"); + //}); + + //var options = new ScanOptions + //{ + // MaxDepth = 0, // 无限深度 + // FollowSymlinks = false, + // IgnoreHiddenFiles = true, + // IgnoreSystemFiles = true, + // IgnoreHiddenDirectories = true, + // FileFoundCallback = (file) => { + // // 可选文件处理回调,此处为空以提高性能 + // } + //}; + + //var scanner = new FastFileScanner(options, progress: progress); + //var cts = new CancellationTokenSource(); + + //// 捕获Ctrl+C以取消扫描 + //Console.CancelKeyPress += (s, e) => { + // e.Cancel = true; + // cts.Cancel(); + // Console.WriteLine("\n扫描已被用户取消..."); + //}; + + //try + //{ + // var result = await scanner.ScanAsync(path, cts.Token); + + // Console.WriteLine("\n\n扫描完成!"); + // Console.WriteLine($"总计文件: {result.TotalFiles:N0}"); + // Console.WriteLine($"总计目录: {result.TotalDirectories:N0}"); + // Console.WriteLine($"总计大小: {result.FormattedTotalSize}"); + // Console.WriteLine($"耗时: {result.ElapsedTime.TotalSeconds:F2} 秒"); + // Console.WriteLine($"速度: {result.FormattedBytesPerSecond}"); + // Console.WriteLine($"每秒处理项目: {result.ItemsPerSecond:F2} 项/秒"); + + // if (result.Errors.Count > 0) + // Console.WriteLine($"错误数量: {result.Errors.Count}"); + //} + //catch (OperationCanceledException) + //{ + // Console.WriteLine("\n扫描已取消"); + //} + //catch (Exception ex) + //{ + // Console.WriteLine($"\n扫描时发生错误: {ex.Message}"); + //} + + return; //sw.Restart(); //LocalStorage.RunRestore(); @@ -29,5 +206,20 @@ static void Main(string[] args) Console.WriteLine("Hello, World!"); } + + private static string FormatSize(long bytes) + { + string[] units = { "B", "KB", "MB", "GB", "TB" }; + double size = bytes; + int unit = 0; + + while (size >= 1024 && unit < units.Length - 1) + { + size /= 1024; + unit++; + } + + return $"{size:0.##} {units[unit]}"; + } } -} +} \ No newline at end of file diff --git a/src/MDriveSync.Core/MDriveSync.Core.csproj b/src/MDriveSync.Core/MDriveSync.Core.csproj index c1249da..0acdc10 100644 --- a/src/MDriveSync.Core/MDriveSync.Core.csproj +++ b/src/MDriveSync.Core/MDriveSync.Core.csproj @@ -20,15 +20,6 @@ --> - - - - - - - - - diff --git a/src/MDriveSync.Core/Services/FastFileScanner1.cs b/src/MDriveSync.Core/Services/FastFileScanner1.cs new file mode 100644 index 0000000..1989288 --- /dev/null +++ b/src/MDriveSync.Core/Services/FastFileScanner1.cs @@ -0,0 +1,816 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace MDriveSync.Core.Services +{ + /// + /// 高性能文件扫描器,基于 Kopia 风格设计,针对 .NET Core 优化 + /// + public class FastFileScanner1 + { + private readonly int _maxParallelism; + private readonly int _queueCapacity; + private readonly IProgress _progress; + private readonly ScanOptions _options; + private readonly ConcurrentDictionary _visitedSymlinks; + private readonly ConcurrentDictionary _visitedDirectories; + + /// + /// 创建高性能文件扫描器实例 + /// + /// 扫描选项 + /// 最大并行度,默认为处理器核心数的两倍 + /// 队列容量,默认为 100,000 + /// 进度报告回调 + public FastFileScanner1( + ScanOptions options = null, + int? maxParallelism = null, + int queueCapacity = 100_000, + IProgress progress = null) + { + _options = options ?? new ScanOptions(); + _maxParallelism = maxParallelism ?? Environment.ProcessorCount * 2; + _queueCapacity = queueCapacity; + _progress = progress; + _visitedSymlinks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _visitedDirectories = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// 异步扫描指定路径下的所有文件和目录 + /// + /// 要扫描的根路径 + /// 取消令牌 + /// 扫描结果 + public async Task ScanAsync(string rootPath, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(rootPath)) + throw new ArgumentException("根路径不能为空", nameof(rootPath)); + + if (!Directory.Exists(rootPath)) + throw new DirectoryNotFoundException($"目录不存在: {rootPath}"); + + var result = new ScanResult + { + RootPath = rootPath, + StartTime = DateTime.UtcNow + }; + + var stats = new ScanStatistics(); + var stopwatch = Stopwatch.StartNew(); + var lastProgressUpdate = DateTime.UtcNow; + var progressUpdateInterval = TimeSpan.FromMilliseconds(200); + + // 创建容量有限的阻塞集合作为工作队列 + using var queue = new BlockingCollection(_queueCapacity); + var errors = new ConcurrentBag(); + + // 添加根目录作为第一个工作项 + queue.Add(new WorkItem { Path = rootPath, Depth = 0 }); + _visitedDirectories.TryAdd(rootPath, true); + + // 创建并启动工作任务 + var tasks = new Task[_maxParallelism]; + for (int i = 0; i < _maxParallelism; i++) + { + tasks[i] = Task.Run(() => ProcessQueue(queue, stats, errors, cancellationToken), cancellationToken); + } + + // 标记队列为完成添加 + var completionTask = Task.Run(async () => + { + try + { + // 等待所有工作项处理完毕 + while (!queue.IsCompleted && !cancellationToken.IsCancellationRequested) + { + // 定期报告进度 + if (_progress != null && (DateTime.UtcNow - lastProgressUpdate) > progressUpdateInterval) + { + ReportProgress(stats, stopwatch.Elapsed, result, queue.Count); + lastProgressUpdate = DateTime.UtcNow; + } + await Task.Delay(50, cancellationToken); + + // 检查队列是否为空但仍有任务在处理 - 这是防止死锁的额外安全措施 + if (queue.Count == 0 && tasks.All(t => t.Status == TaskStatus.WaitingForActivation)) + { + break; + } + } + } + finally + { + // 确保队列被标记为完成添加 + if (!queue.IsAddingCompleted) + queue.CompleteAdding(); + } + }, cancellationToken); + + // 等待所有任务完成 + try + { + await Task.WhenAll(tasks.Append(completionTask).ToArray()); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // 扫描被取消,记录取消状态 + result.WasCancelled = true; + } + catch (Exception ex) + { + errors.Add(new ScanError + { + Path = rootPath, + ErrorMessage = $"扫描过程中发生未处理异常: {ex.Message}", + Exception = ex + }); + } + + stopwatch.Stop(); + result.ElapsedTime = stopwatch.Elapsed; + result.TotalFiles = stats.FileCount; + result.TotalDirectories = stats.DirectoryCount; + result.TotalSize = stats.TotalSize; + result.Errors = errors.ToList(); + result.EndTime = DateTime.UtcNow; + + // 最终进度更新 + if (_progress != null) + { + ReportProgress(stats, result.ElapsedTime, result, 0); + } + + return result; + } + + private void ReportProgress(ScanStatistics stats, TimeSpan elapsed, ScanResult result, int queueSize) + { + _progress?.Report(new ScanProgress + { + FilesProcessed = stats.FileCount, + DirectoriesProcessed = stats.DirectoryCount, + BytesProcessed = stats.TotalSize, + ElapsedTime = elapsed, + CurrentQueueSize = queueSize, + ItemsPerSecond = elapsed.TotalSeconds > 0 + ? (stats.FileCount + stats.DirectoryCount) / elapsed.TotalSeconds + : 0, + CurrentResult = result + }); + } + + private void ProcessQueue( + BlockingCollection queue, + ScanStatistics stats, + ConcurrentBag errors, + CancellationToken cancellationToken) + { + try + { + foreach (var item in queue.GetConsumingEnumerable(cancellationToken)) + { + try + { + if (cancellationToken.IsCancellationRequested) + break; + + ProcessWorkItem(item, queue, stats, errors, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + errors.Add(new ScanError + { + Path = item.Path, + ErrorMessage = ex.Message, + Exception = ex + }); + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // 任务取消,正常退出 + } + catch (InvalidOperationException) + { + // 队列已关闭,正常退出 + } + } + + private void ProcessWorkItem( + WorkItem item, + BlockingCollection queue, + ScanStatistics stats, + ConcurrentBag errors, + CancellationToken cancellationToken) + { + // 检查是否超过最大深度 + if (_options.MaxDepth > 0 && item.Depth > _options.MaxDepth) + return; + + try + { + var dirInfo = new DirectoryInfo(item.Path); + + // 检查是否为符号链接 + bool isSymlink = IsSymbolicLink(dirInfo); + if (isSymlink) + { + if (!_options.FollowSymlinks) + return; + + // 处理符号链接循环 + string target = GetSymlinkTarget(dirInfo.FullName); + if (string.IsNullOrEmpty(target) || !_visitedSymlinks.TryAdd(target, true)) + return; + } + + // 目录计数递增 + Interlocked.Increment(ref stats.DirectoryCount); + + // 处理当前目录中的文件 + IEnumerable files; + try + { + files = dirInfo.EnumerateFiles(); + } + catch (Exception ex) + { + errors.Add(new ScanError + { + Path = item.Path, + ErrorMessage = $"枚举文件失败: {ex.Message}", + Exception = ex + }); + files = Enumerable.Empty(); + } + + // 使用并行处理文件,但限制并行度以避免过度并行 + Parallel.ForEach( + files, + new ParallelOptions + { + MaxDegreeOfParallelism = Math.Max(1, _maxParallelism / 4), + CancellationToken = cancellationToken + }, + file => + { + if (cancellationToken.IsCancellationRequested) + return; + + try + { + if (ShouldProcessFile(file)) + { + // 文件计数递增 + Interlocked.Increment(ref stats.FileCount); + // 总大小递增 + Interlocked.Add(ref stats.TotalSize, file.Length); + + // 如果有文件处理回调,则执行 + _options.FileFoundCallback?.Invoke(new FileEntry + { + Path = file.FullName, + Size = file.Length, + CreationTime = file.CreationTimeUtc, + LastWriteTime = file.LastWriteTimeUtc, + LastAccessTime = file.LastAccessTimeUtc, + Attributes = file.Attributes + }); + } + } + catch (Exception ex) + { + errors.Add(new ScanError + { + Path = file.FullName, + ErrorMessage = $"处理文件失败: {ex.Message}", + Exception = ex + }); + } + }); + + // 处理子目录 + IEnumerable subdirs; + try + { + subdirs = dirInfo.EnumerateDirectories(); + } + catch (Exception ex) + { + errors.Add(new ScanError + { + Path = item.Path, + ErrorMessage = $"枚举子目录失败: {ex.Message}", + Exception = ex + }); + subdirs = Enumerable.Empty(); + } + + foreach (var subdir in subdirs) + { + if (cancellationToken.IsCancellationRequested) + break; + + if (ShouldProcessDirectory(subdir)) + { + string fullPath = subdir.FullName; + + // 避免处理已访问过的目录(处理硬链接和交叉文件系统挂载点) + if (_visitedDirectories.TryAdd(fullPath, true)) + { + // 添加子目录到队列 + queue.Add(new WorkItem + { + Path = fullPath, + Depth = item.Depth + 1 + }); + } + } + } + } + catch (DirectoryNotFoundException) + { + // 目录在扫描过程中被删除,忽略 + } + catch (UnauthorizedAccessException ex) + { + errors.Add(new ScanError + { + Path = item.Path, + ErrorMessage = $"访问被拒绝: {ex.Message}", + Exception = ex + }); + } + catch (Exception ex) + { + errors.Add(new ScanError + { + Path = item.Path, + ErrorMessage = ex.Message, + Exception = ex + }); + } + } + + private bool ShouldProcessFile(FileInfo file) + { + try + { + if (_options.IgnoreHiddenFiles && (file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) + return false; + + if (_options.IgnoreSystemFiles && (file.Attributes & FileAttributes.System) == FileAttributes.System) + return false; + + if (_options.ExcludePatterns != null && _options.ExcludePatterns.Any(pattern => + MatchesPattern(file.Name, pattern))) + return false; + + if (_options.IncludePatterns != null && _options.IncludePatterns.Any() && + !_options.IncludePatterns.Any(pattern => + MatchesPattern(file.Name, pattern))) + return false; + + if (_options.MinFileSize.HasValue && file.Length < _options.MinFileSize.Value) + return false; + + if (_options.MaxFileSize.HasValue && file.Length > _options.MaxFileSize.Value) + return false; + + if (_options.MinAge.HasValue && file.LastWriteTimeUtc > DateTime.UtcNow.Subtract(_options.MinAge.Value)) + return false; + + if (_options.MaxAge.HasValue && file.LastWriteTimeUtc < DateTime.UtcNow.Subtract(_options.MaxAge.Value)) + return false; + + return true; + } + catch + { + // 文件访问错误,忽略此文件 + return false; + } + } + + private bool ShouldProcessDirectory(DirectoryInfo dir) + { + try + { + string dirName = dir.Name; + + if (_options.IgnoreHiddenDirectories && (dir.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) + return false; + + if (_options.IgnoreSystemDirectories && (dir.Attributes & FileAttributes.System) == FileAttributes.System) + return false; + + if (_options.ExcludeDirectories != null && _options.ExcludeDirectories.Contains(dirName, StringComparer.OrdinalIgnoreCase)) + return false; + + if (_options.ExcludePatterns != null && _options.ExcludePatterns.Any(pattern => + MatchesPattern(dirName, pattern))) + return false; + + return true; + } + catch + { + // 目录访问错误,忽略此目录 + return false; + } + } + + private bool MatchesPattern(string name, string pattern) + { + return Regex.IsMatch(name, WildcardToRegex(pattern), RegexOptions.IgnoreCase); + } + + private static string WildcardToRegex(string pattern) + { + return "^" + Regex.Escape(pattern) + .Replace("\\*", ".*") + .Replace("\\?", ".") + "$"; + } + + private bool IsSymbolicLink(FileSystemInfo info) + { + return (info.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; + } + + private string GetSymlinkTarget(string path) + { + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Path.GetFullPath(path); + } + else + { + // Unix systems + return Path.GetFullPath(System.IO.File.ReadAllText(path)); + } + } + catch + { + return null; + } + } + + /// + /// 工作项,表示待处理的目录 + /// + private class WorkItem + { + public string Path { get; set; } + public int Depth { get; set; } + } + + /// + /// 扫描统计信息 + /// + public class ScanStatistics + { + public long FileCount; + public long DirectoryCount; + public long TotalSize; + } + } + + /// + /// 扫描选项 + /// + public class ScanOptions + { + /// + /// 最大扫描深度,0 表示无限制 + /// + public int MaxDepth { get; set; } = 0; + + /// + /// 是否跟踪符号链接 + /// + public bool FollowSymlinks { get; set; } = false; + + /// + /// 是否忽略隐藏文件 + /// + public bool IgnoreHiddenFiles { get; set; } = true; + + /// + /// 是否忽略系统文件 + /// + public bool IgnoreSystemFiles { get; set; } = true; + + /// + /// 是否忽略隐藏目录 + /// + public bool IgnoreHiddenDirectories { get; set; } = true; + + /// + /// 是否忽略系统目录 + /// + public bool IgnoreSystemDirectories { get; set; } = true; + + /// + /// 要排除的目录名列表 + /// + public HashSet ExcludeDirectories { get; set; } = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "$RECYCLE.BIN", + "System Volume Information", + "RECYCLER", + "RECYCLED", + "lost+found", + "node_modules", + ".git", + ".svn", + "bin", + "obj", + "packages" + }; + + /// + /// 排除的文件或目录通配符模式列表 + /// + public List ExcludePatterns { get; set; } = new List + { + "*.tmp", + "~*", + "thumbs.db", + "*.swp", + "*.bak", + "*.log", + "*.cache" + }; + + /// + /// 包含的文件通配符模式列表,如果为空,则包含所有文件 + /// + public List IncludePatterns { get; set; } = new List(); + + /// + /// 最小文件大小(字节) + /// + public long? MinFileSize { get; set; } = null; + + /// + /// 最大文件大小(字节) + /// + public long? MaxFileSize { get; set; } = null; + + /// + /// 文件最小年龄(从现在起向后计算) + /// + public TimeSpan? MinAge { get; set; } = null; + + /// + /// 文件最大年龄(从现在起向后计算) + /// + public TimeSpan? MaxAge { get; set; } = null; + + /// + /// 找到文件时的回调函数 + /// + public Action FileFoundCallback { get; set; } = null; + } + + /// + /// 扫描结果 + /// + public class ScanResult + { + /// + /// 扫描的根路径 + /// + public string RootPath { get; set; } + + /// + /// 扫描开始时间 + /// + public DateTime StartTime { get; set; } + + /// + /// 扫描结束时间 + /// + public DateTime EndTime { get; set; } + + /// + /// 扫描耗时 + /// + public TimeSpan ElapsedTime { get; set; } + + /// + /// 扫描的文件总数 + /// + public long TotalFiles { get; set; } + + /// + /// 扫描的目录总数 + /// + public long TotalDirectories { get; set; } + + /// + /// 扫描的文件总大小(字节) + /// + public long TotalSize { get; set; } + + /// + /// 扫描过程中的错误列表 + /// + public List Errors { get; set; } = new List(); + + /// + /// 指示扫描是否被取消 + /// + public bool WasCancelled { get; set; } + + /// + /// 每秒处理的项目数 + /// + public double ItemsPerSecond => ElapsedTime.TotalSeconds > 0 + ? (TotalFiles + TotalDirectories) / ElapsedTime.TotalSeconds + : 0; + + /// + /// 平均处理速度(字节/秒) + /// + public double BytesPerSecond => ElapsedTime.TotalSeconds > 0 + ? TotalSize / ElapsedTime.TotalSeconds + : 0; + + /// + /// 获取格式化的总大小(自动选择合适的单位) + /// + public string FormattedTotalSize => FormatSize(TotalSize); + + /// + /// 获取格式化的处理速度(自动选择合适的单位) + /// + public string FormattedBytesPerSecond => $"{FormatSize(BytesPerSecond)}/s"; + + private static string FormatSize(double bytes) + { + string[] units = { "B", "KB", "MB", "GB", "TB", "PB" }; + int unitIndex = 0; + + while (bytes >= 1024 && unitIndex < units.Length - 1) + { + bytes /= 1024; + unitIndex++; + } + + return $"{bytes:0.##} {units[unitIndex]}"; + } + } + + /// + /// 文件条目信息 + /// + public class FileEntry + { + /// + /// 文件完整路径 + /// + public string Path { get; set; } + + /// + /// 文件大小(字节) + /// + public long Size { get; set; } + + /// + /// 文件创建时间(UTC) + /// + public DateTime CreationTime { get; set; } + + /// + /// 文件最后修改时间(UTC) + /// + public DateTime LastWriteTime { get; set; } + + /// + /// 文件最后访问时间(UTC) + /// + public DateTime LastAccessTime { get; set; } + + /// + /// 文件属性 + /// + public FileAttributes Attributes { get; set; } + + /// + /// 获取格式化的文件大小 + /// + public string FormattedSize + { + get + { + string[] units = { "B", "KB", "MB", "GB", "TB" }; + double size = Size; + int unit = 0; + + while (size >= 1024 && unit < units.Length - 1) + { + size /= 1024; + unit++; + } + + return $"{size:0.##} {units[unit]}"; + } + } + } + + /// + /// 扫描错误信息 + /// + public class ScanError + { + /// + /// 发生错误的路径 + /// + public string Path { get; set; } + + /// + /// 错误消息 + /// + public string ErrorMessage { get; set; } + + /// + /// 原始异常 + /// + public Exception Exception { get; set; } + } + + /// + /// 扫描进度信息 + /// + public class ScanProgress + { + /// + /// 已处理的文件数 + /// + public long FilesProcessed { get; set; } + + /// + /// 已处理的目录数 + /// + public long DirectoriesProcessed { get; set; } + + /// + /// 已处理的字节数 + /// + public long BytesProcessed { get; set; } + + /// + /// 已用时间 + /// + public TimeSpan ElapsedTime { get; set; } + + /// + /// 当前队列大小 + /// + public int CurrentQueueSize { get; set; } + + /// + /// 每秒处理的项目数 + /// + public double ItemsPerSecond { get; set; } + + /// + /// 当前扫描结果(部分完成) + /// + public ScanResult CurrentResult { get; set; } + + /// + /// 获取格式化的处理速度 + /// + public string FormattedBytesPerSecond + { + get + { + string[] units = { "B", "KB", "MB", "GB", "TB" }; + double bytesPerSec = ElapsedTime.TotalSeconds > 0 + ? BytesProcessed / ElapsedTime.TotalSeconds + : 0; + int unit = 0; + + while (bytesPerSec >= 1024 && unit < units.Length - 1) + { + bytesPerSec /= 1024; + unit++; + } + + return $"{bytesPerSec:0.##} {units[unit]}/s"; + } + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileFastScanner.cs b/src/MDriveSync.Core/Services/FileFastScanner.cs new file mode 100644 index 0000000..cb5a341 --- /dev/null +++ b/src/MDriveSync.Core/Services/FileFastScanner.cs @@ -0,0 +1,190 @@ +using System.Collections.Concurrent; + +namespace MDriveSync.Core.Services +{ + /// + /// 高性能文件系统遍历工具 + /// + public class FileFastScanner + { + /// + /// 采用生产者-消费者模式遍历文件系统 + /// + public static IEnumerable EnumerateFiles( + string rootPath, + string searchPattern = "*", + IEnumerable ignorePatterns = null, + int? maxDegreeOfParallelism = null, + Action errorHandler = null) + { + if (string.IsNullOrEmpty(rootPath)) + throw new ArgumentException("根路径不能为空", nameof(rootPath)); + + // 规范化根路径 + rootPath = Path.GetFullPath(rootPath); + + // 创建忽略规则集 + var ignoreRules = new FileIgnoreRuleSet(rootPath, ignorePatterns); + + // 并行度设置 + int parallelism = maxDegreeOfParallelism ?? Math.Max(1, Environment.ProcessorCount); + + // 创建生产者-消费者模式的数据结构 + var directoryQueue = new BlockingCollection(new ConcurrentQueue(), 10000); + var fileResults = new BlockingCollection(new ConcurrentQueue(), 100000); + + // 启动目录生产者任务 + var directoryProducerTask = Task.Run(() => + { + try + { + // 第一个目录是根目录(如果不应被忽略) + if (!ignoreRules.ShouldIgnore(rootPath)) + { + directoryQueue.Add(rootPath); + } + + // 使用广度优先遍历而不是预先收集所有目录 + ProcessDirectories(rootPath, ignoreRules, directoryQueue, errorHandler); + } + finally + { + // 标记目录队列已完成 + directoryQueue.CompleteAdding(); + } + }); + + // 启动文件处理任务 + var fileProcessorTask = Task.Run(() => + { + // 创建并行选项 + var options = new ParallelOptions { MaxDegreeOfParallelism = parallelism }; + + // 从目录队列中并行处理目录 + Parallel.ForEach(directoryQueue.GetConsumingEnumerable(), options, directory => + { + try + { + // 处理单个目录中的文件 + foreach (var file in ProcessFiles(directory, searchPattern, ignoreRules, errorHandler)) + { + fileResults.Add(file); + } + } + catch (Exception ex) + { + errorHandler?.Invoke(directory, ex); + } + }); + + // 所有目录处理完成后,标记文件结果集已完成 + fileResults.CompleteAdding(); + }); + + // 返回文件结果 + foreach (var file in fileResults.GetConsumingEnumerable()) + { + yield return file; + } + + // 确保所有任务完成 + Task.WaitAll(directoryProducerTask, fileProcessorTask); + } + + /// + /// 处理目录(流式方式) + /// + private static void ProcessDirectories( + string rootPath, + FileIgnoreRuleSet ignoreRules, + BlockingCollection directoryQueue, + Action errorHandler = null) + { + // 目录遍历设置 + var options = new EnumerationOptions + { + RecurseSubdirectories = false, + AttributesToSkip = 0, + IgnoreInaccessible = true + }; + + // 已处理目录集合,避免循环引用 + var processedDirs = new HashSet(StringComparer.OrdinalIgnoreCase); + processedDirs.Add(rootPath); + + // 使用队列进行广度优先遍历,但不预先加载所有目录 + var pendingDirs = new Queue(); + pendingDirs.Enqueue(rootPath); + + while (pendingDirs.Count > 0) + { + string currentDir = pendingDirs.Dequeue(); + + try + { + foreach (var subDir in Directory.EnumerateDirectories(currentDir, "*", options)) + { + // 检查目录是否应被忽略 + if (!ignoreRules.ShouldIgnore(subDir) && processedDirs.Add(subDir)) + { + directoryQueue.Add(subDir); + pendingDirs.Enqueue(subDir); + } + } + } + catch (Exception ex) when (ex is UnauthorizedAccessException || + ex is DirectoryNotFoundException || + ex is IOException) + { + errorHandler?.Invoke(currentDir, ex); + } + } + } + + /// + /// 处理单个目录中的文件 + /// + private static IEnumerable ProcessFiles( + string directory, + string searchPattern, + FileIgnoreRuleSet ignoreRules, + Action errorHandler = null) + { + if (string.IsNullOrEmpty(searchPattern)) + { + searchPattern = "*"; + } + + // 避免在包含yield return的方法体中使用try-catch + IEnumerable files; + try + { + var options = new EnumerationOptions + { + RecurseSubdirectories = false, + AttributesToSkip = FileAttributes.System, + IgnoreInaccessible = true + }; + + files = Directory.EnumerateFiles(directory, searchPattern, options); + } + catch (Exception ex) when (ex is UnauthorizedAccessException || + ex is DirectoryNotFoundException || + ex is IOException) + { + errorHandler?.Invoke(directory, ex); + yield break; + } + + // 处理找到的文件 + foreach (var file in files) + { + // 检查文件是否应被忽略 + if (!ignoreRules.ShouldIgnore(file)) + { + yield return file; + } + } + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileIgnoreHelper.cs b/src/MDriveSync.Core/Services/FileIgnoreHelper.cs new file mode 100644 index 0000000..3bf22a9 --- /dev/null +++ b/src/MDriveSync.Core/Services/FileIgnoreHelper.cs @@ -0,0 +1,228 @@ +using System.Text.RegularExpressions; +using static MDriveSync.Core.Services.FileIgnoreHelper; + +namespace MDriveSync.Core.Services +{ + /// + /// 文件忽略助手(兼容 kopia 忽略规则) + /// + public class FileIgnoreHelper + { + /// + /// 忽略规则类型 + /// + public enum IgnoreRuleType + { + Include, // 包含规则(以!开头的规则) + Exclude // 排除规则(标准规则) + } + + /// + /// 忽略模式列表 + /// + public static List BuildIgnorePatterns(params string[] patterns) + { + var result = new List + { + // 添加常见的默认忽略 + "*.tmp", + "*.temp", + "*~", + ".DS_Store", + "Thumbs.db", + "$RECYCLE.BIN", + "System Volume Information" + }; + + // 添加用户自定义规则 + if (patterns != null && patterns.Length > 0) + { + result.AddRange(patterns.Where(p => !string.IsNullOrWhiteSpace(p))); + } + + return result; + } + } + + /// + /// 忽略规则 + /// + public class FileIgnoreRule + { + public string Pattern { get; } // 原始模式 + public IgnoreRuleType Type { get; } // 规则类型 + public Regex CompiledPattern { get; } // 编译后的正则表达式 + public bool IsRootOnly { get; } // 是否只适用于根目录 + + public FileIgnoreRule(string pattern) + { + if (string.IsNullOrWhiteSpace(pattern)) + { + throw new ArgumentException("忽略规则不能为空", nameof(pattern)); + } + + // 检查是否为包含规则(以!开头) + if (pattern.StartsWith("!")) + { + Type = IgnoreRuleType.Include; + pattern = pattern.Substring(1); + } + else + { + Type = IgnoreRuleType.Exclude; + } + + // 检查是否为根目录专用规则 + if (pattern.StartsWith("/")) + { + IsRootOnly = true; + pattern = pattern.Substring(1); + } + else + { + IsRootOnly = false; + } + + Pattern = pattern; + + // 将通配符模式转换为正则表达式 + string regexPattern = WildcardToRegex(pattern); + CompiledPattern = new Regex(regexPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + } + + /// + /// 将通配符转换为正则表达式 - 修复版 + /// + private string WildcardToRegex(string pattern) + { + if (string.IsNullOrEmpty(pattern)) + return "^$"; + + // 逐字符处理通配符模式,避免复杂的转义问题 + string result = "^"; + + for (int i = 0; i < pattern.Length; i++) + { + char c = pattern[i]; + + if (c == '*' && i + 1 < pattern.Length && pattern[i + 1] == '*') + { + // 处理 ** 模式(匹配任意路径) + result += ".*?"; + i++; // 跳过下一个 * + } + else if (c == '*') + { + // 处理 * 模式(匹配单层中的任意字符) + result += "[^/\\\\]*"; + } + else if (c == '?') + { + // 处理 ? 模式(匹配单个字符) + result += "[^/\\\\]"; + } + else if (c == '[' || c == ']') + { + // 方括号原样保留 + result += c; + } + else + { + // 其他字符需要转义 + result += Regex.Escape(c.ToString()); + } + } + + return result + "$"; + } + + /// + /// 检查路径是否匹配当前规则 + /// + public bool IsMatch(string path, string rootPath) + { + if (string.IsNullOrEmpty(path)) + return false; + + // 标准化路径分隔符 + path = path.Replace('\\', '/'); + + // 如果是根目录规则,确保路径相对于根目录 + if (IsRootOnly && !string.IsNullOrEmpty(rootPath)) + { + rootPath = rootPath.Replace('\\', '/').TrimEnd('/') + "/"; + + // 获取相对于根目录的路径 + if (path.StartsWith(rootPath)) + { + path = path.Substring(rootPath.Length).TrimStart('/'); + } + else + { + return false; // 不在根目录下,不适用此规则 + } + } + + // 执行正则表达式匹配 + return CompiledPattern.IsMatch(path); + } + } + + /// + /// 忽略规则集合 + /// + public class FileIgnoreRuleSet + { + private readonly List _rules = new List(); + private readonly string _rootPath; + + public FileIgnoreRuleSet(string rootPath, IEnumerable patterns) + { + if (string.IsNullOrEmpty(rootPath)) + throw new ArgumentException("根路径不能为空", nameof(rootPath)); + + _rootPath = rootPath.Replace('\\', '/').TrimEnd('/') + "/"; + + patterns ??= BuildIgnorePatterns(); + + if (patterns != null) + { + foreach (var pattern in patterns.Where(p => !string.IsNullOrWhiteSpace(p))) + { + AddRule(pattern); + } + } + } + + public void AddRule(string pattern) + { + if (!string.IsNullOrWhiteSpace(pattern)) + { + _rules.Add(new FileIgnoreRule(pattern)); + } + } + + /// + /// 检查路径是否应被忽略 + /// + public bool ShouldIgnore(string path) + { + if (string.IsNullOrEmpty(path)) + return false; + + // 默认不忽略 + bool shouldExclude = false; + + // 规则按顺序执行,后面的规则可以覆盖前面的规则 + foreach (var rule in _rules) + { + if (rule.IsMatch(path, _rootPath)) + { + shouldExclude = (rule.Type == IgnoreRuleType.Exclude); + } + } + + return shouldExclude; + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileSearcher.cs b/src/MDriveSync.Core/Services/FileSearcher.cs index fd2e46a..31462f3 100644 --- a/src/MDriveSync.Core/Services/FileSearcher.cs +++ b/src/MDriveSync.Core/Services/FileSearcher.cs @@ -1,53 +1,145 @@ using System.Collections.Concurrent; +using System.Diagnostics; +using System.Text.RegularExpressions; namespace MDriveSync.Core.Services { public class FileSearcher { /// - /// 文件搜索 - 在处理小型目录或需要立即访问所有文件和目录的场景中更有优势。 + /// 文件搜索(少量文件) - 在处理小型目录或需要立即访问所有文件和目录的场景中更有优势。 /// /// Directory.GetDirectories 和 Directory.GetFiles /// 行为: 这两个方法在返回结果之前,会检索并存储目标路径下的所有目录或文件。也就是说,它们会完全遍历指定路径下的所有目录或文件,并将它们作为一个数组返回。 /// 适用场景: 当您需要立即访问所有结果,并且结果集不太大(不会导致显著的内存占用)时,这两个方法是合适的。 /// 效率: 对于小型目录,这些方法通常效率很高,因为它们一次性加载所有数据。但对于包含大量文件或目录的路径,它们可能会导致显著的性能开销,因为需要等待整个目录树被遍历完毕。 /// - /// - /// - /// - public static ConcurrentBag SearchFiles(string rootPath, int processorCount = 4) + /// 根路径 + /// 处理器数量 + /// 是否报告进度 + /// 进度报告间隔(毫秒) + /// 是否跟踪符号链接 + /// 排除的目录名列表 + /// 取消令牌 + /// 文件集合 + public static ConcurrentBag SearchFiles( + string rootPath, + int processorCount = 4, + bool reportProgress = false, + int progressIntervalMs = 100, + bool followSymlinks = false, + IList excludeDirs = null, + CancellationToken cancellationToken = default) { var files = new ConcurrentBag(); - var options = new ParallelOptions { MaxDegreeOfParallelism = processorCount }; + var options = new ParallelOptions + { + MaxDegreeOfParallelism = processorCount, + CancellationToken = cancellationToken + }; + var stopwatch = new Stopwatch(); + var lastProgressReport = DateTime.UtcNow; + var processedItems = 0; + var visitedPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + var ignoreRule = new FileIgnoreRuleSet(rootPath, excludeDirs); + + stopwatch.Start(); try { - Parallel.ForEach(Directory.GetDirectories(rootPath, "*", SearchOption.AllDirectories), options, directory => + // 获取所有子目录(排除指定目录) + var allDirectories = Directory.GetDirectories(rootPath, "*", SearchOption.AllDirectories); + + // 过滤排除的目录和符号链接 + var directories = allDirectories.Where(dir => !ignoreRule.ShouldIgnore(dir) && + (followSymlinks || !IsSymbolicLink(dir))) + .ToArray(); + + // 并行处理每个目录 + Parallel.ForEach(directories, options, directory => { - foreach (var file in Directory.GetFiles(directory)) + if (cancellationToken.IsCancellationRequested) + return; + + try { - files.Add(file); + foreach (var file in Directory.GetFiles(directory)) + { + if (cancellationToken.IsCancellationRequested) + break; + + if (ignoreRule.ShouldIgnore(file)) + continue; + + files.Add(file); + Interlocked.Increment(ref processedItems); + + // 更新进度 + if (reportProgress && (DateTime.UtcNow - lastProgressReport).TotalMilliseconds >= progressIntervalMs) + { + lock (stopwatch) + { + if ((DateTime.UtcNow - lastProgressReport).TotalMilliseconds >= progressIntervalMs) + { + ReportProgress(processedItems, files.Count, stopwatch.Elapsed); + lastProgressReport = DateTime.UtcNow; + } + } + } + } + } + catch (UnauthorizedAccessException ex) + { + Console.WriteLine($"访问目录被拒绝: {directory},错误: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"处理目录发生错误: {directory},错误: {ex.Message}"); } }); - foreach (var file in Directory.GetFiles(rootPath)) + // 处理根目录下的文件 + if (!cancellationToken.IsCancellationRequested) + { + foreach (var file in Directory.GetFiles(rootPath)) + { + if (cancellationToken.IsCancellationRequested) + break; + + files.Add(file); + Interlocked.Increment(ref processedItems); + } + } + + // 最终进度报告 + if (reportProgress && !cancellationToken.IsCancellationRequested) { - files.Add(file); + ReportProgress(processedItems, files.Count, stopwatch.Elapsed, true); } } + catch (OperationCanceledException) + { + Console.WriteLine("搜索操作已取消"); + } catch (UnauthorizedAccessException ex) { - Console.WriteLine("Access Denied: " + ex.Message); + Console.WriteLine("访问被拒绝: " + ex.Message); } catch (Exception ex) { - Console.WriteLine("Exception: " + ex.Message); + Console.WriteLine("异常: " + ex.Message); } + finally + { + stopwatch.Stop(); + } + return files; } /// - /// 文件搜索 - 在处理大型目录和需要逐个处理文件或目录的场景中更有效,尤其是在性能和内存使用方面。 + /// 文件搜索(大量文件) - 在处理大型目录和需要逐个处理文件或目录的场景中更有效,尤其是在性能和内存使用方面。 /// /// Directory.EnumerateDirectories 和 Directory.EnumerateFiles /// 行为: 这两个方法使用延迟执行(lazy evaluation)。它们返回一个可迭代的集合,该集合在遍历过程中一次只加载一个目录或文件。 @@ -55,94 +147,157 @@ public static ConcurrentBag SearchFiles(string rootPath, int processorCo /// 效率: 对于大型目录,这些方法通常更高效,因为它们不需要一开始就加载所有数据。它们在内存占用方面也更加高效,特别是在处理大型文件系统时。 /// /// - /// - /// - /// - public static ConcurrentBag<(string, long, DateTime)> GetFiles(string rootPath, int processorCount = 4) + /// 根路径 + /// 处理器数量 + /// 是否报告进度 + /// 进度报告间隔(毫秒) + /// 是否跟踪符号链接 + /// 排除的目录名列表 + /// 取消令牌 + /// 文件信息元组集合 (文件路径, 大小, 修改时间) + public static ConcurrentBag<(string, long, DateTime)> GetFiles( + string rootPath, + int processorCount = 4, + bool reportProgress = false, + int progressIntervalMs = 100, + bool followSymlinks = false, + IList excludeDirs = null, + CancellationToken cancellationToken = default) { var fileInfoBag = new ConcurrentBag<(string, long, DateTime)>(); + var options = new ParallelOptions + { + MaxDegreeOfParallelism = processorCount, + CancellationToken = cancellationToken + }; + var stopwatch = new Stopwatch(); + var lastProgressReport = DateTime.UtcNow; + var processedItems = 0; + var visitedPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + var ignoreRule = new FileIgnoreRuleSet(rootPath, excludeDirs); + + stopwatch.Start(); - var options = new ParallelOptions { MaxDegreeOfParallelism = processorCount }; - Parallel.ForEach(Directory.EnumerateDirectories(rootPath, "*", SearchOption.AllDirectories), options, directory => + try { - try + // 使用延迟枚举目录,过滤排除的目录 + var directories = Directory.EnumerateDirectories(rootPath, "*", SearchOption.AllDirectories) + .Where(dir => !ignoreRule.ShouldIgnore(dir) && + (followSymlinks || !IsSymbolicLink(dir))); + + Parallel.ForEach(directories, options, directory => { - foreach (var file in Directory.EnumerateFiles(directory)) + if (cancellationToken.IsCancellationRequested) + return; + + try + { + foreach (var file in Directory.EnumerateFiles(directory)) + { + if (cancellationToken.IsCancellationRequested) + break; + + if (ignoreRule.ShouldIgnore(file)) + continue; + + try + { + var fileInfo = new FileInfo(file); + fileInfoBag.Add((file, fileInfo.Length, fileInfo.LastWriteTime)); + Interlocked.Increment(ref processedItems); + + // 更新进度 + if (reportProgress && (DateTime.UtcNow - lastProgressReport).TotalMilliseconds >= progressIntervalMs) + { + lock (stopwatch) + { + if ((DateTime.UtcNow - lastProgressReport).TotalMilliseconds >= progressIntervalMs) + { + ReportProgress(processedItems, fileInfoBag.Count, stopwatch.Elapsed); + lastProgressReport = DateTime.UtcNow; + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"处理文件失败: {file},错误: {ex.Message}"); + } + } + } + catch (Exception ex) { + Console.WriteLine($"枚举目录文件失败: {directory},错误: {ex.Message}"); + } + }); + + // 处理根目录下的文件 + if (!cancellationToken.IsCancellationRequested) + { + foreach (var file in Directory.EnumerateFiles(rootPath)) + { + if (cancellationToken.IsCancellationRequested) + break; + try { var fileInfo = new FileInfo(file); fileInfoBag.Add((file, fileInfo.Length, fileInfo.LastWriteTime)); + Interlocked.Increment(ref processedItems); } catch (Exception ex) { - Console.WriteLine(ex.Message); + Console.WriteLine($"处理文件失败: {file},错误: {ex.Message}"); } } } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - } - }); - - return fileInfoBag; - } - /* - * 测试其他方案 - * - private static ConcurrentBag files = new ConcurrentBag(); - private static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(10); - - public async Task SearchAsync(string rootPath) - { - await semaphoreSlim.WaitAsync(); // 等待获取信号量 - - try - { - string[] directories = Directory.GetDirectories(rootPath); - string[] filesInCurrentDir = Directory.GetFiles(rootPath); - - foreach (var file in filesInCurrentDir) + // 最终进度报告 + if (reportProgress && !cancellationToken.IsCancellationRequested) { - files.Add(file); + ReportProgress(processedItems, fileInfoBag.Count, stopwatch.Elapsed, true); } - - var tasks = new List(); - foreach (var dir in directories) - { - // 限制同时运行的任务数量 - if (semaphoreSlim.CurrentCount == 0) - { - // 等待已启动的任务之一完成 - await Task.WhenAny(tasks.ToArray()); - tasks.RemoveAll(t => t.IsCompleted); // 移除已完成的任务 - } - - semaphoreSlim.Wait(); // 同步获取信号量 - var task = SearchAsync(dir); - tasks.Add(task); - - // 异步释放信号量,允许其他任务继续 - _ = task.ContinueWith(t => semaphoreSlim.Release()); - } - - await Task.WhenAll(tasks); // 等待所有任务完成 } - catch (UnauthorizedAccessException ex) + catch (OperationCanceledException) { - Console.WriteLine("Access Denied: " + ex.Message); + Console.WriteLine("搜索操作已取消"); } catch (Exception ex) { - Console.WriteLine("Exception: " + ex.Message); + Console.WriteLine($"搜索过程发生错误: {ex.Message}"); } finally { - semaphoreSlim.Release(); // 释放信号量 + stopwatch.Stop(); } + + return fileInfoBag; + } + + + /// + /// 判断是否是符号链接 + /// + private static bool IsSymbolicLink(string path) + { + var attr = File.GetAttributes(path); + return (attr & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; + } + + /// + /// 报告进度 + /// + private static void ReportProgress(int processedItems, int fileCount, TimeSpan elapsed, bool final = false) + { + double itemsPerSec = elapsed.TotalSeconds > 0 + ? processedItems / elapsed.TotalSeconds + : 0; + + Console.Write($"\r已处理: {processedItems:N0} | 文件: {fileCount:N0} | 耗时: {elapsed.TotalSeconds:F2}秒 | {itemsPerSec:N0} 项/秒"); + + if (final) + Console.WriteLine(); } - */ } } \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileUltraScanner.cs b/src/MDriveSync.Core/Services/FileUltraScanner.cs new file mode 100644 index 0000000..57f0165 --- /dev/null +++ b/src/MDriveSync.Core/Services/FileUltraScanner.cs @@ -0,0 +1,518 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace MDriveSync.Core.Services +{ + /// + /// 与 Kopia 算法保持一致的超高性能文件扫描器 + /// + public class FileUltraScanner + { + // Windows API 相关常量 + private const int MAX_PATH = 260; + + private const int MAX_ALTERNATIVE_PATH = 32767; + + private const int FILE_ATTRIBUTE_DIRECTORY = 0x10; + private const int FILE_ATTRIBUTE_REPARSE_POINT = 0x400; + + private const int ERROR_NO_MORE_FILES = 18; + private const uint INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private struct WIN32_FIND_DATA + { + public FileAttributes dwFileAttributes; + public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime; + public uint nFileSizeHigh; + public uint nFileSizeLow; + public uint dwReserved0; + public uint dwReserved1; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)] + public string cFileName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] + public string cAlternateFileName; + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FindClose(IntPtr hFindFile); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern uint GetFileAttributes(string lpFileName); + + // Linux/macOS 相关结构和方法 + [DllImport("libc", SetLastError = true, EntryPoint = "opendir")] + private static extern IntPtr OpenDir(string path); + + [DllImport("libc", SetLastError = true, EntryPoint = "readdir")] + private static extern IntPtr ReadDir(IntPtr dir); + + [DllImport("libc", EntryPoint = "closedir")] + private static extern int CloseDir(IntPtr dir); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + private struct Dirent + { + public ulong d_ino; + public long d_off; + public ushort d_reclen; + public byte d_type; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string d_name; + } + + private const byte DT_DIR = 4; // 目录类型 + private const byte DT_REG = 8; // 普通文件类型 + private const byte DT_LNK = 10; // 符号链接类型 + + // 内部状态和设置 + private readonly int _maxConcurrency; + + private readonly int _batchSize; + private readonly ConcurrentBag _files; + private readonly ConcurrentBag _directories; + private readonly ConcurrentBag _errors; + private readonly ConcurrentDictionary _visitedPaths; + private long _processedItems; + private readonly Stopwatch _stopwatch; + private readonly bool _followSymlinks; + private readonly FileIgnoreRuleSet _fileIgnoreRuleSet; + private readonly string _rootPath; + + /// + /// 创建超高性能文件扫描器 + /// + /// 最大并发数,默认处理器数的2倍 + /// 批处理大小,默认4096 + /// 是否跟踪符号链接,默认false + /// 排除的目录名列表 + public FileUltraScanner( + string rootPath, + int? maxConcurrency = null, + int batchSize = 4096, + bool followSymlinks = false, + IList excludeDirs = null) + { + if (!Directory.Exists(rootPath)) + throw new DirectoryNotFoundException($"目录不存在: {rootPath}"); + + // 确保路径标准化(使用全路径) + _rootPath = Path.GetFullPath(rootPath); + + _maxConcurrency = maxConcurrency ?? Environment.ProcessorCount * 2; + _batchSize = batchSize; + _followSymlinks = followSymlinks; + + // 创建忽略规则集 + _fileIgnoreRuleSet = new FileIgnoreRuleSet(_rootPath, excludeDirs); + + _files = new ConcurrentBag(); + _directories = new ConcurrentBag(); + _errors = new ConcurrentBag(); + _visitedPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _processedItems = 0; + _stopwatch = new Stopwatch(); + } + + /// + /// 扫描文件系统 + /// + /// 根路径 + /// 是否报告进度 + /// 进度报告间隔(毫秒) + /// 取消令牌 + /// 扫描结果 + public async Task ScanAsync(bool reportProgress = false, int progressIntervalMs = 100, CancellationToken cancellationToken = default) + { + // 初始化工作队列和信号量 + var workQueue = new ConcurrentQueue(); + using var semaphore = new SemaphoreSlim(_maxConcurrency); + + // 将根目录加入队列 + workQueue.Enqueue(new WorkItem { Path = _rootPath, Depth = 0 }); + _visitedPaths.TryAdd(_rootPath, true); + _directories.Add(_rootPath); + + // 启动计时器 + _stopwatch.Start(); + var startTime = DateTime.UtcNow; + var lastProgressReport = DateTime.UtcNow; + + // 创建完成事件 + using var scanCompletionEvent = new ManualResetEventSlim(false); + var activeWorkers = 0; + + // 开始处理队列 + while (!workQueue.IsEmpty || activeWorkers > 0) + { + if (cancellationToken.IsCancellationRequested) + break; + + // 尝试获取工作项 + if (workQueue.TryDequeue(out var workItem)) + { + // 等待信号量 + await semaphore.WaitAsync(cancellationToken); + + Interlocked.Increment(ref activeWorkers); + + // 异步处理工作项 + _ = Task.Run(() => + { + try + { + // 使用底层API扫描目录 + var (dirs, files, errors) = ScanDirectoryFast(workItem.Path, workItem.Depth); + + // 记录结果 + foreach (var error in errors) + _errors.Add(error); + + foreach (var file in files) + { + _files.Add(file); + Interlocked.Increment(ref _processedItems); + } + + foreach (var dir in dirs) + { + _directories.Add(dir); + Interlocked.Increment(ref _processedItems); + + // 将新目录加入队列 + if (_visitedPaths.TryAdd(dir, true)) + { + workQueue.Enqueue(new WorkItem { Path = dir, Depth = workItem.Depth + 1 }); + } + } + + // 更新进度 + if (reportProgress && (DateTime.UtcNow - lastProgressReport).TotalMilliseconds >= progressIntervalMs) + { + ReportProgress(workQueue.Count, activeWorkers); + lastProgressReport = DateTime.UtcNow; + } + } + finally + { + // 释放信号量 + semaphore.Release(); + Interlocked.Decrement(ref activeWorkers); + + // 如果所有工作都完成,设置完成事件 + if (workQueue.IsEmpty && activeWorkers == 0) + { + scanCompletionEvent.Set(); + } + } + }, cancellationToken); + } + else if (activeWorkers > 0) + { + // 队列暂时空了,但仍有活动工作,等待20毫秒 + await Task.Delay(20, cancellationToken); + } + else + { + // 所有工作都完成了 + break; + } + } + + // 等待所有任务完成 + try + { + await Task.Run(() => scanCompletionEvent.Wait(cancellationToken), cancellationToken); + } + catch (OperationCanceledException) + { + // 扫描被取消 + } + + _stopwatch.Stop(); + + if (reportProgress) + { + ReportProgress(0, 0, true); + } + + // 创建结果 + return new ScanResult + { + RootPath = _rootPath, + FileCount = _files.Count, + DirectoryCount = _directories.Count, + Files = new List(_files), + Directories = new List(_directories), + Errors = new List(_errors), + ElapsedTime = _stopwatch.Elapsed, + StartTime = startTime, + EndTime = DateTime.UtcNow + }; + } + + private void ReportProgress(int queueCount, int activeWorkers, bool final = false) + { + double itemsPerSec = _stopwatch.Elapsed.TotalSeconds > 0 + ? _processedItems / _stopwatch.Elapsed.TotalSeconds + : 0; + + Console.Write($"\r已处理: {_processedItems:N0} | 文件: {_files.Count:N0} | 目录: {_directories.Count:N0} | " + + $"队列: {queueCount:N0} | 工作线程: {activeWorkers} | {itemsPerSec:N0} 项/秒"); + + if (final) + Console.WriteLine(); + } + + private (List Directories, List Files, List Errors) ScanDirectoryFast(string path, int depth) + { + var dirs = new List(_batchSize); + var files = new List(_batchSize); + var errors = new List(); + + // 根据平台选择不同的实现 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + ScanDirectoryWindows(path, dirs, files, errors); + } + else + { + ScanDirectoryUnix(path, dirs, files, errors); + } + + return (dirs, files, errors); + } + + private void ScanDirectoryWindows(string path, List dirs, List files, List errors) + { + // 使用Windows API的FindFirstFile/FindNextFile快速遍历目录 + var searchPath = Path.Combine(path, "*"); + + IntPtr findHandle = FindFirstFile(searchPath, out WIN32_FIND_DATA findData); + if (findHandle == new IntPtr(-1)) // INVALID_HANDLE_VALUE + { + int error = Marshal.GetLastWin32Error(); + if (error != ERROR_NO_MORE_FILES) + { + errors.Add($"无法读取目录 {path}: 错误代码 {error}"); + } + return; + } + + try + { + do + { + string fileName = findData.cFileName; + + // 跳过 "." 和 ".." + if (fileName == "." || fileName == "..") + continue; + + // 构建完整路径 + string fullPath = Path.Combine(path, fileName); + + if (_fileIgnoreRuleSet.ShouldIgnore(fullPath)) + { + continue; + } + + // 检查是否是目录 + bool isDirectory = (findData.dwFileAttributes & FileAttributes.Directory) != 0; + bool isReparsePoint = (findData.dwFileAttributes & FileAttributes.ReparsePoint) != 0; + + if (isDirectory) + { + // 处理符号链接 + if (isReparsePoint && !_followSymlinks) + continue; + + dirs.Add(fullPath); + } + else + { + // 添加文件 + files.Add(fullPath); + } + } while (FindNextFile(findHandle, out findData)); + + // 检查是否因为错误而退出循环 + int lastError = Marshal.GetLastWin32Error(); + if (lastError != ERROR_NO_MORE_FILES) + { + errors.Add($"遍历目录时发生错误 {path}: 错误代码 {lastError}"); + } + } + finally + { + // 关闭查找句柄 + FindClose(findHandle); + } + } + + private void ScanDirectoryUnix(string path, List dirs, List files, List errors) + { + // 使用libc的opendir/readdir快速遍历Unix/Linux/macOS目录 + IntPtr dirPtr = OpenDir(path); + if (dirPtr == IntPtr.Zero) + { + errors.Add($"无法打开目录 {path}: {Marshal.GetLastWin32Error()}"); + return; + } + + try + { + while (true) + { + IntPtr entryPtr = ReadDir(dirPtr); + if (entryPtr == IntPtr.Zero) + break; + + // 将指针转换为dirent结构 + Dirent entry = Marshal.PtrToStructure(entryPtr); + string fileName = entry.d_name; + + // 跳过 "." 和 ".." + if (fileName == "." || fileName == "..") + continue; + + string fullPath = Path.Combine(path, fileName); + + if (_fileIgnoreRuleSet.ShouldIgnore(fullPath)) + { + continue; + } + + // 根据d_type判断文件类型 + if (entry.d_type == DT_DIR) + { + dirs.Add(fullPath); + } + else if (entry.d_type == DT_REG) + { + files.Add(fullPath); + } + else if (entry.d_type == DT_LNK && _followSymlinks) + { + // 处理符号链接 + try + { + FileAttributes attr = File.GetAttributes(fullPath); + if ((attr & FileAttributes.Directory) == FileAttributes.Directory) + { + dirs.Add(fullPath); + } + else + { + files.Add(fullPath); + } + } + catch (Exception ex) + { + errors.Add($"解析符号链接失败 {fullPath}: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + errors.Add($"处理目录时发生错误 {path}: {ex.Message}"); + } + finally + { + // 关闭目录句柄 + CloseDir(dirPtr); + } + } + + private struct WorkItem + { + public string Path; + public int Depth; + } + + /// + /// 扫描结果 + /// + public class ScanResult + { + /// + /// 扫描的根路径 + /// + public string RootPath { get; set; } + + /// + /// 文件数量 + /// + public int FileCount { get; set; } + + /// + /// 目录数量 + /// + public int DirectoryCount { get; set; } + + /// + /// 文件列表 + /// + public List Files { get; set; } + + /// + /// 目录列表 + /// + public List Directories { get; set; } + + /// + /// 错误列表 + /// + public List Errors { get; set; } + + /// + /// 扫描耗时 + /// + public TimeSpan ElapsedTime { get; set; } + + /// + /// 开始时间 + /// + public DateTime StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime EndTime { get; set; } + + /// + /// 每秒处理项目数 + /// + public double ItemsPerSecond => ElapsedTime.TotalSeconds > 0 + ? (FileCount + DirectoryCount) / ElapsedTime.TotalSeconds + : 0; + } + + /// + /// 获取只包含文件路径的列表(无其他元数据) + /// + /// 根路径 + /// 取消令牌 + /// 文件路径列表 + public static async Task> GetFilesOnlyAsync(string rootPath, CancellationToken cancellationToken = default) + { + var scanner = new FileUltraScanner(rootPath); + var result = await scanner.ScanAsync(cancellationToken: cancellationToken); + return result.Files; + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Infrastructure/MDriveSync.Infrastructure.csproj b/src/MDriveSync.Infrastructure/MDriveSync.Infrastructure.csproj index 81a68c4..b22355d 100644 --- a/src/MDriveSync.Infrastructure/MDriveSync.Infrastructure.csproj +++ b/src/MDriveSync.Infrastructure/MDriveSync.Infrastructure.csproj @@ -1,8 +1,20 @@  - - net8.0 - enable - + + net8.0 + enable + + + + + + + + + + + + + diff --git a/src/MDriveSync.Test/FileSearcherPerformanceTests.cs b/src/MDriveSync.Test/FileSearcherPerformanceTests.cs new file mode 100644 index 0000000..e4e8d9b --- /dev/null +++ b/src/MDriveSync.Test/FileSearcherPerformanceTests.cs @@ -0,0 +1,439 @@ +using MDriveSync.Core.Services; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace MDriveSync.Test +{ + /// + /// FileSearcherָ + /// + public class FileSearcherPerformanceTests : BaseTests + { + /// + /// ļ - ߳ܲ + /// + /// + [Fact] + public async Task TestFileSearcherPerformanceBig() + { + // òԲ + string[] testDirectories = [@"E:\guanpeng\__my"]; + int[] processorCounts = { Environment.ProcessorCount, Environment.ProcessorCount * 2 }; + + // ռ + var results = new Dictionary>>(); + foreach (var dir in testDirectories) + { + results[dir] = new Dictionary>(); + foreach (var count in processorCounts) + { + results[dir][count] = new List<(double, int)>(); + } + } + + // в + int iterations = 3; // ִжȡƽֵ + Console.WriteLine("ʼFileSearcher..."); + + foreach (var directory in testDirectories) + { + if (!Directory.Exists(directory)) + { + Console.WriteLine($"Ŀ¼ڣ: {directory}"); + continue; + } + + Console.WriteLine($"Ŀ¼: {directory}"); + string dirInfo = GetDirectoryInfo(directory); + Console.WriteLine(dirInfo); + + foreach (var processorCount in processorCounts) + { + // GetFiles + Console.WriteLine($" GetFiles (ж: {processorCount})"); + for (int i = 0; i < iterations; i++) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var fileInfos = FileSearcher.GetFiles(directory, processorCount, true); + stopwatch.Stop(); + double searchTime = stopwatch.Elapsed.TotalMilliseconds; + + Console.WriteLine($" {i + 1}: ʱ = {searchTime:F2} ms, ļ = {fileInfos.Count}"); + + // + GC.Collect(); + Thread.Sleep(50); + } + + // UltraFastScanner + Console.WriteLine($" UltraFastScanner (ж: {processorCount})"); + + for (int i = 0; i < iterations; i++) + { + var scanner = new FileUltraScanner( + directory, + maxConcurrency: processorCount, + batchSize: 8192, + followSymlinks: false, + [] + ); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var result = await scanner.ScanAsync(reportProgress: true); + stopwatch.Stop(); + double searchTime = stopwatch.Elapsed.TotalMilliseconds; + Console.WriteLine($" {i + 1}: ʱ = {searchTime:F2} ms, ļ = {result.FileCount}"); + // + GC.Collect(); + Thread.Sleep(50); + } + } + } + + // ܱ + GeneratePerformanceReport(results, testDirectories, processorCounts); + } + + [Fact] + public async Task TestFileSearcherPerformance() + { + // òԲ + string[] testDirectories = GetTestDirectories(); + int[] processorCounts = { 1, 2, 4, 8, Environment.ProcessorCount }; + + // ռ + var results = new Dictionary>>(); + foreach (var dir in testDirectories) + { + results[dir] = new Dictionary>(); + foreach (var count in processorCounts) + { + results[dir][count] = new List<(double, int)>(); + } + } + + // в + int iterations = 3; // ִжȡƽֵ + Console.WriteLine("ʼFileSearcher..."); + + foreach (var directory in testDirectories) + { + if (!Directory.Exists(directory)) + { + Console.WriteLine($"Ŀ¼ڣ: {directory}"); + continue; + } + + Console.WriteLine($"Ŀ¼: {directory}"); + string dirInfo = GetDirectoryInfo(directory); + Console.WriteLine(dirInfo); + + foreach (var processorCount in processorCounts) + { + Console.WriteLine($" SearchFiles ж: {processorCount}"); + + for (int i = 0; i < iterations; i++) + { + // SearchFiles + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var files = FileSearcher.SearchFiles(directory, processorCount, false); + stopwatch.Stop(); + double searchTime = stopwatch.Elapsed.TotalMilliseconds; + + results[directory][processorCount].Add((searchTime, files.Count)); + Console.WriteLine($" {i + 1}: ʱ = {searchTime:F2} ms, ļ = {files.Count}"); + + // + GC.Collect(); + Thread.Sleep(500); // ϵͳʱԴ + } + + // GetFiles + Console.WriteLine($" GetFiles (ж: {processorCount})"); + for (int i = 0; i < iterations; i++) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var fileInfos = FileSearcher.GetFiles(directory, processorCount, false); + stopwatch.Stop(); + double searchTime = stopwatch.Elapsed.TotalMilliseconds; + + Console.WriteLine($" {i + 1}: ʱ = {searchTime:F2} ms, ļ = {fileInfos.Count}"); + + // + GC.Collect(); + Thread.Sleep(500); + } + + // UltraFastScanner + Console.WriteLine($" UltraFastScanner (ж: {processorCount})"); + + for (int i = 0; i < iterations; i++) + { + var scanner = new FileUltraScanner( + directory, + maxConcurrency: processorCount, + batchSize: 8192, + followSymlinks: false, + [] + ); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var result = await scanner.ScanAsync(); + stopwatch.Stop(); + double searchTime = stopwatch.Elapsed.TotalMilliseconds; + Console.WriteLine($" {i + 1}: ʱ = {searchTime:F2} ms, ļ = {result.FileCount}"); + // + GC.Collect(); + Thread.Sleep(500); + } + } + } + + // ܱ + GeneratePerformanceReport(results, testDirectories, processorCounts); + } + + /// + /// ȡĿ¼б + /// + private string[] GetTestDirectories() + { + // һ鲻ͬСĿ¼ڲ + return new string[] + { + // СĿ¼ (100ļ) + //Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "SmallTestFolder"), + + // Ŀ¼ (ٵǧļ) + //Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + + // Ŀ¼ (ļ) + //Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + + // ʵӻ޸Щ· + //@"E:\guanpeng\__my" + + @"E:\guanpeng\docs" + }; + } + + /// + /// ȡĿ¼ĻϢ + /// + private string GetDirectoryInfo(string directory) + { + try + { + int fileCount = Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly).Length; + int dirCount = Directory.GetDirectories(directory, "*", SearchOption.TopDirectoryOnly).Length; + + return $" ļ: {fileCount}, Ŀ¼: {dirCount}"; + } + catch (Exception ex) + { + return $" ȡĿ¼Ϣʧ: {ex.Message}"; + } + } + + /// + /// ܲԱ + /// + private void GeneratePerformanceReport( + Dictionary>> results, + string[] directories, + int[] processorCounts) + { + StringBuilder report = new StringBuilder(); + report.AppendLine("# FileSearcher ܲԱ"); + report.AppendLine(); + report.AppendLine("## Ի"); + report.AppendLine($"- ϵͳ: {RuntimeInformation.OSDescription}"); + report.AppendLine($"- : {Environment.ProcessorCount} "); + report.AppendLine($"- .NET 汾: {Environment.Version}"); + report.AppendLine($"- ʱ: {DateTime.Now}"); + report.AppendLine(); + + report.AppendLine("## Խ"); + report.AppendLine(); + + foreach (var directory in directories) + { + if (!results.ContainsKey(directory)) + continue; + + report.AppendLine($"### Ŀ¼: {directory}"); + report.AppendLine(); + report.AppendLine("| ж | ƽʱ (ms) | ļ | ÿ봦ļ |"); + report.AppendLine("|--------|-----------------|--------|---------------|"); + + foreach (var processorCount in processorCounts) + { + if (!results[directory].ContainsKey(processorCount) || + results[directory][processorCount].Count == 0) + continue; + + // һεĽԤȣ + var validResults = results[directory][processorCount].Skip(1).ToList(); + if (validResults.Count == 0) + validResults = results[directory][processorCount]; + + double avgTime = validResults.Average(r => r.SearchTime); + int avgFileCount = (int)validResults.Average(r => r.FileCount); + double filesPerSecond = avgFileCount / (avgTime / 1000.0); + + report.AppendLine($"| {processorCount} | {avgTime:F2} | {avgFileCount} | {filesPerSecond:F0} |"); + } + report.AppendLine(); + + // Ӽٱȷ + if (results[directory].ContainsKey(1) && results[directory][1].Count > 0) + { + report.AppendLine("### мٱȷ"); + report.AppendLine(); + report.AppendLine("| ж | ٱ | Ч |"); + report.AppendLine("|--------|--------|------|"); + + double singleThreadTime = results[directory][1].Skip(1).Average(r => r.SearchTime); + + foreach (var processorCount in processorCounts.Where(p => p > 1)) + { + if (!results[directory].ContainsKey(processorCount) || + results[directory][processorCount].Count <= 1) + continue; + + double multiThreadTime = results[directory][processorCount].Skip(1).Average(r => r.SearchTime); + double speedup = singleThreadTime / multiThreadTime; + double efficiency = speedup / processorCount; + + report.AppendLine($"| {processorCount} | {speedup:F2}x | {efficiency:P2} |"); + } + report.AppendLine(); + } + } + + // Ż + report.AppendLine("## Ż"); + report.AppendLine(); + report.AppendLine("ڲԽǿԵó½:"); + report.AppendLine(); + report.AppendLine("1. **Ѳж**: жΪܻͨܣIOܼͲϿҪߵIJжȡ"); + report.AppendLine("2. **Ŀ¼**: ڴĿ¼`GetFiles`(ʹ`EnumerateFiles`)ͨ`SearchFiles`Чرڴ޵ĻС"); + report.AppendLine("3. **Ч**: ŲжӣЧͨ½ҪܴIOƣӴܲܡ"); + report.AppendLine("4. **Ż**: ڲƷиļϵͳ(SSD vs HDD)Ŀ¼С̬жȡ"); + report.AppendLine(); + + // 汨 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "FileSearcherܲԱ.md"); + File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); + + Console.WriteLine($"ܲԱѱ: {reportPath}"); + Assert.True(File.Exists(reportPath)); + } + + [Fact] + public async Task TestFileSearcherMemoryUsage() + { + // ѡһϴĿ¼ڲڴʹ + string testDirectory = @"E:\guanpeng\docs"; // Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (!Directory.Exists(testDirectory)) + { + Console.WriteLine($"Ŀ¼: {testDirectory}"); + return; + } + + Console.WriteLine($"Ŀ¼: {testDirectory}"); + Console.WriteLine("SearchFilesGetFilesڴʹ..."); + + // ǰ¼ڴ׼ + GC.Collect(); + long beforeMemory = GC.GetTotalMemory(true); + + // SearchFilesڴʹ + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var files = FileSearcher.SearchFiles(testDirectory); + stopwatch.Stop(); + + // ռڴʹ + long afterSearchFilesMemory = GC.GetTotalMemory(false); + long searchFilesMemoryUsage = afterSearchFilesMemory - beforeMemory; + + Console.WriteLine($"SearchFiles : {files.Count} ļ"); + Console.WriteLine($"SearchFiles ʱ: {stopwatch.ElapsedMilliseconds} ms"); + Console.WriteLine($"SearchFiles ڴʹ: {searchFilesMemoryUsage / 1024 / 1024} MB"); + + // + files = null; + GC.Collect(); + Thread.Sleep(1000); + beforeMemory = GC.GetTotalMemory(true); + + // GetFilesڴʹ + stopwatch.Restart(); + var fileInfos = FileSearcher.GetFiles(testDirectory); + stopwatch.Stop(); + + // ռڴʹ + long afterGetFilesMemory = GC.GetTotalMemory(false); + long getFilesMemoryUsage = afterGetFilesMemory - beforeMemory; + + Console.WriteLine($"GetFiles : {fileInfos.Count} ļ"); + Console.WriteLine($"GetFiles ʱ: {stopwatch.ElapsedMilliseconds} ms"); + Console.WriteLine($"GetFiles ڴʹ: {getFilesMemoryUsage / 1024 / 1024} MB"); + + // UltraFastScanner ڴʹ + + GC.Collect(); + Thread.Sleep(1000); + beforeMemory = GC.GetTotalMemory(true); + + var scanner = new FileUltraScanner( + testDirectory, + maxConcurrency: Environment.ProcessorCount, + batchSize: 8192, + followSymlinks: false, + [] + ); + stopwatch.Restart(); + var result = await scanner.ScanAsync(); + stopwatch.Stop(); + long afterUltraFastScannerMemory = GC.GetTotalMemory(false); + long ultraFastScannerMemoryUsage = afterUltraFastScannerMemory - afterGetFilesMemory; + Console.WriteLine($"UltraFastScanner : {result.FileCount} ļ"); + Console.WriteLine($"UltraFastScanner ʱ: {stopwatch.ElapsedMilliseconds} ms"); + Console.WriteLine($"UltraFastScanner ڴʹ: {ultraFastScannerMemoryUsage / 1024 / 1024} MB"); + // + result = null; + GC.Collect(); + Thread.Sleep(1000); + + + // 򵥶ȷЧ + Assert.True(files != null && fileInfos != null); + + // ɼ򵥵ڴʹñ + StringBuilder report = new StringBuilder(); + report.AppendLine("# FileSearcher ڴʹòԱ"); + report.AppendLine(); + report.AppendLine($"Ŀ¼: {testDirectory}"); + report.AppendLine($"ʱ: {DateTime.Now}"); + report.AppendLine(); + report.AppendLine("| | ļ | ִʱ(ms) | ڴʹ(MB) |"); + report.AppendLine("|------|----------|--------------|-------------|"); + report.AppendLine($"| SearchFiles | {files.Count} | {stopwatch.ElapsedMilliseconds} | {searchFilesMemoryUsage / 1024 / 1024} |"); + report.AppendLine($"| GetFiles | {fileInfos.Count} | {stopwatch.ElapsedMilliseconds} | {getFilesMemoryUsage / 1024 / 1024} |"); + + // 汨 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "FileSearcherڴʹñ.md"); + File.WriteAllText(reportPath, report.ToString(), Encoding.UTF8); + + Console.WriteLine($"ڴʹñѱ: {reportPath}"); + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Test/HashHelperTests.cs b/src/MDriveSync.Test/HashHelperTests.cs index 02c4ec9..0f493df 100644 --- a/src/MDriveSync.Test/HashHelperTests.cs +++ b/src/MDriveSync.Test/HashHelperTests.cs @@ -15,7 +15,7 @@ public void TestHashPerformance(int dataSize, int iterations) new Random().NextBytes(data); // 定义要测试的哈希算法 - string[] algorithms = { "SHA256", "SHA1", "MD5", "BLAKE3", "XXH3", "XXH128" }; + string[] algorithms = { "SHA256", "SHA1", "MD5", "BLAKE3", "XXH3", "XXH128", "SHA3" }; foreach (var algorithm in algorithms) { diff --git a/src/MDriveSync.Test/HashPerformanceTests.cs b/src/MDriveSync.Test/HashPerformanceTests.cs index bffdb81..c5aac2a 100644 --- a/src/MDriveSync.Test/HashPerformanceTests.cs +++ b/src/MDriveSync.Test/HashPerformanceTests.cs @@ -127,7 +127,7 @@ public void TestSpecificHashPerformance(int dataSize, int iterations) new Random().NextBytes(data); // 定义要测试的哈希算法 - string[] algorithms = { "SHA256", "SHA1", "MD5", "BLAKE3", "XXH3", "XXH128" }; + string[] algorithms = { "SHA256", "SHA1", "MD5", "BLAKE3", "XXH3", "XXH128", "SHA3" }; foreach (var algorithm in algorithms) { From 559d1052d804cec9a98fb26036b2326cb0b10138 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Sat, 10 May 2025 15:40:02 +0800 Subject: [PATCH 64/90] filetest --- src/MDriveSync.Client/Program.cs | 75 ++++- .../Services/FileFastScanner.cs | 318 ++++++++++++++++-- .../Services/FileScanResult.cs | 65 ++++ .../FileSearcherPerformanceTests.cs | 35 +- 4 files changed, 436 insertions(+), 57 deletions(-) create mode 100644 src/MDriveSync.Core/Services/FileScanResult.cs diff --git a/src/MDriveSync.Client/Program.cs b/src/MDriveSync.Client/Program.cs index 62edc20..e9a9cdd 100644 --- a/src/MDriveSync.Client/Program.cs +++ b/src/MDriveSync.Client/Program.cs @@ -1,5 +1,6 @@ using MDriveSync.Core.Services; using MDriveSync.Security; +using System.Collections.Concurrent; using System.Diagnostics; namespace MDriveSync.Client @@ -13,19 +14,6 @@ private static async Task Main(string[] args) var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns("**/node_modules/*", "**/bin/*", "**/obj/*"); Console.WriteLine($"开始扫描目录: {rootPath}"); - // 示例使用方法 - - sw.Start(); - var files = FileFastScanner.EnumerateFiles( - rootPath, - "*.*", - ignorePatterns, - maxDegreeOfParallelism: 4, - errorHandler: (path, ex) => Console.WriteLine($"Error processing {path}: {ex.Message}") - ).ToList(); - sw.Stop(); - - Console.WriteLine($"扫描完成! 耗时: {sw.Elapsed.TotalSeconds:F2} 秒, count: {files.Count}"); var cts = new CancellationTokenSource(); @@ -37,6 +25,25 @@ private static async Task Main(string[] args) e.Cancel = true; }; + var progress = new Progress(p => + { + Console.Write($"\r文件: {p.FileCount:N0}, 目录: {p.DirectoryCount:N0}, " + + $"耗时: {p.ElapsedTime.TotalSeconds}s, 速度: {(int)p.ItemsPerSecond}/s"); + }); + + sw.Start(); + var fileRes = FileFastScanner.ScanAsync( + rootPath, + "*.*", + ignorePatterns, + maxDegreeOfParallelism: 4, + progress: progress, + cancellationToken: cts.Token + ); + sw.Stop(); + Console.WriteLine($"扫描完成! 耗时: {sw.Elapsed.TotalSeconds:F2} 秒, count: {fileRes.Files.Count}"); + + try { var scanner = new FileUltraScanner( @@ -60,6 +67,48 @@ private static async Task Main(string[] args) Console.WriteLine($"扫描耗时: {result.ElapsedTime.TotalSeconds:F2} 秒"); Console.WriteLine($"处理速度: {result.ItemsPerSecond:N0} 项/秒"); + var f1Files = new ConcurrentDictionary(); + foreach (var file in fileRes.Files) + { + f1Files.TryAdd(file, 0); + } + + var f2Files = new ConcurrentDictionary(); + foreach (var file in result.Files) + { + f2Files.TryAdd(file, 0); + } + + // 比较f1 与 f2 的差异,如果 f1中有而 f2 中没有,则输出 + var f1Only = f1Files.Keys.Except(f2Files.Keys).ToList(); + if (f1Only.Count > 0) + { + Console.WriteLine($"\n\nf1中有而f2中没有的文件: {f1Only.Count}"); + foreach (var file in f1Only) + { + Console.WriteLine(file); + } + } + else + { + Console.WriteLine("\nf1与f2完全一致"); + } + + // 比较f2 与 f1 的差异,如果 f2中有而 f1 中没有,则输出 + var f2Only = f2Files.Keys.Except(f1Files.Keys).ToList(); + if (f2Only.Count > 0) + { + Console.WriteLine($"\n\nf2中有而f1中没有的文件: {f2Only.Count}"); + foreach (var file in f2Only) + { + Console.WriteLine(file); + } + } + else + { + Console.WriteLine("\nf2与f1完全一致"); + } + if (result.Errors.Count > 0) { Console.WriteLine($"扫描过程中发生了 {result.Errors.Count} 个错误"); diff --git a/src/MDriveSync.Core/Services/FileFastScanner.cs b/src/MDriveSync.Core/Services/FileFastScanner.cs index cb5a341..46e426d 100644 --- a/src/MDriveSync.Core/Services/FileFastScanner.cs +++ b/src/MDriveSync.Core/Services/FileFastScanner.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics; namespace MDriveSync.Core.Services { @@ -8,14 +9,24 @@ namespace MDriveSync.Core.Services public class FileFastScanner { /// - /// 采用生产者-消费者模式遍历文件系统 + /// 采用生产者-消费者模式遍历文件系统并返回文件路径集合 /// + /// 根路径 + /// 搜索模式,默认为"*" + /// 忽略的模式列表 + /// 最大并行度,默认为处理器数量 + /// 错误处理器 + /// 进度报告回调 + /// 取消令牌 + /// 文件路径的集合 public static IEnumerable EnumerateFiles( string rootPath, string searchPattern = "*", IEnumerable ignorePatterns = null, int? maxDegreeOfParallelism = null, - Action errorHandler = null) + Action errorHandler = null, + IProgress progress = null, + CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(rootPath)) throw new ArgumentException("根路径不能为空", nameof(rootPath)); @@ -33,6 +44,14 @@ public static IEnumerable EnumerateFiles( var directoryQueue = new BlockingCollection(new ConcurrentQueue(), 10000); var fileResults = new BlockingCollection(new ConcurrentQueue(), 100000); + // 进度跟踪 + var stopwatch = Stopwatch.StartNew(); + long processedItems = 0; + int dirCount = 0; + int fileCount = 0; + DateTime lastProgressReport = DateTime.UtcNow; + const int progressIntervalMs = 100; // 默认进度报告间隔毫秒数 + // 启动目录生产者任务 var directoryProducerTask = Task.Run(() => { @@ -42,53 +61,193 @@ public static IEnumerable EnumerateFiles( if (!ignoreRules.ShouldIgnore(rootPath)) { directoryQueue.Add(rootPath); + Interlocked.Increment(ref dirCount); + Interlocked.Increment(ref processedItems); } // 使用广度优先遍历而不是预先收集所有目录 - ProcessDirectories(rootPath, ignoreRules, directoryQueue, errorHandler); + ProcessDirectories(rootPath, ignoreRules, directoryQueue, errorHandler, + ref processedItems, ref dirCount, cancellationToken); } finally { // 标记目录队列已完成 directoryQueue.CompleteAdding(); } - }); + }, cancellationToken); // 启动文件处理任务 var fileProcessorTask = Task.Run(() => { // 创建并行选项 - var options = new ParallelOptions { MaxDegreeOfParallelism = parallelism }; + var options = new ParallelOptions + { + MaxDegreeOfParallelism = parallelism, + CancellationToken = cancellationToken + }; // 从目录队列中并行处理目录 - Parallel.ForEach(directoryQueue.GetConsumingEnumerable(), options, directory => + try { - try + Parallel.ForEach(directoryQueue.GetConsumingEnumerable(), options, directory => { - // 处理单个目录中的文件 - foreach (var file in ProcessFiles(directory, searchPattern, ignoreRules, errorHandler)) + // 检查是否取消 + cancellationToken.ThrowIfCancellationRequested(); + + try { - fileResults.Add(file); - } - } - catch (Exception ex) - { - errorHandler?.Invoke(directory, ex); - } - }); + // 处理单个目录中的文件 + foreach (var file in ProcessFiles(directory, searchPattern, ignoreRules, errorHandler, cancellationToken)) + { + fileResults.Add(file); + Interlocked.Increment(ref fileCount); + Interlocked.Increment(ref processedItems); - // 所有目录处理完成后,标记文件结果集已完成 - fileResults.CompleteAdding(); - }); + // 检查是否需要报告进度 + if (progress != null && (DateTime.UtcNow - lastProgressReport).TotalMilliseconds >= progressIntervalMs) + { + ReportProgress(progress, ref lastProgressReport, stopwatch, processedItems, dirCount, fileCount, directoryQueue.Count); + } + } + } + catch (OperationCanceledException) + { + throw; // 重新抛出取消异常 + } + catch (Exception ex) + { + errorHandler?.Invoke(directory, ex); + } + }); + } + catch (OperationCanceledException) + { + // 任务被取消,无需处理 + } + finally + { + // 所有目录处理完成后,标记文件结果集已完成 + fileResults.CompleteAdding(); + } + }, cancellationToken); // 返回文件结果 foreach (var file in fileResults.GetConsumingEnumerable()) { + // 如果请求取消,提前退出遍历 + if (cancellationToken.IsCancellationRequested) + break; + yield return file; } - // 确保所有任务完成 - Task.WaitAll(directoryProducerTask, fileProcessorTask); + try + { + // 确保所有任务完成 + Task.WaitAll(new[] { directoryProducerTask, fileProcessorTask }, cancellationToken); + } + catch (OperationCanceledException) + { + // 任务被取消,无需处理 + } + finally + { + // 报告最终进度 + if (progress != null) + { + stopwatch.Stop(); + progress.Report(new ScanProgress + { + ProcessedItems = processedItems, + DirectoryCount = dirCount, + FileCount = fileCount, + QueueCount = 0, + ElapsedTime = stopwatch.Elapsed, + IsComplete = true + }); + } + } + } + + /// + /// 异步扫描文件系统并返回包含更多信息的结果 + /// + /// 根路径 + /// 搜索模式,默认为"*" + /// 忽略的模式列表 + /// 最大并行度,默认为处理器数量 + /// 是否报告进度 + /// 进度报告回调 + /// 取消令牌 + /// 扫描结果 + public static FileScanResult ScanAsync( + string rootPath, + string searchPattern = "*", + IEnumerable ignorePatterns = null, + int? maxDegreeOfParallelism = null, + bool reportProgress = false, + IProgress progress = null, + CancellationToken cancellationToken = default) + { + var startTime = DateTime.UtcNow; + var stopwatch = Stopwatch.StartNew(); + var errors = new ConcurrentBag(); + + var files = new List(); + + // 错误处理器 + Action errorHandler = (path, ex) => + { + errors.Add($"{path}: {ex.Message}"); + }; + + try + { + // 使用枚举器收集文件 + files = EnumerateFiles( + rootPath, + searchPattern, + ignorePatterns, + maxDegreeOfParallelism, + errorHandler, + progress, + cancellationToken).ToList(); + + stopwatch.Stop(); + + return new FileScanResult + { + RootPath = rootPath, + FileCount = files.Count, + DirectoryCount = 0, // 我们不保留目录列表,所以这里未填充 + Files = files, + Directories = null, // 我们不保留目录列表 + Errors = errors.ToList(), + ElapsedTime = stopwatch.Elapsed, + StartTime = startTime, + EndTime = DateTime.UtcNow, + WasCancelled = cancellationToken.IsCancellationRequested + }; + } + catch (OperationCanceledException) + { + // 扫描被取消 + stopwatch.Stop(); + + return new FileScanResult + { + RootPath = rootPath, + FileCount = files.Count, + DirectoryCount = 0, + Files = files, + Directories = null, + Errors = errors.ToList(), + ElapsedTime = stopwatch.Elapsed, + StartTime = startTime, + EndTime = DateTime.UtcNow, + WasCancelled = true + }; + } } /// @@ -98,7 +257,10 @@ private static void ProcessDirectories( string rootPath, FileIgnoreRuleSet ignoreRules, BlockingCollection directoryQueue, - Action errorHandler = null) + Action errorHandler, + ref long processedItems, + ref int dirCount, + CancellationToken cancellationToken) { // 目录遍历设置 var options = new EnumerationOptions @@ -116,7 +278,7 @@ private static void ProcessDirectories( var pendingDirs = new Queue(); pendingDirs.Enqueue(rootPath); - while (pendingDirs.Count > 0) + while (pendingDirs.Count > 0 && !cancellationToken.IsCancellationRequested) { string currentDir = pendingDirs.Dequeue(); @@ -124,14 +286,24 @@ private static void ProcessDirectories( { foreach (var subDir in Directory.EnumerateDirectories(currentDir, "*", options)) { + // 检查取消状态 + if (cancellationToken.IsCancellationRequested) + break; + // 检查目录是否应被忽略 if (!ignoreRules.ShouldIgnore(subDir) && processedDirs.Add(subDir)) { directoryQueue.Add(subDir); pendingDirs.Enqueue(subDir); + Interlocked.Increment(ref dirCount); + Interlocked.Increment(ref processedItems); } } } + catch (OperationCanceledException) + { + throw; // 重新抛出取消异常 + } catch (Exception ex) when (ex is UnauthorizedAccessException || ex is DirectoryNotFoundException || ex is IOException) @@ -148,7 +320,8 @@ private static IEnumerable ProcessFiles( string directory, string searchPattern, FileIgnoreRuleSet ignoreRules, - Action errorHandler = null) + Action errorHandler, + CancellationToken cancellationToken) { if (string.IsNullOrEmpty(searchPattern)) { @@ -161,13 +334,18 @@ private static IEnumerable ProcessFiles( { var options = new EnumerationOptions { - RecurseSubdirectories = false, + RecurseSubdirectories = false, // 不递归子目录 + //AttributesToSkip = 0, // 不跳过任何文件 // FileAttributes.System, AttributesToSkip = FileAttributes.System, - IgnoreInaccessible = true + IgnoreInaccessible = true // 忽略访问权限受限的文件 }; files = Directory.EnumerateFiles(directory, searchPattern, options); } + catch (OperationCanceledException) + { + throw; // 重新抛出取消异常 + } catch (Exception ex) when (ex is UnauthorizedAccessException || ex is DirectoryNotFoundException || ex is IOException) @@ -179,6 +357,10 @@ ex is DirectoryNotFoundException || // 处理找到的文件 foreach (var file in files) { + // 检查取消状态 + if (cancellationToken.IsCancellationRequested) + yield break; + // 检查文件是否应被忽略 if (!ignoreRules.ShouldIgnore(file)) { @@ -186,5 +368,85 @@ ex is DirectoryNotFoundException || } } } + + /// + /// 报告扫描进度 + /// + private static void ReportProgress( + IProgress progress, + ref DateTime lastReportTime, + Stopwatch stopwatch, + long processedItems, + int dirCount, + int fileCount, + int queueCount) + { + lastReportTime = DateTime.UtcNow; + + progress.Report(new ScanProgress + { + ProcessedItems = processedItems, + DirectoryCount = dirCount, + FileCount = fileCount, + QueueCount = queueCount, + ElapsedTime = stopwatch.Elapsed, + IsComplete = false + }); + } + + /// + /// 扫描进度信息类 + /// + public class ScanProgress + { + /// + /// 已处理的项目数(文件和目录) + /// + public long ProcessedItems { get; set; } + + /// + /// 已扫描的文件数 + /// + public int FileCount { get; set; } + + /// + /// 已扫描的目录数 + /// + public int DirectoryCount { get; set; } + + /// + /// 待处理队列中的项目数 + /// + public int QueueCount { get; set; } + + /// + /// 已消耗时间 + /// + public TimeSpan ElapsedTime { get; set; } + + /// + /// 是否完成扫描 + /// + public bool IsComplete { get; set; } + + /// + /// 每秒处理项目数 + /// + public double ItemsPerSecond => ElapsedTime.TotalSeconds > 0 + ? ProcessedItems / ElapsedTime.TotalSeconds + : 0; + } + + /// + /// 获取只包含文件路径的列表(无其他元数据) + /// + /// 根路径 + /// 取消令牌 + /// 文件路径列表 + public static List GetFilesOnlyAsync(string rootPath, CancellationToken cancellationToken = default) + { + var result = ScanAsync(rootPath, cancellationToken: cancellationToken); + return result.Files; + } } -} \ No newline at end of file +} diff --git a/src/MDriveSync.Core/Services/FileScanResult.cs b/src/MDriveSync.Core/Services/FileScanResult.cs new file mode 100644 index 0000000..5d33538 --- /dev/null +++ b/src/MDriveSync.Core/Services/FileScanResult.cs @@ -0,0 +1,65 @@ +namespace MDriveSync.Core.Services +{ + /// + /// 扫描结果类 + /// + public class FileScanResult + { + /// + /// 扫描的根路径 + /// + public string RootPath { get; set; } + + /// + /// 文件数量 + /// + public int FileCount { get; set; } + + /// + /// 目录数量 + /// + public int DirectoryCount { get; set; } + + /// + /// 文件列表 + /// + public List Files { get; set; } + + /// + /// 目录列表 + /// + public List Directories { get; set; } + + /// + /// 错误列表 + /// + public List Errors { get; set; } + + /// + /// 扫描耗时 + /// + public TimeSpan ElapsedTime { get; set; } + + /// + /// 开始时间 + /// + public DateTime StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime EndTime { get; set; } + + /// + /// 扫描是否被取消 + /// + public bool WasCancelled { get; set; } + + /// + /// 每秒处理项目数 + /// + public double ItemsPerSecond => ElapsedTime.TotalSeconds > 0 + ? (FileCount + DirectoryCount) / ElapsedTime.TotalSeconds + : 0; + } +} \ No newline at end of file diff --git a/src/MDriveSync.Test/FileSearcherPerformanceTests.cs b/src/MDriveSync.Test/FileSearcherPerformanceTests.cs index e4e8d9b..8d39e0f 100644 --- a/src/MDriveSync.Test/FileSearcherPerformanceTests.cs +++ b/src/MDriveSync.Test/FileSearcherPerformanceTests.cs @@ -50,22 +50,6 @@ public async Task TestFileSearcherPerformanceBig() foreach (var processorCount in processorCounts) { - // GetFiles - Console.WriteLine($" GetFiles (ж: {processorCount})"); - for (int i = 0; i < iterations; i++) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - var fileInfos = FileSearcher.GetFiles(directory, processorCount, true); - stopwatch.Stop(); - double searchTime = stopwatch.Elapsed.TotalMilliseconds; - - Console.WriteLine($" {i + 1}: ʱ = {searchTime:F2} ms, ļ = {fileInfos.Count}"); - - // - GC.Collect(); - Thread.Sleep(50); - } // UltraFastScanner Console.WriteLine($" UltraFastScanner (ж: {processorCount})"); @@ -90,6 +74,25 @@ public async Task TestFileSearcherPerformanceBig() GC.Collect(); Thread.Sleep(50); } + + // GetFiles + Console.WriteLine($" GetFiles (ж: {processorCount})"); + for (int i = 0; i < iterations; i++) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var fileInfos = FileSearcher.GetFiles(directory, processorCount, true); + stopwatch.Stop(); + double searchTime = stopwatch.Elapsed.TotalMilliseconds; + + Console.WriteLine($" {i + 1}: ʱ = {searchTime:F2} ms, ļ = {fileInfos.Count}"); + + // + GC.Collect(); + Thread.Sleep(50); + } + + } } From 73b9c08470137282fa0b23ebba0fff4c3a97e9bb Mon Sep 17 00:00:00 2001 From: trueai-org Date: Sat, 10 May 2025 18:52:04 +0800 Subject: [PATCH 65/90] =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=BF=BD=E7=95=A5?= =?UTF-8?q?=E8=A7=84=E5=88=99=E6=B5=8B=E8=AF=95=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MDriveSync.Client/Program.cs | 48 +--- .../Services/FileFastScanner.cs | 30 +- .../Services/FileIgnoreHelper.cs | 2 +- src/MDriveSync.Test/BaseTests.cs | 7 +- src/MDriveSync.Test/FileIgnoreTests.cs | 257 ++++++++++++++++++ 5 files changed, 290 insertions(+), 54 deletions(-) create mode 100644 src/MDriveSync.Test/FileIgnoreTests.cs diff --git a/src/MDriveSync.Client/Program.cs b/src/MDriveSync.Client/Program.cs index e9a9cdd..b9869ae 100644 --- a/src/MDriveSync.Client/Program.cs +++ b/src/MDriveSync.Client/Program.cs @@ -9,9 +9,10 @@ internal class Program { private static async Task Main(string[] args) { + var sw = new Stopwatch(); var rootPath = "E:\\guanpeng"; // args.Length > 0 ? args[0] : Environment.CurrentDirectory; - var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns("**/node_modules/*", "**/bin/*", "**/obj/*"); + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns("**/node_modules/*", "**/bin/*", "**/obj/*", "**/.git/*"); Console.WriteLine($"开始扫描目录: {rootPath}"); @@ -32,9 +33,9 @@ private static async Task Main(string[] args) }); sw.Start(); - var fileRes = FileFastScanner.ScanAsync( + var fileRes = new FileFastScanner().ScanAsync( rootPath, - "*.*", + "*", ignorePatterns, maxDegreeOfParallelism: 4, progress: progress, @@ -67,47 +68,6 @@ private static async Task Main(string[] args) Console.WriteLine($"扫描耗时: {result.ElapsedTime.TotalSeconds:F2} 秒"); Console.WriteLine($"处理速度: {result.ItemsPerSecond:N0} 项/秒"); - var f1Files = new ConcurrentDictionary(); - foreach (var file in fileRes.Files) - { - f1Files.TryAdd(file, 0); - } - - var f2Files = new ConcurrentDictionary(); - foreach (var file in result.Files) - { - f2Files.TryAdd(file, 0); - } - - // 比较f1 与 f2 的差异,如果 f1中有而 f2 中没有,则输出 - var f1Only = f1Files.Keys.Except(f2Files.Keys).ToList(); - if (f1Only.Count > 0) - { - Console.WriteLine($"\n\nf1中有而f2中没有的文件: {f1Only.Count}"); - foreach (var file in f1Only) - { - Console.WriteLine(file); - } - } - else - { - Console.WriteLine("\nf1与f2完全一致"); - } - - // 比较f2 与 f1 的差异,如果 f2中有而 f1 中没有,则输出 - var f2Only = f2Files.Keys.Except(f1Files.Keys).ToList(); - if (f2Only.Count > 0) - { - Console.WriteLine($"\n\nf2中有而f1中没有的文件: {f2Only.Count}"); - foreach (var file in f2Only) - { - Console.WriteLine(file); - } - } - else - { - Console.WriteLine("\nf2与f1完全一致"); - } if (result.Errors.Count > 0) { diff --git a/src/MDriveSync.Core/Services/FileFastScanner.cs b/src/MDriveSync.Core/Services/FileFastScanner.cs index 46e426d..f1c32d9 100644 --- a/src/MDriveSync.Core/Services/FileFastScanner.cs +++ b/src/MDriveSync.Core/Services/FileFastScanner.cs @@ -8,6 +8,15 @@ namespace MDriveSync.Core.Services /// public class FileFastScanner { + private readonly ConcurrentBag _files; + private readonly ConcurrentBag _directories; + + public FileFastScanner() + { + _files = new ConcurrentBag(); + _directories = new ConcurrentBag(); + } + /// /// 采用生产者-消费者模式遍历文件系统并返回文件路径集合 /// @@ -19,7 +28,7 @@ public class FileFastScanner /// 进度报告回调 /// 取消令牌 /// 文件路径的集合 - public static IEnumerable EnumerateFiles( + public IEnumerable EnumerateFiles( string rootPath, string searchPattern = "*", IEnumerable ignorePatterns = null, @@ -61,6 +70,8 @@ public static IEnumerable EnumerateFiles( if (!ignoreRules.ShouldIgnore(rootPath)) { directoryQueue.Add(rootPath); + _directories.Add(rootPath); // 添加根目录到目录集合 + Interlocked.Increment(ref dirCount); Interlocked.Increment(ref processedItems); } @@ -180,7 +191,7 @@ public static IEnumerable EnumerateFiles( /// 进度报告回调 /// 取消令牌 /// 扫描结果 - public static FileScanResult ScanAsync( + public FileScanResult ScanAsync( string rootPath, string searchPattern = "*", IEnumerable ignorePatterns = null, @@ -219,9 +230,9 @@ public static FileScanResult ScanAsync( { RootPath = rootPath, FileCount = files.Count, - DirectoryCount = 0, // 我们不保留目录列表,所以这里未填充 + DirectoryCount = _directories.Count(), // 我们不保留目录列表,所以这里未填充 Files = files, - Directories = null, // 我们不保留目录列表 + Directories = _directories.ToList(), // 我们不保留目录列表 Errors = errors.ToList(), ElapsedTime = stopwatch.Elapsed, StartTime = startTime, @@ -238,9 +249,9 @@ public static FileScanResult ScanAsync( { RootPath = rootPath, FileCount = files.Count, - DirectoryCount = 0, + DirectoryCount = _directories.Count(), // 我们不保留目录列表,所以这里未填充 Files = files, - Directories = null, + Directories = _directories.ToList(), // 我们不保留目录列表 Errors = errors.ToList(), ElapsedTime = stopwatch.Elapsed, StartTime = startTime, @@ -253,7 +264,7 @@ public static FileScanResult ScanAsync( /// /// 处理目录(流式方式) /// - private static void ProcessDirectories( + private void ProcessDirectories( string rootPath, FileIgnoreRuleSet ignoreRules, BlockingCollection directoryQueue, @@ -294,7 +305,9 @@ private static void ProcessDirectories( if (!ignoreRules.ShouldIgnore(subDir) && processedDirs.Add(subDir)) { directoryQueue.Add(subDir); + _directories.Add(subDir); pendingDirs.Enqueue(subDir); + Interlocked.Increment(ref dirCount); Interlocked.Increment(ref processedItems); } @@ -361,6 +374,7 @@ ex is DirectoryNotFoundException || if (cancellationToken.IsCancellationRequested) yield break; + // 检查文件是否应被忽略 if (!ignoreRules.ShouldIgnore(file)) { @@ -443,7 +457,7 @@ public class ScanProgress /// 根路径 /// 取消令牌 /// 文件路径列表 - public static List GetFilesOnlyAsync(string rootPath, CancellationToken cancellationToken = default) + public List GetFilesOnlyAsync(string rootPath, CancellationToken cancellationToken = default) { var result = ScanAsync(rootPath, cancellationToken: cancellationToken); return result.Files; diff --git a/src/MDriveSync.Core/Services/FileIgnoreHelper.cs b/src/MDriveSync.Core/Services/FileIgnoreHelper.cs index 3bf22a9..f7088b0 100644 --- a/src/MDriveSync.Core/Services/FileIgnoreHelper.cs +++ b/src/MDriveSync.Core/Services/FileIgnoreHelper.cs @@ -99,7 +99,7 @@ private string WildcardToRegex(string pattern) return "^$"; // 逐字符处理通配符模式,避免复杂的转义问题 - string result = "^"; + string result = ""; // 移除 ^ for (int i = 0; i < pattern.Length; i++) { diff --git a/src/MDriveSync.Test/BaseTests.cs b/src/MDriveSync.Test/BaseTests.cs index 05dbdbb..ff9a8c5 100644 --- a/src/MDriveSync.Test/BaseTests.cs +++ b/src/MDriveSync.Test/BaseTests.cs @@ -5,7 +5,7 @@ namespace MDriveSync.Test /// /// 测试基类 /// - public class BaseTests + public class BaseTests : IDisposable { public BaseTests() { @@ -13,6 +13,11 @@ public BaseTests() Console.OutputEncoding = Encoding.UTF8; } + public virtual void Dispose() + { + + } + /// /// 避免中文输出乱码问题 - 用于方法 /// diff --git a/src/MDriveSync.Test/FileIgnoreTests.cs b/src/MDriveSync.Test/FileIgnoreTests.cs new file mode 100644 index 0000000..dea087f --- /dev/null +++ b/src/MDriveSync.Test/FileIgnoreTests.cs @@ -0,0 +1,257 @@ +using MDriveSync.Core.Services; + +namespace MDriveSync.Test +{ + /// + /// 文件忽略规则测试类 + /// + public class FileIgnoreTests : BaseTests + { + private readonly string _testRootPath; + + public FileIgnoreTests() + { + // 创建测试根目录 + _testRootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(_testRootPath); + } + + [Fact] + public void TestBasicWildcards() + { + // 测试基本通配符 + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "*.txt", // 匹配所有txt文件 + "file?.log", // 匹配单个字符 + "**/logs" // 匹配任意多层路径 + ); + + var ignoreRules = new FileIgnoreRuleSet(_testRootPath, ignorePatterns); + + // 测试 *.txt + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file.txt"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "another.txt"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file.log"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "subfolder", "file.log"))); + + // 测试 file?.log + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file1.log"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "fileA.log"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file.log"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file12.log"))); + + // 测试 **/logs + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "logs"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "app", "logs"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "app", "sub", "logs"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "logs.txt"))); + } + + [Fact] + public void TestPathRules() + { + // 测试路径规则 + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "/bin", // 只匹配根目录下的bin + "temp/", // 匹配目录 + "lib" // 匹配任何位置的lib + ); + + var ignoreRules = new FileIgnoreRuleSet(_testRootPath, ignorePatterns); + + // 测试 /bin + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "bin"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "bin", "file.exe"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "app", "bin"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "app", "bin", "file.exe"))); + + // 测试 temp/ + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temp"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temp", "file.txt"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temp.txt"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temporary"))); + + // 测试 lib + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "lib"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "app", "lib"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "lib", "file.dll"))); + } + + [Fact] + public void TestSpecialMatching() + { + // 测试特殊匹配规则 + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "*.txt", // 忽略所有txt文件 + "!important.txt", // 但不忽略important.txt + "file[123].log", // 忽略file1.log, file2.log, file3.log + "report[a-c].pdf" // 忽略reporta.pdf到reportc.pdf + ); + + var ignoreRules = new FileIgnoreRuleSet(_testRootPath, ignorePatterns); + + // 测试否定规则 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "regular.txt"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "important.txt"))); + + // 测试字符集合 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file1.log"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file2.log"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file3.log"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file4.log"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file.log"))); + + // 测试字符范围 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "reporta.pdf"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "reportb.pdf"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "reportc.pdf"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "reportd.pdf"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "report.pdf"))); + } + + [Fact] + public void TestRulePriority() + { + // 测试规则优先级 + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "*.log", // 忽略所有日志文件 + "!error.log", // 但不忽略错误日志 + "debug/*.log", // 忽略debug目录下的所有日志 + "!debug/critical.log" // 但不忽略debug目录下的critical.log + ); + + var ignoreRules = new FileIgnoreRuleSet(_testRootPath, ignorePatterns); + + // 测试基本规则和否定规则的顺序 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "info.log"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "error.log"))); + + // 测试更具体的目录规则和最终的否定规则 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "debug", "info.log"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "debug", "warning.log"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "debug", "critical.log"))); + } + + [Fact] + public void TestCompoundRules() + { + // 测试复合规则 + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "**/node_modules/**", // 忽略所有node_modules及其内容 + "**/.git/**", // 忽略所有.git目录及其内容 + "**/bin/**/Debug/**", // 忽略所有bin目录下的Debug子目录 + "!**/bin/**/Debug/important.config" // 但不忽略特定配置文件 + ); + + var ignoreRules = new FileIgnoreRuleSet(_testRootPath, ignorePatterns); + + // 测试node_modules忽略 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "node_modules"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "node_modules", "package.json"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "project", "node_modules", "lib", "index.js"))); + + // 测试.git忽略 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, ".git"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, ".git", "index"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "project", ".git", "objects"))); + + // 测试Debug目录忽略 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "bin", "Debug"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "bin", "Debug", "app.exe"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "project", "bin", "x64", "Debug", "lib.dll"))); + + // 测试重要文件不忽略 + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "bin", "Debug", "important.config"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "project", "bin", "x64", "Debug", "important.config"))); + } + + [Fact] + public void TestDefaultIgnorePatterns() + { + // 测试默认忽略模式 + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns(); + var ignoreRules = new FileIgnoreRuleSet(_testRootPath, ignorePatterns); + + // 测试一些常见的应该被忽略的文件和目录 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, ".git"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "node_modules"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "bin", "Debug"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temp"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file.tmp"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file.bak"))); + } + + [Fact] + public void TestCustomIgnorePatternsForLargeSystem() + { + // 测试为大型文件系统优化的忽略规则 + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "*.bak", + "*.tmp", + "**/temp/**", + "**/cache/**" + ); + + var ignoreRules = new FileIgnoreRuleSet(_testRootPath, ignorePatterns); + + // 测试应该被忽略的文件和目录 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "data.bak"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "config.tmp"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temp"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temp", "file.txt"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "project", "temp", "data.bin"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "cache"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "cache", "data.cache"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "project", "cache", "user.cache"))); + + // 测试不应被忽略的文件和目录 + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "data.txt"))); + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temporary.txt"))); // 不匹配temp目录模式 + Assert.False(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "project", "data", "important.dat"))); + } + + [Fact] + public void TestCaseInsensitiveMatching() + { + // 测试大小写不敏感匹配 + var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns( + "*.TXT", + "Temp/", + "**/LOG/**" + ); + + var ignoreRules = new FileIgnoreRuleSet(_testRootPath, ignorePatterns); + + // 测试不同大小写的文件和目录 + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "file.txt"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "FILE.TXT"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "File.Txt"))); + + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "temp"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "TEMP"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "Temp"))); + + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "log"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "LOG"))); + Assert.True(ignoreRules.ShouldIgnore(Path.Combine(_testRootPath, "project", "Log", "system.log"))); + } + + public override void Dispose() + { + // 清理临时测试目录 + if (Directory.Exists(_testRootPath)) + { + try + { + Directory.Delete(_testRootPath, true); + } + catch + { + // 忽略清理错误 + } + } + + base.Dispose(); + } + } +} \ No newline at end of file From 39ba654a6aed88ae4a1a807ab4d9f1ac9e331897 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Tue, 13 May 2025 09:44:53 +0800 Subject: [PATCH 66/90] cdc tests --- .../Controllers/AliyunStorageController.cs | 2 +- src/MDriveSync.Client/Program.cs | 2 +- src/MDriveSync.Core/Models/DownloadTask.cs | 2 +- src/MDriveSync.Core/Services/BuzhashCDC.cs | 235 ++++++++ .../Services/BuzhashPlusCDC.cs | 304 ++++++++++ src/MDriveSync.Infrastructure/Extensions.cs | 55 +- .../MDriveSync.Infrastructure.csproj | 2 +- src/MDriveSync.Test/CDCBuzhashBaseTests.cs | 359 ++++++++++++ src/MDriveSync.Test/CDCBuzhashPlusTests.cs | 500 ++++++++++++++++ .../CDCChunkProcessingTests.cs | 522 +++++++++++++++++ src/MDriveSync.Test/CDCRealWorldTests.cs | 533 ++++++++++++++++++ 11 files changed, 2502 insertions(+), 14 deletions(-) create mode 100644 src/MDriveSync.Core/Services/BuzhashCDC.cs create mode 100644 src/MDriveSync.Core/Services/BuzhashPlusCDC.cs create mode 100644 src/MDriveSync.Test/CDCBuzhashBaseTests.cs create mode 100644 src/MDriveSync.Test/CDCBuzhashPlusTests.cs create mode 100644 src/MDriveSync.Test/CDCChunkProcessingTests.cs create mode 100644 src/MDriveSync.Test/CDCRealWorldTests.cs diff --git a/src/MDriveSync.Client.App/Controllers/AliyunStorageController.cs b/src/MDriveSync.Client.App/Controllers/AliyunStorageController.cs index 5d1b9bc..725bb3b 100644 --- a/src/MDriveSync.Client.App/Controllers/AliyunStorageController.cs +++ b/src/MDriveSync.Client.App/Controllers/AliyunStorageController.cs @@ -1323,7 +1323,7 @@ public Result GetGlobalDownloadSpeed() return Result.Ok(new { Speed = speed, - SpeedString = speed.ToFileSizeString() + "/s" + SpeedString = speed.ToFileSize() + "/s" }); } diff --git a/src/MDriveSync.Client/Program.cs b/src/MDriveSync.Client/Program.cs index b9869ae..a4a06f2 100644 --- a/src/MDriveSync.Client/Program.cs +++ b/src/MDriveSync.Client/Program.cs @@ -11,7 +11,7 @@ private static async Task Main(string[] args) { var sw = new Stopwatch(); - var rootPath = "E:\\guanpeng"; // args.Length > 0 ? args[0] : Environment.CurrentDirectory; + var rootPath = "E:\\program_files"; // args.Length > 0 ? args[0] : Environment.CurrentDirectory; var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns("**/node_modules/*", "**/bin/*", "**/obj/*", "**/.git/*"); Console.WriteLine($"开始扫描目录: {rootPath}"); diff --git a/src/MDriveSync.Core/Models/DownloadTask.cs b/src/MDriveSync.Core/Models/DownloadTask.cs index c0fb017..3e5ad16 100644 --- a/src/MDriveSync.Core/Models/DownloadTask.cs +++ b/src/MDriveSync.Core/Models/DownloadTask.cs @@ -76,7 +76,7 @@ public string FileName /// /// 文件大小 /// - public string FileSize => ((double)TotalBytes).ToFileSizeString(); + public string FileSize => ((double)TotalBytes).ToFileSize(); /// /// 获取或设置文件的总字节数。 diff --git a/src/MDriveSync.Core/Services/BuzhashCDC.cs b/src/MDriveSync.Core/Services/BuzhashCDC.cs new file mode 100644 index 0000000..ec8930f --- /dev/null +++ b/src/MDriveSync.Core/Services/BuzhashCDC.cs @@ -0,0 +1,235 @@ +namespace MDriveSync.Core.Services +{ + /// + /// 基于 Buzhash 算法的内容定义分块实现 + /// 适用于数据同步与备份场景,确保稳定性和确定性 + /// 标准 CDC 算法的行为:标准的内容定义分块算法会受到早期字节变化的影响,即:第一个字节发生变化,将会重新计算所有分块。 + /// + public class BuzhashCDC + { + // 固定的哈希表 - 使用确定性种子生成的伪随机值 + private readonly uint[] _hashTable = new uint[256]; + + // 预计算的移位表,用于高效移除窗口最左侧字节 + private readonly uint[] _shiftedTable = new uint[256]; + + // 分块参数 + private readonly int _windowSize; // 滑动窗口大小 + + private readonly uint _maskBits; // 掩码位数 + private readonly uint _mask; // 分块掩码 + private readonly int _minSize; // 最小块大小 + private readonly int _maxSize; // 最大块大小 + + /// + /// 初始化 Buzhash CDC 分块器 + /// + /// 滑动窗口大小,影响对局部变化的敏感度 + /// 目标平均块大小,影响同步效率和元数据大小 + /// 最小块大小,避免过多小块 + /// 最大块大小,避免块太大无法有效同步 + public BuzhashCDC(int windowSize = 48, int averageSize = 1024 * 1024, + int minSize = 64 * 1024, int maxSize = 4 * 1024 * 1024) + { + if (windowSize <= 0) throw new ArgumentOutOfRangeException(nameof(windowSize), "窗口大小必须为正值"); + if (averageSize <= 0) throw new ArgumentOutOfRangeException(nameof(averageSize), "平均块大小必须为正值"); + if (minSize <= 0) throw new ArgumentOutOfRangeException(nameof(minSize), "最小块大小必须为正值"); + if (maxSize <= minSize) throw new ArgumentOutOfRangeException(nameof(maxSize), "最大块大小必须大于最小块大小"); + + _windowSize = windowSize; + _minSize = minSize; + _maxSize = maxSize; + + // 根据平均大小计算掩码位数 (log₂(avg)) + _maskBits = (uint)Math.Log(averageSize, 2); + _mask = (1U << (int)_maskBits) - 1; + + // 初始化哈希表和移位表 + InitializeHashTable(); + } + + /// + /// 初始化预计算的哈希表和移位表 - 使用固定种子确保确定性 + /// + private void InitializeHashTable() + { + // 使用固定种子的随机数生成器,确保每次生成相同的哈希表 + // 种子值在 int 范围内,0x3FD79A47 = 1070677575 + var random = new Random(0x3FD79A47); + + for (int i = 0; i < 256; i++) + { + // 生成32位随机值,确保高位也有随机性 + _hashTable[i] = (uint)((random.Next() & 0x7FFFFFFF) | (random.Next() & 0x1) << 31); + + // 预计算每个字节的循环右移值,用于高效更新哈希 + _shiftedTable[i] = RotateRight(_hashTable[i], _windowSize - 1); + } + } + + /// + /// 对数据流进行内容定义分块,不改变流的原始位置 + /// + /// 输入数据流 + /// 块处理回调,接收(块数据,块长度) + public void Split(Stream inputStream, Action chunkHandler) + { + if (inputStream == null) throw new ArgumentNullException(nameof(inputStream)); + if (chunkHandler == null) throw new ArgumentNullException(nameof(chunkHandler)); + if (!inputStream.CanRead) throw new ArgumentException("输入流必须可读", nameof(inputStream)); + + // 保存原始位置,确保处理后还原 + long originalPosition = inputStream.Position; + + try + { + // 执行实际分块 + SplitInternal(inputStream, chunkHandler); + } + finally + { + // 恢复流的原始位置 + if (inputStream.CanSeek) + { + inputStream.Position = originalPosition; + } + } + } + + /// + /// 内部分块实现 + /// + private void SplitInternal(Stream inputStream, Action chunkHandler) + { + byte[] buffer = new byte[Math.Max(_windowSize, 16 * 1024)]; // 读取缓冲区 + byte[] window = new byte[_windowSize]; // 滑动窗口 + + // 文件长度检查 + long fileLength = inputStream.CanSeek ? inputStream.Length - inputStream.Position : -1; + + // 1. 处理小文件情况(小于窗口大小) + if (fileLength >= 0 && fileLength <= _windowSize) + { + byte[] smallFile = new byte[fileLength]; + int bytesRead = inputStream.Read(smallFile, 0, (int)fileLength); + if (bytesRead > 0) + { + chunkHandler(smallFile, bytesRead); + } + return; + } + + // 初始窗口读取 + int initialBytesRead = inputStream.Read(window, 0, _windowSize); + if (initialBytesRead < _windowSize) + { + // 文件小于窗口大小,通常不应发生(已在前面检查) + if (initialBytesRead > 0) + { + chunkHandler(window, initialBytesRead); + } + return; + } + + // 用于块数据的内存流,使用指定初始容量减少内存重分配 + using (var memoryStream = new MemoryStream(Math.Min(_maxSize, 1024 * 1024))) + { + // 2. 处理主体分块逻辑 + ProcessChunks(inputStream, chunkHandler, buffer, window, memoryStream); + } + } + + /// + /// 处理数据流分块的主要逻辑 + /// + private void ProcessChunks(Stream inputStream, Action chunkHandler, + byte[] buffer, byte[] window, MemoryStream memoryStream) + { + // 初始化 Buzhash 值 + uint hash = CalculateInitialHash(window); + + // 写入初始窗口 + memoryStream.Write(window, 0, _windowSize); + int currentSize = _windowSize; + + // 缓冲区读取循环 + while (true) + { + int bufferSize = inputStream.Read(buffer, 0, buffer.Length); + if (bufferSize <= 0) break; // 到达流末尾 + + // 处理缓冲区内的每个字节 + for (int i = 0; i < bufferSize; i++) + { + byte inByte = buffer[i]; + byte outByte = window[currentSize % _windowSize]; + + // 滑动窗口 + window[currentSize % _windowSize] = inByte; + + // 更新哈希值 - 标准 Buzhash 滚动哈希更新公式 + hash = ((hash << 1) | (hash >> 31)) ^ // 循环左移 + _hashTable[inByte] ^ // 添加新字节 + _shiftedTable[outByte]; // 移除旧字节 + + // 写入当前字节 + memoryStream.WriteByte(inByte); + currentSize++; + + // 检查是否达到分块点 + if (currentSize >= _minSize && + ((hash & _mask) == 0 || currentSize >= _maxSize)) + { + // 生成块 + byte[] chunk = memoryStream.ToArray(); + chunkHandler(chunk, chunk.Length); + + // 重置状态准备下一个块 + memoryStream.SetLength(0); + currentSize = 0; + + // 也需要重置哈希值,以保持块间独立性 + // 这确保了即使在相同数据流中,每个块的边界判定相互独立 + hash = 0; + } + } + } + + // 处理最后一个块(如果有) + if (memoryStream.Length > 0) + { + byte[] chunk = memoryStream.ToArray(); + chunkHandler(chunk, chunk.Length); + } + } + + /// + /// 计算初始窗口的哈希值 + /// + private uint CalculateInitialHash(byte[] window) + { + uint hash = 0; + for (int i = 0; i < _windowSize; i++) + { + hash = ((hash << 1) | (hash >> 31)) ^ _hashTable[window[i]]; + } + return hash; + } + + /// + /// 循环右移操作 + /// + private static uint RotateRight(uint value, int bits) + { + return (value >> bits) | (value << (32 - bits)); + } + + /// + /// 获取当前配置的块大小统计信息 + /// + public (int MinSize, int MaxSize, double AvgSize) GetChunkSizeStats() + { + return (_minSize, _maxSize, Math.Pow(2, _maskBits)); + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/BuzhashPlusCDC.cs b/src/MDriveSync.Core/Services/BuzhashPlusCDC.cs new file mode 100644 index 0000000..9396717 --- /dev/null +++ b/src/MDriveSync.Core/Services/BuzhashPlusCDC.cs @@ -0,0 +1,304 @@ +namespace MDriveSync.Core.Services +{ + /// + /// 优化的 Buzhash 内容定义分块实现 + /// 支持分块重置点,提供可预测的分块结果 + /// + /// 分块重置点: + /// 每隔固定字节(默认4MB)重置分块状态 + /// 防止早期变化影响整个文件的分块 + /// 实现优点: + /// 简单高效,低内存占用 + /// 变化影响限制在重置点范围内 + /// 支持任意大小的文件或流 + /// + public class BuzhashPlusCDC + { + // 分块参数 + private readonly int _windowSize; // 滑动窗口大小 + private readonly int _maskBits; // 掩码位数 + private readonly uint _mask; // 分块掩码 + private readonly int _minSize; // 最小块大小 + private readonly int _maxSize; // 最大块大小 + private readonly int _resetInterval; // 分块重置间隔(字节数) + + // 哈希表和预计算表 + private readonly uint[] _hashTable = new uint[256]; + private readonly uint[] _shiftedTable = new uint[256]; + + // 缓冲区 + private readonly byte[] _window; + + /// + /// 初始化 Buzhash CDC 分块器 + /// + /// 滑动窗口大小,通常为 32-64 字节 + /// 目标平均块大小,通常为 1MB 左右 + /// 最小块大小(字节) + /// 最大块大小(字节) + /// 分块重置间隔(字节),0表示禁用重置 + public BuzhashPlusCDC( + int windowSize = 48, + int averageSize = 1024 * 1024, + int minSize = 64 * 1024, + int maxSize = 4 * 1024 * 1024, + int resetInterval = 2 * 4 * 1024 * 1024) + { + // 参数验证 + if (windowSize <= 0) throw new ArgumentOutOfRangeException(nameof(windowSize)); + if (averageSize <= 0) throw new ArgumentOutOfRangeException(nameof(averageSize)); + if (minSize <= 0) throw new ArgumentOutOfRangeException(nameof(minSize)); + if (maxSize <= minSize) throw new ArgumentOutOfRangeException(nameof(maxSize)); + + _windowSize = windowSize; + _minSize = minSize; + _maxSize = maxSize; + _maskBits = (int)Math.Log(averageSize, 2); + _mask = (1U << _maskBits) - 1; + _resetInterval = resetInterval; + + // 预分配窗口缓冲区 + _window = new byte[windowSize]; + + // 初始化哈希表 + InitializeHashTable(); + } + + /// + /// 初始化哈希表和移位表 + /// + private void InitializeHashTable() + { + // 使用固定种子确保确定性 + var random = new Random(0x3FD79A47); + + for (int i = 0; i < 256; i++) + { + _hashTable[i] = (uint)((random.Next() & 0x7FFFFFFF) | (random.Next() & 0x1) << 31); + _shiftedTable[i] = RotateRight(_hashTable[i], _windowSize - 1); + } + } + + /// + /// 对数据流进行分块 + /// + public void Split(Stream inputStream, Action chunkHandler) + { + if (inputStream == null) throw new ArgumentNullException(nameof(inputStream)); + if (chunkHandler == null) throw new ArgumentNullException(nameof(chunkHandler)); + + // 保存原始位置 + long originalPosition = inputStream.Position; + + try + { + // 执行分块 + DoSplit(inputStream, chunkHandler); + } + finally + { + // 恢复流位置 + if (inputStream.CanSeek) + { + inputStream.Position = originalPosition; + } + } + } + + /// + /// 主分块方法 + /// + private void DoSplit(Stream inputStream, Action chunkHandler) + { + // 处理小文件 + if (inputStream.CanSeek && (inputStream.Length - inputStream.Position) <= _windowSize) + { + HandleSmallFile(inputStream, chunkHandler); + return; + } + + // 读取初始窗口 + int bytesRead = inputStream.Read(_window, 0, _windowSize); + if (bytesRead < _windowSize) + { + if (bytesRead > 0) + { + chunkHandler(_window, bytesRead); + } + return; + } + + // 内存流和缓冲区 + using (var memoryStream = new MemoryStream(Math.Min(_maxSize * 2, 4 * 1024 * 1024))) + { + byte[] buffer = new byte[64 * 1024]; // 读取缓冲区 + + // 跟踪状态 + uint hash = CalculateHash(_window, 0, _windowSize); + int chunkSize = 0; + long totalBytes = 0; + long nextResetPoint = _resetInterval > 0 ? _resetInterval : long.MaxValue; + + // 写入初始窗口 + memoryStream.Write(_window, 0, _windowSize); + chunkSize += _windowSize; + totalBytes += _windowSize; + + // 循环处理 + while (true) + { + int bufferSize = inputStream.Read(buffer, 0, buffer.Length); + if (bufferSize <= 0) break; + + for (int i = 0; i < bufferSize; i++) + { + byte inByte = buffer[i]; + + // 检查是否到达重置点 + if (_resetInterval > 0 && totalBytes == nextResetPoint && chunkSize >= _minSize) + { + // 强制输出当前块 + OutputChunk(memoryStream, chunkHandler); + + // 重置重置点和状态 + nextResetPoint = totalBytes + _resetInterval; + + // 读取新窗口 + if (i + _windowSize <= bufferSize) + { + // 窗口可以完全从缓冲区读取 + Array.Copy(buffer, i, _window, 0, _windowSize); + memoryStream.Write(_window, 0, _windowSize); + + // 重新计算哈希值和更新状态 + hash = CalculateHash(_window, 0, _windowSize); + chunkSize = _windowSize; + totalBytes += _windowSize; + i += _windowSize - 1; // -1 是因为循环会递增 i + continue; + } + else + { + // 窗口需要从缓冲区和流中读取 + int remaining = bufferSize - i; + Array.Copy(buffer, i, _window, 0, remaining); + + // 从流中读取剩余的窗口数据 + int needMore = _windowSize - remaining; + int moreRead = inputStream.Read(_window, remaining, needMore); + + // 如果流结束,处理剩余部分 + if (moreRead < needMore) + { + int totalRead = remaining + moreRead; + if (totalRead > 0) + { + chunkHandler(_window, totalRead); + } + return; + } + + // 重新计算哈希值和更新状态 + hash = CalculateHash(_window, 0, _windowSize); + memoryStream.Write(_window, 0, _windowSize); + chunkSize = _windowSize; + totalBytes += _windowSize; + + // 跳出本次循环,开始新循环 + i = bufferSize; // 设置为缓冲区末尾,强制读取新数据 + break; + } + } + + // 正常分块处理 + // 滑动窗口 + byte outByte = _window[chunkSize % _windowSize]; + _window[chunkSize % _windowSize] = inByte; + + // 更新哈希值 + hash = ((hash << 1) | (hash >> 31)) ^ + _hashTable[inByte] ^ + _shiftedTable[outByte]; + + // 写入字节 + memoryStream.WriteByte(inByte); + chunkSize++; + totalBytes++; + + // 检查是否达到分块点 + if (chunkSize >= _minSize && + ((hash & _mask) == 0 || chunkSize >= _maxSize)) + { + OutputChunk(memoryStream, chunkHandler); + + // 重置哈希值 + hash = 0; + chunkSize = 0; + } + } + } + + // 处理最后一个块 + if (memoryStream.Length > 0) + { + OutputChunk(memoryStream, chunkHandler); + } + } + } + + /// + /// 输出当前块 + /// + private void OutputChunk(MemoryStream memoryStream, Action chunkHandler) + { + byte[] chunk = memoryStream.ToArray(); + chunkHandler(chunk, chunk.Length); + memoryStream.SetLength(0); + } + + /// + /// 处理小文件 + /// + private void HandleSmallFile(Stream inputStream, Action chunkHandler) + { + int bytesToRead = (int)(inputStream.Length - inputStream.Position); + byte[] data = new byte[bytesToRead]; + int bytesRead = inputStream.Read(data, 0, bytesToRead); + + if (bytesRead > 0) + { + chunkHandler(data, bytesRead); + } + } + + /// + /// 计算哈希值 + /// + private uint CalculateHash(byte[] data, int offset, int count) + { + uint hash = 0; + for (int i = 0; i < count; i++) + { + hash = ((hash << 1) | (hash >> 31)) ^ _hashTable[data[offset + i]]; + } + return hash; + } + + /// + /// 循环右移操作 + /// + private static uint RotateRight(uint value, int bits) + { + return (value >> bits) | (value << (32 - bits)); + } + + + /// + /// 获取当前配置的块大小统计信息 + /// + public (int MinSize, int MaxSize, double AvgSize, int ResetInterval) GetChunkSizeStats() + { + return (_minSize, _maxSize, Math.Pow(2, _maskBits), _resetInterval); + } + } +} diff --git a/src/MDriveSync.Infrastructure/Extensions.cs b/src/MDriveSync.Infrastructure/Extensions.cs index 9a93f01..175127c 100644 --- a/src/MDriveSync.Infrastructure/Extensions.cs +++ b/src/MDriveSync.Infrastructure/Extensions.cs @@ -141,17 +141,13 @@ public static string ToHex(this byte[] hash) /// /// /// - public static string ToFileSizeString(this double size) + public static string ToFileSize(this double size) { - //size switch - //{ - // var s when s >= 1024 * 1024 * 1024 => $"{s / 1024 / 1024 / 1024:F2} GB/s", - // var s when s >= 1024 * 1024 => $"{s / 1024 / 1024:F2} MB/s", - // var s when s >= 1024 => $"{s / 1024:F2} KB/s", - // var s => $"{s:F2} B/s" - //}; - - if (size >= 1024 * 1024 * 1024) + if (size >= 1024 * 1024 * 1024 * 1024L) + { + return $"{size / 1024 / 1024 / 1024 / 1024:F2} TB"; + } + else if (size >= 1024 * 1024 * 1024) { return $"{size / 1024 / 1024 / 1024:F2} GB"; } @@ -169,6 +165,45 @@ public static string ToFileSizeString(this double size) } } + /// + /// 格式化文件大小 + /// + /// + /// + public static string FormatSize(this long bytes) + { + string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; + int order = 0; + double size = bytes; + + while (size >= 1024 && order < suffixes.Length - 1) + { + order++; + size = size / 1024; + } + + return $"{size:F2} {suffixes[order]}"; + } + + /// + /// 格式化文件大小 + /// + /// + /// + public static string FormatSize(this int bytes) + { + return FormatSize((long)bytes); + } + + /// + /// 格式化文件大小 + /// + /// + /// + public static string FormatSize(this double bytes) + { + return FormatSize((long)bytes); + } /// /// 查询条件扩展 diff --git a/src/MDriveSync.Infrastructure/MDriveSync.Infrastructure.csproj b/src/MDriveSync.Infrastructure/MDriveSync.Infrastructure.csproj index b22355d..086a5c2 100644 --- a/src/MDriveSync.Infrastructure/MDriveSync.Infrastructure.csproj +++ b/src/MDriveSync.Infrastructure/MDriveSync.Infrastructure.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/MDriveSync.Test/CDCBuzhashBaseTests.cs b/src/MDriveSync.Test/CDCBuzhashBaseTests.cs new file mode 100644 index 0000000..a58818c --- /dev/null +++ b/src/MDriveSync.Test/CDCBuzhashBaseTests.cs @@ -0,0 +1,359 @@ +using MDriveSync.Core.Services; +using System.Diagnostics; +using System.Text; +using Xunit.Abstractions; + +namespace MDriveSync.Test +{ + /// + /// 内容定义分块算法的单元测试 + /// 测试 BuzhashCDC 的分块行为和性能特性 + /// + public class CDCBuzhashBaseTests : BaseTests + { + private readonly ITestOutputHelper _output; + + public CDCBuzhashBaseTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void TestBasicChunking() + { + // 准备测试数据 + byte[] testData = GenerateTestData(5 * 1024 * 1024); // 5MB 测试数据 + + // 创建 CDC 实例(使用较小的块大小以便测试) + var cdc = new BuzhashCDC( + windowSize: 32, + averageSize: 64 * 1024, // 64KB 平均大小 + minSize: 16 * 1024, // 16KB 最小大小 + maxSize: 256 * 1024 // 256KB 最大大小 + ); + + // 收集分块结果 + var chunks = new List(); + + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunk, size) => + { + chunks.Add(chunk.Take(size).ToArray()); + }); + } + + // 验证基本行为 + Assert.True(chunks.Count > 0, "应该至少产生一个块"); + // 所有块的总大小应等于原始数据大小 + Assert.Equal(testData.Length, chunks.Sum(c => c.Length)); + + // 验证块大小限制 + var stats = cdc.GetChunkSizeStats(); + foreach (var chunk in chunks) + { + Assert.True(chunk.Length >= stats.MinSize || chunk == chunks.Last(), + $"除最后一块外,所有块大小应不小于最小块大小 ({chunk.Length} < {stats.MinSize})"); + Assert.True(chunk.Length <= stats.MaxSize, + $"所有块大小应不大于最大块大小 ({chunk.Length} > {stats.MaxSize})"); + } + + // 输出分块统计信息 + _output.WriteLine($"总块数: {chunks.Count}"); + _output.WriteLine($"平均块大小: {chunks.Average(c => c.Length):F0} 字节"); + _output.WriteLine($"最小块大小: {chunks.Min(c => c.Length):F0} 字节"); + _output.WriteLine($"最大块大小: {chunks.Max(c => c.Length):F0} 字节"); + } + + [Fact] + public void TestChunkingDeterminism() + { + // 准备测试数据 + byte[] testData = GenerateTestData(2 * 1024 * 1024); // 2MB 测试数据 + + // 创建 CDC 实例 + var cdc = new BuzhashCDC( + windowSize: 48, + averageSize: 128 * 1024, // 128KB 平均大小 + minSize: 32 * 1024, // 32KB 最小大小 + maxSize: 512 * 1024 // 512KB 最大大小 + ); + + // 第一次分块 + var firstRun = new List(); + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunk, size) => + { + firstRun.Add(chunk.Take(size).ToArray()); + }); + } + + // 第二次分块(相同数据) + var secondRun = new List(); + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunk, size) => + { + secondRun.Add(chunk.Take(size).ToArray()); + }); + } + + // 验证确定性(相同输入应产生相同的分块) + // 相同数据的多次分块应产生相同数量的块 + Assert.Equal(firstRun.Count, secondRun.Count); + + for (int i = 0; i < firstRun.Count; i++) + { + // $"第 {i + 1} 个块的大小应相同" + Assert.Equal(firstRun[i].Length, secondRun[i].Length); + Assert.True(firstRun[i].SequenceEqual(secondRun[i]), $"第 {i + 1} 个块的内容应相同"); + } + } + + [Fact] + public void TestChunkingWithPrependedData() + { + // 准备原始测试数据 + byte[] originalData = GenerateTestData(1024 * 1024); // 1MB + + // 准备修改后的数据(前面添加100字节) + byte[] prependedData = new byte[originalData.Length + 100]; + Array.Fill(prependedData, 0xAA, 0, 100); // 填充前100字节 + Buffer.BlockCopy(originalData, 0, prependedData, 100, originalData.Length); + + // 创建 CDC 实例 + var cdc = new BuzhashCDC( + windowSize: 48, + averageSize: 128 * 1024 + ); + + // 对原始数据分块 + var originalChunks = new List(); + using (var stream = new MemoryStream(originalData)) + { + cdc.Split(stream, (chunk, size) => + { + originalChunks.Add(chunk.Take(size).ToArray()); + }); + } + + // 对修改后数据分块 + var modifiedChunks = new List(); + using (var stream = new MemoryStream(prependedData)) + { + cdc.Split(stream, (chunk, size) => + { + modifiedChunks.Add(chunk.Take(size).ToArray()); + }); + } + + // 输出块数信息 + _output.WriteLine($"原始数据块数: {originalChunks.Count}"); + _output.WriteLine($"修改后数据块数: {modifiedChunks.Count}"); + + // 验证内容定义分块的本地化特性(后续块应部分保持一致) + int matchedChunks = 0; + for (int i = 0; i < Math.Min(originalChunks.Count, modifiedChunks.Count) - 1; i++) + { + // 从第二个块开始尝试寻找匹配 + for (int j = 1; j < modifiedChunks.Count; j++) + { + if (originalChunks[i].SequenceEqual(modifiedChunks[j])) + { + matchedChunks++; + break; + } + } + } + + _output.WriteLine($"匹配的块数: {matchedChunks}"); + + // 应该有一些块能够匹配上(具体数量取决于数据特性和分块参数) + Assert.True(matchedChunks > 0, "修改后应该有一些块与原始数据的块匹配"); + } + + [Fact] + public void TestPerformance() + { + // 准备较大的测试数据 + byte[] testData = GenerateTestData(20 * 1024 * 1024); // 20MB + + // 创建 CDC 实例 + var cdc = new Core.Services.BuzhashCDC( + windowSize: 48, + averageSize: 1024 * 1024 // 1MB 平均大小 + ); + + // 测量性能 + var stopwatch = new Stopwatch(); + var chunks = new List(); + + stopwatch.Start(); + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunk, size) => + { + chunks.Add(chunk.Take(size).ToArray()); + }); + } + stopwatch.Stop(); + + double mbPerSecond = testData.Length / (1024.0 * 1024.0) / (stopwatch.ElapsedMilliseconds / 1000.0); + + _output.WriteLine($"分块速度: {mbPerSecond:F2} MB/s"); + _output.WriteLine($"总块数: {chunks.Count}"); + _output.WriteLine($"平均块大小: {chunks.Average(c => c.Length):F0} 字节"); + + // 没有严格的性能要求,但应该在合理范围内 + Assert.True(mbPerSecond > 10, "分块速度应该至少达到 10MB/s"); + } + + [Fact] + public void TestCompareWithBuzhashPlusCDC() + { + // 准备测试数据 + byte[] testData = GenerateTestData(5 * 1024 * 1024); // 5MB + + // 常规 Buzhash CDC + var standardCdc = new BuzhashCDC( + windowSize: 48, + averageSize: 256 * 1024 // 256KB 平均大小 + ); + + // Buzhash Plus CDC (带重置点) + var plusCdc = new BuzhashPlusCDC( + windowSize: 48, + averageSize: 256 * 1024, // 256KB 平均大小 + resetInterval: 2 * 1024 * 1024 // 2MB 重置点 + ); + + // 收集常规 CDC 的分块结果 + var standardChunks = new List(); + using (var stream = new MemoryStream(testData)) + { + standardCdc.Split(stream, (chunk, size) => + { + standardChunks.Add(chunk.Take(size).ToArray()); + }); + } + + // 收集 Plus CDC 的分块结果 + var plusChunks = new List(); + using (var stream = new MemoryStream(testData)) + { + plusCdc.Split(stream, (chunk, size) => + { + plusChunks.Add(chunk.Take(size).ToArray()); + }); + } + + _output.WriteLine($"BuzhashCDC 块数: {standardChunks.Count}"); + _output.WriteLine($"BuzhashPlusCDC 块数: {plusChunks.Count}"); + _output.WriteLine($"BuzhashCDC 平均块大小: {standardChunks.Average(c => c.Length):F0} 字节"); + _output.WriteLine($"BuzhashPlusCDC 平均块大小: {plusChunks.Average(c => c.Length):F0} 字节"); + + // 不需要块完全匹配,但应该总大小相同并符合各自的分块特性 + Assert.Equal(testData.Length, standardChunks.Sum(c => c.Length)); + Assert.Equal(testData.Length, plusChunks.Sum(c => c.Length)); + } + + [Fact] + public void TestCDCWithModifiedData() + { + // 1. 原始数据 + byte[] originalData = GenerateTestData(4 * 1024 * 1024); // 4MB 随机数据 + + // 2. 创建修改后的数据副本(中间修改一小部分) + byte[] modifiedData = new byte[originalData.Length]; + Buffer.BlockCopy(originalData, 0, modifiedData, 0, originalData.Length); + + // 在 2MB 处修改 1KB 数据 + var random = new Random(42); + random.NextBytes(modifiedData.AsSpan(2 * 1024 * 1024, 1024)); + + // 3. 初始化分块器 + var cdc = new BuzhashCDC( + windowSize: 48, + averageSize: 256 * 1024, // 256KB 平均块 + minSize: 64 * 1024, // 64KB 最小块 + maxSize: 1024 * 1024 // 1MB 最大块 + ); + + // 4. 分别对原始数据和修改后的数据进行分块 + var originalChunks = ChunkData(cdc, originalData); + var modifiedChunks = ChunkData(cdc, modifiedData); + + // 5. 找出相同的块 + int originalMatches = 0; + int modifiedMatches = 0; + + // 记录已匹配的块,避免重复计数 + HashSet matchedOriginal = new HashSet(); + HashSet matchedModified = new HashSet(); + + // 比较所有块 + for (int i = 0; i < originalChunks.Count; i++) + { + for (int j = 0; j < modifiedChunks.Count; j++) + { + if (matchedModified.Contains(j)) + continue; + + if (originalChunks[i].SequenceEqual(modifiedChunks[j])) + { + originalMatches++; + matchedOriginal.Add(i); + matchedModified.Add(j); + break; + } + } + } + + // 6. 计算匹配率 + double originalMatchRate = (double)originalMatches / originalChunks.Count; + + _output.WriteLine($"原始块数: {originalChunks.Count}"); + _output.WriteLine($"修改后块数: {modifiedChunks.Count}"); + _output.WriteLine($"匹配的块数: {originalMatches}"); + _output.WriteLine($"匹配率: {originalMatchRate:P2}"); + + // 由于只修改了小部分数据,应该有较高的块匹配率 + Assert.True(originalMatchRate > 0.7, "修改小部分数据后,块匹配率应高于70%"); + } + + // 辅助方法:对数据进行分块,并返回块列表 + private List ChunkData(Core.Services.BuzhashCDC cdc, byte[] data) + { + var chunks = new List(); + using (var stream = new MemoryStream(data)) + { + cdc.Split(stream, (chunk, size) => + { + chunks.Add(chunk.Take(size).ToArray()); + }); + } + return chunks; + } + + // 辅助方法:生成测试数据(随机内容,但包含一些重复模式) + private byte[] GenerateTestData(int size) + { + var data = new byte[size]; + var random = new Random(42); // 固定种子以确保结果可重现 + + // 填充大部分随机数据 + random.NextBytes(data); + + // 每 128KB 插入一些有规律的数据,使其更类似实际文件 + byte[] pattern = Encoding.ASCII.GetBytes("This is a repeating pattern to simulate real file content. "); + for (int i = 0; i < size; i += 128 * 1024) + { + int copyLength = Math.Min(pattern.Length, size - i); + Buffer.BlockCopy(pattern, 0, data, i, copyLength); + } + + return data; + } + } +} diff --git a/src/MDriveSync.Test/CDCBuzhashPlusTests.cs b/src/MDriveSync.Test/CDCBuzhashPlusTests.cs new file mode 100644 index 0000000..4a4d08d --- /dev/null +++ b/src/MDriveSync.Test/CDCBuzhashPlusTests.cs @@ -0,0 +1,500 @@ +using MDriveSync.Core.Services; +using System.Security.Cryptography; + +namespace MDriveSync.Test +{ + public class CDCBuzhashPlusTests : BaseTests + { + [Fact] + public void GetChunkSizeStats_ReturnsCorrectValues() + { + // Arrange + int windowSize = 48; + int avgSize = 1024 * 1024; + int minSize = avgSize / 4; + int maxSize = avgSize * 4; + var cdc = new BuzhashCDC(windowSize, avgSize, minSize, maxSize); + + // Act + var stats = cdc.GetChunkSizeStats(); + + // Assert + Assert.Equal(minSize, stats.MinSize); + Assert.Equal(maxSize, stats.MaxSize); + Assert.Equal(Math.Pow(2, Math.Log(avgSize, 2)), stats.AvgSize, 0.1); + } + + [Fact] + public void Split_BasicFunction_ChunksCorrectlySplit() + { + // Arrange + int testDataSize = 10 * 1024 * 1024; // 10MB + byte[] testData = TestHelpers.GenerateRandomData(testDataSize); + var cdc = new BuzhashCDC( + windowSize: 48, + averageSize: 1024 * 1024, // 1MB average + minSize: 256 * 1024, // 256KB min + maxSize: 4 * 1024 * 1024 // 4MB max + ); + var chunks = new List(); + + // Act + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunkData, length) => + { + chunks.Add(chunkData.Take(length).ToArray()); + }); + } + + // Assert + Assert.True(chunks.Count > 0, "应该产生至少一个块"); + // 所有块的总大小应等于原始数据大小 + Assert.Equal(testDataSize, chunks.Sum(c => c.Length)); + + // 验证重构数据与原始数据相同 + byte[] reconstructed = TestHelpers.ReconstructData(chunks); + Assert.Equal(testData, reconstructed); + + // 验证块大小在规定范围内 + foreach (var chunk in chunks) + { + if (chunk == chunks.Last() && testDataSize % cdc.GetChunkSizeStats().MinSize != 0) + { + // 最后一个块可能小于最小大小 + continue; + } + Assert.True(chunk.Length >= cdc.GetChunkSizeStats().MinSize, + $"块大小 {chunk.Length} 应大于或等于最小大小 {cdc.GetChunkSizeStats().MinSize}"); + Assert.True(chunk.Length <= cdc.GetChunkSizeStats().MaxSize, + $"块大小 {chunk.Length} 应小于或等于最大大小 {cdc.GetChunkSizeStats().MaxSize}"); + } + } + + [Fact] + public void Split_SmallFile_SingleChunkReturned() + { + // Arrange + int smallFileSize = 1024; // 1KB,小于窗口大小 + byte[] testData = TestHelpers.GenerateRandomData(smallFileSize); + var cdc = new BuzhashCDC(windowSize: 48); + var chunks = new List(); + + // Act + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunkData, length) => + { + chunks.Add(chunkData.Take(length).ToArray()); + }); + } + + // Assert + Assert.Single(chunks); + Assert.Equal(smallFileSize, chunks[0].Length); + Assert.Equal(testData, chunks[0]); + } + + [Fact] + public void Split_EmptyFile_NoChunksReturned() + { + // Arrange + byte[] emptyData = new byte[0]; + var cdc = new BuzhashCDC(windowSize: 48); + var chunks = new List(); + + // Act + using (var stream = new MemoryStream(emptyData)) + { + cdc.Split(stream, (chunkData, length) => + { + chunks.Add(chunkData.Take(length).ToArray()); + }); + } + + // Assert + Assert.Empty(chunks); + } + + [Fact] + public void Split_IdenticalData_ProducesSameChunks() + { + // Arrange + int testDataSize = 5 * 1024 * 1024; + byte[] testData = TestHelpers.GenerateRandomData(testDataSize); + var cdc = new BuzhashCDC(windowSize: 48); + var firstRun = new List(); + var secondRun = new List(); + + // Act - 第一次分块 + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunkData, length) => + { + firstRun.Add(chunkData.Take(length).ToArray()); + }); + } + + // Act - 第二次分块(相同数据) + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunkData, length) => + { + secondRun.Add(chunkData.Take(length).ToArray()); + }); + } + + // Assert + Assert.Equal(firstRun.Count, secondRun.Count); + for (int i = 0; i < firstRun.Count; i++) + { + Assert.Equal(firstRun[i].Length, secondRun[i].Length); + Assert.Equal(firstRun[i], secondRun[i]); + } + } + + [Fact] + public void Split_ModifiedData_OnlyAffectedChunksChange() + { + // Arrange + int testDataSize = 5 * 1024 * 1024; + byte[] originalData = TestHelpers.GenerateRandomData(testDataSize); + byte[] modifiedData = (byte[])originalData.Clone(); + + // 修改中间的一小部分数据 + int modifyPosition = testDataSize / 2; + modifiedData[modifyPosition] = (byte)(modifiedData[modifyPosition] ^ 0xFF); // 翻转位 + + var cdc = new BuzhashCDC(windowSize: 48); + var originalChunks = new List(); + var modifiedChunks = new List(); + + // Act - 原始数据分块 + using (var stream = new MemoryStream(originalData)) + { + cdc.Split(stream, (chunkData, length) => + { + originalChunks.Add(chunkData.Take(length).ToArray()); + }); + } + + // Act - 修改后数据分块 + using (var stream = new MemoryStream(modifiedData)) + { + cdc.Split(stream, (chunkData, length) => + { + modifiedChunks.Add(chunkData.Take(length).ToArray()); + }); + } + + // Assert + // 比较块数量和总大小 + Assert.Equal(originalData.Length, originalChunks.Sum(c => c.Length)); + Assert.Equal(modifiedData.Length, modifiedChunks.Sum(c => c.Length)); + + // 确认有差异的块 + bool foundDifference = false; + for (int i = 0; i < Math.Min(originalChunks.Count, modifiedChunks.Count); i++) + { + if (!originalChunks[i].SequenceEqual(modifiedChunks[i])) + { + foundDifference = true; + break; + } + } + + Assert.True(foundDifference, "修改数据应该产生至少一个不同的块"); + } + } + + public class BuzhashPlusCDCTests : BaseTests + { + [Fact] + public void GetChunkSizeStats_ReturnsCorrectValues() + { + // Arrange + int windowSize = 48; + int avgSize = 1024 * 1024; + int minSize = avgSize / 4; + int maxSize = avgSize * 4; + int resetInterval = 4 * 1024 * 1024; + var cdc = new BuzhashPlusCDC(windowSize, avgSize, minSize, maxSize, resetInterval); + + // Act + var stats = cdc.GetChunkSizeStats(); + + // Assert + Assert.Equal(minSize, stats.MinSize); + Assert.Equal(maxSize, stats.MaxSize); + Assert.Equal(Math.Pow(2, Math.Log(avgSize, 2)), stats.AvgSize, 0.1); + Assert.Equal(resetInterval, stats.ResetInterval); + } + + [Fact] + public void Split_BasicFunction_ChunksCorrectlySplit() + { + // Arrange + int testDataSize = 10 * 1024 * 1024; // 10MB + byte[] testData = TestHelpers.GenerateRandomData(testDataSize); + var cdc = new BuzhashPlusCDC( + windowSize: 48, + averageSize: 1024 * 1024, // 1MB average + minSize: 256 * 1024, // 256KB min + maxSize: 4 * 1024 * 1024, // 4MB max + resetInterval: 8 * 1024 * 1024 // 8MB reset + ); + var chunks = new List(); + + // Act + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunkData, length) => + { + chunks.Add(chunkData.Take(length).ToArray()); + }); + } + + // Assert + Assert.True(chunks.Count > 0, "应该产生至少一个块"); + // 所有块的总大小应等于原始数据大小 + Assert.Equal(testDataSize, chunks.Sum(c => c.Length)); + + // 验证重构数据与原始数据相同 + byte[] reconstructed = TestHelpers.ReconstructData(chunks); + Assert.Equal(testData, reconstructed); + + // 验证块大小在规定范围内 + foreach (var chunk in chunks) + { + if (chunk == chunks.Last() && testDataSize % cdc.GetChunkSizeStats().MinSize != 0) + { + // 最后一个块可能小于最小大小 + continue; + } + Assert.True(chunk.Length >= cdc.GetChunkSizeStats().MinSize, + $"块大小 {chunk.Length} 应大于或等于最小大小 {cdc.GetChunkSizeStats().MinSize}"); + Assert.True(chunk.Length <= cdc.GetChunkSizeStats().MaxSize, + $"块大小 {chunk.Length} 应小于或等于最大大小 {cdc.GetChunkSizeStats().MaxSize}"); + } + } + + [Fact] + public void Split_WithResetInterval_ResetPointsRespected() + { + // Arrange + int testDataSize = 20 * 1024 * 1024; // 20MB + byte[] testData = TestHelpers.GenerateRandomData(testDataSize); + int resetInterval = 5 * 1024 * 1024; // 5MB重置点 + + // 创建两个CDC实例,一个有重置点,一个无重置点 + var cdcWithReset = new BuzhashPlusCDC( + windowSize: 48, + averageSize: 1024 * 1024, + minSize: 256 * 1024, + maxSize: 4 * 1024 * 1024, + resetInterval: resetInterval + ); + + var cdcNoReset = new BuzhashPlusCDC( + windowSize: 48, + averageSize: 1024 * 1024, + minSize: 256 * 1024, + maxSize: 4 * 1024 * 1024, + resetInterval: 0 // 禁用重置点 + ); + + var chunksWithReset = new List(); + var chunksNoReset = new List(); + + // Act - 使用有重置点的CDC + using (var stream = new MemoryStream(testData)) + { + cdcWithReset.Split(stream, (chunkData, length) => + { + chunksWithReset.Add(chunkData.Take(length).ToArray()); + }); + } + + // Act - 使用无重置点的CDC + using (var stream = new MemoryStream(testData)) + { + cdcNoReset.Split(stream, (chunkData, length) => + { + chunksNoReset.Add(chunkData.Take(length).ToArray()); + }); + } + + // Assert + // 两种方式都应该能正确重构原始数据 + Assert.Equal(testDataSize, chunksWithReset.Sum(c => c.Length)); + Assert.Equal(testDataSize, chunksNoReset.Sum(c => c.Length)); + + // 比较两种分块结果的差异 + // 有重置点的方法分块数量通常会略多一些 + // 注意:这个断言在某些特殊数据上可能不总是成立,但在随机数据上通常是可靠的 + Assert.NotEqual( + chunksWithReset.Select(c => TestHelpers.ComputeHash(c)).ToArray(), + chunksNoReset.Select(c => TestHelpers.ComputeHash(c)).ToArray() + ); + } + + [Fact] + public void Split_SmallFile_SingleChunkReturned() + { + // Arrange + int smallFileSize = 1024; // 1KB,小于窗口大小 + byte[] testData = TestHelpers.GenerateRandomData(smallFileSize); + var cdc = new BuzhashPlusCDC(windowSize: 48); + var chunks = new List(); + + // Act + using (var stream = new MemoryStream(testData)) + { + cdc.Split(stream, (chunkData, length) => + { + chunks.Add(chunkData.Take(length).ToArray()); + }); + } + + // Assert + Assert.Single(chunks); + Assert.Equal(smallFileSize, chunks[0].Length); + Assert.Equal(testData, chunks[0]); + } + + [Fact] + public void Split_ModifiedDataWithReset_LocalizedImpact() + { + // Arrange - 生成一个有规律的数据,以便于控制影响范围 + int testDataSize = 15 * 1024 * 1024; // 15MB + byte[] originalData = TestHelpers.GenerateRandomData(testDataSize); + byte[] modifiedData = (byte[])originalData.Clone(); + + // 修改第1MB的数据 + int modifyPosition = 1 * 1024 * 1024; + modifiedData[modifyPosition] = (byte)(modifiedData[modifyPosition] ^ 0xFF); + + // 使用6MB重置点 + var cdc = new BuzhashPlusCDC( + windowSize: 48, + averageSize: 1024 * 1024, + minSize: 256 * 1024, + maxSize: 4 * 1024 * 1024, + resetInterval: 6 * 1024 * 1024 + ); + + var originalChunks = new List(); + var modifiedChunks = new List(); + var originalHashes = new List(); + var modifiedHashes = new List(); + + // Act - 原始数据分块 + using (var stream = new MemoryStream(originalData)) + { + cdc.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + originalChunks.Add(chunk); + originalHashes.Add(TestHelpers.ComputeHash(chunk)); + }); + } + + // Act - 修改后数据分块 + using (var stream = new MemoryStream(modifiedData)) + { + cdc.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + modifiedChunks.Add(chunk); + modifiedHashes.Add(TestHelpers.ComputeHash(chunk)); + }); + } + + // Assert + // 验证修改只影响了一部分块(不超过重置点后的块) + bool foundDifferenceBefore6MB = false; + bool foundDifferenceAfter6MB = false; + + // 找出第一个重置点(约6MB) + long resetPosition = 6 * 1024 * 1024; + int resetChunkIndex = -1; + long cumulativeSize = 0; + + for (int i = 0; i < originalChunks.Count; i++) + { + cumulativeSize += originalChunks[i].Length; + if (cumulativeSize > resetPosition && resetChunkIndex == -1) + { + resetChunkIndex = i; + break; + } + } + + // 如果没有找到重置点(可能文件太小),则直接断言测试通过 + if (resetChunkIndex == -1) + { + return; + } + + // 检查每个区域的差异 + for (int i = 0; i < Math.Min(originalHashes.Count, modifiedHashes.Count); i++) + { + if (i < resetChunkIndex && originalHashes[i] != modifiedHashes[i]) + { + foundDifferenceBefore6MB = true; + } + else if (i >= resetChunkIndex && originalHashes[i] != modifiedHashes[i]) + { + foundDifferenceAfter6MB = true; + } + } + + // 应该在修改位置前找到差异 + Assert.True(foundDifferenceBefore6MB, "应该在前6MB处找到块差异"); + + // 这个断言验证重置点的效果:修改不应该影响超过重置点的块 + // 注意:在某些极端情况下,这个断言可能不成立,但对于随机数据通常是可靠的 + Assert.False(foundDifferenceAfter6MB, "修改不应影响重置点后的块"); + } + } + + // 测试工具类 + public static class TestHelpers + { + private static readonly Random _random = new Random(42); // 使用固定种子以确保测试的可重复性 + + // 生成随机测试数据 + public static byte[] GenerateRandomData(int size) + { + byte[] data = new byte[size]; + _random.NextBytes(data); + return data; + } + + // 重构原始数据 + public static byte[] ReconstructData(List chunks) + { + int totalSize = chunks.Sum(c => c.Length); + byte[] result = new byte[totalSize]; + int offset = 0; + + foreach (var chunk in chunks) + { + Buffer.BlockCopy(chunk, 0, result, offset, chunk.Length); + offset += chunk.Length; + } + + return result; + } + + // 计算数据的哈希值 + public static string ComputeHash(byte[] data) + { + using (var sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(data); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } +} diff --git a/src/MDriveSync.Test/CDCChunkProcessingTests.cs b/src/MDriveSync.Test/CDCChunkProcessingTests.cs new file mode 100644 index 0000000..ae08c08 --- /dev/null +++ b/src/MDriveSync.Test/CDCChunkProcessingTests.cs @@ -0,0 +1,522 @@ +using MDriveSync.Core.Services; +using MDriveSync.Infrastructure; +using System.Diagnostics; +using System.Security.Cryptography; +using Xunit.Abstractions; + +namespace MDriveSync.Test +{ + /// + /// 内容定义分块处理单元测试 + /// 与原始程序保持逻辑一致,但转换为可测试的形式 + /// + public class CDCChunkProcessingTests : BaseTests + { + private readonly ITestOutputHelper _output; + + public CDCChunkProcessingTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void ChunkAndAnalyzeFile_ExactlyMatchesOriginalLogic() + { + // Arrange + int testDataSize = 20 * 1024 * 1024; // 20MB + byte[] testData = GenerateTestData(testDataSize); + + // string filePath = @"E:\downs\fdm\__分块测试\Driver.rar"; + + //// 对文件的第一个未知添加 1 个字节 + //byte[] data = File.ReadAllBytes(filePath); + //data[0] = 0x01; + //File.WriteAllBytes(filePath, data); + + //// 对文件的最后个未知追加 1 个字节 + //byte[] data = File.ReadAllBytes(filePath); + //data[data.Length - 1] = 0x01; + //File.WriteAllBytes(filePath, data); + + //// 复制文件自身,复制 2 次,最后重新写入 + //var data = File.ReadAllBytes(filePath); + //var data2 = new byte[data.Length * 2]; + //Buffer.BlockCopy(data, 0, data2, 0, data.Length); + //Buffer.BlockCopy(data, 0, data2, data.Length, data.Length); + //File.WriteAllBytes(filePath, data2); + + //// 复制文件自身,复制 n 次,最后重新写入 + //var fn = 3; + //var data = File.ReadAllBytes(filePath); + //var data2 = new byte[data.Length * fn]; + //for (int i = 0; i < fn; i++) + //{ + // Buffer.BlockCopy(data, 0, data2, i * data.Length, data.Length); + //} + //File.WriteAllBytes(filePath, data2); + + // 设置分块参数,与原程序保持一致 + int avgChunkSizeKB = 1024 * 4; // 4MB + int averageChunkSize = avgChunkSizeKB * 1024; + + // 模拟文件路径和输出目录 - 仅用于输出显示 + string mockFilePath = "test_data.bin"; + string outputDir = $"chunks_{Path.GetFileNameWithoutExtension(mockFilePath)}_{DateTime.Now:yyyyMMdd_HHmmss}"; + + _output.WriteLine($"处理文件: {Path.GetFileName(mockFilePath)}"); + _output.WriteLine($"文件大小: {testData.Length.FormatSize()}"); + _output.WriteLine($"配置参数: 平均块 {averageChunkSize.FormatSize()}, 窗口 48 字节"); + _output.WriteLine(""); + + // 创建CDC实例,参照原程序配置 + var cdc = new BuzhashPlusCDC( + windowSize: 48, + averageSize: averageChunkSize, + minSize: averageChunkSize / 4, + maxSize: averageChunkSize * 4, + resetInterval: averageChunkSize * 4 * 2 + ); + + var stats = cdc.GetChunkSizeStats(); + _output.WriteLine($"块大小配置: 最小={stats.MinSize.FormatSize()}, 平均={stats.AvgSize.FormatSize()}, 最大={stats.MaxSize.FormatSize()}"); + + // 准备统计变量 + var chunkSizes = new List(); + var uniqueChunks = new HashSet(); + var stopwatch = Stopwatch.StartNew(); + long totalBytes = 0; + int chunkCount = 0; + + // 测试中不实际保存块文件 + // 设置为 true 将保存实际文件块 + bool saveChunks = false; + + // 创建一个字典保存分块内容和哈希,用于验证 + var chunkData = new Dictionary(); + + _output.WriteLine("\n开始分块处理..."); + + if (saveChunks && !Directory.Exists(outputDir)) + Directory.CreateDirectory(outputDir); + + // Act + using (var memoryStream = new MemoryStream(testData)) + { + cdc.Split(memoryStream, (chunk, length) => + { + chunkCount++; + totalBytes += length; + chunkSizes.Add(length); + + // 计算块哈希值 + string hash = ComputeHash(chunk, length); + bool isUnique = uniqueChunks.Add(hash); + + // 记录块数据(测试中模拟文件保存) + if (saveChunks) + { + chunkData[hash] = chunk.Take(length).ToArray(); + } + + // 可选: 保存块到文件 + if (saveChunks) + { + string chunkFile = Path.Combine(outputDir, $"{hash.Substring(0, 16)}_{length}.bin"); + File.WriteAllBytes(chunkFile, chunk.Take(length).ToArray()); + } + + // 显示进度(每100个块或前10个块) + if (chunkCount % 100 == 0 || chunkCount < 10) + { + _output.WriteLine($"处理进度: {totalBytes * 100 / memoryStream.Length}% | 已处理 {chunkCount} 个块"); + } + }); + } + + stopwatch.Stop(); + _output.WriteLine($"处理完成: {chunkCount} 个块, 总大小: {totalBytes.FormatSize()}"); + + // 显示统计信息 + DisplayStatistics(chunkSizes, uniqueChunks.Count, stopwatch.Elapsed, totalBytes); + + // Assert + // 总处理字节数应等于原始数据大小 + Assert.Equal(testData.Length, totalBytes); + Assert.True(chunkCount > 0, "应至少生成一个块"); + + // 验证块大小在配置范围内 + if (chunkSizes.Count > 1) // 如果数据小于最小块大小可能只有一个块 + { + Assert.True(chunkSizes.Min() >= stats.MinSize || chunkSizes.Count == 1, + $"最小块大小 {chunkSizes.Min()} 应大于等于配置的最小大小 {stats.MinSize}"); + Assert.True(chunkSizes.Max() <= stats.MaxSize, + $"最大块大小 {chunkSizes.Max()} 应小于等于配置的最大大小 {stats.MaxSize}"); + } + } + + [Fact] + public void ChunkProcessing_ModifyFirstByte() + { + // Arrange - 生成基础数据 + int dataSize = 10 * 1024 * 1024; // 10MB + byte[] originalData = GenerateTestData(dataSize); + byte[] modifiedData = (byte[])originalData.Clone(); + + // 修改第一个字节 + modifiedData[0] = 0x01; + + // 执行分块测试并比较结果 + var result = RunComparativeChunkTest(originalData, modifiedData, "修改第一个字节"); + + // Assert + _output.WriteLine($"相同块比例: {result.SameChunkPercentage:F2}%"); + _output.WriteLine($"第一个不同块的索引: {result.FirstDifferentChunkIndex}"); + + // 验证修改的影响是局部的,而不是全局的 + Assert.True(result.FirstDifferentChunkIndex == 0, "第一个块应该受到影响"); + Assert.True(result.SameChunkPercentage > 50, "应有超过50%的块保持不变"); + } + + [Fact] + public void ChunkProcessing_ModifyLastByte() + { + // Arrange - 生成基础数据 + int dataSize = 10 * 1024 * 1024; // 10MB + byte[] originalData = GenerateTestData(dataSize); + byte[] modifiedData = (byte[])originalData.Clone(); + + // 修改最后一个字节 + modifiedData[modifiedData.Length - 1] = 0x01; + + // 执行分块测试并比较结果 + var result = RunComparativeChunkTest(originalData, modifiedData, "修改最后一个字节"); + + // Assert + _output.WriteLine($"相同块比例: {result.SameChunkPercentage:F2}%"); + _output.WriteLine($"第一个不同块的索引: {result.FirstDifferentChunkIndex}"); + + // 验证修改的影响是局部的 + Assert.True(result.FirstDifferentChunkIndex > 0, "第一个块不应受到影响"); + Assert.True(result.SameChunkPercentage > 70, "应有超过70%的块保持不变"); + } + + [Fact] + public void ChunkProcessing_DuplicateContent_TwoTimes() + { + // Arrange + int originalSize = 5 * 1024 * 1024; // 5MB + byte[] originalData = GenerateTestData(originalSize); + + // 复制数据两次 + byte[] duplicatedData = new byte[originalSize * 2]; + Buffer.BlockCopy(originalData, 0, duplicatedData, 0, originalSize); + Buffer.BlockCopy(originalData, 0, duplicatedData, originalSize, originalSize); + + // 执行分块测试 + var result = ChunkAndAnalyzeWithStats(duplicatedData, "复制2次"); + + // Assert + double expectedDuplicateRatio = 50.0; // 理论上有50%的块是重复的 + double actualDuplicateRatio = 100 - (result.UniqueChunks * 100.0 / result.TotalChunks); + + _output.WriteLine($"理论重复率: {expectedDuplicateRatio:F2}%"); + _output.WriteLine($"实际重复率: {actualDuplicateRatio:F2}%"); + + Assert.True(actualDuplicateRatio > 40, "重复率应接近理论值(50%)"); + + // 总字节数应等于原始数据大小 + Assert.Equal(duplicatedData.Length, result.TotalBytes); + } + + [Fact] + public void ChunkProcessing_DuplicateContent_ThreeTimes() + { + // Arrange + int originalSize = 3 * 1024 * 1024; // 3MB + byte[] originalData = GenerateTestData(originalSize); + + // 复制数据三次 + int fn = 3; + byte[] duplicatedData = new byte[originalSize * fn]; + for (int i = 0; i < fn; i++) + { + Buffer.BlockCopy(originalData, 0, duplicatedData, i * originalSize, originalSize); + } + + // 执行分块测试 + var result = ChunkAndAnalyzeWithStats(duplicatedData, "复制3次"); + + // Assert + double expectedDuplicateRatio = 66.7; // 理论上有2/3的块是重复的 + double actualDuplicateRatio = 100 - (result.UniqueChunks * 100.0 / result.TotalChunks); + + _output.WriteLine($"理论重复率: {expectedDuplicateRatio:F2}%"); + _output.WriteLine($"实际重复率: {actualDuplicateRatio:F2}%"); + + Assert.True(actualDuplicateRatio > 50, "重复率应接近理论值(66.7%)"); + + // 总字节数应等于原始数据大小 + Assert.Equal(duplicatedData.Length, result.TotalBytes); + } + + #region Helper Methods + + /// + /// 运行比较测试,分析原始数据和修改后数据的分块差异 + /// + private (double SameChunkPercentage, int FirstDifferentChunkIndex, int SameChunkCount) + RunComparativeChunkTest(byte[] originalData, byte[] modifiedData, string testName) + { + _output.WriteLine($"===== 比较测试: {testName} ====="); + + int avgChunkSizeKB = 1024 * 1; // 1MB 平均块大小 + int averageChunkSize = avgChunkSizeKB * 1024; + + var cdc = new BuzhashPlusCDC( + windowSize: 48, + averageSize: averageChunkSize, + minSize: averageChunkSize / 4, + maxSize: averageChunkSize * 4, + resetInterval: averageChunkSize * 4 * 2 + ); + + // 分块结果 + var originalChunks = new List(); + var modifiedChunks = new List(); + var originalHashes = new List(); + var modifiedHashes = new List(); + + // 原始数据分块 + using (var stream = new MemoryStream(originalData)) + { + cdc.Split(stream, (chunk, length) => + { + originalChunks.Add(chunk.Take(length).ToArray()); + originalHashes.Add(ComputeHash(chunk, length)); + }); + } + + // 修改后数据分块 + using (var stream = new MemoryStream(modifiedData)) + { + cdc.Split(stream, (chunk, length) => + { + modifiedChunks.Add(chunk.Take(length).ToArray()); + modifiedHashes.Add(ComputeHash(chunk, length)); + }); + } + + // 分析差异 + int sameChunkCount = 0; + int firstDifferentIndex = -1; + + for (int i = 0; i < Math.Min(originalHashes.Count, modifiedHashes.Count); i++) + { + if (originalHashes[i] == modifiedHashes[i]) + { + sameChunkCount++; + } + else if (firstDifferentIndex == -1) + { + firstDifferentIndex = i; + } + } + + // 计算相同块的百分比 + double sameChunkPercentage = (double)sameChunkCount / Math.Max(originalHashes.Count, modifiedHashes.Count) * 100; + + // 输出差异统计 + _output.WriteLine($"原始数据块数: {originalChunks.Count}"); + _output.WriteLine($"修改后数据块数: {modifiedChunks.Count}"); + _output.WriteLine($"相同块数: {sameChunkCount}"); + + return (sameChunkPercentage, firstDifferentIndex, sameChunkCount); + } + + /// + /// 对数据进行分块并返回详细统计信息 + /// + private (int TotalChunks, int UniqueChunks, long TotalBytes, TimeSpan ProcessingTime, List ChunkSizes) + ChunkAndAnalyzeWithStats(byte[] data, string testName) + { + _output.WriteLine($"===== 分块测试: {testName} ====="); + + int avgChunkSizeKB = 1024 * 1; // 1MB 平均块大小 + int averageChunkSize = avgChunkSizeKB * 1024; + + var cdc = new BuzhashPlusCDC( + windowSize: 48, + averageSize: averageChunkSize, + minSize: averageChunkSize / 4, + maxSize: averageChunkSize * 4, + resetInterval: averageChunkSize * 4 * 2 + ); + + var chunkSizes = new List(); + var uniqueChunks = new HashSet(); + var stopwatch = Stopwatch.StartNew(); + long totalBytes = 0; + int chunkCount = 0; + + using (var stream = new MemoryStream(data)) + { + cdc.Split(stream, (chunk, length) => + { + chunkCount++; + totalBytes += length; + chunkSizes.Add(length); + + // 计算块哈希值 + string hash = ComputeHash(chunk, length); + uniqueChunks.Add(hash); + }); + } + + stopwatch.Stop(); + + // 显示统计信息 + DisplayStatistics(chunkSizes, uniqueChunks.Count, stopwatch.Elapsed, totalBytes); + + return (chunkCount, uniqueChunks.Count, totalBytes, stopwatch.Elapsed, chunkSizes); + } + + /// + /// 计算哈希值,与原程序保持一致 + /// + private string ComputeHash(byte[] data, int length) + { + using (var sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(data, 0, length); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + + /// + /// 显示块大小分布,与原程序保持一致 + /// + private void DisplayStatistics(List chunkSizes, int uniqueChunks, TimeSpan elapsed, long totalBytes) + { + _output.WriteLine("\n=== 分块统计信息 ==="); + _output.WriteLine($"总块数: {chunkSizes.Count}"); + _output.WriteLine($"唯一块数: {uniqueChunks} ({uniqueChunks * 100.0 / chunkSizes.Count:F2}%)"); + _output.WriteLine($"处理速度: {(totalBytes / Math.Max(1, elapsed.TotalSeconds)).FormatSize()}/秒"); + _output.WriteLine($"处理时间: {elapsed.TotalSeconds:F2} 秒"); + + if (chunkSizes.Count > 0) + { + _output.WriteLine("\n=== 块大小分布 ==="); + _output.WriteLine($"最小块: {chunkSizes.Min().FormatSize()}"); + _output.WriteLine($"最大块: {chunkSizes.Max().FormatSize()}"); + _output.WriteLine($"平均块: {chunkSizes.Average().FormatSize()}"); + _output.WriteLine($"中位数: {Median(chunkSizes).FormatSize()}"); + + // 显示直方图 + DisplayHistogram(chunkSizes); + } + } + + /// + /// 计算中位数,与原程序保持一致 + /// + private int Median(List values) + { + var sortedValues = values.OrderBy(v => v).ToList(); + int mid = sortedValues.Count / 2; + if (sortedValues.Count % 2 == 0) + return (sortedValues[mid - 1] + sortedValues[mid]) / 2; + else + return sortedValues[mid]; + } + + /// + /// 显示直方图,与原程序保持一致 + /// + private void DisplayHistogram(List sizes) + { + // 创建块大小分布的简单直方图 + const int numBuckets = 10; + int min = sizes.Min(); + int max = sizes.Max(); + double bucketSize = (max - min) / (double)numBuckets; + + if (bucketSize <= 0) + { + _output.WriteLine("所有块大小相同,无法创建直方图"); + return; + } + + int[] histogram = new int[numBuckets]; + + foreach (int size in sizes) + { + int bucketIndex = Math.Min(numBuckets - 1, + (int)Math.Floor((size - min) / bucketSize)); + histogram[bucketIndex]++; + } + + _output.WriteLine("\n块大小分布直方图:"); + int maxCount = histogram.Max(); + + if (maxCount == 0) + { + _output.WriteLine("直方图为空"); + return; + } + + for (int i = 0; i < numBuckets; i++) + { + int lower = (int)(min + i * bucketSize); + int upper = (int)(min + (i + 1) * bucketSize); + + // 计算星号数量 + int stars = (int)Math.Round(histogram[i] * 50.0 / maxCount); + + _output.WriteLine($"{lower.FormatSize()}-{upper.FormatSize()}: {new string('*', stars)} ({histogram[i]})"); + } + } + + /// + /// 生成测试数据 - 包含一些模式以模拟真实文件 + /// + private byte[] GenerateTestData(int size) + { + // 使用固定种子确保测试结果可重现 + var random = new Random(2025_05_13); + byte[] data = new byte[size]; + + // 填充随机数据 + random.NextBytes(data); + + // 添加一些重复模式 + if (size > 100000) + { + byte[] pattern = new byte[10000]; + random.NextBytes(pattern); + + // 在数据中插入重复模式 + for (int i = 0; i < size / 50000; i++) + { + int position = random.Next(0, size - pattern.Length); + Buffer.BlockCopy(pattern, 0, data, position, pattern.Length); + } + + // 添加一些零区块 + for (int i = 0; i < size / 100000; i++) + { + int position = random.Next(0, size - 5000); + int length = random.Next(1000, 5000); + for (int j = 0; j < length; j++) + { + if (position + j < data.Length) + data[position + j] = 0; + } + } + } + + return data; + } + + #endregion + } +} diff --git a/src/MDriveSync.Test/CDCRealWorldTests.cs b/src/MDriveSync.Test/CDCRealWorldTests.cs new file mode 100644 index 0000000..a5fe586 --- /dev/null +++ b/src/MDriveSync.Test/CDCRealWorldTests.cs @@ -0,0 +1,533 @@ +using MDriveSync.Core.Services; +using MDriveSync.Infrastructure; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using Xunit.Abstractions; + +namespace MDriveSync.Test +{ + public class CDCRealWorldTests : BaseTests + { + private readonly ITestOutputHelper _output; + + public CDCRealWorldTests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// 测试修改文件首字节对分块的影响 + /// + [Fact] + public void ModifyFirstByte_ShouldOnlyAffectFirstFewChunks() + { + // Arrange + int testDataSize = 10 * 1024 * 1024; // 10MB + byte[] originalData = GenerateRealisticData(testDataSize); + byte[] modifiedData = (byte[])originalData.Clone(); + + // 修改第一个字节 + modifiedData[0] = (byte)(originalData[0] ^ 0xFF); + + var chunkResults = RunChunkingTest(originalData, modifiedData); + + // Assert + _output.WriteLine($"总块数: {chunkResults.OriginalChunks.Count}"); + _output.WriteLine($"修改影响的块数: {chunkResults.DifferentChunkCount}"); + + // 验证修改仅影响前面的块,不影响后面的块 + bool foundSameChunkAfterDifference = false; + for (int i = chunkResults.FirstDifferentChunkIndex + 1; i < Math.Min(chunkResults.OriginalChunks.Count, chunkResults.ModifiedChunks.Count); i++) + { + if (chunkResults.OriginalHashes[i] == chunkResults.ModifiedHashes[i]) + { + foundSameChunkAfterDifference = true; + break; + } + } + + Assert.True(chunkResults.FirstDifferentChunkIndex >= 0, "应至少有一个块受到修改影响"); + Assert.True(foundSameChunkAfterDifference, "修改后应该有相同的块(局部性特性)"); + Assert.True(chunkResults.DifferentChunkCount < chunkResults.OriginalChunks.Count, "不应影响所有块"); + } + + /// + /// 测试修改文件末尾字节对分块的影响 + /// + [Fact] + public void ModifyLastByte_ShouldOnlyAffectLastFewChunks() + { + // Arrange + int testDataSize = 10 * 1024 * 1024; // 10MB + byte[] originalData = GenerateRealisticData(testDataSize); + byte[] modifiedData = (byte[])originalData.Clone(); + + // 修改最后一个字节 + modifiedData[modifiedData.Length - 1] = (byte)(originalData[originalData.Length - 1] ^ 0xFF); + + var chunkResults = RunChunkingTest(originalData, modifiedData); + + // Assert + _output.WriteLine($"总块数: {chunkResults.OriginalChunks.Count}"); + _output.WriteLine($"修改影响的块数: {chunkResults.DifferentChunkCount}"); + + // 验证修改只影响最后部分的块 + Assert.True(chunkResults.FirstDifferentChunkIndex > chunkResults.OriginalChunks.Count / 2, + "修改最后一个字节应只影响后半部分的块"); + Assert.True(chunkResults.DifferentChunkCount < chunkResults.OriginalChunks.Count / 2, + "不应影响超过一半的块"); + } + + /// + /// 测试重复数据的分块识别 + /// + [Fact] + public void DuplicateContent_ShouldProduceSameChunks() + { + // Arrange + int originalSize = 2 * 1024 * 1024; // 2MB 原始数据 + byte[] originalPart = GenerateRealisticData(originalSize); + + // 创建一个包含2份相同数据的文件 + byte[] duplicatedData = new byte[originalSize * 2]; + Buffer.BlockCopy(originalPart, 0, duplicatedData, 0, originalSize); + Buffer.BlockCopy(originalPart, 0, duplicatedData, originalSize, originalSize); + + // 分块参数 + int averageChunkSize = 512 * 1024; // 512KB 平均块大小 + var cdc = new BuzhashCDC( + windowSize: 48, + averageSize: averageChunkSize, + minSize: averageChunkSize / 4, + maxSize: averageChunkSize * 4 + ); + + var chunks = new List(); + var hashes = new List(); + + // Act + using (var stream = new MemoryStream(duplicatedData)) + { + cdc.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + chunks.Add(chunk); + hashes.Add(ComputeHash(chunk)); + }); + } + + // 计算重复哈希的数量 + var hashCounts = new Dictionary(); + foreach (var hash in hashes) + { + if (!hashCounts.ContainsKey(hash)) + hashCounts[hash] = 0; + hashCounts[hash]++; + } + + int duplicateHashes = hashCounts.Count(kv => kv.Value > 1); + int totalDuplicates = hashCounts.Sum(kv => kv.Value - 1); + + // Assert + _output.WriteLine($"总块数: {chunks.Count}"); + _output.WriteLine($"唯一块数: {hashCounts.Count}"); + _output.WriteLine($"有重复的块种类: {duplicateHashes}"); + _output.WriteLine($"重复块总数: {totalDuplicates}"); + + Assert.True(duplicateHashes > 0, "应该识别出重复块"); + Assert.True(hashCounts.Count < chunks.Count, "应该有重复块被识别"); + } + + /// + /// 测试大规模重复内容的分块效率 + /// + [Fact] + public void MultipleRepeats_ShouldOptimizeStorage() + { + // Arrange - 创建一个具有N个重复部分的大文件 + int repeatCount = 5; + int originalSize = 1 * 1024 * 1024; // 1MB 原始数据 + byte[] originalPart = GenerateRealisticData(originalSize); + + // 创建一个包含N份相同数据的文件 + byte[] repeatedData = new byte[originalSize * repeatCount]; + for (int i = 0; i < repeatCount; i++) + { + Buffer.BlockCopy(originalPart, 0, repeatedData, i * originalSize, originalSize); + } + + // 分块参数 + int averageChunkSize = 256 * 1024; // 256KB 平均块大小 + var cdc = new BuzhashCDC( + windowSize: 48, + averageSize: averageChunkSize, + minSize: averageChunkSize / 4, + maxSize: averageChunkSize * 4 + ); + + var chunks = new List(); + var hashes = new List(); + var stopwatch = Stopwatch.StartNew(); + + // Act + using (var stream = new MemoryStream(repeatedData)) + { + cdc.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + chunks.Add(chunk); + hashes.Add(ComputeHash(chunk)); + }); + } + + stopwatch.Stop(); + + // 计算存储效率 + var uniqueHashes = new HashSet(hashes); + double compressionRatio = (double)uniqueHashes.Count / hashes.Count; + var theoreticalSize = uniqueHashes.Count * chunks.Average(c => c.Length); + + // Assert + _output.WriteLine($"处理时间: {stopwatch.ElapsedMilliseconds} 毫秒"); + _output.WriteLine($"总块数: {chunks.Count}"); + _output.WriteLine($"唯一块数: {uniqueHashes.Count}"); + _output.WriteLine($"理论最小存储大小: {theoreticalSize.FormatSize()}"); + _output.WriteLine($"原始数据大小: {repeatedData.Length.FormatSize()}"); + _output.WriteLine($"存储比例: {compressionRatio:P2}"); + + Assert.True(uniqueHashes.Count < chunks.Count, "应该有重复块被识别"); + Assert.True(compressionRatio < 0.5, "存储效率应至少提高50%以上"); + + // 所有块的总大小应等于原始数据大小 + Assert.Equal(repeatedData.Length, chunks.Sum(c => c.Length)); + } + + /// + /// 测试复杂模式的分块鲁棒性 + /// + [Fact] + public void ComplexPatterns_ChunkStability() + { + // Arrange - 创建一个包含复杂模式的数据块 + int baseSize = 5 * 1024 * 1024; // 5MB + byte[] baseData = GenerateRealisticData(baseSize); + + // 创建三种修改的变体 + byte[] variant1 = (byte[])baseData.Clone(); + byte[] variant2 = (byte[])baseData.Clone(); + byte[] variant3 = (byte[])baseData.Clone(); + + // 变体1:修改前25%处的几个字节 + ModifyRange(variant1, 0, baseSize / 4, 10); + + // 变体2:修改中间的几个字节 + ModifyRange(variant2, baseSize / 2 - 100, 200, 10); + + // 变体3:在3/4处插入新内容 + ModifyRange(variant3, baseSize * 3 / 4, 1000, 50); + + // 对所有变体进行分块 + var baseResult = ChunkAndAnalyze(baseData); + var variant1Result = ChunkAndAnalyze(variant1); + var variant2Result = ChunkAndAnalyze(variant2); + var variant3Result = ChunkAndAnalyze(variant3); + + // 计算与基准数据相同的块比例 + double sameRatio1 = CalculateSameChunkRatio(baseResult.Hashes, variant1Result.Hashes); + double sameRatio2 = CalculateSameChunkRatio(baseResult.Hashes, variant2Result.Hashes); + double sameRatio3 = CalculateSameChunkRatio(baseResult.Hashes, variant3Result.Hashes); + + // Assert + _output.WriteLine($"基准数据块数: {baseResult.Chunks.Count}"); + _output.WriteLine($"变体1相同块比例: {sameRatio1:P2}"); + _output.WriteLine($"变体2相同块比例: {sameRatio2:P2}"); + _output.WriteLine($"变体3相同块比例: {sameRatio3:P2}"); + + // 验证修改对分块的影响是局部的 + Assert.True(sameRatio1 > 0.7, "前25%的修改应保留70%以上的块"); + Assert.True(sameRatio2 > 0.8, "中间区域的小修改应保留80%以上的块"); + Assert.True(sameRatio3 > 0.7, "3/4处的修改应保留70%以上的块"); + } + + /// + /// 测试分块重置点功能 + /// + [Fact] + public void ResetInterval_LimitsChangeImpact() + { + // Arrange + int testDataSize = 20 * 1024 * 1024; // 20MB + byte[] originalData = GenerateRealisticData(testDataSize); + byte[] modifiedData = (byte[])originalData.Clone(); + + // 在第1MB位置修改数据 + int modifyPosition = 1 * 1024 * 1024; + ModifyRange(modifiedData, modifyPosition, 1000, 20); + + // 创建带重置点的CDC + int averageChunkSize = 1024 * 1024; // 1MB + var cdcWithReset = new BuzhashPlusCDC( + windowSize: 48, + averageSize: averageChunkSize, + minSize: averageChunkSize / 4, + maxSize: averageChunkSize * 4, + resetInterval: 8 * 1024 * 1024 // 8MB重置点 + ); + + // 分块结果 + var originalChunks = new List(); + var modifiedChunks = new List(); + var originalHashes = new List(); + var modifiedHashes = new List(); + + // 对原始数据分块 + using (var stream = new MemoryStream(originalData)) + { + cdcWithReset.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + originalChunks.Add(chunk); + originalHashes.Add(ComputeHash(chunk)); + }); + } + + // 对修改后数据分块 + using (var stream = new MemoryStream(modifiedData)) + { + cdcWithReset.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + modifiedChunks.Add(chunk); + modifiedHashes.Add(ComputeHash(chunk)); + }); + } + + // 计算每个位置的累计大小,找出重置点所在的块 + long[] originalCumulativeSize = new long[originalChunks.Count]; + long cumulativeSize = 0; + for (int i = 0; i < originalChunks.Count; i++) + { + cumulativeSize += originalChunks[i].Length; + originalCumulativeSize[i] = cumulativeSize; + } + + // 查找第一个重置点(8MB)后的第一个块 + int resetChunkIndex = Array.FindIndex(originalCumulativeSize, size => size >= 8 * 1024 * 1024); + + // 计算修改前后每个区域相同块的比例 + int beforeResetSameCount = 0; + int afterResetSameCount = 0; + + for (int i = 0; i < Math.Min(resetChunkIndex, Math.Min(originalHashes.Count, modifiedHashes.Count)); i++) + { + if (originalHashes[i] == modifiedHashes[i]) + beforeResetSameCount++; + } + + for (int i = resetChunkIndex; i < Math.Min(originalHashes.Count, modifiedHashes.Count); i++) + { + if (originalHashes[i] == modifiedHashes[i]) + afterResetSameCount++; + } + + double beforeResetSameRatio = (double)beforeResetSameCount / resetChunkIndex; + double afterResetSameRatio = (double)afterResetSameCount / (Math.Min(originalHashes.Count, modifiedHashes.Count) - resetChunkIndex); + + // Assert + _output.WriteLine($"总块数: {originalChunks.Count}"); + _output.WriteLine($"重置点索引: {resetChunkIndex}"); + _output.WriteLine($"重置点前相同块比例: {beforeResetSameRatio:P2}"); + _output.WriteLine($"重置点后相同块比例: {afterResetSameRatio:P2}"); + + // 重置点后的块应该有高比例保持相同 + Assert.True(afterResetSameRatio > 0.9, "重置点后的块应有90%以上保持相同"); + // 重置点前由于有修改,应该有一定比例的块发生变化 + Assert.True(beforeResetSameRatio < 0.9, "重置点前的块由于修改应有变化"); + } + + #region Helper Methods + + private (List OriginalChunks, List ModifiedChunks, + List OriginalHashes, List ModifiedHashes, + int FirstDifferentChunkIndex, int DifferentChunkCount) + RunChunkingTest(byte[] originalData, byte[] modifiedData) + { + // 分块参数 + int averageChunkSize = 1024 * 1024; // 1MB average + var cdc = new BuzhashCDC( + windowSize: 48, + averageSize: averageChunkSize, + minSize: averageChunkSize / 4, + maxSize: averageChunkSize * 4 + ); + + // 分块结果 + var originalChunks = new List(); + var modifiedChunks = new List(); + var originalHashes = new List(); + var modifiedHashes = new List(); + + // 对原始数据分块 + using (var stream = new MemoryStream(originalData)) + { + cdc.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + originalChunks.Add(chunk); + originalHashes.Add(ComputeHash(chunk)); + }); + } + + // 对修改后数据分块 + using (var stream = new MemoryStream(modifiedData)) + { + cdc.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + modifiedChunks.Add(chunk); + modifiedHashes.Add(ComputeHash(chunk)); + }); + } + + // 找出第一个不同的块和不同块的总数 + int firstDifferentIndex = -1; + int differentCount = 0; + + for (int i = 0; i < Math.Min(originalHashes.Count, modifiedHashes.Count); i++) + { + if (originalHashes[i] != modifiedHashes[i]) + { + if (firstDifferentIndex == -1) + firstDifferentIndex = i; + differentCount++; + } + } + + // 如果长度不同,余下的块都算作不同 + differentCount += Math.Abs(originalHashes.Count - modifiedHashes.Count); + + return (originalChunks, modifiedChunks, originalHashes, modifiedHashes, + firstDifferentIndex, differentCount); + } + + private (List Chunks, List Hashes) ChunkAndAnalyze(byte[] data) + { + var chunks = new List(); + var hashes = new List(); + + int averageChunkSize = 512 * 1024; // 512KB + var cdc = new BuzhashCDC( + windowSize: 48, + averageSize: averageChunkSize, + minSize: averageChunkSize / 4, + maxSize: averageChunkSize * 4 + ); + + using (var stream = new MemoryStream(data)) + { + cdc.Split(stream, (chunkData, length) => + { + var chunk = chunkData.Take(length).ToArray(); + chunks.Add(chunk); + hashes.Add(ComputeHash(chunk)); + }); + } + + return (chunks, hashes); + } + + private double CalculateSameChunkRatio(List baseHashes, List compareHashes) + { + // 创建哈希查找表 + var baseHashSet = new HashSet(baseHashes); + int sameCount = compareHashes.Count(h => baseHashSet.Contains(h)); + + return (double)sameCount / compareHashes.Count; + } + + private void ModifyRange(byte[] data, int startPos, int length, int step) + { + for (int i = 0; i < length; i += step) + { + if (startPos + i < data.Length) + { + data[startPos + i] = (byte)(data[startPos + i] ^ 0xFF); + } + } + } + + private byte[] GenerateRealisticData(int size) + { + // 生成一些模拟真实数据的字节流,包含重复模式和结构 + var random = new Random(42); // 使用固定种子以确保测试的可重复性 + var data = new byte[size]; + + // 分段填充数据 + int position = 0; + + // 创建一些基本模式 + byte[] pattern1 = Encoding.UTF8.GetBytes("这是一个模拟文件内容的测试数据,包含一些重复的模式和结构..."); + byte[] pattern2 = new byte[1024]; // 1KB的随机数据 + random.NextBytes(pattern2); + + while (position < size) + { + // 决定写入什么类型的数据 + int choice = random.Next(5); + int blockSize = Math.Min(random.Next(4096, 65536), size - position); + + switch (choice) + { + case 0: + // 重复模式1 + for (int i = 0; i < blockSize; i++) + { + data[position + i] = pattern1[i % pattern1.Length]; + } + break; + case 1: + // 重复模式2 + for (int i = 0; i < blockSize; i++) + { + data[position + i] = pattern2[i % pattern2.Length]; + } + break; + case 2: + // 全零段 + for (int i = 0; i < blockSize; i++) + { + data[position + i] = 0; + } + break; + case 3: + // 递增数据 + for (int i = 0; i < blockSize; i++) + { + data[position + i] = (byte)(i % 256); + } + break; + case 4: + // 随机数据 + random.NextBytes(new Span(data, position, blockSize)); + break; + } + + position += blockSize; + } + + return data; + } + + private string ComputeHash(byte[] data) + { + using (var sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(data); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + + #endregion + } +} From 535ea10e591f405d53a3d63c794c40cb4de28f01 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Tue, 13 May 2025 10:34:35 +0800 Subject: [PATCH 67/90] fast cdc --- src/MDriveSync.Client/Program.cs | 262 +++++- src/MDriveSync.Core/MDriveSync.Core.csproj | 1 + .../Services/FastCDCChunker.cs | 372 ++++++++ src/MDriveSync.Core/Services/FastCDCPlus.cs | 805 ++++++++++++++++++ 4 files changed, 1433 insertions(+), 7 deletions(-) create mode 100644 src/MDriveSync.Core/Services/FastCDCChunker.cs create mode 100644 src/MDriveSync.Core/Services/FastCDCPlus.cs diff --git a/src/MDriveSync.Client/Program.cs b/src/MDriveSync.Client/Program.cs index a4a06f2..78f7b47 100644 --- a/src/MDriveSync.Client/Program.cs +++ b/src/MDriveSync.Client/Program.cs @@ -1,7 +1,8 @@ using MDriveSync.Core.Services; +using MDriveSync.Infrastructure; using MDriveSync.Security; -using System.Collections.Concurrent; using System.Diagnostics; +using System.Security.Cryptography; namespace MDriveSync.Client { @@ -9,7 +10,19 @@ internal class Program { private static async Task Main(string[] args) { - + await FastCDCTest1.Start(); + + Console.ReadKey(); + + return; + + await FastCDCPlusTest1.Start(); + + + Console.ReadKey(); + + return; + var sw = new Stopwatch(); var rootPath = "E:\\program_files"; // args.Length > 0 ? args[0] : Environment.CurrentDirectory; var ignorePatterns = FileIgnoreHelper.BuildIgnorePatterns("**/node_modules/*", "**/bin/*", "**/obj/*", "**/.git/*"); @@ -215,20 +228,255 @@ private static async Task Main(string[] args) Console.WriteLine("Hello, World!"); } + } + + /// + /// FastCDC+ 控制台应用程序 + /// + public class FastCDCPlusTest1 + { + public static async Task Start() + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine($"FastCDC+ 内容定义分块工具 [当前时间: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}]"); + Console.WriteLine($"用户: {Environment.UserName}\n"); + + //if (args.Length < 1) + //{ + // Console.WriteLine("用法: FastCDCPlus <文件路径> [最小块大小MB] [平均块大小MB] [最大块大小MB]"); + // return; + //} + + string filePath = @"E:\downs\fdm\__分块测试\Driver.rar"; + + // 解析可选参数 + int minChunkSize = 1 * 1024 * 1024; // 默认1MB + int avgChunkSize = 16 * 1024 * 1024; // 默认16MB + int maxChunkSize = 64 * 1024 * 1024; // 默认64MB + + //if (args.Length >= 2 && int.TryParse(args[1], out int min)) + // minChunkSize = min * 1024 * 1024; + + //if (args.Length >= 3 && int.TryParse(args[2], out int avg)) + // avgChunkSize = avg * 1024 * 1024; + + //if (args.Length >= 4 && int.TryParse(args[3], out int max)) + // maxChunkSize = max * 1024 * 1024; + + try + { + Console.WriteLine($"处理文件: {filePath}"); + Console.WriteLine($"块大小设置: 最小={minChunkSize / 1024 / 1024}MB, 平均={avgChunkSize / 1024 / 1024}MB, 最大={maxChunkSize / 1024 / 1024}MB"); + + var sw = Stopwatch.StartNew(); + + // 选择哈希算法 - 可以使用Blake3/XXHash等更快的算法 + using HashAlgorithm hashAlg = SHA256.Create(); + + // 创建FastCDC+实例并处理 + using var chunker = new FastCDCPlus(minChunkSize, avgChunkSize, maxChunkSize, hashAlg); + + // 创建取消令牌,以便支持按Ctrl+C取消 + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + cts.Cancel(); + Console.WriteLine("\n操作已取消!"); + }; + + // 执行分块 + var chunks = await chunker.ChunkFileAsync(filePath, parallelProcessing: true, cts.Token); + + sw.Stop(); + + Console.WriteLine($"分块完成,耗时: {sw.ElapsedMilliseconds}ms"); + Console.WriteLine($"文件被分割为 {chunks.Count} 个块"); + + // 计算统计信息 + long totalSize = 0; + long minSize = long.MaxValue; + long maxSize = 0; + + foreach (var chunk in chunks) + { + totalSize += chunk.Length; + minSize = Math.Min(minSize, chunk.Length); + maxSize = Math.Max(maxSize, chunk.Length); + } + + double avgSize = chunks.Count > 0 ? totalSize / (double)chunks.Count : 0; + + Console.WriteLine($"块统计信息:"); + Console.WriteLine($" 总大小: {totalSize.FormatSize()}"); + Console.WriteLine($" 平均大小: {avgSize.FormatSize()}"); + Console.WriteLine($" 最小块: {minSize.FormatSize()}"); + Console.WriteLine($" 最大块: {maxSize.FormatSize()}"); + Console.WriteLine($" 处理速度: {(totalSize / (sw.ElapsedMilliseconds / 1000.0)).FormatSize()}/s"); + + // 显示部分块信息 + Console.WriteLine("\n前5个块:"); + for (int i = 0; i < Math.Min(5, chunks.Count); i++) + { + Console.WriteLine($"{i + 1}: {chunks[i]}"); + } + + if (chunks.Count > 10) + { + Console.WriteLine("\n后5个块:"); + for (int i = Math.Max(5, chunks.Count - 5); i < chunks.Count; i++) + { + Console.WriteLine($"{i + 1}: {chunks[i]}"); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine("操作已取消。"); + } + catch (Exception ex) + { + Console.WriteLine($"错误: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + } + } + + + /// + /// FastCDC 算法演示程序 + /// + public class FastCDCTest1 + { + public static async Task Start() + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine($"FastCDC 内容定义分块工具 [当前时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}]"); + Console.WriteLine($"用户: {Environment.UserName}"); + Console.WriteLine(); + + //if (args.Length < 1) + //{ + // Console.WriteLine("用法: FastCDC <文件路径> [最小块大小MB] [平均块大小MB] [最大块大小MB]"); + // return; + //} + + //string filePath = "";// args[0]; + string filePath = @"E:\downs\fdm\__分块测试\Driver.rar"; + + // 解析可选参数 + int minChunkSize = 1 * 1024 * 1024; // 默认1MB + int avgChunkSize = 16 * 1024 * 1024; // 默认16MB + int maxChunkSize = 64 * 1024 * 1024; // 默认64MB + + //if (args.Length >= 2 && int.TryParse(args[1], out int min)) + // minChunkSize = min * 1024 * 1024; + + //if (args.Length >= 3 && int.TryParse(args[2], out int avg)) + // avgChunkSize = avg * 1024 * 1024; + + //if (args.Length >= 4 && int.TryParse(args[3], out int max)) + // maxChunkSize = max * 1024 * 1024; + + try + { + if (!File.Exists(filePath)) + { + Console.WriteLine($"错误: 文件 '{filePath}' 不存在"); + return; + } + + var fileInfo = new FileInfo(filePath); + Console.WriteLine($"处理文件: {filePath}"); + Console.WriteLine($"文件大小: {FormatSize(fileInfo.Length)}"); + Console.WriteLine($"块大小设置: 最小={FormatSize(minChunkSize)}, 平均={FormatSize(avgChunkSize)}, 最大={FormatSize(maxChunkSize)}"); + Console.WriteLine(); + + var sw = Stopwatch.StartNew(); + + using var hashAlg = SHA256.Create(); + + // 创建允许取消的令牌 + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + cts.Cancel(); + Console.WriteLine("\n操作已取消!"); + }; + + // 创建分块器实例 + using var chunker = new FastCDCChunker( + minSize: minChunkSize, + avgSize: avgChunkSize, + maxSize: maxChunkSize, + bufferSize: Math.Min(128 * 1024 * 1024, Math.Max(maxChunkSize * 2, (int)Math.Min(fileInfo.Length, int.MaxValue))), + hashAlgorithm: hashAlg + ); + + // 异步执行分块 + var chunks = await chunker.ChunkFileAsync(filePath, cts.Token); + + sw.Stop(); + + Console.WriteLine($"文件已被分割为 {chunks.Count} 个块"); + Console.WriteLine($"处理时间: {sw.ElapsedMilliseconds}ms ({FormatSize((long)(fileInfo.Length / (sw.ElapsedMilliseconds / 1000.0)))}/s)"); + Console.WriteLine(); + + // 计算统计信息 + long totalSize = chunks.Sum(c => (long)c.Length); + double avgChunkSizeActual = chunks.Count > 0 ? totalSize / (double)chunks.Count : 0; + var minChunk = chunks.Count > 0 ? chunks.Min(c => c.Length) : 0; + var maxChunk = chunks.Count > 0 ? chunks.Max(c => c.Length) : 0; + + Console.WriteLine("块统计信息:"); + Console.WriteLine($" 总大小: {FormatSize(totalSize)}"); + Console.WriteLine($" 平均大小: {FormatSize((long)avgChunkSizeActual)}"); + Console.WriteLine($" 最小块: {FormatSize(minChunk)}"); + Console.WriteLine($" 最大块: {FormatSize(maxChunk)}"); + Console.WriteLine(); + + // 显示部分块信息 + Console.WriteLine("前5个块:"); + for (int i = 0; i < Math.Min(5, chunks.Count); i++) + { + Console.WriteLine($"{i + 1}: {chunks[i]}"); + } + + if (chunks.Count > 10) + { + Console.WriteLine("\n后5个块:"); + for (int i = Math.Max(5, chunks.Count - 5); i < chunks.Count; i++) + { + Console.WriteLine($"{i + 1}: {chunks[i]}"); + } + } + } + catch (OperationCanceledException) + { + Console.WriteLine("操作已取消"); + } + catch (Exception ex) + { + Console.WriteLine($"错误: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + } + // 格式化文件大小显示 private static string FormatSize(long bytes) { - string[] units = { "B", "KB", "MB", "GB", "TB" }; + string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; + int i = 0; double size = bytes; - int unit = 0; - while (size >= 1024 && unit < units.Length - 1) + while (size >= 1024 && i < suffixes.Length - 1) { size /= 1024; - unit++; + i++; } - return $"{size:0.##} {units[unit]}"; + return $"{size:F2} {suffixes[i]}"; } } } \ No newline at end of file diff --git a/src/MDriveSync.Core/MDriveSync.Core.csproj b/src/MDriveSync.Core/MDriveSync.Core.csproj index 0acdc10..5609836 100644 --- a/src/MDriveSync.Core/MDriveSync.Core.csproj +++ b/src/MDriveSync.Core/MDriveSync.Core.csproj @@ -3,6 +3,7 @@ net8.0 enable + true diff --git a/src/MDriveSync.Core/Services/FastCDCChunker.cs b/src/MDriveSync.Core/Services/FastCDCChunker.cs new file mode 100644 index 0000000..1edf6cc --- /dev/null +++ b/src/MDriveSync.Core/Services/FastCDCChunker.cs @@ -0,0 +1,372 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace MDriveSync.Core.Services +{ + /// + /// FastCDC (快速内容定义分块) 算法实现 + /// 根据文件内容的特征自动识别分块点,适用于去重和增量备份等场景 + /// + public class FastCDCChunker : IDisposable + { + #region 常量和字段 + + // 哈希算法掩码常量 + private const uint GEAR_MASK_BIT = 0x0000D8F3; + + // 标准化掩码,用于改善块大小分布 + private const uint NORMALIZATION_MASK_16 = 0x00007FFF; // 16位掩码,产生约8KB的块 + private const uint NORMALIZATION_MASK_20 = 0x0007FFFF; // 20位掩码,产生约1MB的块 + private const uint NORMALIZATION_MASK_24 = 0x007FFFFF; // 24位掩码,产生约16MB的块 + private const uint NORMALIZATION_MASK_28 = 0x07FFFFFF; // 28位掩码,产生约256MB的块 + + // 默认块大小配置 + private const int DEFAULT_MIN_SIZE = 2 * 1024; // 默认最小块大小 2KB + private const int DEFAULT_AVG_SIZE = 16 * 1024; // 默认平均块大小 16KB + private const int DEFAULT_MAX_SIZE = 64 * 1024; // 默认最大块大小 64KB + private const int DEFAULT_BUFFER_SIZE = 8 * 1024 * 1024; // 默认缓冲区大小 8MB + + // 实例字段 + private readonly uint _normalizationMask; + private readonly uint _secondaryMask; // 用于第二阶段的宽松掩码 + private readonly int _minSize; + private readonly int _maxSize; + private readonly int _avgSize; + private readonly uint[] _gearTable; + private readonly int _bufferSize; + private bool _disposed; + + // 哈希计算相关 + private readonly HashAlgorithm _hashAlgorithm; + + #endregion + + #region 构造函数和初始化 + + /// + /// 初始化FastCDC分块器 + /// + /// 最小块大小 + /// 目标平均块大小 + /// 最大块大小 + /// 读取缓冲区大小 + /// 可选的哈希算法,默认使用SHA256 + public FastCDCChunker( + int minSize = DEFAULT_MIN_SIZE, + int avgSize = DEFAULT_AVG_SIZE, + int maxSize = DEFAULT_MAX_SIZE, + int bufferSize = DEFAULT_BUFFER_SIZE, + HashAlgorithm hashAlgorithm = null) + { + // 参数验证 + if (minSize <= 0) + throw new ArgumentException("最小块大小必须大于0", nameof(minSize)); + + if (avgSize <= minSize) + throw new ArgumentException("平均块大小必须大于最小块大小", nameof(avgSize)); + + if (maxSize <= avgSize) + throw new ArgumentException("最大块大小必须大于平均块大小", nameof(maxSize)); + + if (bufferSize < maxSize * 2) + throw new ArgumentException("缓冲区大小应至少为最大块大小的两倍", nameof(bufferSize)); + + _minSize = minSize; + _avgSize = avgSize; + _maxSize = maxSize; + _bufferSize = bufferSize; + _hashAlgorithm = hashAlgorithm ?? SHA256.Create(); + + // 基于平均块大小选择合适的掩码 + if (avgSize <= 8 * 1024) + _normalizationMask = NORMALIZATION_MASK_16; + else if (avgSize <= 1024 * 1024) + _normalizationMask = NORMALIZATION_MASK_20; + else if (avgSize <= 16 * 1024 * 1024) + _normalizationMask = NORMALIZATION_MASK_24; + else + _normalizationMask = NORMALIZATION_MASK_28; + + // 设置第二阶段掩码(更宽松) + _secondaryMask = _normalizationMask >> 1; + + // 初始化Gear哈希表 + _gearTable = InitializeGearTable(); + } + + /// + /// 以确定性方式初始化Gear哈希表 + /// + private uint[] InitializeGearTable() + { + var table = new uint[256]; + + // 使用固定种子确保哈希表的一致性 + byte[] seed = new byte[] { + 0x12, 0x83, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10 + }; + + using (var rng = new DeterministicRng(seed)) + { + for (int i = 0; i < 256; i++) + { + table[i] = rng.NextUInt32() & GEAR_MASK_BIT; + } + } + + return table; + } + + #endregion + + #region 公共方法 + + /// + /// 分割文件为多个块 + /// + /// 要分块的文件路径 + /// 块信息列表 + public List ChunkFile(string filePath) + { + return ChunkFileAsync(filePath, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// + /// 异步分割文件为多个块 + /// + /// 要分块的文件路径 + /// 取消标记 + /// 块信息列表 + public async Task> ChunkFileAsync(string filePath, CancellationToken cancellationToken = default) + { + if (_disposed) + throw new ObjectDisposedException(nameof(FastCDCChunker)); + + if (!File.Exists(filePath)) + throw new FileNotFoundException("找不到指定的文件", filePath); + + var chunkPoints = new List(); + long currentPosition = 0; + + // 使用基于池的缓冲区以减少内存分配 + byte[] buffer = ArrayPool.Shared.Rent(_bufferSize); + + try + { + using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true)) + { + int bytesRead; + int bufferOffset = 0; + + while ((bytesRead = await fileStream.ReadAsync(buffer, bufferOffset, buffer.Length - bufferOffset, cancellationToken)) > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + // 总可用数据量 + int availableBytes = bufferOffset + bytesRead; + + // 处理缓冲区内的数据 + int processedBytes = 0; + + while (processedBytes < availableBytes) + { + // 确保剩余足够的数据以进行分块 + int remainingBytes = availableBytes - processedBytes; + + // 如果剩余不足最小块大小且还有更多文件数据,等待更多数据 + if (remainingBytes < _minSize && fileStream.Position < fileStream.Length) + break; + + // 计算本次处理的实际长度 + int lengthToProcess = Math.Min(remainingBytes, _maxSize); + + // 查找分块点 + int cutpoint = FindCutpoint(buffer, processedBytes, processedBytes + lengthToProcess); + int chunkLength = cutpoint - processedBytes; + + // 计算哈希值 + string hash = ComputeHash(buffer, processedBytes, chunkLength); + + // 添加块信息 + chunkPoints.Add(new ChunkInfo + { + Offset = currentPosition, + Length = chunkLength, + Hash = hash + }); + + // 更新位置 + processedBytes += chunkLength; + currentPosition += chunkLength; + } + + // 移动未处理的数据到缓冲区开始 + if (processedBytes < availableBytes) + { + Buffer.BlockCopy(buffer, processedBytes, buffer, 0, availableBytes - processedBytes); + bufferOffset = availableBytes - processedBytes; + } + else + { + bufferOffset = 0; + } + } + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + return chunkPoints; + } + + #endregion + + #region 内部方法 + + /// + /// 找到下一个分块点 + /// + /// 缓冲区 + /// 起始位置 + /// 结束位置 + /// 分块点位置 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindCutpoint(byte[] buffer, int start, int end) + { + // 如果剩余数据不足最小块大小,直接返回剩余所有数据 + if (end - start <= _minSize) + return end; + + // 计算阶段边界 + int phase1End = Math.Min(start + _minSize + (_avgSize >> 1), end); + int phase2End = Math.Min(start + _avgSize * 2, end); + + uint hash = 0; + int i = start; + + // 第一阶段:跳过最小块大小区域 + i = start + _minSize; + + // 第二阶段:从minSize到avgSize*0.5之间,使用标准掩码 + for (; i < phase1End; i++) + { + hash = (hash << 1) + _gearTable[buffer[i]]; + if ((hash & _normalizationMask) == 0) + { + return i + 1; + } + } + + // 第三阶段:从avgSize*0.5到avgSize*2之间,继续使用标准掩码 + for (; i < phase2End; i++) + { + hash = (hash << 1) + _gearTable[buffer[i]]; + if ((hash & _normalizationMask) == 0) + { + return i + 1; + } + } + + // 第四阶段:从avgSize*2到maxSize,使用更宽松的掩码 + for (; i < end; i++) + { + hash = (hash << 1) + _gearTable[buffer[i]]; + if ((hash & _secondaryMask) == 0) + { + return i + 1; + } + } + + // 如果没找到分块点,返回最大允许块大小或数据结束位置 + return end; + } + + /// + /// 计算数据块的哈希值 + /// + private string ComputeHash(byte[] buffer, int offset, int length) + { + lock (_hashAlgorithm) // 确保线程安全 + { + var hashBytes = _hashAlgorithm.ComputeHash(buffer, offset, length); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + } + + #endregion + + #region IDisposable实现 + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _hashAlgorithm?.Dispose(); + } + + _disposed = true; + } + } + + ~FastCDCChunker() + { + Dispose(false); + } + + #endregion + } + + /// + /// 确定性随机数生成器,确保哈希表初始化的一致性 + /// + internal class DeterministicRng : IDisposable + { + private readonly SHA256 _sha256; + private byte[] _state; + private int _position; + + public DeterministicRng(byte[] seed) + { + if (seed == null || seed.Length < 16) + throw new ArgumentException("种子必须至少为16字节"); + + _sha256 = SHA256.Create(); + _state = new byte[32]; + _position = 0; + + // 初始化状态 + _state = _sha256.ComputeHash(seed); + } + + public uint NextUInt32() + { + if (_position + 4 > _state.Length) + { + // 重新生成状态 + _state = _sha256.ComputeHash(_state); + _position = 0; + } + + uint value = BitConverter.ToUInt32(_state, _position); + _position += 4; + return value; + } + + public void Dispose() + { + _sha256?.Dispose(); + } + } +} diff --git a/src/MDriveSync.Core/Services/FastCDCPlus.cs b/src/MDriveSync.Core/Services/FastCDCPlus.cs new file mode 100644 index 0000000..724344f --- /dev/null +++ b/src/MDriveSync.Core/Services/FastCDCPlus.cs @@ -0,0 +1,805 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using System.Security.Cryptography; + +namespace MDriveSync.Core.Services +{ + /// + /// FastCDC+ - 增强版内容定义分块算法 + /// 提供高性能的文件分块功能,支持SIMD加速和多线程处理 + /// + public class FastCDCPlus : IDisposable + { + #region 常量和字段 + + // 掩码常量 + private const uint GEAR_MASK_BIT = 0x0000D8F3; + + private const uint NORMALIZATION_MASK_16 = 0x00007FFF; // ~8KB + private const uint NORMALIZATION_MASK_20 = 0x0007FFFF; // ~1MB + private const uint NORMALIZATION_MASK_24 = 0x007FFFFF; // ~16MB + private const uint NORMALIZATION_MASK_28 = 0x07FFFFFF; // ~256MB + + // 分块大小参数 + private readonly int _minSize; + + private readonly int _avgSize; + private readonly int _maxSize; + private readonly uint _normalizationMask; + private readonly uint _leveledMask; // 用于两级掩码策略 + + // 用于高速哈希计算的查找表 + private readonly uint[] _gearTable; + + // SIMD支持标志 + private readonly bool _supportsAvx2; + + private readonly bool _supportsAvx512; + private readonly bool _supportsSse42; + + // 哈希算法 + private readonly HashAlgorithm _hashAlgorithm; + + // 缓冲池和资源管理 + private readonly ArrayPool _bufferPool; + + private readonly int _processingBlockSize; + private bool _disposed; + + #endregion 常量和字段 + + #region 构造函数和初始化 + + /// + /// 初始化FastCDC+分块器 + /// + /// 最小块大小,默认2MB + /// 平均块大小,默认16MB + /// 最大块大小,默认64MB + /// 哈希算法,null表示使用SHA256 + public FastCDCPlus( + int minSize = 2 * 1024 * 1024, // 2MB + int avgSize = 16 * 1024 * 1024, // 16MB + int maxSize = 64 * 1024 * 1024, // 64MB + HashAlgorithm hashAlgorithm = null) + { + // 验证参数 + if (minSize <= 0) + throw new ArgumentException("最小块大小必须大于0", nameof(minSize)); + if (avgSize <= minSize) + throw new ArgumentException("平均块大小必须大于最小块大小", nameof(avgSize)); + if (maxSize <= avgSize) + throw new ArgumentException("最大块大小必须大于平均块大小", nameof(maxSize)); + + _minSize = minSize; + _avgSize = avgSize; + _maxSize = maxSize; + + // 检测可用的SIMD指令集 + _supportsAvx2 = Avx2.IsSupported; + _supportsAvx512 = Avx512F.IsSupported; + _supportsSse42 = Sse42.IsSupported; + + // 基于目标平均块大小选择掩码 + if (avgSize <= 8 * 1024) + _normalizationMask = NORMALIZATION_MASK_16; + else if (avgSize <= 1024 * 1024) + _normalizationMask = NORMALIZATION_MASK_20; + else if (avgSize <= 16 * 1024 * 1024) + _normalizationMask = NORMALIZATION_MASK_24; + else + _normalizationMask = NORMALIZATION_MASK_28; + + // 次级掩码(更宽松的条件,用于最大块大小范围) + _leveledMask = _normalizationMask >> 1; + + // 初始化优化的Gear哈希表 + _gearTable = InitializeOptimizedGearTable(); + + // 设置哈希算法,默认SHA256 + _hashAlgorithm = hashAlgorithm ?? SHA256.Create(); + + // 初始化缓冲池和处理块大小 + _bufferPool = ArrayPool.Shared; + _processingBlockSize = 128 * 1024 * 1024; // 128MB 处理块大小 + } + + /// + /// 使用确定性种子初始化Gear哈希表,确保跨会话的一致性 + /// + private uint[] InitializeOptimizedGearTable() + { + var table = new uint[256]; + + // 使用固定种子以确保相同的哈希表 + byte[] seed = new byte[] { + 0x37, 0x91, 0xC4, 0x8B, 0x6F, 0x24, 0xE7, 0x4F, + 0xD3, 0xF6, 0x9A, 0x53, 0x6D, 0xA8, 0x3C, 0xD1 + }; + + using var deterministicRng = new FastCspRng(seed); + + // 生成表项 + for (int i = 0; i < 256; i++) + { + uint value = deterministicRng.NextUInt32(); + // 应用特定掩码,以优化哈希分布 + table[i] = value & GEAR_MASK_BIT; + } + + return table; + } + + #endregion 构造函数和初始化 + + #region 公共方法 + + /// + /// 执行文件分块并返回块信息 + /// + /// 要分块的文件路径 + /// 是否启用并行处理 + /// 取消标记 + /// 块信息列表 + public async Task> ChunkFileAsync( + string filePath, + bool parallelProcessing = true, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + var fileInfo = new FileInfo(filePath); + + if (!fileInfo.Exists) + throw new FileNotFoundException("找不到指定的文件", filePath); + + // 对于小文件,关闭并行处理以减少开销 + if (fileInfo.Length < 100 * 1024 * 1024) // 100MB + parallelProcessing = false; + + // 创建结果列表 + var chunkInfos = new List(); + + // 确定处理块大小 - 使用一个较大的缓冲区提高性能 + int blockSize = Math.Min(_processingBlockSize, (int)fileInfo.Length); + + using var fileStream = new FileStream( + filePath, FileMode.Open, FileAccess.Read, FileShare.Read, + 4096, FileOptions.SequentialScan | FileOptions.Asynchronous); + + long filePosition = 0; + long fileLength = fileStream.Length; + + if (parallelProcessing) + { + await ChunkFileParallelAsync(fileStream, fileLength, blockSize, chunkInfos, cancellationToken); + } + else + { + await ChunkFileSequentialAsync(fileStream, fileLength, blockSize, chunkInfos, cancellationToken); + } + + return chunkInfos; + } + + /// + /// 执行文件分块并返回块信息 (同步版本) + /// + /// 要分块的文件路径 + /// 是否启用并行处理 + /// 块信息列表 + public List ChunkFile(string filePath, bool parallelProcessing = true) + { + return ChunkFileAsync(filePath, parallelProcessing).GetAwaiter().GetResult(); + } + + /// + /// 对内存缓冲区执行分块 + /// + /// 数据缓冲区 + /// 起始偏移量 + /// 要处理的长度 + /// 块信息列表 + public List ChunkBuffer(byte[] buffer, int offset, int length) + { + ThrowIfDisposed(); + + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + if (offset < 0 || offset >= buffer.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + + if (length <= 0 || offset + length > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(length)); + + var chunks = new List(); + int start = offset; + int end = offset + length; + + while (start < end) + { + // 查找下一个分块点 + int cutPoint = FindCutpointWithSIMD(buffer, start, end); + + // 计算当前块的哈希 + string hash = ComputeFastHash(buffer, start, cutPoint - start); + + // 添加找到的块 + chunks.Add(new ChunkInfo + { + Offset = start, + Length = cutPoint - start, + Hash = hash + }); + + // 移动到下一个块的起始位置 + start = cutPoint; + } + + return chunks; + } + + #endregion 公共方法 + + #region 内部实现方法 + + /// + /// 并行处理文件分块 + /// + private async Task ChunkFileParallelAsync( + FileStream fileStream, + long fileLength, + int blockSize, + List chunkInfos, + CancellationToken cancellationToken) + { + // 计算可能的处理块数 + int numBlocks = (int)Math.Ceiling((double)fileLength / blockSize); + var blockInfos = new List(numBlocks); + + // 读取所有数据块 + for (int i = 0; i < numBlocks; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + int currentBlockSize = (int)Math.Min(blockSize, fileLength - fileStream.Position); + var buffer = _bufferPool.Rent(currentBlockSize); + + int bytesRead = await fileStream.ReadAsync( + buffer, 0, currentBlockSize, cancellationToken); + + blockInfos.Add(new BlockInfo + { + Buffer = buffer, + Size = bytesRead, + Position = fileStream.Position - bytesRead + }); + } + + // 并行处理所有块 + var results = new ConcurrentBag<(long Position, List Chunks)>(); + + await Task.Run(() => + { + Parallel.ForEach(blockInfos, new ParallelOptions + { + CancellationToken = cancellationToken, + MaxDegreeOfParallelism = Environment.ProcessorCount + }, blockInfo => + { + var chunks = ChunkBufferInternal(blockInfo.Buffer, 0, blockInfo.Size); + results.Add((blockInfo.Position, chunks)); + }); + }, cancellationToken); + + // 合并结果并按位置排序 + var orderedResults = results.OrderBy(r => r.Position).ToList(); + + foreach (var result in orderedResults) + { + foreach (var chunk in result.Chunks) + { + chunkInfos.Add(new ChunkInfo + { + Offset = result.Position + chunk.Offset, + Length = chunk.Length, + Hash = chunk.Hash + }); + } + } + + // 释放所有缓冲区 + foreach (var blockInfo in blockInfos) + { + _bufferPool.Return(blockInfo.Buffer); + } + } + + /// + /// 顺序处理文件分块 + /// + private async Task ChunkFileSequentialAsync( + FileStream fileStream, + long fileLength, + int blockSize, + List chunkInfos, + CancellationToken cancellationToken) + { + var buffer = _bufferPool.Rent(blockSize); + + try + { + long currentPosition = 0; + int bytesRead; + + while ((bytesRead = await fileStream.ReadAsync( + buffer, 0, buffer.Length, cancellationToken)) > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + var chunks = ChunkBufferInternal(buffer, 0, bytesRead); + + foreach (var chunk in chunks) + { + chunkInfos.Add(new ChunkInfo + { + Offset = currentPosition + chunk.Offset, + Length = chunk.Length, + Hash = chunk.Hash + }); + } + + currentPosition += bytesRead; + + // 如果已读取完文件,退出循环 + if (fileStream.Position >= fileLength) + break; + } + } + finally + { + _bufferPool.Return(buffer); + } + } + + /// + /// 对缓冲区数据进行分块(内部实现) + /// + private List ChunkBufferInternal(byte[] buffer, int offset, int length) + { + var chunks = new List(); + int start = offset; + int end = offset + length; + + while (start < end) + { + // 查找下一个分块点 + int cutPoint = FindCutpointWithSIMD(buffer, start, end); + + // 计算当前块的哈希 + string hash = ComputeFastHash(buffer, start, cutPoint - start); + + // 添加找到的块 + chunks.Add(new ChunkInfo + { + Offset = start, + Length = cutPoint - start, + Hash = hash + }); + + // 移动到下一个块的起始位置 + start = cutPoint; + } + + return chunks; + } + + /// + /// 使用SIMD加速找到下一个分块点(如果支持) + /// + private int FindCutpointWithSIMD(byte[] buffer, int start, int length) + { + // 如果剩余数据不足最小块大小,直接返回剩余所有数据 + if (length - start <= _minSize) + return length; + + // 确定搜索范围 + int end = Math.Min(start + _maxSize, length); + + // 如果支持AVX-512,使用它进行加速 + if (_supportsAvx512 && end - start >= Vector512.Count) + { + return FindCutpointAVX512(buffer, start, end); + } + // 否则如果支持AVX2,使用AVX2 + else if (_supportsAvx2 && end - start >= Vector256.Count) + { + return FindCutpointAVX2(buffer, start, end); + } + // 如果支持SSE4.2,使用SSE + else if (_supportsSse42 && end - start >= Vector128.Count) + { + return FindCutpointSSE(buffer, start, end); + } + // 回退到标准实现 + else + { + return FindCutpointStandard(buffer, start, end); + } + } + + /// + /// 使用标准算法查找分块点 + /// + private int FindCutpointStandard(byte[] buffer, int start, int end) + { + // 计算第一阶段和第二阶段的边界 + int phase1End = Math.Min(start + _minSize + (_avgSize >> 1), end); + int phase2End = Math.Min(start + _avgSize * 2, end); + + uint hash = 0; + + // 阶段1: 跳过最小块大小,不检查 + int i = start; + + // 阶段2: 使用正常掩码 (从_minSize到平均大小的一半) + for (i = start + _minSize; i < phase1End; i++) + { + // 使用优化的Gear哈希 + hash = (hash << 1) + _gearTable[buffer[i]]; + if ((hash & _normalizationMask) == 0) + { + return i + 1; + } + } + + // 阶段3: 使用正常掩码 (从平均大小的一半到平均大小的2倍) + for (; i < phase2End; i++) + { + hash = (hash << 1) + _gearTable[buffer[i]]; + if ((hash & _normalizationMask) == 0) + { + return i + 1; + } + } + + // 阶段4: 使用次级掩码 (更宽松的条件,增加命中几率) + for (; i < end; i++) + { + hash = (hash << 1) + _gearTable[buffer[i]]; + if ((hash & _leveledMask) == 0) + { + return i + 1; + } + } + + // 如果没找到分块点,返回搜索范围结束位置 + return end; + } + + /// + /// 使用SSE指令集加速分块点查找 - 使用unsafe方法处理指针 + /// + private unsafe int FindCutpointSSE(byte[] buffer, int start, int end) + { + // 至少跳过最小块大小 + int i = start + _minSize; + + // 计算阶段边界 + int phase1End = Math.Min(start + _minSize + (_avgSize >> 1), end); + int phase2End = Math.Min(start + _avgSize * 2, end); + + uint hash = 0; + + // 预处理第一批字节建立滚动哈希状态 + for (int j = start; j < start + _minSize; j++) + { + hash = (hash << 1) + _gearTable[buffer[j]]; + } + + // SSE4.2 加速阶段 + if (_supportsSse42 && i + Vector128.Count <= phase1End) + { + // SSE4.2 批量处理 + int vectorEnd = phase1End - Vector128.Count; + + // 阶段2 (最小块大小到平均大小的一半) + fixed (byte* pBuffer = buffer) + { + for (; i <= vectorEnd; i += 8) // 每次处理8字节 + { + if (_supportsSse42) + { + // 使用SSE指令加载16字节 - 使用正确的指针方法 + Vector128 chunk = Sse2.LoadVector128(pBuffer + i); + + // 计算哈希 + for (int j = 0; j < 8; j++) + { + hash = (hash << 1) + _gearTable[buffer[i + j]]; + if ((hash & _normalizationMask) == 0) + { + return i + j + 1; + } + } + } + } + } + } + + // 回退到标准处理 + return FindCutpointStandard(buffer, i, end); + } + + /// + /// 使用AVX2指令集加速分块点查找 - 使用unsafe方法处理指针 + /// + private unsafe int FindCutpointAVX2(byte[] buffer, int start, int end) + { + // 至少跳过最小块大小 + int i = start + _minSize; + + // 计算阶段边界 + int phase1End = Math.Min(start + _minSize + (_avgSize >> 1), end); + int phase2End = Math.Min(start + _avgSize * 2, end); + + uint hash = 0; + + // 预处理第一批字节建立滚动哈希状态 + for (int j = start; j < start + _minSize; j++) + { + hash = (hash << 1) + _gearTable[buffer[j]]; + } + + // 批量处理阶段 - 使用AVX2 + if (i + Vector256.Count <= phase1End) + { + // 批量处理直到接近阶段1结束 + int vectorEnd = phase1End - Vector256.Count; + + // 阶段2 使用SIMD批量处理 + fixed (byte* pBuffer = buffer) + { + for (; i <= vectorEnd; i += 16) // 每次处理16字节 + { + if (_supportsAvx2) + { + // 使用AVX2加载32字节 - 使用正确的指针方法 + Vector256 chunk = Avx2.LoadVector256(pBuffer + i); + + // 计算哈希和检查分块点 + for (int j = 0; j < 16; j++) + { + hash = (hash << 1) + _gearTable[buffer[i + j]]; + if ((hash & _normalizationMask) == 0) + { + return i + j + 1; + } + } + } + } + } + } + + // 回退到标准处理剩余字节 + return FindCutpointStandard(buffer, i, end); + } + + /// + /// 使用AVX-512指令集加速分块点查找 - 使用unsafe方法处理指针 + /// + private unsafe int FindCutpointAVX512(byte[] buffer, int start, int end) + { + // 至少跳过最小块大小 + int i = start + _minSize; + + // 计算阶段边界 + int phase1End = Math.Min(start + _minSize + (_avgSize >> 1), end); + int phase2End = Math.Min(start + _avgSize * 2, end); + + uint hash = 0; + + // 预处理第一批字节建立滚动哈希状态 + for (int j = start; j < start + _minSize; j++) + { + hash = (hash << 1) + _gearTable[buffer[j]]; + } + + // 批量处理阶段 - 使用AVX-512 + if (_supportsAvx512 && i + Vector512.Count <= phase1End) + { + // 批量处理直到接近阶段1结束 + int vectorEnd = phase1End - Vector512.Count; + + // 阶段2 使用SIMD批量处理 + fixed (byte* pBuffer = buffer) + { + for (; i <= vectorEnd; i += 32) // 每次处理32字节 + { + if (_supportsAvx512) + { + // 使用AVX-512加载64字节 - 使用正确的指针方法 + Vector512 chunk = Avx512F.LoadVector512(pBuffer + i); + + // 计算哈希和检查分块点 + for (int j = 0; j < 32; j++) + { + hash = (hash << 1) + _gearTable[buffer[i + j]]; + if ((hash & _normalizationMask) == 0) + { + return i + j + 1; + } + } + } + } + } + } + + // 如果找不到分块点或不支持AVX-512,回退到AVX2 + return FindCutpointAVX2(buffer, i, end); + } + + /// + /// 计算数据块的快速哈希值 + /// + private string ComputeFastHash(byte[] buffer, int offset, int length) + { + // 防止偏移量或长度不正确 + if (offset < 0 || length < 0 || offset + length > buffer.Length) + throw new ArgumentOutOfRangeException("偏移量或长度超出缓冲区范围"); + + lock (_hashAlgorithm) // 确保线程安全 + { + var hashBytes = _hashAlgorithm.ComputeHash(buffer, offset, length); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } + } + + /// + /// 检查对象是否已被释放 + /// + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(FastCDCPlus)); + } + + #endregion 内部实现方法 + + #region IDisposable 实现 + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _hashAlgorithm?.Dispose(); + } + + _disposed = true; + } + } + + ~FastCDCPlus() + { + Dispose(false); + } + + #endregion IDisposable 实现 + } + + /// + /// 确定性随机数生成器,确保跨会话一致性 + /// + internal class FastCspRng : IDisposable + { + private byte[] _state; + private int _position; + + public FastCspRng(byte[] seed) + { + if (seed == null || seed.Length < 16) + throw new ArgumentException("种子必须至少为16字节"); + + _state = new byte[1024]; // 大缓冲区 + _position = 0; + + // 使用种子填充初始状态 + using (var sha = SHA256.Create()) + { + byte[] expandedSeed = new byte[_state.Length]; + + // 复制初始种子 + Array.Copy(seed, expandedSeed, seed.Length); + + // 展开种子到整个状态 + for (int i = 0; i < _state.Length / 32; i++) + { + expandedSeed[16] = (byte)i; + var hash = sha.ComputeHash(expandedSeed); + Array.Copy(hash, 0, _state, i * 32, 32); + } + } + } + + public void Dispose() + { + _state = null; + _position = 0; + } + + public uint NextUInt32() + { + if (_position + 4 > _state.Length) + { + // 重新填充状态 + using (var sha = SHA256.Create()) + { + byte[] newState = new byte[_state.Length]; + + // 使用旧状态生成新状态 + for (int i = 0; i < _state.Length / 32; i++) + { + byte[] block = new byte[64]; + Array.Copy(_state, block, Math.Min(64, _state.Length)); + block[0] ^= (byte)i; + var hash = sha.ComputeHash(block); + Array.Copy(hash, 0, newState, i * 32, 32); + } + + _state = newState; + _position = 0; + } + } + + uint value = BinaryPrimitives.ReadUInt32LittleEndian(_state.AsSpan(_position, 4)); + _position += 4; + return value; + } + } + + /// + /// 处理块信息 + /// + internal class BlockInfo + { + public byte[] Buffer { get; set; } + public int Size { get; set; } + public long Position { get; set; } + } + + /// + /// 表示一个分块范围 + /// + public class ChunkRange + { + public int Offset { get; set; } + public int Length { get; set; } + } + + /// + /// 表示完整的块信息 + /// + public class ChunkInfo + { + public long Offset { get; set; } + public int Length { get; set; } + public string Hash { get; set; } + + public override string ToString() + { + return $"Offset: {Offset}, Length: {Length}, Hash: {Hash?.Substring(0, 8)}..."; + } + } +} \ No newline at end of file From 6cf1f92eaaef9595f9ef51a8cb7cc02085cfea19 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Tue, 13 May 2025 15:29:39 +0800 Subject: [PATCH 68/90] file sync cli --- .gitignore | 1 + MDriveSync.sln | 6 + src/MDriveSync.Cli/MDriveSync.Cli.csproj | 37 + src/MDriveSync.Cli/Program.cs | 653 +++++ .../Properties/launchSettings.json | 8 + src/MDriveSync.Cli/README.md | 272 ++ src/MDriveSync.Cli/logo.png | Bin 0 -> 3364 bytes src/MDriveSync.Cli/sync.json | 26 + .../MDriveSync.Client.csproj | 1 - ...ileScanner1.cs => FileFastScannerKopia.cs} | 4 +- .../Services/FileOperationAPIWrapper.cs | 234 ++ .../Services/FileSyncHelper.cs | 2242 +++++++++++++++++ src/MDriveSync.Security/Models/EHashType.cs | 48 + src/MDriveSync.Test/FileSyncHelperTests.cs | 808 ++++++ 14 files changed, 4337 insertions(+), 3 deletions(-) create mode 100644 src/MDriveSync.Cli/MDriveSync.Cli.csproj create mode 100644 src/MDriveSync.Cli/Program.cs create mode 100644 src/MDriveSync.Cli/Properties/launchSettings.json create mode 100644 src/MDriveSync.Cli/README.md create mode 100644 src/MDriveSync.Cli/logo.png create mode 100644 src/MDriveSync.Cli/sync.json rename src/MDriveSync.Core/Services/{FastFileScanner1.cs => FileFastScannerKopia.cs} (99%) create mode 100644 src/MDriveSync.Core/Services/FileOperationAPIWrapper.cs create mode 100644 src/MDriveSync.Core/Services/FileSyncHelper.cs create mode 100644 src/MDriveSync.Security/Models/EHashType.cs create mode 100644 src/MDriveSync.Test/FileSyncHelperTests.cs diff --git a/.gitignore b/.gitignore index 2835219..0fda336 100644 --- a/.gitignore +++ b/.gitignore @@ -368,3 +368,4 @@ FodyWeavers.xsd *.d *.cache .vscode/ +/src/MDriveSync.Cli/push.bat diff --git a/MDriveSync.sln b/MDriveSync.sln index 039bcb1..02c2c03 100644 --- a/MDriveSync.sln +++ b/MDriveSync.sln @@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDriveSync.Test", "src\MDri EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDriveSync.Client", "src\MDriveSync.Client\MDriveSync.Client.csproj", "{6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MDriveSync.Cli", "src\MDriveSync.Cli\MDriveSync.Cli.csproj", "{04233EB5-6D45-4A8C-B586-E209CD8FC810}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -80,6 +82,10 @@ Global {6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D137DB3-41A2-4B3A-AB4F-67D5DA851F7D}.Release|Any CPU.Build.0 = Release|Any CPU + {04233EB5-6D45-4A8C-B586-E209CD8FC810}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04233EB5-6D45-4A8C-B586-E209CD8FC810}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04233EB5-6D45-4A8C-B586-E209CD8FC810}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04233EB5-6D45-4A8C-B586-E209CD8FC810}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MDriveSync.Cli/MDriveSync.Cli.csproj b/src/MDriveSync.Cli/MDriveSync.Cli.csproj new file mode 100644 index 0000000..f9bf8cb --- /dev/null +++ b/src/MDriveSync.Cli/MDriveSync.Cli.csproj @@ -0,0 +1,37 @@ + + + + Exe + net8.0 + enable + true + true + true + trueai-org + trueai-org + mdrive + 使用 mdrive 快速同步/备份,安装:dotnet tool install -g mdrive + https://github.com/trueai-org/mdrive + https://github.com/trueai-org/mdrive + mdrive + 1.0.1 + README.md + + + + + + + + + + + + + + + Always + + + + diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs new file mode 100644 index 0000000..79ff517 --- /dev/null +++ b/src/MDriveSync.Cli/Program.cs @@ -0,0 +1,653 @@ +using MDriveSync.Core.Services; +using MDriveSync.Security.Models; +using Serilog; +using System.Text.Json; + +namespace MDriveSync.Cli +{ + internal class Program + { + private static async Task Main(string[] args) + { + // 配置日志 + ConfigureLogging(); + + try + { + if (args.Length == 0 || args[0] == "--help" || args[0] == "-h") + { + ShowHelp(); + return 0; + } + + string command = args[0].ToLower(); + + switch (command) + { + case "sync": + return await HandleSyncCommand(args.Skip(1).ToArray()); + + case "config": + return HandleConfigCommand(args.Skip(1).ToArray()); + + case "version": + ShowVersion(); + return 0; + + default: + Log.Error($"未知命令: {command}"); + ShowHelp(); + return 1; + } + } + catch (Exception ex) + { + Log.Error(ex, "程序执行过程中发生错误"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + /// + /// 配置日志系统 + /// + private static void ConfigureLogging() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Console() + .WriteTo.File("logs/mdrive-cli.log", rollingInterval: RollingInterval.Day) + .CreateLogger(); + + AppDomain.CurrentDomain.ProcessExit += (s, e) => Log.CloseAndFlush(); + } + + /// + /// 显示帮助信息 + /// + private static void ShowHelp() + { + Console.WriteLine("MDriveSync - 多平台文件同步工具"); + Console.WriteLine(); + Console.WriteLine("用法:"); + Console.WriteLine(" mdrive [命令] [选项]"); + Console.WriteLine(); + Console.WriteLine("命令:"); + Console.WriteLine(" sync 执行文件同步操作"); + Console.WriteLine(" config 管理同步配置文件"); + Console.WriteLine(" version 显示程序版本信息"); + Console.WriteLine(); + Console.WriteLine("选项:"); + Console.WriteLine(" --help, -h 显示帮助信息"); + Console.WriteLine(); + Console.WriteLine("使用 'mdrive [命令] --help' 查看特定命令的帮助信息"); + } + + /// + /// 处理同步命令 + /// + private static async Task HandleSyncCommand(string[] args) + { + if (args.Contains("--help") || args.Contains("-h")) + { + ShowSyncHelp(); + return 0; + } + + var options = ParseSyncOptions(args); + if (options == null) + { + return 1; + } + + try + { + // 验证同步选项 + if (!Directory.Exists(options.SourcePath)) + { + Log.Error($"源目录不存在: {options.SourcePath}"); + return 1; + } + + Log.Information($"开始同步操作..."); + Log.Information($"源目录: {options.SourcePath}"); + Log.Information($"目标目录: {options.TargetPath}"); + Log.Information($"同步模式: {options.SyncMode}"); + Log.Information($"比较方法: {options.CompareMethod}"); + + if (options.PreviewOnly) + { + Log.Information("预览模式:不会执行实际文件操作"); + } + + // 配置进度报告 + var progress = new Progress(progress => + { + if (options.Verbose || progress.ProgressPercentage == 100 || progress.ProgressPercentage % 10 == 0) + { + Log.Information($"{progress.Message} - {progress.ProgressPercentage}% - {progress.FormattedSpeed} - 剩余时间: {progress.FormattedTimeRemaining}"); + } + }); + + // 执行同步 + var syncHelper = new FileSyncHelper(options, progress); + var result = await syncHelper.SyncAsync(); + + // 显示结果 + Log.Information($"同步操作完成,状态: {result.Status}"); + Log.Information($"总耗时: {result.ElapsedTime.TotalSeconds:F2} 秒"); + + if (result.Statistics != null) + { + Log.Information($"文件复制: {result.Statistics.FilesCopied} 个"); + Log.Information($"文件更新: {result.Statistics.FilesUpdated} 个"); + Log.Information($"文件删除: {result.Statistics.FilesDeleted} 个"); + Log.Information($"文件跳过: {result.Statistics.FilesSkipped} 个"); + Log.Information($"目录创建: {result.Statistics.DirectoriesCreated} 个"); + Log.Information($"目录删除: {result.Statistics.DirectoriesDeleted} 个"); + Log.Information($"错误数量: {result.Statistics.Errors} 个"); + Log.Information($"处理总量: {result.Statistics.BytesProcessed} 字节"); + } + + return result.Status == SyncStatus.Completed ? 0 : 1; + } + catch (Exception ex) + { + Log.Error(ex, "同步操作失败"); + return 1; + } + } + + /// + /// 显示同步命令帮助 + /// + private static void ShowSyncHelp() + { + Console.WriteLine("执行文件同步操作"); + Console.WriteLine(); + Console.WriteLine("用法:"); + Console.WriteLine(" mdrivesync sync [选项]"); + Console.WriteLine(); + Console.WriteLine("选项:"); + Console.WriteLine(" --source, -s 源目录路径 (必需)"); + Console.WriteLine(" --target, -t 目标目录路径 (必需)"); + Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); + Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); + Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256, SHA384, SHA512 (默认: SHA256)"); + Console.WriteLine(" --config, -f 配置文件路径"); + Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); + Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); + Console.WriteLine(" --verbose, -v 显示详细日志信息 (默认: false)"); + Console.WriteLine(" --threads, -j 并行操作的最大线程数 (默认: CPU核心数)"); + Console.WriteLine(" --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true)"); + Console.WriteLine(" --preserve-time 保留原始文件时间 (默认: true)"); + Console.WriteLine(" --help 显示帮助信息"); + } + + /// + /// 解析同步命令参数 + /// + private static SyncOptions ParseSyncOptions(string[] args) + { + var options = new SyncOptions + { + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.DateTimeAndSize, + MaxParallelOperations = Math.Max(1, Environment.ProcessorCount), + PreviewOnly = false, + UseRecycleBin = true, + PreserveFileTime = true, + Verbose = false + }; + + string configFilePath = null; + var excludePatterns = new List(); + bool configLoaded = false; + + for (int i = 0; i < args.Length; i++) + { + string arg = args[i]; + string value = (i + 1 < args.Length) ? args[i + 1] : null; + + if (value != null && value.StartsWith("-")) + { + value = null; + } + else if (value != null) + { + i++; + } + + switch (arg) + { + case "--source": + case "-s": + if (value != null) options.SourcePath = value; + break; + + case "--target": + case "-t": + if (value != null) options.TargetPath = value; + break; + + case "--mode": + case "-m": + if (value != null && Enum.TryParse(value, true, out var mode)) + options.SyncMode = mode; + break; + + case "--compare": + case "-c": + if (value != null && Enum.TryParse(value, true, out var compare)) + options.CompareMethod = compare; + break; + + case "--hash": + case "-h": + if (value != null && Enum.TryParse(value, true, out var hash)) + options.HashAlgorithm = hash; + break; + + case "--config": + case "-f": + if (value != null) + { + configFilePath = value; + // 加载配置文件 + if (File.Exists(configFilePath)) + { + try + { + var loadedOptions = FileSyncHelper.LoadFromJsonFile(configFilePath); + options = loadedOptions; + configLoaded = true; + Log.Information($"从配置文件加载选项: {configFilePath}"); + } + catch (Exception ex) + { + Log.Error(ex, $"无法加载配置文件: {configFilePath}"); + return null; + } + } + else + { + Log.Error($"配置文件不存在: {configFilePath}"); + return null; + } + } + break; + + case "--exclude": + case "-e": + if (value != null) + excludePatterns.Add(value); + break; + + case "--preview": + case "-p": + options.PreviewOnly = true; + break; + + case "--verbose": + case "-v": + options.Verbose = true; + break; + + case "--threads": + case "-j": + if (value != null && int.TryParse(value, out int threads) && threads > 0) + options.MaxParallelOperations = threads; + break; + + case "--recycle-bin": + case "-r": + if (value != null && bool.TryParse(value, out bool useRecycle)) + options.UseRecycleBin = useRecycle; + else + options.UseRecycleBin = true; + break; + + case "--preserve-time": + if (value != null && bool.TryParse(value, out bool preserveTime)) + options.PreserveFileTime = preserveTime; + else + options.PreserveFileTime = true; + break; + } + } + + // 如果有排除模式,则设置到选项中 + if (excludePatterns.Count > 0) + { + options.IgnorePatterns = excludePatterns.ToArray(); + } + + // 验证必需参数 + bool isValid = true; + + if (string.IsNullOrEmpty(options.SourcePath)) + { + Log.Error("必须指定源目录 (--source, -s)"); + isValid = false; + } + + if (string.IsNullOrEmpty(options.TargetPath)) + { + Log.Error("必须指定目标目录 (--target, -t)"); + isValid = false; + } + + return isValid ? options : null; + } + + /// + /// 处理配置命令 + /// + private static int HandleConfigCommand(string[] args) + { + if (args.Length == 0 || args.Contains("--help") || args.Contains("-h")) + { + ShowConfigHelp(); + return 0; + } + + string subCommand = args[0].ToLower(); + + switch (subCommand) + { + case "create": + return HandleConfigCreateCommand(args.Skip(1).ToArray()); + + case "view": + return HandleConfigViewCommand(args.Skip(1).ToArray()); + + default: + Log.Error($"未知的配置子命令: {subCommand}"); + ShowConfigHelp(); + return 1; + } + } + + /// + /// 显示配置命令帮助 + /// + private static void ShowConfigHelp() + { + Console.WriteLine("管理同步配置文件"); + Console.WriteLine(); + Console.WriteLine("用法:"); + Console.WriteLine(" mdrivesync config [子命令] [选项]"); + Console.WriteLine(); + Console.WriteLine("子命令:"); + Console.WriteLine(" create 创建新的配置文件"); + Console.WriteLine(" view 查看现有配置文件内容"); + Console.WriteLine(); + Console.WriteLine("使用 'mdrivesync config [子命令] --help' 查看特定子命令的帮助信息"); + } + + /// + /// 处理配置创建命令 + /// + private static int HandleConfigCreateCommand(string[] args) + { + if (args.Contains("--help") || args.Contains("-h")) + { + ShowConfigCreateHelp(); + return 0; + } + + string outputPath = null; + string sourcePath = null; + string targetPath = null; + SyncMode mode = SyncMode.OneWay; + + for (int i = 0; i < args.Length; i++) + { + string arg = args[i]; + string value = (i + 1 < args.Length) ? args[i + 1] : null; + + if (value != null && value.StartsWith("-")) + { + value = null; + } + else if (value != null) + { + i++; + } + + switch (arg) + { + case "--output": + case "-o": + if (value != null) outputPath = value; + break; + + case "--source": + case "-s": + if (value != null) sourcePath = value; + break; + + case "--target": + case "-t": + if (value != null) targetPath = value; + break; + + case "--mode": + case "-m": + if (value != null && Enum.TryParse(value, true, out var syncMode)) + mode = syncMode; + break; + } + } + + // 验证必需参数 + bool isValid = true; + + if (string.IsNullOrEmpty(outputPath)) + { + Log.Error("必须指定输出文件路径 (--output, -o)"); + isValid = false; + } + + if (string.IsNullOrEmpty(sourcePath)) + { + Log.Error("必须指定源目录路径 (--source, -s)"); + isValid = false; + } + + if (string.IsNullOrEmpty(targetPath)) + { + Log.Error("必须指定目标目录路径 (--target, -t)"); + isValid = false; + } + + if (!isValid) + { + ShowConfigCreateHelp(); + return 1; + } + + try + { + CreateConfigFile(new FileInfo(outputPath), new DirectoryInfo(sourcePath), new DirectoryInfo(targetPath), mode); + return 0; + } + catch (Exception ex) + { + Log.Error(ex, "创建配置文件失败"); + return 1; + } + } + + /// + /// 显示配置创建命令帮助 + /// + private static void ShowConfigCreateHelp() + { + Console.WriteLine("创建新的配置文件"); + Console.WriteLine(); + Console.WriteLine("用法:"); + Console.WriteLine(" mdrivesync config create [选项]"); + Console.WriteLine(); + Console.WriteLine("选项:"); + Console.WriteLine(" --output, -o 输出文件路径 (必需)"); + Console.WriteLine(" --source, -s 源目录路径 (必需)"); + Console.WriteLine(" --target, -t 目标目录路径 (必需)"); + Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); + Console.WriteLine(" --help, -h 显示帮助信息"); + } + + /// + /// 处理配置查看命令 + /// + private static int HandleConfigViewCommand(string[] args) + { + if (args.Contains("--help") || args.Contains("-h")) + { + ShowConfigViewHelp(); + return 0; + } + + string filePath = null; + + for (int i = 0; i < args.Length; i++) + { + string arg = args[i]; + string value = (i + 1 < args.Length) ? args[i + 1] : null; + + if (value != null && value.StartsWith("-")) + { + value = null; + } + else if (value != null) + { + i++; + } + + switch (arg) + { + case "--file": + case "-f": + if (value != null) filePath = value; + break; + } + } + + if (string.IsNullOrEmpty(filePath)) + { + Log.Error("必须指定配置文件路径 (--file, -f)"); + ShowConfigViewHelp(); + return 1; + } + + try + { + ViewConfigFile(new FileInfo(filePath)); + return 0; + } + catch (Exception ex) + { + Log.Error(ex, "查看配置文件失败"); + return 1; + } + } + + /// + /// 显示配置查看命令帮助 + /// + private static void ShowConfigViewHelp() + { + Console.WriteLine("查看现有配置文件内容"); + Console.WriteLine(); + Console.WriteLine("用法:"); + Console.WriteLine(" mdrivesync config view [选项]"); + Console.WriteLine(); + Console.WriteLine("选项:"); + Console.WriteLine(" --file, -f 配置文件路径 (必需)"); + Console.WriteLine(" --help, -h 显示帮助信息"); + } + + /// + /// 创建配置文件 + /// + private static void CreateConfigFile(FileInfo output, DirectoryInfo source, DirectoryInfo target, SyncMode mode) + { + try + { + // 创建基本配置 + var options = new SyncOptions + { + SourcePath = source.FullName, + TargetPath = target.FullName, + SyncMode = mode, + CompareMethod = CompareMethod.DateTimeAndSize, + + MaxParallelOperations = Math.Max(1, Environment.ProcessorCount), + PreviewOnly = false, + UseRecycleBin = true, + PreserveFileTime = true, + IgnorePatterns = new string[] + { + "**/System Volume Information/**", + "**/$RECYCLE.BIN/**", + "**/Thumbs.db", + "**/*.tmp", + "**/*.temp", + "**/*.bak" + } + }; + + // 确保目录存在 + Directory.CreateDirectory(Path.GetDirectoryName(output.FullName)); + + // 保存到文件 + FileSyncHelper.SaveToJsonFile(options, output.FullName); + Log.Information($"配置文件已创建: {output.FullName}"); + } + catch (Exception ex) + { + Log.Error(ex, "创建配置文件失败"); + throw; + } + } + + /// + /// 查看配置文件内容 + /// + private static void ViewConfigFile(FileInfo file) + { + try + { + if (!file.Exists) + { + Log.Error($"配置文件不存在: {file.FullName}"); + throw new FileNotFoundException($"配置文件不存在: {file.FullName}", file.FullName); + } + + // 加载配置 + var options = FileSyncHelper.LoadFromJsonFile(file.FullName); + + // 格式化输出 + var json = JsonSerializer.Serialize(options, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine(json); + } + catch (Exception ex) + { + Log.Error(ex, "查看配置文件失败"); + throw; + } + } + + /// + /// 显示版本信息 + /// + private static void ShowVersion() + { + var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; + Console.WriteLine($"MDriveSync CLI 版本: {version}"); + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json new file mode 100644 index 0000000..97626a1 --- /dev/null +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "MDriveSync.Cli": { + "commandName": "Project", + "commandLineArgs": "sync -f sync.json" + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Cli/README.md b/src/MDriveSync.Cli/README.md new file mode 100644 index 0000000..2b19fe0 --- /dev/null +++ b/src/MDriveSync.Cli/README.md @@ -0,0 +1,272 @@ +# MDriveSync CLI + +## 概述 + +MDriveSync CLI 是一个功能强大的命令行工具,用于执行多平台文件同步操作。该工具提供了灵活的配置选项,支持多种同步模式、文件比较方法和哈希算法,适用于各种文件同步和备份场景。 + +## 安装 + + +### 使用 .NET Global Tool(推荐) + +```bash +# 安装 +dotnet tool install -g mdrive + +# 升级 +dotnet tool update -g mdrive + +# 同步 +mdrive sync -s /a -t /b +``` + +### 发布 + +发布到 newget.org + +发布后,将 Release 目录下的 newgut.1.1.0.nupkg 上传到 newget.org + +```bash +dotnet pack -c Release +dotnet nuget push ./bin/Release/mdrive.1.1.0.nupkg -k xxx -s https://api.nuget.org/v3/index.json + +dotnet pack --configuration Release +dotnet nuget push bin/Release/mdrive.1.1.0.nupkg --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json +``` + +### 直接下载二进制文件 + +从 [GitHub Releases](https://github.com/trueai-org/mdrive/releases) 下载最新版本的二进制文件。 + +```shell +# 下载最新版本 +# 解压后使用命令行运行 +dotnet MDriveSync.Cli.dll [命令] [选项] + +``` + +## 基本命令 + +MDriveSync CLI 提供了以下主要命令: + + +``` +MDriveSync - 多平台文件同步工具 + +用法: + MDriveSync.Cli [命令] [选项] + +命令: + sync 执行文件同步操作 + config 管理同步配置文件 + version 显示程序版本信息 + +选项: + --help 显示帮助信息 + +``` + +## 同步命令 (sync) + +同步命令用于在两个目录之间执行文件同步操作。 + + +``` +sync - 执行文件同步操作 + +用法: + MDriveSync.Cli sync [选项] + +选项: + -s, --source 源目录路径 (必需) + -t, --target 目标目录路径 (必需) + -m, --mode 同步模式:OneWay(单向)、Mirror(镜像)、TwoWay(双向) [默认: OneWay] + -c, --compare 文件比较方法:Size(大小)、DateTime(修改时间)、DateTimeAndSize(时间和大小)、 + Content(内容)、Hash(哈希) [默认: DateTimeAndSize] + -h, --hash 哈希算法:MD5、SHA1、SHA256、SHA384、SHA512 [默认: SHA256] + -f, --config 配置文件路径 + -e, --exclude 排除的文件或目录模式(支持通配符,可多次指定) + -p, --preview 预览模式,不实际执行操作 + -v, --verbose 显示详细日志信息 + -j, --threads 并行操作的最大线程数 [默认: 系统处理器数量] + -r, --recycle-bin 使用回收站代替直接删除文件 [默认: true] + --preserve-time 保留原始文件时间 [默认: true] + --help 显示帮助信息 + +``` + +### 示例 + + +```shell +# 基本单向同步 +MDriveSync.Cli sync --source "D:\Documents" --target "E:\Backup\Documents" + +# 使用镜像模式和SHA1哈希算法 +MDriveSync.Cli sync --source "D:\Projects" --target "E:\Backup\Projects" --mode Mirror --hash SHA1 + +# 排除特定文件和目录 +MDriveSync.Cli sync --source "D:\Photos" --target "E:\Backup\Photos" --exclude "*.tmp" --exclude "**/Thumbs.db" + +# 使用预览模式 +MDriveSync.Cli sync --source "D:\Music" --target "E:\Backup\Music" --preview + +# 从配置文件加载同步选项 +MDriveSync.Cli sync --source "D:\Videos" --target "E:\Backup\Videos" --config "sync.config.json" + +# 使用高级选项 +MDriveSync.Cli sync --source "D:\Work" --target "E:\Backup\Work" --mode TwoWay --compare Hash --threads 4 --verbose + +``` + +## 配置命令 (config) + +配置命令用于管理同步配置文件。 + + +``` +config - 管理同步配置文件 + +用法: + MDriveSync.Cli config [子命令] [选项] + +子命令: + create 创建新的配置文件 + view 查看现有配置文件内容 + +选项: + --help 显示帮助信息 + +``` + +### 创建配置文件 + + +``` +create - 创建新的配置文件 + +用法: + MDriveSync.Cli config create [选项] + +选项: + -o, --output 输出文件路径 (必需) + -s, --source 源目录路径 (必需) + -t, --target 目标目录路径 (必需) + -m, --mode 同步模式:OneWay(单向)、Mirror(镜像)、TwoWay(双向) [默认: OneWay] + --help 显示帮助信息 + +``` + +### 查看配置文件 + + +``` +view - 查看现有配置文件内容 + +用法: + MDriveSync.Cli config view [选项] + +选项: + -f, --file 配置文件路径 (必需) + --help 显示帮助信息 + +``` + +### 示例 + + +```shell +# 创建新的配置文件 +MDriveSync.Cli config create --output "sync.config.json" --source "D:\Documents" --target "E:\Backup\Documents" --mode Mirror + +# 查看配置文件内容 +MDriveSync.Cli config view --file "sync.config.json" + +``` + +## 版本命令 (version) + +显示程序版本信息。 + + +```shell +MDriveSync.Cli version + +``` + +## 同步模式说明 + +MDriveSync 支持以下同步模式: + +- **OneWay (单向)**: 从源目录到目标目录的单向同步,仅更新目标目录中的文件。 +- **Mirror (镜像)**: 使目标目录成为源目录的精确副本,包括删除目标目录中多余的文件。 +- **TwoWay (双向)**: 在源目录和目标目录之间进行双向同步,保持两个目录的内容一致。 + +## 文件比较方法 + +可用的文件比较方法包括: + +- **Size**: 仅比较文件大小 +- **DateTime**: 仅比较修改时间 +- **DateTimeAndSize**: 比较修改时间和文件大小(默认) +- **Content**: 比较文件内容(读取文件) +- **Hash**: 使用哈希算法比较 + +## 哈希算法 + +支持的哈希算法包括: + +- **MD5** +- **SHA1** +- **SHA256** (默认) +- **SHA384** +- **SHA512** + +## 配置文件格式 + +配置文件使用 JSON 格式,包含以下主要选项: + + +```json +{ + "SourcePath": "D:\\Source", + "TargetPath": "E:\\Target", + "SyncMode": "OneWay", + "CompareMethod": "DateTimeAndSize", + "HashAlgorithm": "SHA256", + "MaxParallelOperations": 8, + "PreviewOnly": false, + "UseRecycleBin": true, + "PreserveFileTime": true, + "IgnorePatterns": [ + "**/System Volume Information/**", + "**/$RECYCLE.BIN/**", + "**/Thumbs.db", + "**/*.tmp", + "**/*.temp", + "**/*.bak" + ] +} + +``` + +## 排除模式 + +您可以使用通配符来指定要排除的文件或目录: + +- `*` 匹配任意数量的字符(不包括目录分隔符) +- `**` 匹配任意数量的字符(包括目录分隔符) +- `?` 匹配单个字符 + +示例: +- `*.tmp` - 排除所有 .tmp 文件 +- `**/node_modules/**` - 排除所有 node_modules 目录及其内容 +- `backup/*` - 排除 backup 目录下的所有文件 + +## 日志 + +MDriveSync CLI 会输出日志到控制台,并存储在 `logs/mdrivesync-cli.log` 文件中。使用 `--verbose` 选项可以获取更详细的日志信息。 + +## 许可证 + +MDriveSync CLI 是一款开源工具,遵循 [MIT 许可证](https://opensource.org/licenses/MIT)。 \ No newline at end of file diff --git a/src/MDriveSync.Cli/logo.png b/src/MDriveSync.Cli/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9af25289c8683fcdef333844a11cbfbee1709b0c GIT binary patch literal 3364 zcmcgvS5%W*xBU`Am0qMu0D%A&ihv=~dqkQC=>kfZ4u>un4bn>}0!oVr2hq@xA|OG! zRFNJ^=)Fk`(*FGS<&JS4@56oAYp=cM9CMAe#-0y5#>ha2fsTg`000K0uI5AXj{onX zp(4kJQ|{CNK)-?1R5J+zZBA0)tb_S<_WFNKTGdKl{la1WhD=uAI=n)+*dDB#~kW#3nibO#L;y$MVB2lii2QUE3g;v zm8%O}i?hJSM!8j5KsU=gQ4F@Pu#FaJFXp@zbG`8m=GQ z`(yegF9=;RHIF0MF3Rute4yMjC6oul6+J+*3*l)Pmyu_3&!wH8)U6e}K3Sy=jLcmq zrB@oX#|4N_6~O9@8%H?(IDH$82JY8!>bufmnZ9Y~bLcHeo%9)gU#)^Js+`0|Upj1k zuQQfi&Zcfnt5r_8i?g?VA7}~gntRBeqD;!m`yIX-x_ImTX{L54F)pi}prmqg9wOAW zz`8i}JM5wunRoOc`1IG{&POYf>(ECguulV5i-~8qzQ%Wb?IKx-6wMW*jd{QD+@k^_ zthw$<dOmLcd(bK)*FGd|Ry~&ddL| zfqDqosBsCiuoGRYUoOG#bpRZ76ue87G@B3YUVAREs^Do=YNGzCX=W9~hhj4RCGREP z##A3tuuWQjS>^RYeC-!DIDsFpmWj5!s`3!(ccifvx}vl9kNrTH*bKbIv~cd$pw>sn zy-<h&Y{M%28)rcjzm2Uy|XW>W$g%se)>y_<5B|y%&QJ^k>UX{uY zkO3$#ATAU=9&iBuKOqK||7%U`LMc0n9?KBA0Psa5u2xfAi+!`jO}@EKH3y$UBT3AD z0wfw)t|uoJ&xTJgiA?Wm5qMae!NuvGXad`Y0KU$3KX`Q3_D3 zMsm|oVR4j$uWlHrPJcG}^u+HQ*QnQzC^_ERNIBk`O)0mN^BW`U0ko=AgID&A$9}gz zB&|0%&Ml3pFqtWyKZE}nrB(S7$Y>Pc;KshqPsp_x;pQS zitOd*2|kwo^|LmY>mk}X9aGXKm>F#+M@QmWZ5mQDRuPWR^4QZifeQzD3<>(}pHJ@$uODo@`C`zQsfYv(qT%O#OLG2oq@((aoMvuZ}kUUSY~-QyPQJF~_k93ETdAe=x7FcP{928WUUz*#1X#eOr(%mieg(=8iZ4Cy zu$$!Nkf&V!hM~^l<~h{l{`xqW6Nf3mYJGHJK~*Wq6Cd{??5DqoTKGaI1|Z&VFJ~Xk zIuLlji>ktW_dNV6Dm*1Ed2eIKWhd(@kKNT?e~zuG?mrjFk1l7#-8P4r-sSlG%%i4= z!9t^7R748I_npC|W#uAnSA4hYENSa{MM>6)82VPRpKXjsDQi30-_E}UQSsH)UQq_K z3B3z)*PmZ${bzoiQU(ePmM#mPEksI^-=U5=sn&HVS|cjQDG>MNLEPs#)h;V-*(Q4@_Nb{66-Gb zLO3cVrHzK@l}d!Y^p6_#Fd$~CS{U3GrN*R?Xo+~aiiRP7!YG`a9=x1@Vx0({gkS6pVhWY%^yeAH2DC)Oh54qoK<6<^ad5D-yj#lBgfP_}TIJziBA<*!~Lh?0K1=_YmC9S3sM?>1PiWI{mp z3>)dScc1RM$#I>9=+1rFOF6ptiY+ zh2gCtUu7&G^a@#(YrkDS$N~mZzTVlUPFA0>uBA@g;m^`x%m9+L!3RREvpB7hQYZK_ zzb^(7Eze3kIm|S!zh+ZP{9}OCLl#4#C0Ox1mmYj)raMQ(OrD{bG+mUg5GW*Bt!#u? zm2fDAS8!Z}&@v;1QI|rv-~OSSC4>^iFBK+WO`{Ya)|P7-8zD|y6;Tcx>XVjs zT~~*fsRR(owuzZeUQvnojh|bWOGi{zb4&i!r!iaY>~=7)lNZ{VTCX|@U?o=GTef(+ z-gY!kSI!Umwdg3dIdOLwMTnWRpDC9aA@ez~d@WC}pRakDe%h;HyYu5l$pinVp+1yB z?;xeG9UqCGg)IJkkNEx(ha_$@T<$3%-PGe(%$6WC5s{*S4_dp)pS}Mc;zEdd6A&Ag znn6|+WX4h#ztl({8=kNu<&WxOJG>lg8xe(U1Pvj4cR@T{zq9FRf~SPGZs43yoObhG z`g;R+rZ}L`L12%Falf7PHcp~M{Se8#*?*ZTm|D^sq*_ZRBm-hBMeEK$~ zgYwb`zY!RU!AbuF5m=E20n+)`4t0Gc+x|$sLPvUW+S-KKKE(7rpZ=!%NTp^Or&_Ecb p^*=myvd@H3WN(qJrP+3J!}Wdz5!GNkO#b} literal 0 HcmV?d00001 diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json new file mode 100644 index 0000000..b0a17bc --- /dev/null +++ b/src/MDriveSync.Cli/sync.json @@ -0,0 +1,26 @@ +{ + "SourcePath": "E:\\downs\\fdm\\__同步测试\\source1", + "TargetPath": "E:\\downs\\fdm\\__同步测试\\target1", + "SyncMode": "Mirror", + "CompareMethod": "DateTimeAndSize", + "HashAlgorithm": "SHA256", + "SamplingRate": 0.1, + "MaxParallelOperations": 4, + "EnableParallelFileOperations": true, + "DateTimeThresholdSeconds": 2, + "PreserveFileTime": true, + "UseRecycleBin": true, + "ConflictResolution": "Newer", + "FollowSymlinks": false, + "PreviewOnly": false, + "ContinueOnError": true, + "MaxRetries": 3, + "IgnorePatterns": [ + "**/System Volume Information/**", + "**/$RECYCLE.BIN/**", + "**/Thumbs.db", + "**/*.tmp", + "**/*.temp", + "**/*.bak" + ] +} \ No newline at end of file diff --git a/src/MDriveSync.Client/MDriveSync.Client.csproj b/src/MDriveSync.Client/MDriveSync.Client.csproj index f2ee9b3..d2d5701 100644 --- a/src/MDriveSync.Client/MDriveSync.Client.csproj +++ b/src/MDriveSync.Client/MDriveSync.Client.csproj @@ -4,7 +4,6 @@ Exe net8.0 enable - enable diff --git a/src/MDriveSync.Core/Services/FastFileScanner1.cs b/src/MDriveSync.Core/Services/FileFastScannerKopia.cs similarity index 99% rename from src/MDriveSync.Core/Services/FastFileScanner1.cs rename to src/MDriveSync.Core/Services/FileFastScannerKopia.cs index 1989288..b9a87c0 100644 --- a/src/MDriveSync.Core/Services/FastFileScanner1.cs +++ b/src/MDriveSync.Core/Services/FileFastScannerKopia.cs @@ -8,7 +8,7 @@ namespace MDriveSync.Core.Services /// /// 高性能文件扫描器,基于 Kopia 风格设计,针对 .NET Core 优化 /// - public class FastFileScanner1 + public class FileFastScannerKopia { private readonly int _maxParallelism; private readonly int _queueCapacity; @@ -24,7 +24,7 @@ public class FastFileScanner1 /// 最大并行度,默认为处理器核心数的两倍 /// 队列容量,默认为 100,000 /// 进度报告回调 - public FastFileScanner1( + public FileFastScannerKopia( ScanOptions options = null, int? maxParallelism = null, int queueCapacity = 100_000, diff --git a/src/MDriveSync.Core/Services/FileOperationAPIWrapper.cs b/src/MDriveSync.Core/Services/FileOperationAPIWrapper.cs new file mode 100644 index 0000000..469bff3 --- /dev/null +++ b/src/MDriveSync.Core/Services/FileOperationAPIWrapper.cs @@ -0,0 +1,234 @@ +using System.Runtime.InteropServices; + +namespace MDriveSync.Core.Services +{ + /// + /// Windows 文件操作 API 包装类 + /// 主要用于实现文件回收站支持等功能 + /// + public static class FileOperationAPIWrapper + { + // Shell32.dll 中的 SHFileOperation 函数定义 + [DllImport("shell32.dll", CharSet = CharSet.Auto)] + private static extern int SHFileOperation(ref SHFILEOPSTRUCT FileOp); + + // IFileOperation 接口(Windows Vista+) + [ComImport] + [Guid("947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IFileOperation + { + uint Advise(IntPtr pfops, out uint pdwCookie); + + void Unadvise(uint dwCookie); + + void SetOperationFlags(uint dwOperationFlags); + + void SetProgressMessage([MarshalAs(UnmanagedType.LPWStr)] string pszMessage); + + void SetProgressDialog([MarshalAs(UnmanagedType.Interface)] object popd); + + void SetProperties([MarshalAs(UnmanagedType.Interface)] object pproparray); + + void SetOwnerWindow(uint hwndParent); + + void ApplyPropertiesToItem([MarshalAs(UnmanagedType.Interface)] object psi); + + void ApplyPropertiesToItems([MarshalAs(UnmanagedType.Interface)] object punkItems); + + void RenameItem([MarshalAs(UnmanagedType.Interface)] object psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IntPtr pfopsItem); + + void RenameItems([MarshalAs(UnmanagedType.Interface)] object pUnkItems, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName); + + void MoveItem([MarshalAs(UnmanagedType.Interface)] object psiItem, [MarshalAs(UnmanagedType.Interface)] object psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IntPtr pfopsItem); + + void MoveItems([MarshalAs(UnmanagedType.Interface)] object punkItems, [MarshalAs(UnmanagedType.Interface)] object psiDestinationFolder); + + void CopyItem([MarshalAs(UnmanagedType.Interface)] object psiItem, [MarshalAs(UnmanagedType.Interface)] object psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IntPtr pfopsItem); + + void CopyItems([MarshalAs(UnmanagedType.Interface)] object punkItems, [MarshalAs(UnmanagedType.Interface)] object psiDestinationFolder); + + void DeleteItem([MarshalAs(UnmanagedType.Interface)] object psiItem, IntPtr pfopsItem); + + void DeleteItems([MarshalAs(UnmanagedType.Interface)] object punkItems); + + void NewItem([MarshalAs(UnmanagedType.Interface)] object psiDestinationFolder, uint dwFileAttributes, [MarshalAs(UnmanagedType.LPWStr)] string pszName, [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName, IntPtr pfopsItem); + + void PerformOperations(); + + void GetAnyOperationsAborted(out bool pfAnyOperationsAborted); + } + + [ComImport] + [Guid("3ad05575-8857-4850-9277-11b85bdb8e09")] + [ClassInterface(ClassInterfaceType.None)] + private class FileOperation + { + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private struct SHFILEOPSTRUCT + { + public IntPtr hwnd; + + [MarshalAs(UnmanagedType.U4)] + public int wFunc; + + public string pFrom; + public string pTo; + public short fFlags; + + [MarshalAs(UnmanagedType.Bool)] + public bool fAnyOperationsAborted; + + public IntPtr hNameMappings; + public string lpszProgressTitle; + } + + // 常量定义 + private const int FO_DELETE = 0x0003; + + private const int FOF_ALLOWUNDO = 0x0040; // 允许撤销(使用回收站) + private const int FOF_NOCONFIRMATION = 0x0010; // 不显示确认对话框 + private const int FOF_NOERRORUI = 0x0400; // 不显示错误用户界面 + private const int FOF_SILENT = 0x0004; // 静默操作,不显示进度对话框 + + /// + /// 将文件或目录移动到回收站 + /// + /// 文件或目录路径 + /// 操作是否成功 + public static bool MoveToRecycleBin(string path) + { + if (string.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + if (!File.Exists(path) && !Directory.Exists(path)) + return false; + + try + { + // 使用 SHFileOperation 实现移动到回收站 + SHFILEOPSTRUCT fileOp = new SHFILEOPSTRUCT + { + wFunc = FO_DELETE, + pFrom = path + '\0' + '\0', // 需要双终止符 + fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT + }; + + int result = SHFileOperation(ref fileOp); + return result == 0; + } + catch (Exception ex) + { + // 出现异常时记录日志并返回失败 + System.Diagnostics.Debug.WriteLine($"移动到回收站失败: {ex.Message}"); + return false; + } + } + + /// + /// 将多个文件或目录移动到回收站 + /// + /// 文件或目录路径数组 + /// 操作是否成功 + public static bool MoveToRecycleBin(string[] paths) + { + if (paths == null || paths.Length == 0) + throw new ArgumentNullException(nameof(paths)); + + try + { + // 构建结束符字符串 + string fromStr = string.Join("\0", paths) + "\0\0"; + + // 使用 SHFileOperation 实现移动到回收站 + SHFILEOPSTRUCT fileOp = new SHFILEOPSTRUCT + { + wFunc = FO_DELETE, + pFrom = fromStr, + fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT + }; + + int result = SHFileOperation(ref fileOp); + return result == 0; + } + catch (Exception ex) + { + // 出现异常时记录日志并返回失败 + System.Diagnostics.Debug.WriteLine($"批量移动到回收站失败: {ex.Message}"); + return false; + } + } + + /// + /// 检查当前系统是否支持回收站操作 + /// + /// 是否支持回收站 + public static bool IsRecycleBinSupported() + { + // 目前仅Windows平台支持回收站功能 + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + + /// + /// 安全删除文件(尝试使用回收站,如不支持则直接删除) + /// + /// 文件路径 + /// 是否使用回收站 + /// 操作是否成功 + public static bool SafeDeleteFile(string filePath, bool useRecycleBin = true) + { + if (!File.Exists(filePath)) + return true; // 文件不存在视为成功 + + try + { + if (useRecycleBin && IsRecycleBinSupported()) + { + return MoveToRecycleBin(filePath); + } + else + { + File.Delete(filePath); + return true; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"删除文件失败: {ex.Message}"); + return false; + } + } + + /// + /// 安全删除目录(尝试使用回收站,如不支持则递归删除) + /// + /// 目录路径 + /// 是否使用回收站 + /// 操作是否成功 + public static bool SafeDeleteDirectory(string directoryPath, bool useRecycleBin = true) + { + if (!Directory.Exists(directoryPath)) + return true; // 目录不存在视为成功 + + try + { + if (useRecycleBin && IsRecycleBinSupported()) + { + return MoveToRecycleBin(directoryPath); + } + else + { + Directory.Delete(directoryPath, true); + return true; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"删除目录失败: {ex.Message}"); + return false; + } + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs new file mode 100644 index 0000000..e04b8ad --- /dev/null +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -0,0 +1,2242 @@ +using MDriveSync.Security; +using MDriveSync.Security.Models; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MDriveSync.Core.Services +{ + /// + /// 文件同步助手,支持单向、镜像、双向等多种同步模式,高性能文件系统扫描及比较 + /// + public class FileSyncHelper + { + private readonly SyncOptions _options; + private readonly IProgress _progress; + private readonly CancellationToken _cancellationToken; + + private readonly Stopwatch _stopwatch = new Stopwatch(); + private DateTime _lastProgressUpdate = DateTime.MinValue; + private int _processedItems = 0; + private int _totalItems = 0; + private SyncStatistics _statistics = new SyncStatistics(); + + /// + /// 初始化文件同步助手 + /// + /// 同步选项 + /// 进度报告回调 + /// 取消令牌 + public FileSyncHelper(SyncOptions options, IProgress progress = null, CancellationToken cancellationToken = default) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _progress = progress; + _cancellationToken = cancellationToken; + } + + /// + /// 执行同步操作 + /// + /// 同步结果 + public async Task SyncAsync() + { + _stopwatch.Restart(); + _statistics = new SyncStatistics(); + var result = new SyncResult + { + StartTime = DateTime.Now, + SourcePath = _options.SourcePath, + TargetPath = _options.TargetPath, + Mode = _options.SyncMode, + Status = SyncStatus.Started + }; + + try + { + // 初始化源目录和目标目录 + ValidatePaths(); + ReportProgress("正在初始化同步操作...", 0); + + // 扫描源目录和目标目录的文件列表 + var (sourceFiles, sourceDirs) = await ScanDirectoryAsync(_options.SourcePath, "源目录"); + var (targetFiles, targetDirs) = await ScanDirectoryAsync(_options.TargetPath, "目标目录"); + + _totalItems = sourceFiles.Count + targetFiles.Count; + ReportProgress($"开始比较文件差异,共 {_totalItems} 个文件需要处理", 0); + + // 根据同步模式执行不同的同步策略 + await ExecuteSyncByMode(sourceFiles, sourceDirs, targetFiles, targetDirs, result); + + result.Status = SyncStatus.Completed; + result.ElapsedTime = _stopwatch.Elapsed; + result.Statistics = _statistics; + ReportProgress($"同步完成,耗时: {_stopwatch.Elapsed.TotalSeconds:F2}秒", 100); + + return result; + } + catch (OperationCanceledException) + { + result.Status = SyncStatus.Canceled; + result.ElapsedTime = _stopwatch.Elapsed; + result.Statistics = _statistics; + ReportProgress("同步操作已取消", -1); + return result; + } + catch (Exception ex) + { + result.Status = SyncStatus.Failed; + result.ErrorMessage = ex.Message; + result.ElapsedTime = _stopwatch.Elapsed; + result.Statistics = _statistics; + ReportProgress($"同步操作失败: {ex.Message}", -1); + return result; + } + finally + { + _stopwatch.Stop(); + result.EndTime = DateTime.Now; + } + } + + /// + /// 根据同步模式执行相应的同步策略 + /// + private async Task ExecuteSyncByMode( + Dictionary sourceFiles, + HashSet sourceDirs, + Dictionary targetFiles, + HashSet targetDirs, + SyncResult result) + { + List actions = new List(); + + switch (_options.SyncMode) + { + case SyncMode.OneWay: + actions = await CreateOneWaySyncActionsAsync(sourceFiles, sourceDirs, targetFiles, targetDirs); + break; + + case SyncMode.Mirror: + actions = await CreateMirrorSyncActionsAsync(sourceFiles, sourceDirs, targetFiles, targetDirs); + break; + + case SyncMode.TwoWay: + actions = await CreateTwoWaySyncActionsAsync(sourceFiles, sourceDirs, targetFiles, targetDirs); + break; + + default: + throw new NotSupportedException($"不支持的同步模式: {_options.SyncMode}"); + } + + // 记录行动计划 + result.Actions = actions; + + // 如果是预览模式,则不执行实际操作 + if (_options.PreviewOnly) + { + ReportProgress("预览模式 - 不执行实际操作", -1); + return; + } + + // 执行同步操作 + await ExecuteSyncActionsAsync(actions); + } + + /// + /// 创建单向同步操作列表(源 -> 目标) + /// + private async Task> CreateOneWaySyncActionsAsync( + Dictionary sourceFiles, + HashSet sourceDirs, + Dictionary targetFiles, + HashSet targetDirs) + { + var actions = new List(); + + // 确保目标目录结构完整 + foreach (var sourceDir in sourceDirs) + { + var relativePath = GetRelativePath(sourceDir, _options.SourcePath); + var targetDirPath = Path.Combine(_options.TargetPath, relativePath); + + if (!targetDirs.Contains(targetDirPath)) + { + actions.Add(new SyncAction + { + ActionType = SyncActionType.CreateDirectory, + SourcePath = sourceDir, + TargetPath = targetDirPath, + RelativePath = relativePath + }); + } + } + + // 比较文件 + foreach (var sourceEntry in sourceFiles) + { + var sourceFilePath = sourceEntry.Key; + var sourceFile = sourceEntry.Value; + var relativePath = GetRelativePath(sourceFilePath, _options.SourcePath); + var targetFilePath = Path.Combine(_options.TargetPath, relativePath); + + if (!targetFiles.TryGetValue(targetFilePath, out var targetFile)) + { + // 目标不存在文件,需要复制 + actions.Add(new SyncAction + { + ActionType = SyncActionType.CopyFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + _statistics.FilesToCopy++; + _statistics.BytesToProcess += sourceFile.Length; + } + else if (NeedsUpdate(sourceFile, targetFile)) + { + // 文件需要更新 + actions.Add(new SyncAction + { + ActionType = SyncActionType.UpdateFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + _statistics.FilesToUpdate++; + _statistics.BytesToProcess += sourceFile.Length; + } + else + { + _statistics.FilesSkipped++; + } + } + + return actions; + } + + /// + /// 创建镜像同步操作列表(源 -> 目标,删除目标中多余内容) + /// + private async Task> CreateMirrorSyncActionsAsync( + Dictionary sourceFiles, + HashSet sourceDirs, + Dictionary targetFiles, + HashSet targetDirs) + { + // 首先创建单向同步的操作 + var actions = await CreateOneWaySyncActionsAsync(sourceFiles, sourceDirs, targetFiles, targetDirs); + + // 查找目标中需要删除的文件和目录 + var sourceRelativePaths = sourceFiles.Keys + .Select(path => GetRelativePath(path, _options.SourcePath)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var targetRelativePaths = targetFiles.Keys + .Select(path => GetRelativePath(path, _options.TargetPath)); + + foreach (var targetRelativePath in targetRelativePaths) + { + if (!sourceRelativePaths.Contains(targetRelativePath)) + { + var targetFilePath = Path.Combine(_options.TargetPath, targetRelativePath); + // 目标文件在源中不存在,需要删除 + actions.Add(new SyncAction + { + ActionType = SyncActionType.DeleteFile, + TargetPath = targetFilePath, + RelativePath = targetRelativePath, + Size = targetFiles[targetFilePath].Length + }); + _statistics.FilesToDelete++; + } + } + + // 查找目标中需要删除的目录(倒序处理,先删除子目录) + var sourceRelativeDirs = sourceDirs + .Select(path => GetRelativePath(path, _options.SourcePath)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var targetRelativeDirs = targetDirs + .Where(d => d != _options.TargetPath) // 排除根目录 + .Select(path => GetRelativePath(path, _options.TargetPath)) + .OrderByDescending(p => p.Length); // 倒序排列,先删除深层目录 + + foreach (var targetRelativeDir in targetRelativeDirs) + { + if (!sourceRelativeDirs.Contains(targetRelativeDir)) + { + var targetDirPath = Path.Combine(_options.TargetPath, targetRelativeDir); + // 目标目录在源中不存在,需要删除 + actions.Add(new SyncAction + { + ActionType = SyncActionType.DeleteDirectory, + TargetPath = targetDirPath, + RelativePath = targetRelativeDir + }); + _statistics.DirectoriesToDelete++; + } + } + + return actions; + } + + /// + /// 创建双向同步操作列表(源 <-> 目标,解决冲突) + /// + private async Task> CreateTwoWaySyncActionsAsync( + Dictionary sourceFiles, + HashSet sourceDirs, + Dictionary targetFiles, + HashSet targetDirs) + { + var actions = new List(); + var processedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + + // 第1步:同步目录结构 + var allSourceRelativeDirs = sourceDirs + .Select(path => GetRelativePath(path, _options.SourcePath)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var allTargetRelativeDirs = targetDirs + .Where(d => d != _options.TargetPath) + .Select(path => GetRelativePath(path, _options.TargetPath)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + // 在目标中创建源中存在的目录 + foreach (var sourceRelativeDir in allSourceRelativeDirs) + { + var targetDirPath = Path.Combine(_options.TargetPath, sourceRelativeDir); + if (!targetDirs.Contains(targetDirPath)) + { + actions.Add(new SyncAction + { + ActionType = SyncActionType.CreateDirectory, + SourcePath = Path.Combine(_options.SourcePath, sourceRelativeDir), + TargetPath = targetDirPath, + RelativePath = sourceRelativeDir + }); + } + } + + // 在源中创建目标中存在的目录 + foreach (var targetRelativeDir in allTargetRelativeDirs) + { + var sourceDirPath = Path.Combine(_options.SourcePath, targetRelativeDir); + if (!sourceDirs.Contains(sourceDirPath)) + { + actions.Add(new SyncAction + { + ActionType = SyncActionType.CreateDirectory, + SourcePath = targetRelativeDir, + TargetPath = sourceDirPath, + RelativePath = targetRelativeDir, + Direction = SyncDirection.TargetToSource + }); + } + } + + // 第2步:处理文件 + // 从源处理到目标 + foreach (var sourceEntry in sourceFiles) + { + var sourceFilePath = sourceEntry.Key; + var sourceFile = sourceEntry.Value; + var relativePath = GetRelativePath(sourceFilePath, _options.SourcePath); + var targetFilePath = Path.Combine(_options.TargetPath, relativePath); + + processedPaths.Add(relativePath); + + if (!targetFiles.TryGetValue(targetFilePath, out var targetFile)) + { + // 目标不存在,复制到目标 + actions.Add(new SyncAction + { + ActionType = SyncActionType.CopyFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + _statistics.FilesToCopy++; + _statistics.BytesToProcess += sourceFile.Length; + } + else + { + // 两边都存在,需要解决冲突 + var conflictResult = ResolveConflict(sourceFile, targetFile); + switch (conflictResult) + { + case ConflictResolution.SourceWins: + actions.Add(new SyncAction + { + ActionType = SyncActionType.UpdateFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length, + ConflictResolution = conflictResult + }); + _statistics.FilesToUpdate++; + _statistics.BytesToProcess += sourceFile.Length; + break; + + case ConflictResolution.TargetWins: + actions.Add(new SyncAction + { + ActionType = SyncActionType.UpdateFile, + SourcePath = targetFilePath, + TargetPath = sourceFilePath, + RelativePath = relativePath, + Size = targetFile.Length, + Direction = SyncDirection.TargetToSource, + ConflictResolution = conflictResult + }); + _statistics.FilesToUpdate++; + _statistics.BytesToProcess += targetFile.Length; + break; + + case ConflictResolution.KeepBoth: + // 保留两个版本,重命名目标文件 + string targetNewName = GetConflictFileName(targetFilePath); + actions.Add(new SyncAction + { + ActionType = SyncActionType.RenameFile, + SourcePath = targetFilePath, + TargetPath = targetNewName, + RelativePath = GetRelativePath(targetNewName, _options.TargetPath), + ConflictResolution = conflictResult + }); + // 然后复制源文件到目标 + actions.Add(new SyncAction + { + ActionType = SyncActionType.CopyFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + _statistics.FilesToCopy++; + _statistics.BytesToProcess += sourceFile.Length; + break; + + case ConflictResolution.Skip: + _statistics.FilesSkipped++; + break; + } + } + } + + // 从目标处理到源 + foreach (var targetEntry in targetFiles) + { + var targetFilePath = targetEntry.Key; + var targetFile = targetEntry.Value; + var relativePath = GetRelativePath(targetFilePath, _options.TargetPath); + + // 跳过已处理的文件 + if (processedPaths.Contains(relativePath)) + continue; + + var sourceFilePath = Path.Combine(_options.SourcePath, relativePath); + + // 源不存在,复制到源 + actions.Add(new SyncAction + { + ActionType = SyncActionType.CopyFile, + SourcePath = targetFilePath, + TargetPath = sourceFilePath, + RelativePath = relativePath, + Size = targetFile.Length, + Direction = SyncDirection.TargetToSource + }); + _statistics.FilesToCopy++; + _statistics.BytesToProcess += targetFile.Length; + } + + return actions; + } + + /// + /// 执行同步操作 + /// + private async Task ExecuteSyncActionsAsync(List actions) + { + // 按操作类型排序:先创建目录,然后复制/更新文件,最后删除文件和目录 + var orderedActions = actions + .OrderBy(a => GetActionPriority(a.ActionType)) + .ToList(); + + _totalItems = orderedActions.Count; + _processedItems = 0; + + ReportProgress($"开始执行同步操作,共 {_totalItems} 个任务", 0); + + if (_options.MaxParallelOperations > 1 && _options.EnableParallelFileOperations) + { + // 并行处理 + await ProcessActionsInParallelAsync(orderedActions); + } + else + { + // 串行处理 + await ProcessActionsSequentiallyAsync(orderedActions); + } + } + + /// + /// 并行处理同步操作 + /// + private async Task ProcessActionsInParallelAsync(List actions) + { + // 分组处理不同类型的操作 + var actionGroups = actions + .GroupBy(a => GetActionPriority(a.ActionType)) + .OrderBy(g => g.Key); + + foreach (var group in actionGroups) + { + var groupActions = group.ToList(); + var actionType = groupActions.FirstOrDefault()?.ActionType; + + ReportProgress($"处理 {GetActionTypeDescription(actionType.Value)} 操作,共 {groupActions.Count} 项", + CalculateProgress(_processedItems, _totalItems)); + + // 设置并行度,文件操作使用配置的并行度,目录操作单线程执行 + int parallelism = IsFileOperation(actionType.Value) + ? _options.MaxParallelOperations + : 1; + + await Parallel.ForEachAsync( + groupActions, + new ParallelOptions + { + MaxDegreeOfParallelism = parallelism, + CancellationToken = _cancellationToken + }, + async (action, ct) => + { + await ExecuteSingleActionAsync(action); + Interlocked.Increment(ref _processedItems); + + if (_processedItems % 10 == 0 || _processedItems == _totalItems) + { + ReportProgress( + $"正在处理 {GetActionTypeDescription(action.ActionType)} 操作 ({_processedItems}/{_totalItems})", + CalculateProgress(_processedItems, _totalItems) + ); + } + }); + } + } + + /// + /// 串行处理同步操作 + /// + private async Task ProcessActionsSequentiallyAsync(List actions) + { + for (int i = 0; i < actions.Count; i++) + { + _cancellationToken.ThrowIfCancellationRequested(); + + var action = actions[i]; + await ExecuteSingleActionAsync(action); + + _processedItems++; + + if (i % 5 == 0 || i == actions.Count - 1) + { + ReportProgress( + $"正在处理 {GetActionTypeDescription(action.ActionType)} 操作 ({_processedItems}/{_totalItems})", + CalculateProgress(_processedItems, _totalItems) + ); + } + } + } + + /// + /// 执行单个同步操作 + /// + private async Task ExecuteSingleActionAsync(SyncAction action) + { + try + { + _cancellationToken.ThrowIfCancellationRequested(); + + // 确定源和目标路径(考虑同步方向) + string actualSource = action.Direction == SyncDirection.SourceToTarget + ? action.SourcePath + : action.TargetPath; + + string actualTarget = action.Direction == SyncDirection.SourceToTarget + ? action.TargetPath + : action.SourcePath; + + switch (action.ActionType) + { + case SyncActionType.CreateDirectory: + if (!Directory.Exists(actualTarget)) + { + Directory.CreateDirectory(actualTarget); + _statistics.DirectoriesCreated++; + } + break; + + case SyncActionType.CopyFile: + await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); + await CopyFileWithRetryAsync(actualSource, actualTarget); + _statistics.FilesCopied++; + _statistics.BytesProcessed += action.Size; + break; + + case SyncActionType.UpdateFile: + await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); + await CopyFileWithRetryAsync(actualSource, actualTarget); + _statistics.FilesUpdated++; + _statistics.BytesProcessed += action.Size; + break; + + case SyncActionType.DeleteFile: + if (File.Exists(actualTarget)) + { + if (_options.UseRecycleBin) + { + // 使用回收站删除文件 + FileOperationAPIWrapper.MoveToRecycleBin(actualTarget); + } + else + { + File.Delete(actualTarget); + } + _statistics.FilesDeleted++; + } + break; + + case SyncActionType.DeleteDirectory: + if (Directory.Exists(actualTarget)) + { + if (_options.UseRecycleBin) + { + // 使用回收站删除目录 + FileOperationAPIWrapper.MoveToRecycleBin(actualTarget); + } + else + { + Directory.Delete(actualTarget, true); + } + _statistics.DirectoriesDeleted++; + } + break; + + case SyncActionType.RenameFile: + if (File.Exists(actualSource) && !File.Exists(actualTarget)) + { + await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); + File.Move(actualSource, actualTarget); + _statistics.FilesRenamed++; + } + break; + } + + action.Status = SyncActionStatus.Completed; + } + catch (Exception ex) + { + action.Status = SyncActionStatus.Failed; + action.ErrorMessage = ex.Message; + _statistics.Errors++; + + if (_options.ContinueOnError) + { + // 记录错误但继续执行 + ReportProgress($"错误: {ex.Message}", -1); + } + else + { + // 出错时中断操作 + throw; + } + } + } + + /// + /// 检查两个文件是否需要更新 + /// + private bool NeedsUpdate(FileInfo sourceFile, FileInfo targetFile) + { + // 检查文件大小 + if (sourceFile.Length != targetFile.Length) + return true; + + // 检查修改时间 + if (_options.CompareMethod == CompareMethod.DateTime || + _options.CompareMethod == CompareMethod.DateTimeAndSize) + { + // 使用阈值比较时间,避免因时区等问题导致的微小差异 + var timeDiff = Math.Abs((sourceFile.LastWriteTimeUtc - targetFile.LastWriteTimeUtc).TotalSeconds); + if (timeDiff > _options.DateTimeThresholdSeconds) + return true; + } + + // 检查文件内容 + if (_options.CompareMethod == CompareMethod.Content || + _options.CompareMethod == CompareMethod.Hash) + { + try + { + return !FilesAreEqual(sourceFile, targetFile); + } + catch (Exception) + { + // 文件比较出错,安全起见认为需要更新 + return true; + } + } + + return false; + } + + /// + /// 比较两个文件内容是否相同 + /// + private bool FilesAreEqual(FileInfo sourceFile, FileInfo targetFile) + { + if (_options.CompareMethod == CompareMethod.Hash) + { + return CompareFileHash(sourceFile, targetFile); + } + else // 内容比较 + { + return CompareFileContent(sourceFile, targetFile); + } + } + + /// + /// 使用哈希算法比较文件 + /// + private bool CompareFileHash(FileInfo sourceFile, FileInfo targetFile) + { + if (_options.SamplingRate < 1.0) + { + return CompareFileHashWithSampling(sourceFile, targetFile); + } + + // 全文件哈希比较 + var hashAlgorithm = GetHashAlgorithm().ToString(); + + // 计算源文件哈希 + byte[] sourceHash; + using (var stream = sourceFile.OpenRead()) + { + sourceHash = HashHelper.ComputeHash(stream, hashAlgorithm); + } + + // 计算目标文件哈希 + byte[] targetHash; + using (var stream = targetFile.OpenRead()) + { + targetHash = HashHelper.ComputeHash(stream, hashAlgorithm); + } + + // 比较哈希值 + return CompareHash(sourceHash, targetHash); + } + + /// + /// 使用抽样哈希比较文件 + /// + private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFile) + { + const int headerSize = 8192; // 头部始终比较的大小 + const int randomSampleSize = 8192; // 随机抽样的块大小 + + // 如果文件较小,直接全文比较 + if (sourceFile.Length < headerSize * 2) + return CompareFileHash(sourceFile, targetFile); + + var hashAlgorithm = GetHashAlgorithm().ToString(); + + using var sourceStream = sourceFile.OpenRead(); + using var targetStream = targetFile.OpenRead(); + + // 比较文件头部 + byte[] sourceHeader = new byte[headerSize]; + byte[] targetHeader = new byte[headerSize]; + + sourceStream.Read(sourceHeader, 0, headerSize); + targetStream.Read(targetHeader, 0, headerSize); + + if (!CompareBytes(sourceHeader, targetHeader)) + return false; + + // 比较文件尾部 + byte[] sourceFooter = new byte[headerSize]; + byte[] targetFooter = new byte[headerSize]; + + sourceStream.Seek(-headerSize, SeekOrigin.End); + targetStream.Seek(-headerSize, SeekOrigin.End); + + sourceStream.Read(sourceFooter, 0, headerSize); + targetStream.Read(targetFooter, 0, headerSize); + + if (!CompareBytes(sourceFooter, targetFooter)) + return false; + + // 生成随机抽样点 + Random random = new Random(sourceFile.FullName.GetHashCode()); + int samplesCount = (int)Math.Max(1, (sourceFile.Length - headerSize * 2) * _options.SamplingRate / randomSampleSize); + long range = sourceFile.Length - headerSize * 2 - randomSampleSize; + + for (int i = 0; i < samplesCount; i++) + { + // 生成随机位置(避开头尾已比较的部分) + long position = headerSize + (long)(random.NextDouble() * range); + + byte[] sourceSample = new byte[randomSampleSize]; + byte[] targetSample = new byte[randomSampleSize]; + + sourceStream.Seek(position, SeekOrigin.Begin); + targetStream.Seek(position, SeekOrigin.Begin); + + sourceStream.Read(sourceSample, 0, randomSampleSize); + targetStream.Read(targetSample, 0, randomSampleSize); + + if (!CompareBytes(sourceSample, targetSample)) + return false; + } + + return true; + } + + /// + /// 比较文件内容 + /// + private bool CompareFileContent(FileInfo sourceFile, FileInfo targetFile) + { + const int bufferSize = 4096; + + using var sourceStream = sourceFile.OpenRead(); + using var targetStream = targetFile.OpenRead(); + + byte[] sourceBuffer = new byte[bufferSize]; + byte[] targetBuffer = new byte[bufferSize]; + + while (true) + { + int sourceBytesRead = sourceStream.Read(sourceBuffer, 0, bufferSize); + int targetBytesRead = targetStream.Read(targetBuffer, 0, bufferSize); + + if (sourceBytesRead != targetBytesRead) + return false; + + if (sourceBytesRead == 0) + break; + + for (int i = 0; i < sourceBytesRead; i++) + { + if (sourceBuffer[i] != targetBuffer[i]) + return false; + } + } + + return true; + } + + /// + /// 扫描目录并获取文件和子目录列表 + /// + private async Task<(Dictionary Files, HashSet Directories)> ScanDirectoryAsync(string path, string description) + { + ReportProgress($"正在扫描{description}...", -1); + + try + { + // 尝试使用 FileUltraScanner(高性能扫描器) + var ultraScanResult = await ScanWithFileUltraScannerAsync(path); + return ultraScanResult; + } + catch (Exception ex) + { + ReportProgress($"高性能扫描器失败,切换到备用扫描器: {ex.Message}", -1); + + // 退回到 FileFastScanner(备用扫描器) + var fastScanResult = await ScanWithFileFastScannerAsync(path); + return fastScanResult; + } + } + + /// + /// 使用 FileUltraScanner 扫描目录 + /// + private async Task<(Dictionary, HashSet)> ScanWithFileUltraScannerAsync(string path) + { + FileUltraScanner scanner = new FileUltraScanner( + path, + _options.MaxParallelOperations, + 8192, + _options.FollowSymlinks, + _options.IgnorePatterns?.ToList() + ); + + var result = await scanner.ScanAsync( + reportProgress: false, + cancellationToken: _cancellationToken + ); + + var files = result.Files + .ToDictionary( + f => f, + f => new FileInfo(f), + StringComparer.OrdinalIgnoreCase + ); + + var directories = new HashSet(result.Directories, StringComparer.OrdinalIgnoreCase); + + return (files, directories); + } + + /// + /// 使用 FileFastScanner 扫描目录 + /// + private async Task<(Dictionary, HashSet)> ScanWithFileFastScannerAsync(string path) + { + var scanner = new FileFastScanner(); + var result = scanner.ScanAsync( + path, + "*", + _options.IgnorePatterns, + _options.MaxParallelOperations, + reportProgress: false, + cancellationToken: _cancellationToken + ); + + var files = result.Files + .ToDictionary( + f => f, + f => new FileInfo(f), + StringComparer.OrdinalIgnoreCase + ); + + var directories = new HashSet(result.Directories, StringComparer.OrdinalIgnoreCase); + + return (files, directories); + } + + /// + /// 解析冲突 + /// + private ConflictResolution ResolveConflict(FileInfo sourceFile, FileInfo targetFile) + { + switch (_options.ConflictResolution) + { + case ConflictResolution.SourceWins: + return ConflictResolution.SourceWins; + + case ConflictResolution.TargetWins: + return ConflictResolution.TargetWins; + + case ConflictResolution.KeepBoth: + return ConflictResolution.KeepBoth; + + case ConflictResolution.Skip: + return ConflictResolution.Skip; + + case ConflictResolution.Newer: + return sourceFile.LastWriteTimeUtc > targetFile.LastWriteTimeUtc + ? ConflictResolution.SourceWins + : ConflictResolution.TargetWins; + + case ConflictResolution.Older: + return sourceFile.LastWriteTimeUtc < targetFile.LastWriteTimeUtc + ? ConflictResolution.SourceWins + : ConflictResolution.TargetWins; + + case ConflictResolution.Larger: + return sourceFile.Length > targetFile.Length + ? ConflictResolution.SourceWins + : ConflictResolution.TargetWins; + + default: + return ConflictResolution.Skip; + } + } + + /// + /// 获取用于解决冲突的新文件名 + /// + private string GetConflictFileName(string originalPath) + { + string dir = Path.GetDirectoryName(originalPath); + string fileName = Path.GetFileNameWithoutExtension(originalPath); + string ext = Path.GetExtension(originalPath); + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + + return Path.Combine(dir, $"{fileName} ({timestamp}){ext}"); + } + + /// + /// 获取操作优先级(用于排序) + /// + private int GetActionPriority(SyncActionType actionType) + { + switch (actionType) + { + case SyncActionType.CreateDirectory: + return 1; + + case SyncActionType.CopyFile: + case SyncActionType.UpdateFile: + return 2; + + case SyncActionType.RenameFile: + return 3; + + case SyncActionType.DeleteFile: + return 4; + + case SyncActionType.DeleteDirectory: + return 5; + + default: + return 99; + } + } + + /// + /// 获取操作类型描述 + /// + private string GetActionTypeDescription(SyncActionType actionType) + { + switch (actionType) + { + case SyncActionType.CreateDirectory: + return "创建目录"; + + case SyncActionType.CopyFile: + return "复制文件"; + + case SyncActionType.UpdateFile: + return "更新文件"; + + case SyncActionType.DeleteFile: + return "删除文件"; + + case SyncActionType.DeleteDirectory: + return "删除目录"; + + case SyncActionType.RenameFile: + return "重命名文件"; + + default: + return "未知操作"; + } + } + + /// + /// 获取文件的相对路径 + /// + private string GetRelativePath(string fullPath, string basePath) + { + // 确保路径以分隔符结尾以正确处理相对路径 + if (!basePath.EndsWith(Path.DirectorySeparatorChar.ToString())) + basePath += Path.DirectorySeparatorChar; + + if (fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) + { + var relativePath = fullPath.Substring(basePath.Length); + return relativePath; + } + + return fullPath; + } + + /// + /// 确保目录存在 + /// + private Task EnsureDirectoryExistsAsync(string directory) + { + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + return Task.CompletedTask; + } + + /// + /// 创建哈希算法实例 + /// + private EHashType? GetHashAlgorithm() + { + return _options.HashAlgorithm; + } + + /// + /// 比较两个哈希值是否相同 + /// + private bool CompareHash(byte[] hash1, byte[] hash2) + { + if (hash1.Length != hash2.Length) + return false; + + for (int i = 0; i < hash1.Length; i++) + { + if (hash1[i] != hash2[i]) + return false; + } + + return true; + } + + /// + /// 比较两个字节数组是否相同 + /// + private bool CompareBytes(byte[] buffer1, byte[] buffer2) + { + if (buffer1.Length != buffer2.Length) + return false; + + for (int i = 0; i < buffer1.Length; i++) + { + if (buffer1[i] != buffer2[i]) + return false; + } + + return true; + } + + /// + /// 带重试的文件复制操作 + /// + private async Task CopyFileWithRetryAsync(string sourcePath, string targetPath) + { + int maxRetries = _options.MaxRetries; + int currentRetry = 0; + bool success = false; + + while (!success && currentRetry <= maxRetries) + { + try + { + if (currentRetry > 0) + { + // 添加延迟重试 + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, currentRetry - 1)), _cancellationToken); + ReportProgress($"重试复制文件 {Path.GetFileName(sourcePath)} (尝试 {currentRetry}/{maxRetries})", -1); + } + + if (_options.PreserveFileTime) + { + // 保留原始时间戳 + using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan)) + using (FileStream targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan)) + { + await sourceStream.CopyToAsync(targetStream, 81920, _cancellationToken); + } + + // 设置目标文件的时间戳与源文件相同 + File.SetCreationTimeUtc(targetPath, File.GetCreationTimeUtc(sourcePath)); + File.SetLastWriteTimeUtc(targetPath, File.GetLastWriteTimeUtc(sourcePath)); + File.SetLastAccessTimeUtc(targetPath, File.GetLastAccessTimeUtc(sourcePath)); + } + else + { + // 简单复制,不保留时间戳 + File.Copy(sourcePath, targetPath, true); + } + + success = true; + } + catch (IOException) when (currentRetry < maxRetries) + { + currentRetry++; + } + catch (Exception) + { + // 其他异常直接抛出 + throw; + } + } + + if (!success) + { + throw new IOException($"复制文件 {sourcePath} 到 {targetPath} 失败,已重试 {maxRetries} 次"); + } + } + + /// + /// 计算进度百分比 + /// + private int CalculateProgress(int current, int total) + { + if (total <= 0) return 0; + return (int)((current / (double)total) * 100); + } + + /// + /// 报告进度 + /// + private void ReportProgress(string message, int progressPercentage) + { + if (_progress == null) return; + + // 控制进度更新频率 + var now = DateTime.Now; + if ((now - _lastProgressUpdate).TotalMilliseconds < 100 && progressPercentage >= 0 && progressPercentage < 100) + return; + + _lastProgressUpdate = now; + + double bytesPerSecond = _stopwatch.Elapsed.TotalSeconds > 0 + ? _statistics.BytesProcessed / _stopwatch.Elapsed.TotalSeconds + : 0; + + double itemsPerSecond = _stopwatch.Elapsed.TotalSeconds > 0 + ? _processedItems / _stopwatch.Elapsed.TotalSeconds + : 0; + + _progress.Report(new SyncProgress + { + Message = message, + ProgressPercentage = progressPercentage, + ElapsedTime = _stopwatch.Elapsed, + ProcessedItems = _processedItems, + TotalItems = _totalItems, + BytesProcessed = _statistics.BytesProcessed, + BytesToProcess = _statistics.BytesToProcess, + BytesPerSecond = bytesPerSecond, + ItemsPerSecond = itemsPerSecond, + Statistics = _statistics + }); + } + + /// + /// 验证同步路径 + /// + private void ValidatePaths() + { + // 验证源路径 + if (string.IsNullOrEmpty(_options.SourcePath)) + throw new ArgumentException("源路径不能为空"); + + if (!Directory.Exists(_options.SourcePath)) + throw new DirectoryNotFoundException($"源目录不存在: {_options.SourcePath}"); + + // 验证目标路径 + if (string.IsNullOrEmpty(_options.TargetPath)) + throw new ArgumentException("目标路径不能为空"); + + // 目标路径可能不存在,需要创建 + if (!Directory.Exists(_options.TargetPath) && !_options.PreviewOnly) + { + Directory.CreateDirectory(_options.TargetPath); + } + + // 检查路径不能互相包含 + string normalizedSource = Path.GetFullPath(_options.SourcePath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + string normalizedTarget = Path.GetFullPath(_options.TargetPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + + if (normalizedSource.StartsWith(normalizedTarget, StringComparison.OrdinalIgnoreCase) || + normalizedTarget.StartsWith(normalizedSource, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("源路径和目标路径不能互相包含"); + } + } + + /// + /// 判断操作是否为文件操作 + /// + private bool IsFileOperation(SyncActionType actionType) + { + return actionType == SyncActionType.CopyFile || + actionType == SyncActionType.UpdateFile || + actionType == SyncActionType.RenameFile || + actionType == SyncActionType.DeleteFile; + } + + /// + /// 从JSON配置文件加载同步选项 + /// + public static SyncOptions LoadFromJsonFile(string configFilePath) + { + if (!File.Exists(configFilePath)) + throw new FileNotFoundException($"配置文件不存在: {configFilePath}"); + + string json = File.ReadAllText(configFilePath); + var options = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }); + + return options; + } + + /// + /// 保存同步选项到JSON配置文件 + /// + public static void SaveToJsonFile(SyncOptions options, string configFilePath) + { + string json = JsonSerializer.Serialize(options, new JsonSerializerOptions + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() } + }); + + File.WriteAllText(configFilePath, json); + } + } + + /// + /// 同步选项类 + /// + public class SyncOptions + { + /// + /// 源目录路径 + /// + public string SourcePath { get; set; } + + /// + /// 目标目录路径 + /// + public string TargetPath { get; set; } + + /// + /// 同步模式 + /// + public SyncMode SyncMode { get; set; } = SyncMode.OneWay; + + /// + /// 文件比较方法 + /// + public CompareMethod CompareMethod { get; set; } = CompareMethod.DateTimeAndSize; + + /// + /// 哈希算法类型 + /// + public EHashType? HashAlgorithm { get; set; } + + /// + /// 哈希抽样率(0.0-1.0) + /// + public double SamplingRate { get; set; } = 0.1; + + /// + /// 最大并行操作数 + /// + public int MaxParallelOperations { get; set; } = Math.Max(1, Environment.ProcessorCount); + + /// + /// 是否启用并行文件操作 + /// + public bool EnableParallelFileOperations { get; set; } = true; + + /// + /// 日期时间比较阈值(秒) + /// + public int DateTimeThresholdSeconds { get; set; } = 2; + + /// + /// 是否保留原始文件时间 + /// + public bool PreserveFileTime { get; set; } = true; + + /// + /// 是否使用回收站删除文件 + /// + public bool UseRecycleBin { get; set; } = true; + + /// + /// 冲突解决策略 + /// + public ConflictResolution ConflictResolution { get; set; } = ConflictResolution.Newer; + + /// + /// 是否跟踪符号链接 + /// + public bool FollowSymlinks { get; set; } = false; + + /// + /// 是否仅预览,不执行实际操作 + /// + public bool PreviewOnly { get; set; } = false; + + /// + /// 是否在出错时继续 + /// + public bool ContinueOnError { get; set; } = true; + + /// + /// 最大重试次数 + /// + public int MaxRetries { get; set; } = 3; + + /// + /// 是否启用详细输出 + /// + public bool Verbose { get; set; } = false; + + /// + /// 要忽略的文件/目录模式 + /// + public IEnumerable IgnorePatterns { get; set; } = new List + { + "**/System Volume Information/**", + "**/$RECYCLE.BIN/**", + "**/Thumbs.db", + "**/*.tmp", + "**/*.temp", + "**/*.bak" + }; + } + + /// + /// 同步模式 + /// + public enum SyncMode + { + /// + /// 单向同步:源 -> 目标 + /// + OneWay, + + /// + /// 镜像同步:源 -> 目标,删除目标中多余内容 + /// + Mirror, + + /// + /// 双向同步:源 <-> 目标 + /// + TwoWay + } + + /// + /// 文件比较方法 + /// + public enum CompareMethod + { + /// + /// 仅比较文件大小 + /// + Size, + + /// + /// 仅比较修改时间 + /// + DateTime, + + /// + /// 比较修改时间和文件大小 + /// + DateTimeAndSize, + + /// + /// 比较文件内容(读取文件) + /// + Content, + + /// + /// 使用哈希算法比较 + /// + Hash + } + + /// + /// 哈希算法类型 + /// + public enum HashAlgorithmType + { + MD5, + SHA1, + SHA256, + SHA384, + SHA512 + } + + /// + /// 冲突解决策略 + /// + public enum ConflictResolution + { + /// + /// 源文件优先 + /// + SourceWins, + + /// + /// 目标文件优先 + /// + TargetWins, + + /// + /// 保留两者 + /// + KeepBoth, + + /// + /// 跳过冲突文件 + /// + Skip, + + /// + /// 更新的文件优先 + /// + Newer, + + /// + /// 较早的文件优先 + /// + Older, + + /// + /// 较大的文件优先 + /// + Larger + } + + /// + /// 同步操作类型 + /// + public enum SyncActionType + { + CreateDirectory, + CopyFile, + UpdateFile, + DeleteFile, + DeleteDirectory, + RenameFile + } + + /// + /// 同步方向 + /// + public enum SyncDirection + { + SourceToTarget, + TargetToSource + } + + /// + /// 同步操作状态 + /// + public enum SyncActionStatus + { + Pending, + Running, + Completed, + Failed + } + + /// + /// 同步状态 + /// + public enum SyncStatus + { + NotStarted, + Started, + Running, + Completed, + Failed, + Canceled + } + + /// + /// 同步操作 + /// + public class SyncAction + { + /// + /// 操作类型 + /// + public SyncActionType ActionType { get; set; } + + /// + /// 源路径 + /// + public string SourcePath { get; set; } + + /// + /// 目标路径 + /// + public string TargetPath { get; set; } + + /// + /// 相对路径 + /// + public string RelativePath { get; set; } + + /// + /// 文件大小 + /// + public long Size { get; set; } + + /// + /// 同步方向 + /// + public SyncDirection Direction { get; set; } = SyncDirection.SourceToTarget; + + /// + /// 操作状态 + /// + public SyncActionStatus Status { get; set; } = SyncActionStatus.Pending; + + /// + /// 错误消息 + /// + public string ErrorMessage { get; set; } + + /// + /// 冲突解决策略 + /// + public ConflictResolution? ConflictResolution { get; set; } + } + + /// + /// 同步统计信息 + /// + public class SyncStatistics + { + /// + /// 待复制文件数 + /// + public int FilesToCopy { get; set; } + + /// + /// 待更新文件数 + /// + public int FilesToUpdate { get; set; } + + /// + /// 待删除文件数 + /// + public int FilesToDelete { get; set; } + + /// + /// 已创建目录数 + /// + public int DirectoriesCreated { get; set; } + + /// + /// 待删除目录数 + /// + public int DirectoriesToDelete { get; set; } + + /// + /// 已复制文件数 + /// + public int FilesCopied { get; set; } + + /// + /// 已更新文件数 + /// + public int FilesUpdated { get; set; } + + /// + /// 已删除文件数 + /// + public int FilesDeleted { get; set; } + + /// + /// 已重命名文件数 + /// + public int FilesRenamed { get; set; } + + /// + /// 已删除目录数 + /// + public int DirectoriesDeleted { get; set; } + + /// + /// 已跳过文件数 + /// + public int FilesSkipped { get; set; } + + /// + /// 错误数 + /// + public int Errors { get; set; } + + /// + /// 总处理字节数 + /// + public long BytesProcessed { get; set; } + + /// + /// 待处理字节数 + /// + public long BytesToProcess { get; set; } + } + + /// + /// 同步进度信息 + /// + public class SyncProgress + { + /// + /// 进度消息 + /// + public string Message { get; set; } + + /// + /// 进度百分比(-1表示不确定) + /// + public int ProgressPercentage { get; set; } + + /// + /// 已用时间 + /// + public TimeSpan ElapsedTime { get; set; } + + /// + /// 已处理项目数 + /// + public int ProcessedItems { get; set; } + + /// + /// 总项目数 + /// + public int TotalItems { get; set; } + + /// + /// 已处理字节数 + /// + public long BytesProcessed { get; set; } + + /// + /// 待处理字节数 + /// + public long BytesToProcess { get; set; } + + /// + /// 每秒处理字节数 + /// + public double BytesPerSecond { get; set; } + + /// + /// 每秒处理项目数 + /// + public double ItemsPerSecond { get; set; } + + /// + /// 统计信息 + /// + public SyncStatistics Statistics { get; set; } + + /// + /// 获取格式化的处理速度 + /// + public string FormattedSpeed => FormatSize(BytesPerSecond) + "/s"; + + /// + /// 获取格式化的已处理大小 + /// + public string FormattedBytesProcessed => FormatSize(BytesProcessed); + + /// + /// 获取格式化的总大小 + /// + public string FormattedBytesToProcess => FormatSize(BytesToProcess); + + /// + /// 格式化大小显示 + /// + private string FormatSize(double bytes) + { + string[] suffixes = { "B", "KB", "MB", "GB", "TB", "PB" }; + int i = 0; + while (bytes >= 1024 && i < suffixes.Length - 1) + { + bytes /= 1024; + i++; + } + return $"{bytes:0.##} {suffixes[i]}"; + } + + /// + /// 估计剩余时间 + /// + public TimeSpan EstimatedTimeRemaining + { + get + { + if (BytesProcessed <= 0 || BytesToProcess <= 0 || BytesPerSecond <= 0) + return TimeSpan.Zero; + + double remainingBytes = BytesToProcess - BytesProcessed; + double remainingSeconds = remainingBytes / BytesPerSecond; + return TimeSpan.FromSeconds(remainingSeconds); + } + } + + /// + /// 获取格式化的估计剩余时间 + /// + public string FormattedTimeRemaining + { + get + { + var time = EstimatedTimeRemaining; + if (time.TotalSeconds < 1) + return "完成"; + + if (time.TotalHours >= 1) + return $"{(int)time.TotalHours}小时 {time.Minutes}分钟"; + else if (time.TotalMinutes >= 1) + return $"{time.Minutes}分钟 {time.Seconds}秒"; + else + return $"{time.Seconds}秒"; + } + } + } + + /// + /// 同步结果 + /// + public class SyncResult + { + /// + /// 源路径 + /// + public string SourcePath { get; set; } + + /// + /// 目标路径 + /// + public string TargetPath { get; set; } + + /// + /// 同步模式 + /// + public SyncMode Mode { get; set; } + + /// + /// 同步状态 + /// + public SyncStatus Status { get; set; } + + /// + /// 开始时间 + /// + public DateTime StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime EndTime { get; set; } + + /// + /// 耗时 + /// + public TimeSpan ElapsedTime { get; set; } + + /// + /// 错误消息 + /// + public string ErrorMessage { get; set; } + + /// + /// 同步操作列表 + /// + public List Actions { get; set; } = new List(); + + /// + /// 同步统计信息 + /// + public SyncStatistics Statistics { get; set; } + + /// + /// 是否成功完成同步 + /// + public bool IsSuccessful => Status == SyncStatus.Completed; + + /// + /// 已处理的总文件数 + /// + public int TotalFilesProcessed => Statistics?.FilesCopied + Statistics?.FilesUpdated + Statistics?.FilesDeleted + Statistics?.FilesSkipped ?? 0; + + /// + /// 已处理的总目录数 + /// + public int TotalDirectoriesProcessed => Statistics?.DirectoriesCreated + Statistics?.DirectoriesDeleted ?? 0; + + /// + /// 同步总项目数 + /// + public int TotalItemsProcessed => TotalFilesProcessed + TotalDirectoriesProcessed; + + /// + /// 每秒处理的平均项目数 + /// + public double ItemsPerSecond => ElapsedTime.TotalSeconds > 0 + ? TotalItemsProcessed / ElapsedTime.TotalSeconds + : 0; + + /// + /// 每秒处理的平均字节数 + /// + public double BytesPerSecond => ElapsedTime.TotalSeconds > 0 && Statistics != null + ? Statistics.BytesProcessed / ElapsedTime.TotalSeconds + : 0; + + /// + /// 获取同步操作的简要摘要 + /// + /// 摘要文本 + public string GetSummary() + { + if (Status == SyncStatus.NotStarted) + return "同步尚未开始"; + + if (Status == SyncStatus.Failed) + return $"同步失败: {ErrorMessage}"; + + if (Status == SyncStatus.Canceled) + return "同步被取消"; + + if (Statistics == null) + return "没有同步统计信息"; + + var summary = new StringBuilder(); + summary.AppendLine($"同步模式: {GetSyncModeDescription(Mode)}"); + summary.AppendLine($"源路径: {SourcePath}"); + summary.AppendLine($"目标路径: {TargetPath}"); + summary.AppendLine($"状态: {GetStatusDescription(Status)}"); + summary.AppendLine($"开始时间: {StartTime:yyyy-MM-dd HH:mm:ss}"); + summary.AppendLine($"结束时间: {EndTime:yyyy-MM-dd HH:mm:ss}"); + summary.AppendLine($"总耗时: {FormatTimeSpan(ElapsedTime)}"); + summary.AppendLine($"处理速度: {FormatBytesPerSecond(BytesPerSecond)}"); + + summary.AppendLine("文件统计:"); + summary.AppendLine($" - 复制: {Statistics.FilesCopied} 个文件"); + summary.AppendLine($" - 更新: {Statistics.FilesUpdated} 个文件"); + summary.AppendLine($" - 删除: {Statistics.FilesDeleted} 个文件"); + summary.AppendLine($" - 跳过: {Statistics.FilesSkipped} 个文件"); + summary.AppendLine($" - 重命名: {Statistics.FilesRenamed} 个文件"); + + summary.AppendLine("目录统计:"); + summary.AppendLine($" - 创建: {Statistics.DirectoriesCreated} 个目录"); + summary.AppendLine($" - 删除: {Statistics.DirectoriesDeleted} 个目录"); + + summary.AppendLine($"错误数量: {Statistics.Errors}"); + + if (Statistics.Errors > 0 && !string.IsNullOrEmpty(ErrorMessage)) + { + summary.AppendLine($"最后错误: {ErrorMessage}"); + } + + summary.AppendLine($"总处理字节: {FormatBytes(Statistics.BytesProcessed)}"); + + return summary.ToString(); + } + + /// + /// 获取同步模式的描述 + /// + private string GetSyncModeDescription(SyncMode mode) + { + return mode switch + { + SyncMode.OneWay => "单向同步", + SyncMode.Mirror => "镜像同步", + SyncMode.TwoWay => "双向同步", + _ => mode.ToString() + }; + } + + /// + /// 获取同步状态的描述 + /// + private string GetStatusDescription(SyncStatus status) + { + return status switch + { + SyncStatus.NotStarted => "未开始", + SyncStatus.Started => "已开始", + SyncStatus.Running => "运行中", + SyncStatus.Completed => "已完成", + SyncStatus.Failed => "失败", + SyncStatus.Canceled => "已取消", + _ => status.ToString() + }; + } + + /// + /// 格式化时间间隔 + /// + private string FormatTimeSpan(TimeSpan timeSpan) + { + if (timeSpan.TotalHours >= 1) + { + return $"{(int)timeSpan.TotalHours}小时 {timeSpan.Minutes}分钟 {timeSpan.Seconds}秒"; + } + else if (timeSpan.TotalMinutes >= 1) + { + return $"{timeSpan.Minutes}分钟 {timeSpan.Seconds}秒"; + } + else + { + return $"{timeSpan.Seconds}.{timeSpan.Milliseconds}秒"; + } + } + + /// + /// 格式化字节大小 + /// + private string FormatBytes(long bytes) + { + string[] suffixes = { "B", "KB", "MB", "GB", "TB", "PB" }; + int i = 0; + double size = bytes; + while (size >= 1024 && i < suffixes.Length - 1) + { + size /= 1024; + i++; + } + return $"{size:0.##} {suffixes[i]}"; + } + + /// + /// 格式化每秒字节数 + /// + private string FormatBytesPerSecond(double bytesPerSecond) + { + return $"{FormatBytes((long)bytesPerSecond)}/秒"; + } + + /// + /// 生成完整的同步报告 + /// + /// 详细报告文本 + public string GenerateReport() + { + var report = new StringBuilder(); + report.AppendLine("=================================="); + report.AppendLine(" 同步操作报告 "); + report.AppendLine("=================================="); + report.AppendLine(); + report.Append(GetSummary()); + report.AppendLine(); + + if (Actions.Count > 0) + { + report.AppendLine("=================================="); + report.AppendLine(" 详细操作记录 "); + report.AppendLine("=================================="); + + // 按操作类型分组显示 + var actionsByType = Actions.GroupBy(a => a.ActionType); + foreach (var group in actionsByType.OrderBy(g => (int)g.Key)) + { + report.AppendLine(); + report.AppendLine($"【{GetActionTypeDescription(group.Key)}】操作记录 - 共 {group.Count()} 项"); + report.AppendLine("----------------------------------"); + + int count = 0; + foreach (var action in group.OrderBy(a => a.RelativePath).Take(100)) // 限制每类显示最多100条记录 + { + count++; + var status = action.Status == SyncActionStatus.Completed ? "✓" : + action.Status == SyncActionStatus.Failed ? "✗" : "?"; + + var path = action.RelativePath?.Length > 80 + ? "..." + action.RelativePath.Substring(action.RelativePath.Length - 77) + : action.RelativePath; + + report.AppendLine($"{status} {path ?? "-"}"); + + if (action.Status == SyncActionStatus.Failed && !string.IsNullOrEmpty(action.ErrorMessage)) + { + report.AppendLine($" 错误: {action.ErrorMessage}"); + } + } + + if (group.Count() > 100) + { + report.AppendLine($"... 还有 {group.Count() - 100} 项未显示 ..."); + } + } + } + + report.AppendLine(); + report.AppendLine("=================================="); + report.AppendLine(" 报告生成完毕 "); + report.AppendLine("=================================="); + + return report.ToString(); + } + + /// + /// 获取操作类型的描述 + /// + private string GetActionTypeDescription(SyncActionType actionType) + { + return actionType switch + { + SyncActionType.CreateDirectory => "创建目录", + SyncActionType.CopyFile => "复制文件", + SyncActionType.UpdateFile => "更新文件", + SyncActionType.DeleteFile => "删除文件", + SyncActionType.DeleteDirectory => "删除目录", + SyncActionType.RenameFile => "重命名文件", + _ => actionType.ToString() + }; + } + + /// + /// 创建一个基于项目同步API返回的标准Result对象 + /// + /// 包含同步结果的Result对象 + public Result ToApiResult() + { + var dto = new SyncResultDto + { + SourcePath = this.SourcePath, + TargetPath = this.TargetPath, + SyncMode = this.Mode.ToString(), + Status = this.Status.ToString(), + StartTime = this.StartTime, + EndTime = this.EndTime, + ElapsedTimeSeconds = this.ElapsedTime.TotalSeconds, + IsSuccessful = this.IsSuccessful, + ErrorMessage = this.ErrorMessage, + TotalFilesProcessed = this.TotalFilesProcessed, + TotalDirectoriesProcessed = this.TotalDirectoriesProcessed, + BytesProcessed = this.Statistics?.BytesProcessed ?? 0, + Summary = this.GetSummary() + }; + + if (this.IsSuccessful) + { + return Result.Ok(dto, "同步操作已成功完成"); + } + else if (this.Status == SyncStatus.Canceled) + { + return Result.Ok(dto, "同步操作被用户取消"); + } + else + { + return Result.Fail(dto, this.ErrorMessage ?? "同步操作失败"); + } + } + + /// + /// 创建一个失败的同步结果 + /// + /// 源路径 + /// 目标路径 + /// 错误消息 + /// 失败的同步结果 + public static SyncResult Failed(string sourcePath, string targetPath, string errorMessage) + { + return new SyncResult + { + SourcePath = sourcePath, + TargetPath = targetPath, + Status = SyncStatus.Failed, + ErrorMessage = errorMessage, + StartTime = DateTime.Now, + EndTime = DateTime.Now, + ElapsedTime = TimeSpan.Zero, + Statistics = new SyncStatistics() + }; + } + + /// + /// 创建一个取消的同步结果 + /// + /// 源路径 + /// 目标路径 + /// 已耗时间 + /// 已有的统计信息 + /// 取消的同步结果 + public static SyncResult Canceled(string sourcePath, string targetPath, TimeSpan elapsedTime, SyncStatistics statistics) + { + return new SyncResult + { + SourcePath = sourcePath, + TargetPath = targetPath, + Status = SyncStatus.Canceled, + StartTime = DateTime.Now - elapsedTime, + EndTime = DateTime.Now, + ElapsedTime = elapsedTime, + Statistics = statistics ?? new SyncStatistics(), + ErrorMessage = "同步操作被用户取消" + }; + } + } + + /// + /// 同步结果数据传输对象(用于API返回) + /// + public class SyncResultDto + { + /// + /// 源路径 + /// + public string SourcePath { get; set; } + + /// + /// 目标路径 + /// + public string TargetPath { get; set; } + + /// + /// 同步模式 + /// + public string SyncMode { get; set; } + + /// + /// 同步状态 + /// + public string Status { get; set; } + + /// + /// 开始时间 + /// + public DateTime StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime EndTime { get; set; } + + /// + /// 耗时(秒) + /// + public double ElapsedTimeSeconds { get; set; } + + /// + /// 是否成功 + /// + public bool IsSuccessful { get; set; } + + /// + /// 错误消息 + /// + public string ErrorMessage { get; set; } + + /// + /// 处理的总文件数 + /// + public int TotalFilesProcessed { get; set; } + + /// + /// 处理的总目录数 + /// + public int TotalDirectoriesProcessed { get; set; } + + /// + /// 处理的总字节数 + /// + public long BytesProcessed { get; set; } + + /// + /// 同步操作摘要 + /// + public string Summary { get; set; } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Security/Models/EHashType.cs b/src/MDriveSync.Security/Models/EHashType.cs new file mode 100644 index 0000000..ffbbbfa --- /dev/null +++ b/src/MDriveSync.Security/Models/EHashType.cs @@ -0,0 +1,48 @@ +namespace MDriveSync.Security.Models +{ + /// + /// MD5、SHA1、SHA256、SHA3/SHA384、BLAKE3、XXH3、XXH128 + /// + public enum EHashType + { + /// + /// MD5 + /// + MD5, + + /// + /// SHA1 + /// + SHA1, + + /// + /// SHA256 + /// + SHA256, + + /// + /// SHA3/SHA384 + /// + SHA3, + + /// + /// SHA384 + /// + SHA384, + + /// + /// BLAKE3 + /// + BLAKE3, + + /// + /// XXH3 + /// + XXH3, + + /// + /// XXH128 + /// + XXH128, + } +} \ No newline at end of file diff --git a/src/MDriveSync.Test/FileSyncHelperTests.cs b/src/MDriveSync.Test/FileSyncHelperTests.cs new file mode 100644 index 0000000..bf8f12d --- /dev/null +++ b/src/MDriveSync.Test/FileSyncHelperTests.cs @@ -0,0 +1,808 @@ +using MDriveSync.Core.Services; +using MDriveSync.Security.Models; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using Xunit.Abstractions; + +namespace MDriveSync.Test +{ + /// + /// FileSyncHelper Ԫ + /// + public class FileSyncHelperTests : BaseTests + { + private readonly ITestOutputHelper _output; + private readonly string _testSourceDir; + private readonly string _testTargetDir; + + public FileSyncHelperTests(ITestOutputHelper output) + { + _output = output; + + // Ŀ¼ + _testSourceDir = Path.Combine(Path.GetTempPath(), $"MDriveSync_Test_Source_{Guid.NewGuid()}"); + _testTargetDir = Path.Combine(Path.GetTempPath(), $"MDriveSync_Test_Target_{Guid.NewGuid()}"); + + Directory.CreateDirectory(_testSourceDir); + Directory.CreateDirectory(_testTargetDir); + } + + public override void Dispose() + { + // Ŀ¼ + try + { + if (Directory.Exists(_testSourceDir)) + Directory.Delete(_testSourceDir, true); + + if (Directory.Exists(_testTargetDir)) + Directory.Delete(_testTargetDir, true); + } + catch (Exception ex) + { + _output.WriteLine($"Ŀ¼ʱ: {ex.Message}"); + } + + base.Dispose(); + } + + #region + + /// + /// ļṹ + /// + private void CreateTestFileStructure(string rootDir, Dictionary files, string[] directories) + { + // Ŀ¼ + foreach (var dir in directories) + { + string fullPath = Path.Combine(rootDir, dir); + Directory.CreateDirectory(fullPath); + } + + // ļ + foreach (var file in files) + { + string fullPath = Path.Combine(rootDir, file.Key); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + File.WriteAllBytes(fullPath, file.Value); + } + } + + /// + /// + /// + private byte[] GenerateRandomData(int size, int seed = 0) + { + var random = seed == 0 ? new Random() : new Random(seed); + var data = new byte[size]; + random.NextBytes(data); + return data; + } + + /// + /// ļ޸ʱ + /// + private void SetFileLastWriteTime(string path, DateTime dateTime) + { + File.SetLastWriteTime(path, dateTime); + } + + /// + /// ֤Ŀ¼ṹƥ + /// + private void VerifyDirectoryMatch(string sourceDir, string targetDir, bool expectMatch = true) + { + var sourceFiles = Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories) + .Select(f => f.Substring(sourceDir.Length).TrimStart(Path.DirectorySeparatorChar)) + .OrderBy(f => f) + .ToList(); + + var targetFiles = Directory.GetFiles(targetDir, "*", SearchOption.AllDirectories) + .Select(f => f.Substring(targetDir.Length).TrimStart(Path.DirectorySeparatorChar)) + .OrderBy(f => f) + .ToList(); + + if (expectMatch) + { + Assert.Equal(sourceFiles.Count, targetFiles.Count); + + for (int i = 0; i < sourceFiles.Count; i++) + { + Assert.Equal(sourceFiles[i], targetFiles[i]); + + // Ƚļ + var sourceContent = File.ReadAllBytes(Path.Combine(sourceDir, sourceFiles[i])); + var targetContent = File.ReadAllBytes(Path.Combine(targetDir, targetFiles[i])); + Assert.Equal(sourceContent, targetContent); + } + } + else if (sourceFiles.Count == targetFiles.Count) + { + // ӦвͬļǷһͬ + bool hasDifference = false; + + for (int i = 0; i < sourceFiles.Count; i++) + { + if (sourceFiles[i] != targetFiles[i]) + { + hasDifference = true; + break; + } + + var sourceContent = File.ReadAllBytes(Path.Combine(sourceDir, sourceFiles[i])); + var targetContent = File.ReadAllBytes(Path.Combine(targetDir, targetFiles[i])); + + if (!sourceContent.SequenceEqual(targetContent)) + { + hasDifference = true; + break; + } + } + + Assert.True(hasDifference, "ԴĿ¼ĿĿ¼в죬ʵȫƥ"); + } + } + + /// + /// ȱ + /// + private IProgress CreateProgressReporter() + { + return new Progress(progress => + { + _output.WriteLine($": {progress.ProgressPercentage}%, Ϣ: {progress.Message}"); + _output.WriteLine($": {progress.ProcessedItems}/{progress.TotalItems}, ٶ: {progress.FormattedSpeed}"); + + if (progress.Statistics != null) + { + _output.WriteLine($": {progress.Statistics.FilesCopied}/{progress.Statistics.FilesToCopy}, " + + $": {progress.Statistics.FilesUpdated}/{progress.Statistics.FilesToUpdate}, " + + $"ɾ: {progress.Statistics.FilesDeleted}/{progress.Statistics.FilesToDelete}"); + } + }); + } + + #endregion + + #region Ԫ + + [Fact] + public async Task OneWaySync_EmptyTarget_ShouldCopyAllFiles() + { + // ׼ + var sourceFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("This is file 1"), + ["file2.txt"] = Encoding.UTF8.GetBytes("This is file 2"), + ["subdir/file3.txt"] = Encoding.UTF8.GetBytes("This is file 3"), + }; + + var sourceDirs = new[] { "subdir", "emptydir" }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, sourceDirs); + + // ͬѡ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Content, + MaxParallelOperations = 2 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + Assert.Equal(3, result.Statistics.FilesCopied); // Ӧø3ļ + Assert.True(result.Statistics.DirectoriesCreated >= 2); // Ӧô2Ŀ¼ + + // ֤Ŀ¼ṹƥ + VerifyDirectoryMatch(_testSourceDir, _testTargetDir); + } + + [Fact] + public async Task OneWaySync_ExistingTarget_ShouldUpdateChangedFiles() + { + // ׼ԴĿ¼ + var sourceFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("This is file 1 - updated"), + ["file2.txt"] = Encoding.UTF8.GetBytes("This is file 2"), + ["subdir/file3.txt"] = Encoding.UTF8.GetBytes("This is file 3"), + ["newfile.txt"] = Encoding.UTF8.GetBytes("This is a new file"), + }; + + var sourceDirs = new[] { "subdir", "newdir" }; + + // ׼ĿĿ¼ݣݲͬ + var targetFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("This is file 1"), + ["file2.txt"] = Encoding.UTF8.GetBytes("This is file 2"), + ["subdir/file3.txt"] = Encoding.UTF8.GetBytes("This is file 3"), + ["oldfile.txt"] = Encoding.UTF8.GetBytes("This is an old file"), + }; + + var targetDirs = new[] { "subdir", "olddir" }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, sourceDirs); + CreateTestFileStructure(_testTargetDir, targetFiles, targetDirs); + + // ͬѡ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Content, + MaxParallelOperations = 2 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + Assert.Equal(1, result.Statistics.FilesUpdated); // file1.txt Ӧñ + Assert.Equal(1, result.Statistics.FilesCopied); // newfile.txt Ӧñ + Assert.True(result.Statistics.DirectoriesCreated >= 1); // newdir Ӧñ + + // ֤ oldfile.txt olddir ȻڣͬɾĿдڵԴвڵļ + Assert.True(File.Exists(Path.Combine(_testTargetDir, "oldfile.txt"))); + Assert.True(Directory.Exists(Path.Combine(_testTargetDir, "olddir"))); + + // ֤ file1.txt Ѹ + var file1Content = File.ReadAllText(Path.Combine(_testTargetDir, "file1.txt")); + Assert.Equal("This is file 1 - updated", file1Content); + + // ֤ newfile.txt Ѹ + var newfileContent = File.ReadAllText(Path.Combine(_testTargetDir, "newfile.txt")); + Assert.Equal("This is a new file", newfileContent); + } + + [Fact] + public async Task MirrorSync_ShouldDeleteFilesNotInSource() + { + // ׼ԴĿ¼ + var sourceFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("This is file 1"), + ["file2.txt"] = Encoding.UTF8.GetBytes("This is file 2"), + ["subdir/file3.txt"] = Encoding.UTF8.GetBytes("This is file 3"), + }; + + var sourceDirs = new[] { "subdir" }; + + // ׼ĿĿ¼ݣļ + var targetFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("This is file 1"), + ["file2.txt"] = Encoding.UTF8.GetBytes("This is file 2"), + ["subdir/file3.txt"] = Encoding.UTF8.GetBytes("This is file 3"), + ["oldfile.txt"] = Encoding.UTF8.GetBytes("This is an old file"), + ["olddir/oldsubfile.txt"] = Encoding.UTF8.GetBytes("This is an old sub file"), + }; + + var targetDirs = new[] { "subdir", "olddir" }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, sourceDirs); + CreateTestFileStructure(_testTargetDir, targetFiles, targetDirs); + + // ͬѡ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.Mirror, + CompareMethod = CompareMethod.Content, + MaxParallelOperations = 2, + // ʹͨɾʹûվԣ + UseRecycleBin = false + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + Assert.Equal(0, result.Statistics.FilesUpdated); // ûļҪ + Assert.Equal(0, result.Statistics.FilesCopied); // ûļҪ + Assert.Equal(2, result.Statistics.FilesDeleted); // 2ļӦñɾ + Assert.Equal(1, result.Statistics.DirectoriesDeleted); // 1Ŀ¼Ӧñɾ + + // ֤ oldfile.txt olddir ٴ + Assert.False(File.Exists(Path.Combine(_testTargetDir, "oldfile.txt"))); + Assert.False(Directory.Exists(Path.Combine(_testTargetDir, "olddir"))); + + // ֤Ŀ¼ṹƥ + VerifyDirectoryMatch(_testSourceDir, _testTargetDir); + } + + [Fact] + public async Task TwoWaySync_WithChangesOnBothSides_ShouldResolveConflicts() + { + // ׼ͬijʼ + var commonFiles = new Dictionary + { + ["common.txt"] = Encoding.UTF8.GetBytes("This is a common file"), + ["unchanged.txt"] = Encoding.UTF8.GetBytes("This file will not change"), + ["subdir/file.txt"] = Encoding.UTF8.GetBytes("This is a subdirectory file"), + }; + + var commonDirs = new[] { "subdir" }; + + // ʼͬṹ + CreateTestFileStructure(_testSourceDir, commonFiles, commonDirs); + CreateTestFileStructure(_testTargetDir, commonFiles, commonDirs); + + // ԴһЩ + File.WriteAllText(Path.Combine(_testSourceDir, "common.txt"), "This is updated in source"); + File.WriteAllText(Path.Combine(_testSourceDir, "source_only.txt"), "This file only exists in source"); + Directory.CreateDirectory(Path.Combine(_testSourceDir, "source_dir")); + + // ĿһЩ + File.WriteAllText(Path.Combine(_testTargetDir, "common.txt"), "This is updated in target"); + File.WriteAllText(Path.Combine(_testTargetDir, "target_only.txt"), "This file only exists in target"); + Directory.CreateDirectory(Path.Combine(_testTargetDir, "target_dir")); + + // ʱʹԴļĿļ + var sourceTime = DateTime.Now; + var targetTime = sourceTime.AddMinutes(-30); + + SetFileLastWriteTime(Path.Combine(_testSourceDir, "common.txt"), sourceTime); + SetFileLastWriteTime(Path.Combine(_testTargetDir, "common.txt"), targetTime); + + // ͬѡʹ""Խͻ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.TwoWay, + CompareMethod = CompareMethod.DateTimeAndSize, + ConflictResolution = ConflictResolution.Newer, + MaxParallelOperations = 2 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + + // ֤ͻcommon.txt ӦʹԴ汾£ + var commonContent = File.ReadAllText(Path.Combine(_testTargetDir, "common.txt")); + Assert.Equal("This is updated in source", commonContent); + + // ֤ļѸƣsource_only.txt ӦĿд + Assert.True(File.Exists(Path.Combine(_testTargetDir, "source_only.txt"))); + + // ֤һļҲѸƣtarget_only.txt ӦԴд + Assert.True(File.Exists(Path.Combine(_testSourceDir, "target_only.txt"))); + + // ֤Ŀ¼Ҳͬ + Assert.True(Directory.Exists(Path.Combine(_testTargetDir, "source_dir"))); + Assert.True(Directory.Exists(Path.Combine(_testSourceDir, "target_dir"))); + } + + [Fact] + public async Task ConflictResolution_KeepBoth_ShouldRenameTarget() + { + // ׼ͬijʼ + var commonFiles = new Dictionary + { + ["conflict.txt"] = Encoding.UTF8.GetBytes("Original content"), + }; + + // ʼͬṹ + CreateTestFileStructure(_testSourceDir, commonFiles, new string[0]); + CreateTestFileStructure(_testTargetDir, commonFiles, new string[0]); + + // ԴĿ޸ͬһļ + File.WriteAllText(Path.Combine(_testSourceDir, "conflict.txt"), "Source modified content"); + File.WriteAllText(Path.Combine(_testTargetDir, "conflict.txt"), "Target modified content"); + + // ͬѡʹ""Խͻ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.TwoWay, + CompareMethod = CompareMethod.Content, + ConflictResolution = ConflictResolution.KeepBoth, + MaxParallelOperations = 1 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + + // ֤汾 + var targetContent = File.ReadAllText(Path.Combine(_testTargetDir, "conflict.txt")); + Assert.Equal("Source modified content", targetContent); // ĿļӦԴļ + + // ĿӦһʱļ + var targetFiles = Directory.GetFiles(_testTargetDir); + Assert.Equal(2, targetFiles.Length); + + var renamedFile = targetFiles.First(f => !f.EndsWith("conflict.txt")); + Assert.Contains("conflict", Path.GetFileName(renamedFile)); // ļӦԭ + + var renamedContent = File.ReadAllText(renamedFile); + Assert.Equal("Target modified content", renamedContent); // ļӦĿԭʼ + } + + [Fact] + public async Task HashComparison_ShouldDetectContentChanges() + { + // ׼ԴĿ¼ + var sourceFiles = new Dictionary + { + ["smallfile.bin"] = GenerateRandomData(1024, 1), + ["largefile.bin"] = GenerateRandomData(1024 * 1024, 2), // 1MB + }; + + // ׼ĿĿ¼ݣļСͬвͬ + var targetFiles = new Dictionary + { + ["smallfile.bin"] = GenerateRandomData(1024, 1), // ȫͬ + ["largefile.bin"] = GenerateRandomData(1024 * 1024, 3), // ͬӣݲͬ + }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, new string[0]); + CreateTestFileStructure(_testTargetDir, targetFiles, new string[0]); + + // ȷ޸ʱֻͬͨϣ仯 + var sameTime = DateTime.Now; + SetFileLastWriteTime(Path.Combine(_testSourceDir, "smallfile.bin"), sameTime); + SetFileLastWriteTime(Path.Combine(_testTargetDir, "smallfile.bin"), sameTime); + SetFileLastWriteTime(Path.Combine(_testSourceDir, "largefile.bin"), sameTime); + SetFileLastWriteTime(Path.Combine(_testTargetDir, "largefile.bin"), sameTime); + + // ͬѡʹùϣȽ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Hash, + HashAlgorithm = EHashType.SHA256, + SamplingRate = 0.1, // 10% + MaxParallelOperations = 1 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + Assert.Equal(1, result.Statistics.FilesUpdated); // ֻ largefile.bin Ӧñ + Assert.Equal(0, result.Statistics.FilesCopied); + + // ֤ļѸ + using (var md5 = MD5.Create()) + { + var sourceHash = md5.ComputeHash(File.ReadAllBytes(Path.Combine(_testSourceDir, "largefile.bin"))); + var targetHash = md5.ComputeHash(File.ReadAllBytes(Path.Combine(_testTargetDir, "largefile.bin"))); + Assert.Equal(sourceHash, targetHash); // ϣӦͬ + } + } + + [Fact] + public async Task PreviewMode_ShouldNotMakeChanges() + { + // ׼ԴĿ¼ + var sourceFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("Source file 1"), + ["file2.txt"] = Encoding.UTF8.GetBytes("Source file 2"), + }; + + // ׼ĿĿ¼ݣͬݣ + var targetFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("Target file 1"), + // file2.txt Ŀ + }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, new string[0]); + CreateTestFileStructure(_testTargetDir, targetFiles, new string[0]); + + // ¼ĿĿ¼ԭʼ״̬ + var originalTargetContent = File.ReadAllText(Path.Combine(_testTargetDir, "file1.txt")); + var originalTargetFileCount = Directory.GetFiles(_testTargetDir).Length; + + // ͬѡԤģʽ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Content, + PreviewOnly = true, + MaxParallelOperations = 1 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + + // ֤ƻ + Assert.Equal(1, result.Actions.Count(a => a.ActionType == SyncActionType.UpdateFile)); // file1.txt Ӧñ + Assert.Equal(1, result.Actions.Count(a => a.ActionType == SyncActionType.CopyFile)); // file2.txt Ӧñ + + // ԤģʽʵļӦûб仯 + var currentTargetContent = File.ReadAllText(Path.Combine(_testTargetDir, "file1.txt")); + var currentTargetFileCount = Directory.GetFiles(_testTargetDir).Length; + + Assert.Equal(originalTargetContent, currentTargetContent); // Ӧûб仯 + Assert.Equal(originalTargetFileCount, currentTargetFileCount); // ļӦûб仯 + Assert.False(File.Exists(Path.Combine(_testTargetDir, "file2.txt"))); // file2.txt Ӧñ + } + + [Fact] + public async Task IgnorePatterns_ShouldSkipMatchingFiles() + { + // ׼ԴĿ¼ + var sourceFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("Regular file"), + ["temp.txt"] = Encoding.UTF8.GetBytes("Temporary file"), + ["file.tmp"] = Encoding.UTF8.GetBytes("Another temporary file"), + ["logs/log.txt"] = Encoding.UTF8.GetBytes("Log file"), + ["backup.bak"] = Encoding.UTF8.GetBytes("Backup file"), + }; + + var sourceDirs = new[] { "logs", "cache" }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, sourceDirs); + + // ͬѡģʽ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Content, + IgnorePatterns = new List + { + "*.tmp", + "*.bak", + "**/logs/**" + }, + MaxParallelOperations = 1 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + + // ֤ǺļѸ + Assert.True(File.Exists(Path.Combine(_testTargetDir, "file1.txt"))); + Assert.True(File.Exists(Path.Combine(_testTargetDir, "temp.txt"))); + + // ֤ļδ + Assert.False(File.Exists(Path.Combine(_testTargetDir, "file.tmp"))); + Assert.False(File.Exists(Path.Combine(_testTargetDir, "backup.bak"))); + Assert.False(File.Exists(Path.Combine(_testTargetDir, "logs", "log.txt"))); + Assert.False(Directory.Exists(Path.Combine(_testTargetDir, "logs"))); + } + + [Fact] + public async Task ConfigFileLoad_ShouldApplyCorrectSettings() + { + // ļ + string configPath = Path.Combine(_testSourceDir, "sync.config"); + var configOptions = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Hash, + HashAlgorithm = EHashType.SHA256, + SamplingRate = 0.2, + MaxParallelOperations = 2, + IgnorePatterns = new List { "*.tmp", "*.bak" } + }; + + FileSyncHelper.SaveToJsonFile(configOptions, configPath); + + // ׼ļ + var sourceFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("Test file"), + ["ignore.tmp"] = Encoding.UTF8.GetBytes("Should be ignored"), + }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, new string[0]); + + // ļѡ + var loadedOptions = FileSyncHelper.LoadFromJsonFile(configPath); + + // ִͬ + var syncHelper = new FileSyncHelper(loadedOptions, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + + // ֤ǷȷӦ˺Թ + Assert.True(File.Exists(Path.Combine(_testTargetDir, "file1.txt"))); + Assert.False(File.Exists(Path.Combine(_testTargetDir, "ignore.tmp"))); + } + + [Fact] + public async Task ErrorHandling_ShouldContinueOnError() + { + // ׼ԴĿ¼ + var sourceFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("File 1"), + ["file2.txt"] = Encoding.UTF8.GetBytes("File 2"), + ["file3.txt"] = Encoding.UTF8.GetBytes("File 3"), + }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, new string[0]); + + // Ŀṹ + Directory.CreateDirectory(_testTargetDir); + + // Ŀеֻļ + var readOnlyFile = Path.Combine(_testTargetDir, "file2.txt"); + File.WriteAllText(readOnlyFile, "Read-only content"); + File.SetAttributes(readOnlyFile, FileAttributes.ReadOnly); + + // ͬѡô + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Content, + ContinueOnError = true, + MaxParallelOperations = 1 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ֻԣںɾ + File.SetAttributes(readOnlyFile, FileAttributes.Normal); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); // Ӧɣд + Assert.True(result.IsSuccessful); + Assert.True(result.Statistics.Errors > 0); // Ӧд¼ + + // ֤ļǷȷͬ + Assert.True(File.Exists(Path.Combine(_testTargetDir, "file1.txt"))); + Assert.True(File.Exists(Path.Combine(_testTargetDir, "file3.txt"))); + + // file2.txt Ӧԭ + Assert.Equal("Read-only content", File.ReadAllText(Path.Combine(_testTargetDir, "file2.txt"))); + } + + [Fact] + public async Task Sync_LargeFiles_ShouldHandleThemCorrectly() + { + // һϴԴļԼ2MB + var largeFileData = GenerateRandomData(2 * 1024 * 1024, 42); + var sourceFiles = new Dictionary + { + ["largefile.dat"] = largeFileData, + }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, new string[0]); + + // ͬѡݱȽ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Content, + MaxParallelOperations = 1 + }; + + // ͬʱ + var stopwatch = Stopwatch.StartNew(); + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + stopwatch.Stop(); + + // Ϣ + _output.WriteLine($"ͬ2MBļĺʱ: {stopwatch.ElapsedMilliseconds}ms"); + _output.WriteLine($"ͬٶ: {result.BytesPerSecond / (1024 * 1024):F2} MB/s"); + + // ֤ + Assert.Equal(SyncStatus.Completed, result.Status); + Assert.True(result.IsSuccessful); + Assert.Equal(1, result.Statistics.FilesCopied); + + // ֤ļ + var targetFileData = File.ReadAllBytes(Path.Combine(_testTargetDir, "largefile.dat")); + Assert.Equal(largeFileData, targetFileData); + } + + [Fact] + public async Task FileSyncReport_ShouldContainDetailedInformation() + { + // ׼ + var sourceFiles = new Dictionary + { + ["file1.txt"] = Encoding.UTF8.GetBytes("File 1"), + ["file2.txt"] = Encoding.UTF8.GetBytes("File 2"), + ["subdir/file3.txt"] = Encoding.UTF8.GetBytes("File 3"), + }; + + var sourceDirs = new[] { "subdir" }; + + CreateTestFileStructure(_testSourceDir, sourceFiles, sourceDirs); + + // ͬѡ + var options = new SyncOptions + { + SourcePath = _testSourceDir, + TargetPath = _testTargetDir, + SyncMode = SyncMode.OneWay, + CompareMethod = CompareMethod.Content, + MaxParallelOperations = 1 + }; + + // ִͬ + var syncHelper = new FileSyncHelper(options, CreateProgressReporter()); + var result = await syncHelper.SyncAsync(); + + // ȡ + string summary = result.GetSummary(); + string report = result.GenerateReport(); + + // ֤ + Assert.Contains("ͬģʽ: ͬ", summary); + Assert.Contains($"Դ·: {_testSourceDir}", summary); + Assert.Contains($"Ŀ·: {_testTargetDir}", summary); + Assert.Contains("ļͳ:", summary); + Assert.Contains(": 3 ļ", summary); + + Assert.Contains("ͬ", report); + Assert.Contains("ϸ¼", report); + Assert.Contains("ļ¼", report); + + // Թ + _output.WriteLine("=== ͬժҪ ==="); + _output.WriteLine(summary); + _output.WriteLine("=== ϸ ==="); + _output.WriteLine(report); + } + + #endregion + } +} From cdafdf1e548854c39acd5ff61b8d6d91997d5540 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Tue, 13 May 2025 16:21:13 +0800 Subject: [PATCH 69/90] v1.0.4 --- README.md | 2 +- src/MDriveSync.Cli/MDriveSync.Cli.csproj | 17 ++++--- src/MDriveSync.Cli/Program.cs | 41 ++++++++++++----- src/MDriveSync.Cli/README.md | 35 +++++++-------- src/MDriveSync.Cli/sync.json | 4 +- src/MDriveSync.Security/HashHelper.cs | 50 +++++++++++++++------ src/MDriveSync.Security/Models/EHashType.cs | 7 ++- 7 files changed, 102 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 8385fd7..d693c6a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - 加密算法:**AES256-GCM(Default)、ChaCha20-Poly1305。** - 压缩算法:**Zstd(Default)、LZ4、Snappy。** -- 哈希算法:**SHA256(Default)、SHA1、SHA3、MD5、XXH3、XXH128、BLAKE3。** +- 哈希算法:**SHA256(Default)、SHA1、SHA3、SHA384、SHA512、MD5、XXH3、XXH128、BLAKE3。** - 分块算法:**Buzhash+(Default)、FastCDC+、FastCDC。** - 分块策略:**Balanced(Default)、Dynamic、Fixed。** diff --git a/src/MDriveSync.Cli/MDriveSync.Cli.csproj b/src/MDriveSync.Cli/MDriveSync.Cli.csproj index f9bf8cb..a6d57d6 100644 --- a/src/MDriveSync.Cli/MDriveSync.Cli.csproj +++ b/src/MDriveSync.Cli/MDriveSync.Cli.csproj @@ -4,17 +4,20 @@ Exe net8.0 enable - true true + true + + true - trueai-org - trueai-org - mdrive - 使用 mdrive 快速同步/备份,安装:dotnet tool install -g mdrive + mdrive + mdrive + 1.0.4 + TrueAI.ORG + 多平台文件同步命令行工具 + MIT https://github.com/trueai-org/mdrive https://github.com/trueai-org/mdrive - mdrive - 1.0.1 + sync;file;backup;aliyun;cli README.md diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index 79ff517..cc5e36f 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -35,8 +35,22 @@ private static async Task Main(string[] args) return 0; default: - Log.Error($"未知命令: {command}"); - ShowHelp(); + { + Log.Error($"未知命令: {command}"); + ShowHelp(); + + // sync 参数 + ShowSyncHelp(); + Console.WriteLine(); + + // config 参数 + ShowConfigHelp(); + Console.WriteLine(); + + // 示例 + Console.WriteLine("示例:"); + Console.WriteLine("mdrive sync --source C:\\Source --target D:\\Targetd"); + } return 1; } } @@ -70,20 +84,23 @@ private static void ConfigureLogging() /// private static void ShowHelp() { - Console.WriteLine("MDriveSync - 多平台文件同步工具"); + var version = "v" + string.Join(".", typeof(Program).Assembly.GetName().Version.ToString().Split('.').Where((a, b) => b <= 2)); + + Console.WriteLine($"mdrive - 多平台文件同步工具 {version}"); Console.WriteLine(); Console.WriteLine("用法:"); - Console.WriteLine(" mdrive [命令] [选项]"); + Console.WriteLine("mdrive [命令] [选项]"); Console.WriteLine(); Console.WriteLine("命令:"); - Console.WriteLine(" sync 执行文件同步操作"); - Console.WriteLine(" config 管理同步配置文件"); - Console.WriteLine(" version 显示程序版本信息"); + Console.WriteLine(" sync 执行文件同步操作"); + Console.WriteLine(" config 管理同步配置文件"); + Console.WriteLine(" version 显示程序版本信息"); Console.WriteLine(); Console.WriteLine("选项:"); - Console.WriteLine(" --help, -h 显示帮助信息"); + Console.WriteLine(" --help, -h 显示帮助信息"); Console.WriteLine(); Console.WriteLine("使用 'mdrive [命令] --help' 查看特定命令的帮助信息"); + Console.WriteLine(); } /// @@ -96,6 +113,10 @@ private static async Task HandleSyncCommand(string[] args) ShowSyncHelp(); return 0; } + else + { + ShowHelp(); + } var options = ParseSyncOptions(args); if (options == null) @@ -169,7 +190,7 @@ private static void ShowSyncHelp() Console.WriteLine("执行文件同步操作"); Console.WriteLine(); Console.WriteLine("用法:"); - Console.WriteLine(" mdrivesync sync [选项]"); + Console.WriteLine(" mdrive sync [选项]"); Console.WriteLine(); Console.WriteLine("选项:"); Console.WriteLine(" --source, -s 源目录路径 (必需)"); @@ -584,7 +605,7 @@ private static void CreateConfigFile(FileInfo output, DirectoryInfo source, Dire TargetPath = target.FullName, SyncMode = mode, CompareMethod = CompareMethod.DateTimeAndSize, - + MaxParallelOperations = Math.Max(1, Environment.ProcessorCount), PreviewOnly = false, UseRecycleBin = true, diff --git a/src/MDriveSync.Cli/README.md b/src/MDriveSync.Cli/README.md index 2b19fe0..1a47902 100644 --- a/src/MDriveSync.Cli/README.md +++ b/src/MDriveSync.Cli/README.md @@ -42,7 +42,6 @@ dotnet nuget push bin/Release/mdrive.1.1.0.nupkg --api-key YOUR_API_KEY --source # 下载最新版本 # 解压后使用命令行运行 dotnet MDriveSync.Cli.dll [命令] [选项] - ``` ## 基本命令 @@ -51,10 +50,10 @@ MDriveSync CLI 提供了以下主要命令: ``` -MDriveSync - 多平台文件同步工具 +多平台文件同步工具 用法: - MDriveSync.Cli [命令] [选项] + mdrive [命令] [选项] 命令: sync 执行文件同步操作 @@ -75,7 +74,7 @@ MDriveSync - 多平台文件同步工具 sync - 执行文件同步操作 用法: - MDriveSync.Cli sync [选项] + mdrive sync [选项] 选项: -s, --source 源目录路径 (必需) @@ -100,22 +99,22 @@ sync - 执行文件同步操作 ```shell # 基本单向同步 -MDriveSync.Cli sync --source "D:\Documents" --target "E:\Backup\Documents" +mdrive sync --source "D:\Documents" --target "E:\Backup\Documents" # 使用镜像模式和SHA1哈希算法 -MDriveSync.Cli sync --source "D:\Projects" --target "E:\Backup\Projects" --mode Mirror --hash SHA1 +mdrive sync --source "D:\Projects" --target "E:\Backup\Projects" --mode Mirror --hash SHA1 # 排除特定文件和目录 -MDriveSync.Cli sync --source "D:\Photos" --target "E:\Backup\Photos" --exclude "*.tmp" --exclude "**/Thumbs.db" +mdrive sync --source "D:\Photos" --target "E:\Backup\Photos" --exclude "*.tmp" --exclude "**/Thumbs.db" # 使用预览模式 -MDriveSync.Cli sync --source "D:\Music" --target "E:\Backup\Music" --preview +mdrive sync --source "D:\Music" --target "E:\Backup\Music" --preview # 从配置文件加载同步选项 -MDriveSync.Cli sync --source "D:\Videos" --target "E:\Backup\Videos" --config "sync.config.json" +mdrive sync --source "D:\Videos" --target "E:\Backup\Videos" --config "sync.config.json" # 使用高级选项 -MDriveSync.Cli sync --source "D:\Work" --target "E:\Backup\Work" --mode TwoWay --compare Hash --threads 4 --verbose +mdrive sync --source "D:\Work" --target "E:\Backup\Work" --mode TwoWay --compare Hash --threads 4 --verbose ``` @@ -128,7 +127,7 @@ MDriveSync.Cli sync --source "D:\Work" --target "E:\Backup\Work" --mode TwoWay - config - 管理同步配置文件 用法: - MDriveSync.Cli config [子命令] [选项] + mdrive config [子命令] [选项] 子命令: create 创建新的配置文件 @@ -146,7 +145,7 @@ config - 管理同步配置文件 create - 创建新的配置文件 用法: - MDriveSync.Cli config create [选项] + mdrive config create [选项] 选项: -o, --output 输出文件路径 (必需) @@ -164,7 +163,7 @@ create - 创建新的配置文件 view - 查看现有配置文件内容 用法: - MDriveSync.Cli config view [选项] + mdrive config view [选项] 选项: -f, --file 配置文件路径 (必需) @@ -177,11 +176,10 @@ view - 查看现有配置文件内容 ```shell # 创建新的配置文件 -MDriveSync.Cli config create --output "sync.config.json" --source "D:\Documents" --target "E:\Backup\Documents" --mode Mirror +mdrive config create --output "sync.config.json" --source "D:\Documents" --target "E:\Backup\Documents" --mode Mirror # 查看配置文件内容 -MDriveSync.Cli config view --file "sync.config.json" - +mdrive config view --file "sync.config.json" ``` ## 版本命令 (version) @@ -190,7 +188,7 @@ MDriveSync.Cli config view --file "sync.config.json" ```shell -MDriveSync.Cli version +mdrive version ``` @@ -218,9 +216,8 @@ MDriveSync 支持以下同步模式: - **MD5** - **SHA1** -- **SHA256** (默认) +- **SHA256** - **SHA384** -- **SHA512** ## 配置文件格式 diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index b0a17bc..5f59164 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,6 +1,6 @@ { - "SourcePath": "E:\\downs\\fdm\\__同步测试\\source1", - "TargetPath": "E:\\downs\\fdm\\__同步测试\\target1", + "SourcePath": "E:\\downs", + "TargetPath": "E:\\_temp\\____synctest", "SyncMode": "Mirror", "CompareMethod": "DateTimeAndSize", "HashAlgorithm": "SHA256", diff --git a/src/MDriveSync.Security/HashHelper.cs b/src/MDriveSync.Security/HashHelper.cs index 1250ab7..ea70613 100644 --- a/src/MDriveSync.Security/HashHelper.cs +++ b/src/MDriveSync.Security/HashHelper.cs @@ -1,12 +1,12 @@ -using System.Buffers.Binary; +using Blake3; +using System.Buffers.Binary; using System.IO.Hashing; using System.Security.Cryptography; -using Blake3; namespace MDriveSync.Security { /// - /// 哈希算法(MD5、SHA1、SHA256、SHA3/SHA384、BLAKE3、XXH3、XXH128) + /// 哈希算法(MD5、SHA1、SHA256、SHA3/SHA384、SHA512、BLAKE3、XXH3、XXH128) /// 用于生成数据块或文件的哈希值,以验证数据的完整性和唯一性 /// 默认:SHA256 /// @@ -23,31 +23,36 @@ public static byte[] ComputeHash(byte[] data, string algorithm = "SHA256") { switch (algorithm.ToUpper()) { + case "SHA1": + using (SHA1 sha1 = SHA1.Create()) + { + return sha1.ComputeHash(data); + } case "SHA256": using (SHA256 sha256 = SHA256.Create()) { return sha256.ComputeHash(data); } - case "BLAKE3": + case "SHA3": + case "SHA384": + using (SHA384 sha3 = SHA384.Create()) { - return Hasher.Hash(data).AsSpan().ToArray(); + return sha3.ComputeHash(data); } - case "SHA1": - using (SHA1 sha1 = SHA1.Create()) + case "SHA512": + using (SHA512 sha512 = SHA512.Create()) { - return sha1.ComputeHash(data); + return sha512.ComputeHash(data); + } + case "BLAKE3": + { + return Hasher.Hash(data).AsSpan().ToArray(); } case "MD5": using (MD5 md5 = MD5.Create()) { return md5.ComputeHash(data); } - case "SHA3": - case "SHA384": - using (SHA384 sha3 = SHA384.Create()) - { - return sha3.ComputeHash(data); - } case "XXH3": { return XxHash3.Hash(data); @@ -85,6 +90,11 @@ public static byte[] ComputeHash(Stream stream, string algorithm = "SHA256") { return sha256.ComputeHash(stream); } + case "SHA512": + using (SHA512 sha512 = SHA512.Create()) + { + return sha512.ComputeHash(stream); + } case "BLAKE3": { using var blake3Stream = new Blake3Stream(stream); @@ -218,6 +228,18 @@ public static string ComputeHashHex(string filePath, string algorithm = "SHA256" } } } + // SHA512 + else if (algorithm == "SHA512") + { + using (SHA512 sha512 = SHA512.Create()) + { + using (FileStream fileStream = File.OpenRead(filePath)) + { + var hashBytes = sha512.ComputeHash(fileStream); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); + } + } + } else if (algorithm == "SHA3" || algorithm == "SHA384") { using (SHA384 sha3 = SHA384.Create()) diff --git a/src/MDriveSync.Security/Models/EHashType.cs b/src/MDriveSync.Security/Models/EHashType.cs index ffbbbfa..8a6f303 100644 --- a/src/MDriveSync.Security/Models/EHashType.cs +++ b/src/MDriveSync.Security/Models/EHashType.cs @@ -1,7 +1,7 @@ namespace MDriveSync.Security.Models { /// - /// MD5、SHA1、SHA256、SHA3/SHA384、BLAKE3、XXH3、XXH128 + /// MD5、SHA1、SHA256、SHA3/SHA384、SHA512、BLAKE3、XXH3、XXH128 /// public enum EHashType { @@ -20,6 +20,11 @@ public enum EHashType /// SHA256, + /// + /// SHA512 + /// + SHA512, + /// /// SHA3/SHA384 /// From 14fb8001596aa5ec0e47793ea202a2689643228a Mon Sep 17 00:00:00 2001 From: trueai-org Date: Tue, 13 May 2025 16:27:50 +0800 Subject: [PATCH 70/90] 1.0.5 --- src/MDriveSync.Cli/MDriveSync.Cli.csproj | 2 +- src/MDriveSync.Cli/Program.cs | 13 +++++++++++++ src/MDriveSync.Cli/sync.json | 10 ++++++++-- src/MDriveSync.Core/Services/FileSyncHelper.cs | 4 ++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/MDriveSync.Cli/MDriveSync.Cli.csproj b/src/MDriveSync.Cli/MDriveSync.Cli.csproj index a6d57d6..61d8f3a 100644 --- a/src/MDriveSync.Cli/MDriveSync.Cli.csproj +++ b/src/MDriveSync.Cli/MDriveSync.Cli.csproj @@ -11,7 +11,7 @@ true mdrive mdrive - 1.0.4 + 1.0.5 TrueAI.ORG 多平台文件同步命令行工具 MIT diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index cc5e36f..b6d06f3 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -17,6 +17,19 @@ private static async Task Main(string[] args) if (args.Length == 0 || args[0] == "--help" || args[0] == "-h") { ShowHelp(); + + // sync 参数 + ShowSyncHelp(); + Console.WriteLine(); + + // config 参数 + ShowConfigHelp(); + Console.WriteLine(); + + // 示例 + Console.WriteLine("示例:"); + Console.WriteLine("mdrive sync --source C:\\Source --target D:\\Targetd"); + return 0; } diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index 5f59164..7d13fa3 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,5 +1,5 @@ { - "SourcePath": "E:\\downs", + "SourcePath": "E:\\guanpeng", "TargetPath": "E:\\_temp\\____synctest", "SyncMode": "Mirror", "CompareMethod": "DateTimeAndSize", @@ -15,12 +15,18 @@ "PreviewOnly": false, "ContinueOnError": true, "MaxRetries": 3, + "Verbose": true, "IgnorePatterns": [ "**/System Volume Information/**", "**/$RECYCLE.BIN/**", "**/Thumbs.db", "**/*.tmp", "**/*.temp", - "**/*.bak" + "**/*.bak", + "**/.git/*", + "**/node_modules/*", + "**/bin/*", + "**/obj/*", + "**/.git/*" ] } \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index e04b8ad..72a3124 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -882,7 +882,7 @@ private bool CompareFileContent(FileInfo sourceFile, FileInfo targetFile) ); var result = await scanner.ScanAsync( - reportProgress: false, + reportProgress: _options.Verbose, cancellationToken: _cancellationToken ); @@ -909,7 +909,7 @@ private bool CompareFileContent(FileInfo sourceFile, FileInfo targetFile) "*", _options.IgnorePatterns, _options.MaxParallelOperations, - reportProgress: false, + reportProgress: _options.Verbose, cancellationToken: _cancellationToken ); From ce6fb782dfb4cb1a77f83346d4ea09c56c66e9cd Mon Sep 17 00:00:00 2001 From: trueai-org Date: Tue, 13 May 2025 16:33:28 +0800 Subject: [PATCH 71/90] mdrive.1.0.5 --- src/MDriveSync.Cli/Program.cs | 3 ++- src/MDriveSync.Cli/sync.json | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index b6d06f3..7994526 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -1,4 +1,5 @@ using MDriveSync.Core.Services; +using MDriveSync.Infrastructure; using MDriveSync.Security.Models; using Serilog; using System.Text.Json; @@ -183,7 +184,7 @@ private static async Task HandleSyncCommand(string[] args) Log.Information($"目录创建: {result.Statistics.DirectoriesCreated} 个"); Log.Information($"目录删除: {result.Statistics.DirectoriesDeleted} 个"); Log.Information($"错误数量: {result.Statistics.Errors} 个"); - Log.Information($"处理总量: {result.Statistics.BytesProcessed} 字节"); + Log.Information($"处理总量: {result.Statistics.BytesProcessed.FormatSize()}"); } return result.Status == SyncStatus.Completed ? 0 : 1; diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index 7d13fa3..da49b28 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -3,9 +3,9 @@ "TargetPath": "E:\\_temp\\____synctest", "SyncMode": "Mirror", "CompareMethod": "DateTimeAndSize", - "HashAlgorithm": "SHA256", + "HashAlgorithm": null, "SamplingRate": 0.1, - "MaxParallelOperations": 4, + "MaxParallelOperations": 24, "EnableParallelFileOperations": true, "DateTimeThresholdSeconds": 2, "PreserveFileTime": true, From 695b578ff7bb76d55ab534fccadd87eee40f3aa6 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Tue, 13 May 2025 16:44:46 +0800 Subject: [PATCH 72/90] readme --- README.md | 22 ++++++++++++++++++++++ src/MDriveSync.Cli/Program.cs | 4 ++-- src/MDriveSync.Security/HashHelper.cs | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d693c6a..94f49fc 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,28 @@ ![挂载](/docs/screenshots/mount.png) ![macOS](/docs/screenshots/macOS.png) +## 同步助手 + +MDriveSync CLI 是一个功能强大的命令行工具,用于执行多平台文件同步操作。该工具提供了灵活的配置选项,支持多种同步模式、文件比较方法和哈希算法,适用于各种文件同步和备份场景。 + +更多详情参考 [README.md](src/MDriveSync.Cli/README.md) + +### 使用 .NET Global Tool(推荐) + +```bash +# 安装 +dotnet tool install -g mdrive + +# 升级 +dotnet tool update -g mdrive + +# 同步 +mdrive sync -s /a -t /b + +# 帮助 +mdirve -h +``` + ## 安装与使用 ### 快速启动 diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index 7994526..e62d214 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -211,8 +211,8 @@ private static void ShowSyncHelp() Console.WriteLine(" --target, -t 目标目录路径 (必需)"); Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); - Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256, SHA384, SHA512 (默认: SHA256)"); - Console.WriteLine(" --config, -f 配置文件路径"); + Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); + Console.WriteLine(" --config, -f 配置文件路径, 示例: -f sync.json"); Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); Console.WriteLine(" --verbose, -v 显示详细日志信息 (默认: false)"); diff --git a/src/MDriveSync.Security/HashHelper.cs b/src/MDriveSync.Security/HashHelper.cs index ea70613..434af46 100644 --- a/src/MDriveSync.Security/HashHelper.cs +++ b/src/MDriveSync.Security/HashHelper.cs @@ -6,7 +6,7 @@ namespace MDriveSync.Security { /// - /// 哈希算法(MD5、SHA1、SHA256、SHA3/SHA384、SHA512、BLAKE3、XXH3、XXH128) + /// 哈希算法(MD5、SHA1、SHA256、SHA3、SHA384、SHA512、BLAKE3、XXH3、XXH128) /// 用于生成数据块或文件的哈希值,以验证数据的完整性和唯一性 /// 默认:SHA256 /// From 71b5aa7a6ca9d10c34f167ec0c9a73d991e4148e Mon Sep 17 00:00:00 2001 From: trueai-org Date: Tue, 13 May 2025 19:15:06 +0800 Subject: [PATCH 73/90] log --- src/MDriveSync.Cli/Program.cs | 27 +- src/MDriveSync.Cli/sync.json | 4 +- .../Services/FileSyncHelper.cs | 2683 ++++++++++++----- src/MDriveSync.Test/FileSyncHelperTests.cs | 78 +- 4 files changed, 2013 insertions(+), 779 deletions(-) diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index e62d214..1b45275 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -163,7 +163,14 @@ private static async Task HandleSyncCommand(string[] args) { if (options.Verbose || progress.ProgressPercentage == 100 || progress.ProgressPercentage % 10 == 0) { - Log.Information($"{progress.Message} - {progress.ProgressPercentage}% - {progress.FormattedSpeed} - 剩余时间: {progress.FormattedTimeRemaining}"); + if (progress.ProgressPercentage < 0) + { + Log.Information($"{progress.Message}"); + } + else + { + Log.Information($"{progress.Message} - {progress.ProgressPercentage}% - {progress.FormattedSpeed} - 剩余时间: {progress.FormattedTimeRemaining}"); + } } }); @@ -187,7 +194,7 @@ private static async Task HandleSyncCommand(string[] args) Log.Information($"处理总量: {result.Statistics.BytesProcessed.FormatSize()}"); } - return result.Status == SyncStatus.Completed ? 0 : 1; + return result.Status == ESyncStatus.Completed ? 0 : 1; } catch (Exception ex) { @@ -229,8 +236,8 @@ private static SyncOptions ParseSyncOptions(string[] args) { var options = new SyncOptions { - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.DateTimeAndSize, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.DateTimeAndSize, MaxParallelOperations = Math.Max(1, Environment.ProcessorCount), PreviewOnly = false, UseRecycleBin = true, @@ -270,13 +277,13 @@ private static SyncOptions ParseSyncOptions(string[] args) case "--mode": case "-m": - if (value != null && Enum.TryParse(value, true, out var mode)) + if (value != null && Enum.TryParse(value, true, out var mode)) options.SyncMode = mode; break; case "--compare": case "-c": - if (value != null && Enum.TryParse(value, true, out var compare)) + if (value != null && Enum.TryParse(value, true, out var compare)) options.CompareMethod = compare; break; @@ -437,7 +444,7 @@ private static int HandleConfigCreateCommand(string[] args) string outputPath = null; string sourcePath = null; string targetPath = null; - SyncMode mode = SyncMode.OneWay; + ESyncMode mode = ESyncMode.OneWay; for (int i = 0; i < args.Length; i++) { @@ -472,7 +479,7 @@ private static int HandleConfigCreateCommand(string[] args) case "--mode": case "-m": - if (value != null && Enum.TryParse(value, true, out var syncMode)) + if (value != null && Enum.TryParse(value, true, out var syncMode)) mode = syncMode; break; } @@ -608,7 +615,7 @@ private static void ShowConfigViewHelp() /// /// 创建配置文件 /// - private static void CreateConfigFile(FileInfo output, DirectoryInfo source, DirectoryInfo target, SyncMode mode) + private static void CreateConfigFile(FileInfo output, DirectoryInfo source, DirectoryInfo target, ESyncMode mode) { try { @@ -618,7 +625,7 @@ private static void CreateConfigFile(FileInfo output, DirectoryInfo source, Dire SourcePath = source.FullName, TargetPath = target.FullName, SyncMode = mode, - CompareMethod = CompareMethod.DateTimeAndSize, + CompareMethod = ESyncCompareMethod.DateTimeAndSize, MaxParallelOperations = Math.Max(1, Environment.ProcessorCount), PreviewOnly = false, diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index da49b28..2a3706c 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,6 +1,6 @@ { - "SourcePath": "E:\\guanpeng", - "TargetPath": "E:\\_temp\\____synctest", + "SourcePath": "E:\\_temp\\____synctest0", + "TargetPath": "E:\\_temp\\____synctest1", "SyncMode": "Mirror", "CompareMethod": "DateTimeAndSize", "HashAlgorithm": null, diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 72a3124..45ba39f 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -1,7 +1,7 @@ using MDriveSync.Security; using MDriveSync.Security.Models; +using Serilog; using System.Diagnostics; -using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -50,7 +50,7 @@ public async Task SyncAsync() SourcePath = _options.SourcePath, TargetPath = _options.TargetPath, Mode = _options.SyncMode, - Status = SyncStatus.Started + Status = ESyncStatus.Started }; try @@ -69,16 +69,17 @@ public async Task SyncAsync() // 根据同步模式执行不同的同步策略 await ExecuteSyncByMode(sourceFiles, sourceDirs, targetFiles, targetDirs, result); - result.Status = SyncStatus.Completed; + result.Status = ESyncStatus.Completed; result.ElapsedTime = _stopwatch.Elapsed; result.Statistics = _statistics; + ReportProgress($"同步完成,耗时: {_stopwatch.Elapsed.TotalSeconds:F2}秒", 100); return result; } catch (OperationCanceledException) { - result.Status = SyncStatus.Canceled; + result.Status = ESyncStatus.Canceled; result.ElapsedTime = _stopwatch.Elapsed; result.Statistics = _statistics; ReportProgress("同步操作已取消", -1); @@ -86,7 +87,7 @@ public async Task SyncAsync() } catch (Exception ex) { - result.Status = SyncStatus.Failed; + result.Status = ESyncStatus.Failed; result.ErrorMessage = ex.Message; result.ElapsedTime = _stopwatch.Elapsed; result.Statistics = _statistics; @@ -114,16 +115,16 @@ private async Task ExecuteSyncByMode( switch (_options.SyncMode) { - case SyncMode.OneWay: - actions = await CreateOneWaySyncActionsAsync(sourceFiles, sourceDirs, targetFiles, targetDirs); + case ESyncMode.OneWay: + actions = CreateOneWaySyncActionsOptimized(sourceFiles, sourceDirs, targetFiles, targetDirs); break; - case SyncMode.Mirror: - actions = await CreateMirrorSyncActionsAsync(sourceFiles, sourceDirs, targetFiles, targetDirs); + case ESyncMode.Mirror: + actions = CreateMirrorSyncActionsOptimized(sourceFiles, sourceDirs, targetFiles, targetDirs); break; - case SyncMode.TwoWay: - actions = await CreateTwoWaySyncActionsAsync(sourceFiles, sourceDirs, targetFiles, targetDirs); + case ESyncMode.TwoWay: + actions = CreateTwoWaySyncActionsOptimized(sourceFiles, sourceDirs, targetFiles, targetDirs); break; default: @@ -141,13 +142,40 @@ private async Task ExecuteSyncByMode( } // 执行同步操作 - await ExecuteSyncActionsAsync(actions); + await ExecuteSyncActionsAsyncOptimized(actions); + } + + /// + /// 执行同步操作 + /// + private async Task ExecuteSyncActionsAsync(List actions) + { + // 按操作类型排序:先创建目录,然后复制/更新文件,最后删除文件和目录 + var orderedActions = actions + .OrderBy(a => GetActionPriority(a.ActionType)) + .ToList(); + + _totalItems = orderedActions.Count; + _processedItems = 0; + + ReportProgress($"开始执行同步操作,共 {_totalItems} 个任务", 0); + + if (_options.MaxParallelOperations > 1 && _options.EnableParallelFileOperations) + { + // 并行处理 + await ProcessActionsInParallelAsync(orderedActions); + } + else + { + // 串行处理 + await ProcessActionsSequentiallyAsync(orderedActions); + } } /// /// 创建单向同步操作列表(源 -> 目标) /// - private async Task> CreateOneWaySyncActionsAsync( + private List CreateOneWaySyncActionsAsync( Dictionary sourceFiles, HashSet sourceDirs, Dictionary targetFiles, @@ -165,7 +193,7 @@ private async Task> CreateOneWaySyncActionsAsync( { actions.Add(new SyncAction { - ActionType = SyncActionType.CreateDirectory, + ActionType = ESyncActionType.CreateDirectory, SourcePath = sourceDir, TargetPath = targetDirPath, RelativePath = relativePath @@ -186,7 +214,7 @@ private async Task> CreateOneWaySyncActionsAsync( // 目标不存在文件,需要复制 actions.Add(new SyncAction { - ActionType = SyncActionType.CopyFile, + ActionType = ESyncActionType.CopyFile, SourcePath = sourceFilePath, TargetPath = targetFilePath, RelativePath = relativePath, @@ -200,7 +228,7 @@ private async Task> CreateOneWaySyncActionsAsync( // 文件需要更新 actions.Add(new SyncAction { - ActionType = SyncActionType.UpdateFile, + ActionType = ESyncActionType.UpdateFile, SourcePath = sourceFilePath, TargetPath = targetFilePath, RelativePath = relativePath, @@ -221,14 +249,14 @@ private async Task> CreateOneWaySyncActionsAsync( /// /// 创建镜像同步操作列表(源 -> 目标,删除目标中多余内容) /// - private async Task> CreateMirrorSyncActionsAsync( + private List CreateMirrorSyncActionsAsync( Dictionary sourceFiles, HashSet sourceDirs, Dictionary targetFiles, HashSet targetDirs) { // 首先创建单向同步的操作 - var actions = await CreateOneWaySyncActionsAsync(sourceFiles, sourceDirs, targetFiles, targetDirs); + var actions = CreateOneWaySyncActionsOptimized(sourceFiles, sourceDirs, targetFiles, targetDirs); // 查找目标中需要删除的文件和目录 var sourceRelativePaths = sourceFiles.Keys @@ -246,7 +274,7 @@ private async Task> CreateMirrorSyncActionsAsync( // 目标文件在源中不存在,需要删除 actions.Add(new SyncAction { - ActionType = SyncActionType.DeleteFile, + ActionType = ESyncActionType.DeleteFile, TargetPath = targetFilePath, RelativePath = targetRelativePath, Size = targetFiles[targetFilePath].Length @@ -273,7 +301,7 @@ private async Task> CreateMirrorSyncActionsAsync( // 目标目录在源中不存在,需要删除 actions.Add(new SyncAction { - ActionType = SyncActionType.DeleteDirectory, + ActionType = ESyncActionType.DeleteDirectory, TargetPath = targetDirPath, RelativePath = targetRelativeDir }); @@ -287,7 +315,7 @@ private async Task> CreateMirrorSyncActionsAsync( /// /// 创建双向同步操作列表(源 <-> 目标,解决冲突) /// - private async Task> CreateTwoWaySyncActionsAsync( + private List CreateTwoWaySyncActionsAsync( Dictionary sourceFiles, HashSet sourceDirs, Dictionary targetFiles, @@ -314,7 +342,7 @@ private async Task> CreateTwoWaySyncActionsAsync( { actions.Add(new SyncAction { - ActionType = SyncActionType.CreateDirectory, + ActionType = ESyncActionType.CreateDirectory, SourcePath = Path.Combine(_options.SourcePath, sourceRelativeDir), TargetPath = targetDirPath, RelativePath = sourceRelativeDir @@ -330,11 +358,11 @@ private async Task> CreateTwoWaySyncActionsAsync( { actions.Add(new SyncAction { - ActionType = SyncActionType.CreateDirectory, + ActionType = ESyncActionType.CreateDirectory, SourcePath = targetRelativeDir, TargetPath = sourceDirPath, RelativePath = targetRelativeDir, - Direction = SyncDirection.TargetToSource + Direction = ESyncDirection.TargetToSource }); } } @@ -355,7 +383,7 @@ private async Task> CreateTwoWaySyncActionsAsync( // 目标不存在,复制到目标 actions.Add(new SyncAction { - ActionType = SyncActionType.CopyFile, + ActionType = ESyncActionType.CopyFile, SourcePath = sourceFilePath, TargetPath = targetFilePath, RelativePath = relativePath, @@ -370,10 +398,10 @@ private async Task> CreateTwoWaySyncActionsAsync( var conflictResult = ResolveConflict(sourceFile, targetFile); switch (conflictResult) { - case ConflictResolution.SourceWins: + case ESyncConflictResolution.SourceWins: actions.Add(new SyncAction { - ActionType = SyncActionType.UpdateFile, + ActionType = ESyncActionType.UpdateFile, SourcePath = sourceFilePath, TargetPath = targetFilePath, RelativePath = relativePath, @@ -384,27 +412,27 @@ private async Task> CreateTwoWaySyncActionsAsync( _statistics.BytesToProcess += sourceFile.Length; break; - case ConflictResolution.TargetWins: + case ESyncConflictResolution.TargetWins: actions.Add(new SyncAction { - ActionType = SyncActionType.UpdateFile, + ActionType = ESyncActionType.UpdateFile, SourcePath = targetFilePath, TargetPath = sourceFilePath, RelativePath = relativePath, Size = targetFile.Length, - Direction = SyncDirection.TargetToSource, + Direction = ESyncDirection.TargetToSource, ConflictResolution = conflictResult }); _statistics.FilesToUpdate++; _statistics.BytesToProcess += targetFile.Length; break; - case ConflictResolution.KeepBoth: + case ESyncConflictResolution.KeepBoth: // 保留两个版本,重命名目标文件 string targetNewName = GetConflictFileName(targetFilePath); actions.Add(new SyncAction { - ActionType = SyncActionType.RenameFile, + ActionType = ESyncActionType.RenameFile, SourcePath = targetFilePath, TargetPath = targetNewName, RelativePath = GetRelativePath(targetNewName, _options.TargetPath), @@ -413,7 +441,7 @@ private async Task> CreateTwoWaySyncActionsAsync( // 然后复制源文件到目标 actions.Add(new SyncAction { - ActionType = SyncActionType.CopyFile, + ActionType = ESyncActionType.CopyFile, SourcePath = sourceFilePath, TargetPath = targetFilePath, RelativePath = relativePath, @@ -423,7 +451,7 @@ private async Task> CreateTwoWaySyncActionsAsync( _statistics.BytesToProcess += sourceFile.Length; break; - case ConflictResolution.Skip: + case ESyncConflictResolution.Skip: _statistics.FilesSkipped++; break; } @@ -446,12 +474,12 @@ private async Task> CreateTwoWaySyncActionsAsync( // 源不存在,复制到源 actions.Add(new SyncAction { - ActionType = SyncActionType.CopyFile, + ActionType = ESyncActionType.CopyFile, SourcePath = targetFilePath, TargetPath = sourceFilePath, RelativePath = relativePath, Size = targetFile.Length, - Direction = SyncDirection.TargetToSource + Direction = ESyncDirection.TargetToSource }); _statistics.FilesToCopy++; _statistics.BytesToProcess += targetFile.Length; @@ -461,942 +489,2153 @@ private async Task> CreateTwoWaySyncActionsAsync( } /// - /// 执行同步操作 + /// 优化的镜像同步创建方法 /// - private async Task ExecuteSyncActionsAsync(List actions) + private List CreateMirrorSyncActionsOptimized( + Dictionary sourceFiles, + HashSet sourceDirs, + Dictionary targetFiles, + HashSet targetDirs) { - // 按操作类型排序:先创建目录,然后复制/更新文件,最后删除文件和目录 - var orderedActions = actions - .OrderBy(a => GetActionPriority(a.ActionType)) - .ToList(); + // 优先创建一次性足够大小的列表,减少扩容 + var estimatedSize = sourceFiles.Count + sourceDirs.Count + targetFiles.Count + targetDirs.Count; + var actions = new List(estimatedSize); - _totalItems = orderedActions.Count; - _processedItems = 0; + // 创建单向同步操作(优化版本) + actions.AddRange(CreateOneWaySyncActionsOptimized(sourceFiles, sourceDirs, targetFiles, targetDirs)); - ReportProgress($"开始执行同步操作,共 {_totalItems} 个任务", 0); + // 使用高效的 HashSet 存储相对路径,提高查找效率 + var sourceRelativePaths = new HashSet( + sourceFiles.Keys.Select(p => GetRelativePath(p, _options.SourcePath)), + StringComparer.OrdinalIgnoreCase); - if (_options.MaxParallelOperations > 1 && _options.EnableParallelFileOperations) + var sourceRelativeDirs = new HashSet( + sourceDirs.Select(p => GetRelativePath(p, _options.SourcePath)), + StringComparer.OrdinalIgnoreCase); + + // 查找并删除目标中需要删除的文件(并行处理较多文件时) + if (targetFiles.Count > 1000 && _options.EnableParallelFileOperations) { - // 并行处理 - await ProcessActionsInParallelAsync(orderedActions); + var deleteFileActions = targetFiles + .AsParallel() + .Where(tf => !sourceRelativePaths.Contains(GetRelativePath(tf.Key, _options.TargetPath))) + .Select(tf => new SyncAction + { + ActionType = ESyncActionType.DeleteFile, + TargetPath = tf.Key, + RelativePath = GetRelativePath(tf.Key, _options.TargetPath), + Size = tf.Value.Length + }) + .ToList(); + + actions.AddRange(deleteFileActions); + _statistics.FilesToDelete += deleteFileActions.Count; } else { - // 串行处理 - await ProcessActionsSequentiallyAsync(orderedActions); + // 少量文件时直接顺序处理 + foreach (var targetFile in targetFiles) + { + var relativePath = GetRelativePath(targetFile.Key, _options.TargetPath); + if (!sourceRelativePaths.Contains(relativePath)) + { + actions.Add(new SyncAction + { + ActionType = ESyncActionType.DeleteFile, + TargetPath = targetFile.Key, + RelativePath = relativePath, + Size = targetFile.Value.Length + }); + _statistics.FilesToDelete++; + } + } + } + + // 删除目录(排序处理,确保先删除子目录) + var dirsToDelete = targetDirs + .Where(d => d != _options.TargetPath) + .Select(d => new { Path = d, RelativePath = GetRelativePath(d, _options.TargetPath) }) + .Where(d => !sourceRelativeDirs.Contains(d.RelativePath)) + .OrderByDescending(d => d.Path.Length) // 确保先删除子目录 + .ToList(); + + foreach (var dir in dirsToDelete) + { + actions.Add(new SyncAction + { + ActionType = ESyncActionType.DeleteDirectory, + TargetPath = dir.Path, + RelativePath = dir.RelativePath + }); + _statistics.DirectoriesToDelete++; } + + return actions; } /// - /// 并行处理同步操作 + /// 优化的执行同步操作方法 /// - private async Task ProcessActionsInParallelAsync(List actions) + private async Task ExecuteSyncActionsAsyncOptimized(List actions) { - // 分组处理不同类型的操作 + if (actions.Count == 0) + { + ReportProgress("没有需要执行的操作", 100); + return; + } + + // 按操作类型对操作进行分组和排序 var actionGroups = actions .GroupBy(a => GetActionPriority(a.ActionType)) - .OrderBy(g => g.Key); + .OrderBy(g => g.Key) + .ToList(); + + _totalItems = actions.Count; + _processedItems = 0; + + // 优化:为大型操作列表预热文件系统缓存(创建目录操作) + var createDirActions = actionGroups + .FirstOrDefault(g => g.First().ActionType == ESyncActionType.CreateDirectory) + ?.ToList() ?? new List(); + + if (createDirActions.Count > 100) + { + ReportProgress($"预热文件系统:准备创建 {createDirActions.Count} 个目录...", 0); + // 批量创建所有目录(通常很快且减少文件系统冲突) + await Task.Run(() => + { + foreach (var action in createDirActions) + { + try + { + if (!Directory.Exists(action.TargetPath)) + { + Directory.CreateDirectory(action.TargetPath); + action.Status = ESyncActionStatus.Completed; + _statistics.DirectoriesCreated++; + } + } + catch (Exception ex) + { + action.Status = ESyncActionStatus.Failed; + action.ErrorMessage = ex.Message; + _statistics.Errors++; + + if (!_options.ContinueOnError) + throw; + } + _processedItems++; + } + }); + + // 报告进度 + ReportProgress($"目录创建完成,处理完成 {createDirActions.Count} 个目录", + CalculateProgress(_processedItems, _totalItems)); + } + // 处理剩余操作组 foreach (var group in actionGroups) { + // 跳过已处理的目录创建组 + if (group.First().ActionType == ESyncActionType.CreateDirectory && + createDirActions.Count > 100) + continue; + var groupActions = group.ToList(); - var actionType = groupActions.FirstOrDefault()?.ActionType; + var actionType = groupActions.First().ActionType; - ReportProgress($"处理 {GetActionTypeDescription(actionType.Value)} 操作,共 {groupActions.Count} 项", + ReportProgress($"处理 {GetActionTypeDescription(actionType)} 操作,共 {groupActions.Count} 项", CalculateProgress(_processedItems, _totalItems)); // 设置并行度,文件操作使用配置的并行度,目录操作单线程执行 - int parallelism = IsFileOperation(actionType.Value) + int parallelism = IsFileOperation(actionType) && _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1; - await Parallel.ForEachAsync( - groupActions, - new ParallelOptions - { - MaxDegreeOfParallelism = parallelism, - CancellationToken = _cancellationToken - }, - async (action, ct) => + // 批处理:每次处理一批操作以平衡内存使用和并行效率 + const int batchSize = 500; + + for (int i = 0; i < groupActions.Count; i += batchSize) + { + var batch = groupActions.Skip(i).Take(batchSize).ToList(); + + // 使用SemaphoreSlim控制并行度 + using var semaphore = new SemaphoreSlim(parallelism); + var tasks = new List(batch.Count); + + foreach (var action in batch) { - await ExecuteSingleActionAsync(action); - Interlocked.Increment(ref _processedItems); + // 获取信号量 + await semaphore.WaitAsync(_cancellationToken); - if (_processedItems % 10 == 0 || _processedItems == _totalItems) + tasks.Add(Task.Run(async () => { - ReportProgress( - $"正在处理 {GetActionTypeDescription(action.ActionType)} 操作 ({_processedItems}/{_totalItems})", - CalculateProgress(_processedItems, _totalItems) - ); - } - }); + try + { + await ExecuteSingleActionAsync(action); + Interlocked.Increment(ref _processedItems); + } + finally + { + // 释放信号量 + semaphore.Release(); + } + }, _cancellationToken)); + } + + // 等待当前批次完成 + await Task.WhenAll(tasks); + + // 每批次后报告进度 + ReportProgress( + $"正在处理 {GetActionTypeDescription(actionType)} 操作 ({_processedItems}/{_totalItems})", + CalculateProgress(_processedItems, _totalItems) + ); + } } } /// - /// 串行处理同步操作 + /// 创建优化的双向同步操作列表(源 <-> 目标,高效解决冲突) /// - private async Task ProcessActionsSequentiallyAsync(List actions) + private List CreateTwoWaySyncActionsOptimized( + Dictionary sourceFiles, + HashSet sourceDirs, + Dictionary targetFiles, + HashSet targetDirs) { - for (int i = 0; i < actions.Count; i++) - { - _cancellationToken.ThrowIfCancellationRequested(); + // 预分配充足的容量来避免列表扩容 + var actions = new List(sourceFiles.Count + targetFiles.Count + sourceDirs.Count + targetDirs.Count); - var action = actions[i]; - await ExecuteSingleActionAsync(action); + // 使用高效的 HashSet 跟踪已处理的路径 + var processedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); - _processedItems++; + // 预计算所有目录的相对路径并缓存(避免重复计算) + var allSourceRelativeDirs = new HashSet( + sourceDirs.Select(path => GetRelativePath(path, _options.SourcePath)), + StringComparer.OrdinalIgnoreCase); - if (i % 5 == 0 || i == actions.Count - 1) + var allTargetRelativeDirs = new HashSet( + targetDirs.Where(d => d != _options.TargetPath) + .Select(path => GetRelativePath(path, _options.TargetPath)), + StringComparer.OrdinalIgnoreCase); + + Log.Information($"准备双向同步: 源目录 {allSourceRelativeDirs.Count} 个, 目标目录 {allTargetRelativeDirs.Count} 个"); + Log.Information($"源文件 {sourceFiles.Count} 个, 目标文件 {targetFiles.Count} 个"); + + // 第1步:同步目录结构(批量处理目录创建操作) + + // 1.1: 在目标中创建源中存在的目录 + foreach (var sourceRelativeDir in allSourceRelativeDirs) + { + var targetDirPath = Path.Combine(_options.TargetPath, sourceRelativeDir); + if (!targetDirs.Contains(targetDirPath)) { - ReportProgress( - $"正在处理 {GetActionTypeDescription(action.ActionType)} 操作 ({_processedItems}/{_totalItems})", - CalculateProgress(_processedItems, _totalItems) - ); + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CreateDirectory, + SourcePath = Path.Combine(_options.SourcePath, sourceRelativeDir), + TargetPath = targetDirPath, + RelativePath = sourceRelativeDir + }); } } - } - /// - /// 执行单个同步操作 - /// - private async Task ExecuteSingleActionAsync(SyncAction action) - { - try + // 1.2: 在源中创建目标中存在的目录 + foreach (var targetRelativeDir in allTargetRelativeDirs) { - _cancellationToken.ThrowIfCancellationRequested(); + var sourceDirPath = Path.Combine(_options.SourcePath, targetRelativeDir); + if (!sourceDirs.Contains(sourceDirPath)) + { + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CreateDirectory, + SourcePath = Path.Combine(_options.TargetPath, targetRelativeDir), + TargetPath = sourceDirPath, + RelativePath = targetRelativeDir, + Direction = ESyncDirection.TargetToSource + }); + } + } - // 确定源和目标路径(考虑同步方向) - string actualSource = action.Direction == SyncDirection.SourceToTarget - ? action.SourcePath - : action.TargetPath; + // 第2步:处理文件(先缓存所有相对路径,避免重复计算) + var sourceRelativePathMap = new Dictionary(sourceFiles.Count, StringComparer.OrdinalIgnoreCase); + var targetRelativePathMap = new Dictionary(targetFiles.Count, StringComparer.OrdinalIgnoreCase); - string actualTarget = action.Direction == SyncDirection.SourceToTarget - ? action.TargetPath - : action.SourcePath; + // 预计算并缓存所有相对路径,提高处理效率 + foreach (var sourceEntry in sourceFiles) + { + var relativePath = GetRelativePath(sourceEntry.Key, _options.SourcePath); + sourceRelativePathMap[relativePath] = (sourceEntry.Key, sourceEntry.Value); + } - switch (action.ActionType) - { - case SyncActionType.CreateDirectory: - if (!Directory.Exists(actualTarget)) - { - Directory.CreateDirectory(actualTarget); - _statistics.DirectoriesCreated++; - } - break; + foreach (var targetEntry in targetFiles) + { + var relativePath = GetRelativePath(targetEntry.Key, _options.TargetPath); + targetRelativePathMap[relativePath] = (targetEntry.Key, targetEntry.Value); + } - case SyncActionType.CopyFile: - await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); - await CopyFileWithRetryAsync(actualSource, actualTarget); - _statistics.FilesCopied++; - _statistics.BytesProcessed += action.Size; - break; + // 2.1: 从源处理到目标 + foreach (var sourceEntry in sourceRelativePathMap) + { + var relativePath = sourceEntry.Key; + var sourceFilePath = sourceEntry.Value.FullPath; + var sourceFile = sourceEntry.Value.Info; + var targetFilePath = Path.Combine(_options.TargetPath, relativePath); - case SyncActionType.UpdateFile: - await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); - await CopyFileWithRetryAsync(actualSource, actualTarget); - _statistics.FilesUpdated++; - _statistics.BytesProcessed += action.Size; - break; + processedPaths.Add(relativePath); - case SyncActionType.DeleteFile: - if (File.Exists(actualTarget)) - { - if (_options.UseRecycleBin) + if (!targetRelativePathMap.TryGetValue(relativePath, out var targetEntry)) + { + // 目标不存在,复制到目标 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + _statistics.FilesToCopy++; + _statistics.BytesToProcess += sourceFile.Length; + } + else + { + var targetFile = targetEntry.Info; + + // 两边都存在,需要解决冲突 + var conflictResult = ResolveConflict(sourceFile, targetFile); + + switch (conflictResult) + { + case ESyncConflictResolution.SourceWins: + // 只有当文件实际需要更新时才添加操作 + if (NeedsUpdate(sourceFile, targetFile)) { - // 使用回收站删除文件 - FileOperationAPIWrapper.MoveToRecycleBin(actualTarget); + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length, + ConflictResolution = conflictResult + }); + _statistics.FilesToUpdate++; + _statistics.BytesToProcess += sourceFile.Length; } else { - File.Delete(actualTarget); + _statistics.FilesSkipped++; } - _statistics.FilesDeleted++; - } - break; + break; - case SyncActionType.DeleteDirectory: - if (Directory.Exists(actualTarget)) - { - if (_options.UseRecycleBin) + case ESyncConflictResolution.TargetWins: + // 只有当文件实际需要更新时才添加操作 + if (NeedsUpdate(targetFile, sourceFile)) { - // 使用回收站删除目录 - FileOperationAPIWrapper.MoveToRecycleBin(actualTarget); + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = targetFilePath, + TargetPath = sourceFilePath, + RelativePath = relativePath, + Size = targetFile.Length, + Direction = ESyncDirection.TargetToSource, + ConflictResolution = conflictResult + }); + _statistics.FilesToUpdate++; + _statistics.BytesToProcess += targetFile.Length; } else { - Directory.Delete(actualTarget, true); + _statistics.FilesSkipped++; } - _statistics.DirectoriesDeleted++; - } - break; + break; - case SyncActionType.RenameFile: - if (File.Exists(actualSource) && !File.Exists(actualTarget)) - { - await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); - File.Move(actualSource, actualTarget); - _statistics.FilesRenamed++; - } - break; - } + case ESyncConflictResolution.KeepBoth: + // 保留两个版本,使用时间戳创建唯一的重命名文件名 + string targetNewName = GetConflictFileName(targetFilePath); + + // 首先重命名目标文件 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.RenameFile, + SourcePath = targetFilePath, + TargetPath = targetNewName, + RelativePath = GetRelativePath(targetNewName, _options.TargetPath), + ConflictResolution = conflictResult + }); + + // 然后复制源文件到目标 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + + _statistics.FilesToCopy++; + _statistics.BytesToProcess += sourceFile.Length; + break; - action.Status = SyncActionStatus.Completed; + case ESyncConflictResolution.Skip: + _statistics.FilesSkipped++; + break; + } + } } - catch (Exception ex) + + // 2.2: 从目标处理到源(只处理源中不存在的文件) + if (targetRelativePathMap.Count > 0) { - action.Status = SyncActionStatus.Failed; - action.ErrorMessage = ex.Message; - _statistics.Errors++; + // 只处理那些尚未处理过的目标文件 + var unprocessedTargetFiles = targetRelativePathMap + .Where(entry => !processedPaths.Contains(entry.Key)) + .ToList(); - if (_options.ContinueOnError) + // 并行处理大量文件时更高效 + if (unprocessedTargetFiles.Count > 1000 && _options.EnableParallelFileOperations) { - // 记录错误但继续执行 - ReportProgress($"错误: {ex.Message}", -1); + var additionalActions = unprocessedTargetFiles + .AsParallel() + .Select(entry => + { + var relativePath = entry.Key; + var targetFilePath = entry.Value.FullPath; + var targetFile = entry.Value.Info; + var sourceFilePath = Path.Combine(_options.SourcePath, relativePath); + + _statistics.FilesToCopy++; + _statistics.BytesToProcess += targetFile.Length; + + return new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = targetFilePath, + TargetPath = sourceFilePath, + RelativePath = relativePath, + Size = targetFile.Length, + Direction = ESyncDirection.TargetToSource + }; + }) + .ToList(); + + actions.AddRange(additionalActions); } else { - // 出错时中断操作 - throw; + // 对于少量文件,顺序处理更有效 + foreach (var entry in unprocessedTargetFiles) + { + var relativePath = entry.Key; + var targetFilePath = entry.Value.FullPath; + var targetFile = entry.Value.Info; + var sourceFilePath = Path.Combine(_options.SourcePath, relativePath); + + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = targetFilePath, + TargetPath = sourceFilePath, + RelativePath = relativePath, + Size = targetFile.Length, + Direction = ESyncDirection.TargetToSource + }); + + _statistics.FilesToCopy++; + _statistics.BytesToProcess += targetFile.Length; + } } } + + // 按优先级排序操作,这样可以确保目录先创建 + return actions.OrderBy(a => GetActionPriority(a.ActionType)).ToList(); } /// - /// 检查两个文件是否需要更新 + /// 优化的单向同步创建方法,减少冗余计算 /// - private bool NeedsUpdate(FileInfo sourceFile, FileInfo targetFile) + private List CreateOneWaySyncActionsOptimized( + Dictionary sourceFiles, + HashSet sourceDirs, + Dictionary targetFiles, + HashSet targetDirs) { - // 检查文件大小 - if (sourceFile.Length != targetFile.Length) - return true; + // 预估总共需要处理的项目数 + var totalItems = sourceDirs.Count + sourceFiles.Count; + var processedItems = 0; + var lastProgressReport = DateTime.MinValue; - // 检查修改时间 - if (_options.CompareMethod == CompareMethod.DateTime || - _options.CompareMethod == CompareMethod.DateTimeAndSize) - { - // 使用阈值比较时间,避免因时区等问题导致的微小差异 - var timeDiff = Math.Abs((sourceFile.LastWriteTimeUtc - targetFile.LastWriteTimeUtc).TotalSeconds); - if (timeDiff > _options.DateTimeThresholdSeconds) - return true; - } + ReportProgress($"正在分析同步计划:共 {sourceDirs.Count} 个目录及 {sourceFiles.Count} 个文件需要处理", 0); - // 检查文件内容 - if (_options.CompareMethod == CompareMethod.Content || - _options.CompareMethod == CompareMethod.Hash) + var actions = new List(sourceFiles.Count + sourceDirs.Count); + var targetDirSet = new HashSet(targetDirs, StringComparer.OrdinalIgnoreCase); + + // 1. 批量处理目录创建 - 预先分配好容量 + ReportProgress("正在分析目录结构...", 0); + var dirStartTime = DateTime.Now; + + // 1. 批量处理目录创建 - 预先分配好容量 + foreach (var sourceDir in sourceDirs) { - try + var relativePath = GetRelativePath(sourceDir, _options.SourcePath); + var targetDirPath = Path.Combine(_options.TargetPath, relativePath); + + if (!targetDirSet.Contains(targetDirPath)) { - return !FilesAreEqual(sourceFile, targetFile); + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CreateDirectory, + SourcePath = sourceDir, + TargetPath = targetDirPath, + RelativePath = relativePath + }); } - catch (Exception) + + // 更新进度 + processedItems++; + if (ShouldReportProgress(processedItems, totalItems, ref lastProgressReport)) { - // 文件比较出错,安全起见认为需要更新 - return true; + double progressPercent = (double)processedItems / totalItems * 100; + ReportProgress($"正在分析目录结构: {processedItems}/{totalItems} ({progressPercent:F1}%)", (int)progressPercent); } } - return false; - } + var dirTime = DateTime.Now - dirStartTime; + ReportProgress($"目录分析完成,用时: {dirTime.TotalSeconds:F2}秒", (int)((double)processedItems / totalItems * 100)); - /// - /// 比较两个文件内容是否相同 - /// - private bool FilesAreEqual(FileInfo sourceFile, FileInfo targetFile) - { - if (_options.CompareMethod == CompareMethod.Hash) - { - return CompareFileHash(sourceFile, targetFile); - } - else // 内容比较 - { - return CompareFileContent(sourceFile, targetFile); - } - } + // 2. 批量处理文件 - 减少字符串操作次数 + ReportProgress($"正在分析文件: 共 {sourceFiles.Count} 个", (int)((double)processedItems / totalItems * 100)); + var fileStartTime = DateTime.Now; - /// - /// 使用哈希算法比较文件 - /// - private bool CompareFileHash(FileInfo sourceFile, FileInfo targetFile) - { - if (_options.SamplingRate < 1.0) - { - return CompareFileHashWithSampling(sourceFile, targetFile); - } + // 将目标文件路径转换为哈希集合供高效查找 + var targetPathLookup = PrepareTargetFileLookup(targetFiles); - // 全文件哈希比较 - var hashAlgorithm = GetHashAlgorithm().ToString(); + int filesToCopy = 0; + int filesToUpdate = 0; + int filesSkipped = 0; + long bytesToProcess = 0; - // 计算源文件哈希 - byte[] sourceHash; - using (var stream = sourceFile.OpenRead()) - { - sourceHash = HashHelper.ComputeHash(stream, hashAlgorithm); - } + int currentFileIndex = 0; - // 计算目标文件哈希 - byte[] targetHash; - using (var stream = targetFile.OpenRead()) + // 2. 批量处理文件 - 减少字符串操作次数 + foreach (var sourceEntry in sourceFiles) { - targetHash = HashHelper.ComputeHash(stream, hashAlgorithm); - } - - // 比较哈希值 - return CompareHash(sourceHash, targetHash); - } - - /// - /// 使用抽样哈希比较文件 - /// - private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFile) - { - const int headerSize = 8192; // 头部始终比较的大小 - const int randomSampleSize = 8192; // 随机抽样的块大小 + var sourceFilePath = sourceEntry.Key; + var sourceFile = sourceEntry.Value; + var relativePath = GetRelativePath(sourceFilePath, _options.SourcePath); + var targetFilePath = Path.Combine(_options.TargetPath, relativePath); - // 如果文件较小,直接全文比较 - if (sourceFile.Length < headerSize * 2) - return CompareFileHash(sourceFile, targetFile); + bool needsAction = false; - var hashAlgorithm = GetHashAlgorithm().ToString(); + // 检查目标文件是否存在 + if (!targetPathLookup.TryGetValue(targetFilePath, out var targetFile)) + { + // 目标不存在文件,需要复制 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + filesToCopy++; + bytesToProcess += sourceFile.Length; + needsAction = true; + } + else if (NeedsUpdate(sourceFile, targetFile)) + { + // 文件需要更新 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + filesToUpdate++; + bytesToProcess += sourceFile.Length; + needsAction = true; + } + else + { + filesSkipped++; + } - using var sourceStream = sourceFile.OpenRead(); - using var targetStream = targetFile.OpenRead(); + // 更新处理进度 + processedItems++; + currentFileIndex++; - // 比较文件头部 - byte[] sourceHeader = new byte[headerSize]; - byte[] targetHeader = new byte[headerSize]; + if (ShouldReportProgress(processedItems, totalItems, ref lastProgressReport)) + { + double progressPercent = (double)processedItems / totalItems * 100; + string actionText = needsAction ? "需要处理" : "可以跳过"; + + // 计算处理速度 (文件/秒) + var elapsed = DateTime.Now - fileStartTime; + double filesPerSecond = elapsed.TotalSeconds > 0 + ? currentFileIndex / elapsed.TotalSeconds + : 0; + + // 预估剩余时间 + var remainingFiles = sourceFiles.Count - currentFileIndex; + string remainingTime = filesPerSecond > 0 + ? $", 剩余时间: {TimeSpan.FromSeconds(remainingFiles / filesPerSecond):mm\\:ss}" + : ""; + + ReportProgress($"正在分析文件: {processedItems}/{totalItems} " + + $"[复制:{filesToCopy}, 更新:{filesToUpdate}, 跳过:{filesSkipped}], " + + $"速度: {filesPerSecond:F1}文件/秒{remainingTime}", + (int)progressPercent); + } + } - sourceStream.Read(sourceHeader, 0, headerSize); - targetStream.Read(targetHeader, 0, headerSize); + var fileTime = DateTime.Now - fileStartTime; - if (!CompareBytes(sourceHeader, targetHeader)) - return false; + // 更新同步统计信息 + _statistics.FilesToCopy = filesToCopy; + _statistics.FilesToUpdate = filesToUpdate; + _statistics.FilesSkipped = filesSkipped; + _statistics.BytesToProcess = bytesToProcess; - // 比较文件尾部 - byte[] sourceFooter = new byte[headerSize]; - byte[] targetFooter = new byte[headerSize]; + ReportProgress($"文件分析完成,用时: {fileTime.TotalSeconds:F2}秒。需要复制: {filesToCopy}个, " + + $"需要更新: {filesToUpdate}个, 可跳过: {filesSkipped}个, 总数据量: {FormatBytes(bytesToProcess)}", 100); - sourceStream.Seek(-headerSize, SeekOrigin.End); - targetStream.Seek(-headerSize, SeekOrigin.End); + return actions; + } - sourceStream.Read(sourceFooter, 0, headerSize); - targetStream.Read(targetFooter, 0, headerSize); + /// + /// 准备目标文件路径查找表以加速查找 + /// + private Dictionary PrepareTargetFileLookup(Dictionary targetFiles) + { + // 对于小规模文件集合,直接返回原字典 + if (targetFiles.Count < 1000) + return targetFiles; - if (!CompareBytes(sourceFooter, targetFooter)) - return false; + // 对于大规模文件集合,创建一个更高效的查找字典 + // 使用相同的字符串比较器确保查找时不区分大小写 + return new Dictionary(targetFiles, StringComparer.OrdinalIgnoreCase); + } - // 生成随机抽样点 - Random random = new Random(sourceFile.FullName.GetHashCode()); - int samplesCount = (int)Math.Max(1, (sourceFile.Length - headerSize * 2) * _options.SamplingRate / randomSampleSize); - long range = sourceFile.Length - headerSize * 2 - randomSampleSize; + /// + /// 格式化字节大小为人类可读格式 + /// + private string FormatBytes(long bytes) + { + string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; + int i = 0; + double size = bytes; - for (int i = 0; i < samplesCount; i++) + while (size >= 1024 && i < suffixes.Length - 1) { - // 生成随机位置(避开头尾已比较的部分) - long position = headerSize + (long)(random.NextDouble() * range); + size /= 1024; + i++; + } - byte[] sourceSample = new byte[randomSampleSize]; - byte[] targetSample = new byte[randomSampleSize]; + return $"{size:F2} {suffixes[i]}"; + } - sourceStream.Seek(position, SeekOrigin.Begin); - targetStream.Seek(position, SeekOrigin.Begin); + /// + /// 判断是否应该报告进度(控制报告频率) + /// + private bool ShouldReportProgress(int processedItems, int totalItems, ref DateTime lastReport) + { + var now = DateTime.Now; - sourceStream.Read(sourceSample, 0, randomSampleSize); - targetStream.Read(targetSample, 0, randomSampleSize); + // 在以下情况报告进度: + // 1. 处理了100个项目 + // 2. 已经过去了200毫秒 + // 3. 是首个项目或最后一个项目 + bool shouldReport = processedItems % 100 == 0 || + (now - lastReport).TotalMilliseconds > 200 || + processedItems == 1 || + processedItems == totalItems; - if (!CompareBytes(sourceSample, targetSample)) - return false; - } + if (shouldReport) + lastReport = now; - return true; + return shouldReport; } /// - /// 比较文件内容 + /// 并行处理同步操作 /// - private bool CompareFileContent(FileInfo sourceFile, FileInfo targetFile) + private async Task ProcessActionsInParallelAsync(List actions) { - const int bufferSize = 4096; - - using var sourceStream = sourceFile.OpenRead(); - using var targetStream = targetFile.OpenRead(); - - byte[] sourceBuffer = new byte[bufferSize]; - byte[] targetBuffer = new byte[bufferSize]; + // 分组处理不同类型的操作 + var actionGroups = actions + .GroupBy(a => GetActionPriority(a.ActionType)) + .OrderBy(g => g.Key); - while (true) + foreach (var group in actionGroups) { - int sourceBytesRead = sourceStream.Read(sourceBuffer, 0, bufferSize); - int targetBytesRead = targetStream.Read(targetBuffer, 0, bufferSize); + var groupActions = group.ToList(); + var actionType = groupActions.FirstOrDefault()?.ActionType; - if (sourceBytesRead != targetBytesRead) - return false; + ReportProgress($"处理 {GetActionTypeDescription(actionType.Value)} 操作,共 {groupActions.Count} 项", + CalculateProgress(_processedItems, _totalItems)); - if (sourceBytesRead == 0) - break; + // 设置并行度,文件操作使用配置的并行度,目录操作单线程执行 + int parallelism = IsFileOperation(actionType.Value) + ? _options.MaxParallelOperations + : 1; + + await Parallel.ForEachAsync( + groupActions, + new ParallelOptions + { + MaxDegreeOfParallelism = parallelism, + CancellationToken = _cancellationToken + }, + async (action, ct) => + { + await ExecuteSingleActionAsync(action); + Interlocked.Increment(ref _processedItems); + + if (_processedItems % 10 == 0 || _processedItems == _totalItems) + { + ReportProgress( + $"正在处理 {GetActionTypeDescription(action.ActionType)} 操作 ({_processedItems}/{_totalItems})", + CalculateProgress(_processedItems, _totalItems) + ); + } + }); + } + } + + /// + /// 串行处理同步操作 + /// + private async Task ProcessActionsSequentiallyAsync(List actions) + { + for (int i = 0; i < actions.Count; i++) + { + _cancellationToken.ThrowIfCancellationRequested(); + + var action = actions[i]; + await ExecuteSingleActionAsync(action); + + _processedItems++; + + if (i % 5 == 0 || i == actions.Count - 1) + { + ReportProgress( + $"正在处理 {GetActionTypeDescription(action.ActionType)} 操作 ({_processedItems}/{_totalItems})", + CalculateProgress(_processedItems, _totalItems) + ); + } + } + } + + /// + /// 执行单个同步操作 + /// + private async Task ExecuteSingleActionAsync(SyncAction action) + { + try + { + _cancellationToken.ThrowIfCancellationRequested(); + + // 确定源和目标路径(考虑同步方向) + string actualSource = action.Direction == ESyncDirection.SourceToTarget + ? action.SourcePath + : action.TargetPath; + + string actualTarget = action.Direction == ESyncDirection.SourceToTarget + ? action.TargetPath + : action.SourcePath; + + switch (action.ActionType) + { + case ESyncActionType.CreateDirectory: + if (!Directory.Exists(actualTarget)) + { + Directory.CreateDirectory(actualTarget); + _statistics.DirectoriesCreated++; + } + break; + + case ESyncActionType.CopyFile: + await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); + await CopyFileWithRetryAsync(actualSource, actualTarget); + _statistics.FilesCopied++; + _statistics.BytesProcessed += action.Size; + break; + + case ESyncActionType.UpdateFile: + await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); + await CopyFileWithRetryAsync(actualSource, actualTarget); + _statistics.FilesUpdated++; + _statistics.BytesProcessed += action.Size; + break; + + case ESyncActionType.DeleteFile: + if (File.Exists(actualTarget)) + { + if (_options.UseRecycleBin) + { + // 使用回收站删除文件 + FileOperationAPIWrapper.MoveToRecycleBin(actualTarget); + } + else + { + File.Delete(actualTarget); + } + _statistics.FilesDeleted++; + } + break; + + case ESyncActionType.DeleteDirectory: + if (Directory.Exists(actualTarget)) + { + if (_options.UseRecycleBin) + { + // 使用回收站删除目录 + FileOperationAPIWrapper.MoveToRecycleBin(actualTarget); + } + else + { + Directory.Delete(actualTarget, true); + } + _statistics.DirectoriesDeleted++; + } + break; + + case ESyncActionType.RenameFile: + if (File.Exists(actualSource) && !File.Exists(actualTarget)) + { + await EnsureDirectoryExistsAsync(Path.GetDirectoryName(actualTarget)); + File.Move(actualSource, actualTarget); + _statistics.FilesRenamed++; + } + break; + } + + action.Status = ESyncActionStatus.Completed; + } + catch (Exception ex) + { + action.Status = ESyncActionStatus.Failed; + action.ErrorMessage = ex.Message; + _statistics.Errors++; + + if (_options.ContinueOnError) + { + // 记录错误但继续执行 + ReportProgress($"错误: {ex.Message}", -1); + } + else + { + // 出错时中断操作 + throw; + } + } + } + + /// + /// 检查两个文件是否需要更新 + /// + private bool NeedsUpdate(FileInfo sourceFile, FileInfo targetFile) + { + // 检查文件大小 + if (sourceFile.Length != targetFile.Length) + return true; + + // 检查修改时间 + if (_options.CompareMethod == ESyncCompareMethod.DateTime || + _options.CompareMethod == ESyncCompareMethod.DateTimeAndSize) + { + // 使用阈值比较时间,避免因时区等问题导致的微小差异 + var timeDiff = Math.Abs((sourceFile.LastWriteTimeUtc - targetFile.LastWriteTimeUtc).TotalSeconds); + if (timeDiff > _options.DateTimeThresholdSeconds) + return true; + } + + // 检查文件内容 + if (_options.CompareMethod == ESyncCompareMethod.Content || + _options.CompareMethod == ESyncCompareMethod.Hash) + { + try + { + return !FilesAreEqual(sourceFile, targetFile); + } + catch (Exception ex) + { + Log.Error(ex, "文件比较失败: {SourceFile} vs {TargetFile}", sourceFile.FullName, targetFile.FullName); + + // 文件比较出错,安全起见认为需要更新 + return true; + } + } + + return false; + } + + /// + /// 比较两个文件内容是否相同 + /// + private bool FilesAreEqual(FileInfo sourceFile, FileInfo targetFile) + { + if (_options.CompareMethod == ESyncCompareMethod.Hash) + { + return CompareFileHash(sourceFile, targetFile); + } + else // 内容比较 + { + return CompareFileContent(sourceFile, targetFile); + } + } + + /// + /// 使用哈希算法比较文件 + /// + private bool CompareFileHash(FileInfo sourceFile, FileInfo targetFile) + { + if (_options.SamplingRate > 0 && _options.SamplingRate < 1.0) + { + return CompareFileHashWithSampling(sourceFile, targetFile); + } + + // 全文件哈希比较 + var hashAlgorithm = GetHashAlgorithm().ToString(); + + // 计算源文件哈希 + byte[] sourceHash; + using (var stream = sourceFile.OpenRead()) + { + sourceHash = HashHelper.ComputeHash(stream, hashAlgorithm); + } + + // 计算目标文件哈希 + byte[] targetHash; + using (var stream = targetFile.OpenRead()) + { + targetHash = HashHelper.ComputeHash(stream, hashAlgorithm); + } + + // 比较哈希值 + return CompareHash(sourceHash, targetHash); + } + + /// + /// 使用抽样哈希比较文件 + /// + private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFile) + { + const int headerSize = 8192; // 头部始终比较的大小 + const int randomSampleSize = 8192; // 随机抽样的块大小 + + // 如果文件较小,直接全文比较 + if (sourceFile.Length < headerSize * 2) + return CompareFileHash(sourceFile, targetFile); + + var hashAlgorithm = GetHashAlgorithm().ToString(); + + using var sourceStream = sourceFile.OpenRead(); + using var targetStream = targetFile.OpenRead(); + + // 比较文件头部 + byte[] sourceHeader = new byte[headerSize]; + byte[] targetHeader = new byte[headerSize]; + + sourceStream.Read(sourceHeader, 0, headerSize); + targetStream.Read(targetHeader, 0, headerSize); + + if (!CompareBytes(sourceHeader, targetHeader)) + return false; + + // 比较文件尾部 + byte[] sourceFooter = new byte[headerSize]; + byte[] targetFooter = new byte[headerSize]; + + sourceStream.Seek(-headerSize, SeekOrigin.End); + targetStream.Seek(-headerSize, SeekOrigin.End); + + sourceStream.Read(sourceFooter, 0, headerSize); + targetStream.Read(targetFooter, 0, headerSize); + + if (!CompareBytes(sourceFooter, targetFooter)) + return false; + + // 生成随机抽样点 + Random random = new Random(sourceFile.FullName.GetHashCode()); + int samplesCount = (int)Math.Max(1, (sourceFile.Length - headerSize * 2) * _options.SamplingRate / randomSampleSize); + long range = sourceFile.Length - headerSize * 2 - randomSampleSize; + + for (int i = 0; i < samplesCount; i++) + { + // 生成随机位置(避开头尾已比较的部分) + long position = headerSize + (long)(random.NextDouble() * range); + + byte[] sourceSample = new byte[randomSampleSize]; + byte[] targetSample = new byte[randomSampleSize]; + + sourceStream.Seek(position, SeekOrigin.Begin); + targetStream.Seek(position, SeekOrigin.Begin); + + sourceStream.Read(sourceSample, 0, randomSampleSize); + targetStream.Read(targetSample, 0, randomSampleSize); + + if (!CompareBytes(sourceSample, targetSample)) + return false; + } + + return true; + } + + /// + /// 比较文件内容 + /// + private bool CompareFileContent(FileInfo sourceFile, FileInfo targetFile) + { + const int bufferSize = 4096; + + using var sourceStream = sourceFile.OpenRead(); + using var targetStream = targetFile.OpenRead(); + + byte[] sourceBuffer = new byte[bufferSize]; + byte[] targetBuffer = new byte[bufferSize]; + + while (true) + { + int sourceBytesRead = sourceStream.Read(sourceBuffer, 0, bufferSize); + int targetBytesRead = targetStream.Read(targetBuffer, 0, bufferSize); + + if (sourceBytesRead != targetBytesRead) + return false; + + if (sourceBytesRead == 0) + break; + + for (int i = 0; i < sourceBytesRead; i++) + { + if (sourceBuffer[i] != targetBuffer[i]) + return false; + } + } + + return true; + } + + /// + /// 扫描目录并获取文件和子目录列表 + /// + private async Task<(Dictionary Files, HashSet Directories)> ScanDirectoryAsync(string path, string description) + { + ReportProgress($"正在扫描{description}...", -1); + + try + { + // 尝试使用 FileUltraScanner(高性能扫描器) + var ultraScanResult = await ScanWithFileUltraScannerAsync(path); + return ultraScanResult; + } + catch (Exception ex) + { + Log.Error(ex, "高性能扫描器失败,切换到备用扫描器"); + + ReportProgress($"高性能扫描器失败,切换到备用扫描器", -1); + + // 退回到 FileFastScanner(备用扫描器) + var fastScanResult = await ScanWithFileFastScannerAsync(path); + return fastScanResult; + } + } + + /// + /// 使用 FileUltraScanner 扫描目录 + /// + private async Task<(Dictionary, HashSet)> ScanWithFileUltraScannerAsync(string path) + { + FileUltraScanner scanner = new FileUltraScanner( + path, + _options.MaxParallelOperations, + 8192, + _options.FollowSymlinks, + _options.IgnorePatterns?.ToList() + ); + + var result = await scanner.ScanAsync( + reportProgress: _options.Verbose, + cancellationToken: _cancellationToken + ); + + var files = result.Files + .ToDictionary( + f => f, + f => new FileInfo(f), + StringComparer.OrdinalIgnoreCase + ); + + var directories = new HashSet(result.Directories, StringComparer.OrdinalIgnoreCase); + + return (files, directories); + } + + /// + /// 使用 FileFastScanner 扫描目录 + /// + private async Task<(Dictionary, HashSet)> ScanWithFileFastScannerAsync(string path) + { + var scanner = new FileFastScanner(); + var result = scanner.ScanAsync( + path, + "*", + _options.IgnorePatterns, + _options.MaxParallelOperations, + reportProgress: _options.Verbose, + cancellationToken: _cancellationToken + ); + + var files = result.Files + .ToDictionary( + f => f, + f => new FileInfo(f), + StringComparer.OrdinalIgnoreCase + ); + + var directories = new HashSet(result.Directories, StringComparer.OrdinalIgnoreCase); + + return (files, directories); + } + + /// + /// 解析冲突 + /// + private ESyncConflictResolution ResolveConflict(FileInfo sourceFile, FileInfo targetFile) + { + switch (_options.ConflictResolution) + { + case ESyncConflictResolution.SourceWins: + return ESyncConflictResolution.SourceWins; + + case ESyncConflictResolution.TargetWins: + return ESyncConflictResolution.TargetWins; + + case ESyncConflictResolution.KeepBoth: + return ESyncConflictResolution.KeepBoth; + + case ESyncConflictResolution.Skip: + return ESyncConflictResolution.Skip; + + case ESyncConflictResolution.Newer: + return sourceFile.LastWriteTimeUtc > targetFile.LastWriteTimeUtc + ? ESyncConflictResolution.SourceWins + : ESyncConflictResolution.TargetWins; + + case ESyncConflictResolution.Older: + return sourceFile.LastWriteTimeUtc < targetFile.LastWriteTimeUtc + ? ESyncConflictResolution.SourceWins + : ESyncConflictResolution.TargetWins; + + case ESyncConflictResolution.Larger: + return sourceFile.Length > targetFile.Length + ? ESyncConflictResolution.SourceWins + : ESyncConflictResolution.TargetWins; + + default: + return ESyncConflictResolution.Skip; + } + } + + /// + /// 获取用于解决冲突的新文件名 + /// + private string GetConflictFileName(string originalPath) + { + string dir = Path.GetDirectoryName(originalPath); + string fileName = Path.GetFileNameWithoutExtension(originalPath); + string ext = Path.GetExtension(originalPath); + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + + return Path.Combine(dir, $"{fileName} ({timestamp}){ext}"); + } + + /// + /// 获取操作优先级(用于排序) + /// + private int GetActionPriority(ESyncActionType actionType) + { + switch (actionType) + { + case ESyncActionType.CreateDirectory: + return 1; + + case ESyncActionType.CopyFile: + case ESyncActionType.UpdateFile: + return 2; + + case ESyncActionType.RenameFile: + return 3; + + case ESyncActionType.DeleteFile: + return 4; + + case ESyncActionType.DeleteDirectory: + return 5; + + default: + return 99; + } + } + + /// + /// 获取操作类型描述 + /// + private string GetActionTypeDescription(ESyncActionType actionType) + { + switch (actionType) + { + case ESyncActionType.CreateDirectory: + return "创建目录"; + + case ESyncActionType.CopyFile: + return "复制文件"; + + case ESyncActionType.UpdateFile: + return "更新文件"; + + case ESyncActionType.DeleteFile: + return "删除文件"; + + case ESyncActionType.DeleteDirectory: + return "删除目录"; + + case ESyncActionType.RenameFile: + return "重命名文件"; + + default: + return "未知操作"; + } + } + + /// + /// 获取文件的相对路径 + /// + private string GetRelativePath(string fullPath, string basePath) + { + // 确保路径以分隔符结尾以正确处理相对路径 + if (!basePath.EndsWith(Path.DirectorySeparatorChar.ToString())) + basePath += Path.DirectorySeparatorChar; + + if (fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) + { + var relativePath = fullPath.Substring(basePath.Length); + return relativePath; + } + + return fullPath; + } + + /// + /// 确保目录存在 + /// + private Task EnsureDirectoryExistsAsync(string directory) + { + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + return Task.CompletedTask; + } + + /// + /// 创建哈希算法实例 + /// + private EHashType? GetHashAlgorithm() + { + return _options.HashAlgorithm; + } + + /// + /// 比较两个哈希值是否相同 + /// + private bool CompareHash(byte[] hash1, byte[] hash2) + { + if (hash1.Length != hash2.Length) + return false; + + for (int i = 0; i < hash1.Length; i++) + { + if (hash1[i] != hash2[i]) + return false; + } + + return true; + } + + /// + /// 比较两个字节数组是否相同 + /// + private bool CompareBytes(byte[] buffer1, byte[] buffer2) + { + if (buffer1.Length != buffer2.Length) + return false; + + for (int i = 0; i < buffer1.Length; i++) + { + if (buffer1[i] != buffer2[i]) + return false; + } + + return true; + } + + /// + /// 带重试的文件复制操作 + /// + private async Task CopyFileWithRetryAsync(string sourcePath, string targetPath) + { + int maxRetries = _options.MaxRetries; + int currentRetry = 0; + bool success = false; + + while (!success && currentRetry <= maxRetries) + { + try + { + if (currentRetry > 0) + { + // 添加延迟重试 + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, currentRetry - 1)), _cancellationToken); + ReportProgress($"重试复制文件 {Path.GetFileName(sourcePath)} (尝试 {currentRetry}/{maxRetries})", -1); + } - for (int i = 0; i < sourceBytesRead; i++) + if (_options.PreserveFileTime) + { + // 保留原始时间戳 + using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan)) + using (FileStream targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan)) + { + await sourceStream.CopyToAsync(targetStream, 81920, _cancellationToken); + } + + // 设置目标文件的时间戳与源文件相同 + File.SetCreationTimeUtc(targetPath, File.GetCreationTimeUtc(sourcePath)); + File.SetLastWriteTimeUtc(targetPath, File.GetLastWriteTimeUtc(sourcePath)); + File.SetLastAccessTimeUtc(targetPath, File.GetLastAccessTimeUtc(sourcePath)); + } + else + { + // 简单复制,不保留时间戳 + File.Copy(sourcePath, targetPath, true); + } + + success = true; + } + catch (IOException) when (currentRetry < maxRetries) { - if (sourceBuffer[i] != targetBuffer[i]) - return false; + currentRetry++; + } + catch (Exception) + { + // 其他异常直接抛出 + throw; } } - return true; - } + if (!success) + { + throw new IOException($"复制文件 {sourcePath} 到 {targetPath} 失败,已重试 {maxRetries} 次"); + } + } + + /// + /// 计算进度百分比 + /// + private int CalculateProgress(int current, int total) + { + if (total <= 0) return 0; + return (int)((current / (double)total) * 100); + } + + /// + /// 报告进度 + /// + private void ReportProgress(string message, int progressPercentage) + { + if (_progress == null) + { + return; + } + + // 控制进度更新频率 + var now = DateTime.Now; + if ((now - _lastProgressUpdate).TotalMilliseconds < 100 && progressPercentage >= 0 && progressPercentage < 100) + return; + + _lastProgressUpdate = now; + + double bytesPerSecond = _stopwatch.Elapsed.TotalSeconds > 0 + ? _statistics.BytesProcessed / _stopwatch.Elapsed.TotalSeconds + : 0; + + double itemsPerSecond = _stopwatch.Elapsed.TotalSeconds > 0 + ? _processedItems / _stopwatch.Elapsed.TotalSeconds + : 0; + + _progress.Report(new SyncProgress + { + Message = message, + ProgressPercentage = progressPercentage, + ElapsedTime = _stopwatch.Elapsed, + ProcessedItems = _processedItems, + TotalItems = _totalItems, + BytesProcessed = _statistics.BytesProcessed, + BytesToProcess = _statistics.BytesToProcess, + BytesPerSecond = bytesPerSecond, + ItemsPerSecond = itemsPerSecond, + Statistics = _statistics + }); + } + + /// + /// 验证同步路径 + /// + private void ValidatePaths() + { + // 验证源路径 + if (string.IsNullOrEmpty(_options.SourcePath)) + throw new ArgumentException("源路径不能为空"); + + if (!Directory.Exists(_options.SourcePath)) + throw new DirectoryNotFoundException($"源目录不存在: {_options.SourcePath}"); + + // 验证目标路径 + if (string.IsNullOrEmpty(_options.TargetPath)) + throw new ArgumentException("目标路径不能为空"); + + // 目标路径可能不存在,需要创建 + if (!Directory.Exists(_options.TargetPath) && !_options.PreviewOnly) + { + Directory.CreateDirectory(_options.TargetPath); + } + + // 检查路径不能互相包含 + string normalizedSource = Path.GetFullPath(_options.SourcePath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + string normalizedTarget = Path.GetFullPath(_options.TargetPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + + if (normalizedSource.StartsWith(normalizedTarget, StringComparison.OrdinalIgnoreCase) || + normalizedTarget.StartsWith(normalizedSource, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("源路径和目标路径不能互相包含"); + } + } + + /// + /// 判断操作是否为文件操作 + /// + private bool IsFileOperation(ESyncActionType actionType) + { + return actionType == ESyncActionType.CopyFile || + actionType == ESyncActionType.UpdateFile || + actionType == ESyncActionType.RenameFile || + actionType == ESyncActionType.DeleteFile; + } + + /// + /// 从JSON配置文件加载同步选项 + /// + public static SyncOptions LoadFromJsonFile(string configFilePath) + { + if (!File.Exists(configFilePath)) + throw new FileNotFoundException($"配置文件不存在: {configFilePath}"); + + string json = File.ReadAllText(configFilePath); + var options = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }); + + return options; + } + + /// + /// 保存同步选项到JSON配置文件 + /// + public static void SaveToJsonFile(SyncOptions options, string configFilePath) + { + string json = JsonSerializer.Serialize(options, new JsonSerializerOptions + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() } + }); + + File.WriteAllText(configFilePath, json); + } + } + + /// + /// 同步选项类 + /// + public class SyncOptions + { + /// + /// 源目录路径 + /// + public string SourcePath { get; set; } + + /// + /// 目标目录路径 + /// + public string TargetPath { get; set; } + + /// + /// 同步模式 + /// + public ESyncMode SyncMode { get; set; } = ESyncMode.OneWay; + + /// + /// 文件比较方法 + /// + public ESyncCompareMethod CompareMethod { get; set; } = ESyncCompareMethod.DateTimeAndSize; + + /// + /// 哈希算法类型 + /// + public EHashType? HashAlgorithm { get; set; } + + /// + /// 哈希抽样率(0.0-1.0) + /// + public double SamplingRate { get; set; } = 0.1; + + /// + /// 最大并行操作数 + /// + public int MaxParallelOperations { get; set; } = Math.Max(1, Environment.ProcessorCount); + + /// + /// 是否启用并行文件操作 + /// + public bool EnableParallelFileOperations { get; set; } = true; + + /// + /// 日期时间比较阈值(秒) + /// + public int DateTimeThresholdSeconds { get; set; } = 0; + + /// + /// 是否保留原始文件时间 + /// + public bool PreserveFileTime { get; set; } = true; + + /// + /// 是否使用回收站删除文件 + /// + public bool UseRecycleBin { get; set; } = true; + + /// + /// 冲突解决策略 + /// + public ESyncConflictResolution ConflictResolution { get; set; } = ESyncConflictResolution.Newer; + + /// + /// 是否跟踪符号链接 + /// + public bool FollowSymlinks { get; set; } = false; + + /// + /// 是否仅预览,不执行实际操作 + /// + public bool PreviewOnly { get; set; } = false; + + /// + /// 是否在出错时继续 + /// + public bool ContinueOnError { get; set; } = true; + + /// + /// 最大重试次数 + /// + public int MaxRetries { get; set; } = 3; + + /// + /// 是否启用详细输出 + /// + public bool Verbose { get; set; } = false; + + /// + /// 要忽略的文件/目录模式 + /// + public IEnumerable IgnorePatterns { get; set; } = new List + { + "**/System Volume Information/**", + "**/$RECYCLE.BIN/**", + "**/Thumbs.db", + "**/*.tmp", + "**/*.temp", + "**/*.bak", + "**/@Recycle/**", + "**/@Recently-Snapshot/**", + "**/.@__thumb/**", + "**/@Transcode/**", + "**/.obsidian/**", + "**/.git/**", + "**/.svn/**", + "**/node_modules/**" + }; + + /// + /// 源存储提供者类型(用于跨服务同步) + /// + public StorageProviderType SourceProviderType { get; set; } = StorageProviderType.Local; /// - /// 扫描目录并获取文件和子目录列表 + /// 源存储提供者配置(用于跨服务同步) /// - private async Task<(Dictionary Files, HashSet Directories)> ScanDirectoryAsync(string path, string description) - { - ReportProgress($"正在扫描{description}...", -1); + public StorageProviderOptions SourceProviderOptions { get; set; } - try - { - // 尝试使用 FileUltraScanner(高性能扫描器) - var ultraScanResult = await ScanWithFileUltraScannerAsync(path); - return ultraScanResult; - } - catch (Exception ex) - { - ReportProgress($"高性能扫描器失败,切换到备用扫描器: {ex.Message}", -1); + /// + /// 目标存储提供者类型(用于跨服务同步) + /// + public StorageProviderType TargetProviderType { get; set; } = StorageProviderType.Local; - // 退回到 FileFastScanner(备用扫描器) - var fastScanResult = await ScanWithFileFastScannerAsync(path); - return fastScanResult; - } - } + /// + /// 目标存储提供者配置(用于跨服务同步) + /// + public StorageProviderOptions TargetProviderOptions { get; set; } + } + /// + /// 存储提供者类型 + /// + public enum StorageProviderType + { /// - /// 使用 FileUltraScanner 扫描目录 + /// 本地文件系统 /// - private async Task<(Dictionary, HashSet)> ScanWithFileUltraScannerAsync(string path) - { - FileUltraScanner scanner = new FileUltraScanner( - path, - _options.MaxParallelOperations, - 8192, - _options.FollowSymlinks, - _options.IgnorePatterns?.ToList() - ); + Local, - var result = await scanner.ScanAsync( - reportProgress: _options.Verbose, - cancellationToken: _cancellationToken - ); + /// + /// FTP服务器 + /// + Ftp, - var files = result.Files - .ToDictionary( - f => f, - f => new FileInfo(f), - StringComparer.OrdinalIgnoreCase - ); + /// + /// SFTP服务器 + /// + Sftp, - var directories = new HashSet(result.Directories, StringComparer.OrdinalIgnoreCase); + /// + /// WebDAV服务器 + /// + WebDav, - return (files, directories); - } + /// + /// 阿里云盘 + /// + AliyunDrive, /// - /// 使用 FileFastScanner 扫描目录 + /// 阿里云OSS /// - private async Task<(Dictionary, HashSet)> ScanWithFileFastScannerAsync(string path) - { - var scanner = new FileFastScanner(); - var result = scanner.ScanAsync( - path, - "*", - _options.IgnorePatterns, - _options.MaxParallelOperations, - reportProgress: _options.Verbose, - cancellationToken: _cancellationToken - ); + AliyunOSS, - var files = result.Files - .ToDictionary( - f => f, - f => new FileInfo(f), - StringComparer.OrdinalIgnoreCase - ); + /// + /// 腾讯云COS + /// + TencentCOS, - var directories = new HashSet(result.Directories, StringComparer.OrdinalIgnoreCase); + /// + /// AWS S3 + /// + S3, - return (files, directories); - } + /// + /// SMB/CIFS共享 + /// + SMB, /// - /// 解析冲突 + /// Google Drive /// - private ConflictResolution ResolveConflict(FileInfo sourceFile, FileInfo targetFile) - { - switch (_options.ConflictResolution) - { - case ConflictResolution.SourceWins: - return ConflictResolution.SourceWins; + GoogleDrive, - case ConflictResolution.TargetWins: - return ConflictResolution.TargetWins; + /// + /// OneDrive + /// + OneDrive, - case ConflictResolution.KeepBoth: - return ConflictResolution.KeepBoth; + /// + /// Dropbox + /// + Dropbox, - case ConflictResolution.Skip: - return ConflictResolution.Skip; + /// + /// 百度网盘 + /// + BaiduPan, - case ConflictResolution.Newer: - return sourceFile.LastWriteTimeUtc > targetFile.LastWriteTimeUtc - ? ConflictResolution.SourceWins - : ConflictResolution.TargetWins; + /// + /// 自定义HTTP API + /// + CustomApi + } - case ConflictResolution.Older: - return sourceFile.LastWriteTimeUtc < targetFile.LastWriteTimeUtc - ? ConflictResolution.SourceWins - : ConflictResolution.TargetWins; + /// + /// 存储提供者选项基类 + /// + [JsonDerivedType(typeof(FtpProviderOptions), typeDiscriminator: "Ftp")] + [JsonDerivedType(typeof(SftpProviderOptions), typeDiscriminator: "Sftp")] + [JsonDerivedType(typeof(WebDavProviderOptions), typeDiscriminator: "WebDav")] + [JsonDerivedType(typeof(AliyunDriveProviderOptions), typeDiscriminator: "AliyunDrive")] + [JsonDerivedType(typeof(S3ProviderOptions), typeDiscriminator: "S3")] + [JsonDerivedType(typeof(SmbProviderOptions), typeDiscriminator: "SMB")] + public abstract class StorageProviderOptions + { + /// + /// 连接超时(秒) + /// + public int ConnectionTimeout { get; set; } = 30; - case ConflictResolution.Larger: - return sourceFile.Length > targetFile.Length - ? ConflictResolution.SourceWins - : ConflictResolution.TargetWins; + /// + /// 操作超时(秒) + /// + public int OperationTimeout { get; set; } = 300; - default: - return ConflictResolution.Skip; - } - } + /// + /// 重试次数 + /// + public int RetryCount { get; set; } = 3; /// - /// 获取用于解决冲突的新文件名 + /// 重试间隔(秒) /// - private string GetConflictFileName(string originalPath) - { - string dir = Path.GetDirectoryName(originalPath); - string fileName = Path.GetFileNameWithoutExtension(originalPath); - string ext = Path.GetExtension(originalPath); - string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + public int RetryInterval { get; set; } = 5; - return Path.Combine(dir, $"{fileName} ({timestamp}){ext}"); - } + /// + /// 初始重试延迟(秒) + /// + public int InitialRetryDelay { get; set; } = 1; /// - /// 获取操作优先级(用于排序) + /// 最大重试延迟(秒) /// - private int GetActionPriority(SyncActionType actionType) - { - switch (actionType) - { - case SyncActionType.CreateDirectory: - return 1; + public int MaxRetryDelay { get; set; } = 60; - case SyncActionType.CopyFile: - case SyncActionType.UpdateFile: - return 2; + /// + /// 代理服务器地址 + /// + public string ProxyAddress { get; set; } - case SyncActionType.RenameFile: - return 3; + /// + /// 使用的代理类型 + /// + public ProxyType? ProxyType { get; set; } + } - case SyncActionType.DeleteFile: - return 4; + /// + /// FTP提供者选项 + /// + public class FtpProviderOptions : StorageProviderOptions + { + /// + /// 服务器地址 + /// + public string Host { get; set; } - case SyncActionType.DeleteDirectory: - return 5; + /// + /// 端口号 + /// + public int Port { get; set; } = 21; - default: - return 99; - } - } + /// + /// 用户名 + /// + public string Username { get; set; } /// - /// 获取操作类型描述 + /// 密码 /// - private string GetActionTypeDescription(SyncActionType actionType) - { - switch (actionType) - { - case SyncActionType.CreateDirectory: - return "创建目录"; + public string Password { get; set; } - case SyncActionType.CopyFile: - return "复制文件"; + /// + /// 是否使用被动模式 + /// + public bool UsePassive { get; set; } = true; - case SyncActionType.UpdateFile: - return "更新文件"; + /// + /// 是否使用FTPS(加密) + /// + public bool UseFTPS { get; set; } = false; - case SyncActionType.DeleteFile: - return "删除文件"; + /// + /// 是否使用显式SSL + /// + public bool UseExplicitSSL { get; set; } = true; - case SyncActionType.DeleteDirectory: - return "删除目录"; + /// + /// 是否使用UTF8编码 + /// + public bool UseUTF8 { get; set; } = true; - case SyncActionType.RenameFile: - return "重命名文件"; + /// + /// 路径前缀 + /// + public string PathPrefix { get; set; } = "/"; + } - default: - return "未知操作"; - } - } + /// + /// SFTP提供者选项 + /// + public class SftpProviderOptions : StorageProviderOptions + { + /// + /// 服务器地址 + /// + public string Host { get; set; } /// - /// 获取文件的相对路径 + /// 端口号 /// - private string GetRelativePath(string fullPath, string basePath) - { - // 确保路径以分隔符结尾以正确处理相对路径 - if (!basePath.EndsWith(Path.DirectorySeparatorChar.ToString())) - basePath += Path.DirectorySeparatorChar; + public int Port { get; set; } = 22; - if (fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) - { - var relativePath = fullPath.Substring(basePath.Length); - return relativePath; - } + /// + /// 用户名 + /// + public string Username { get; set; } - return fullPath; - } + /// + /// 密码(和私钥至少需要一个) + /// + public string Password { get; set; } /// - /// 确保目录存在 + /// 私钥文件路径(和密码至少需要一个) /// - private Task EnsureDirectoryExistsAsync(string directory) - { - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - return Task.CompletedTask; - } + public string PrivateKeyFile { get; set; } /// - /// 创建哈希算法实例 + /// 私钥文件密码(如果私钥有密码保护) /// - private EHashType? GetHashAlgorithm() - { - return _options.HashAlgorithm; - } + public string PrivateKeyPassword { get; set; } /// - /// 比较两个哈希值是否相同 + /// 是否验证主机密钥 /// - private bool CompareHash(byte[] hash1, byte[] hash2) - { - if (hash1.Length != hash2.Length) - return false; + public bool ValidateHostKey { get; set; } = true; - for (int i = 0; i < hash1.Length; i++) - { - if (hash1[i] != hash2[i]) - return false; - } + /// + /// 路径前缀 + /// + public string PathPrefix { get; set; } = "/"; + } - return true; - } + /// + /// WebDAV提供者选项 + /// + public class WebDavProviderOptions : StorageProviderOptions + { + /// + /// WebDAV服务URL + /// + public string Url { get; set; } /// - /// 比较两个字节数组是否相同 + /// 用户名 /// - private bool CompareBytes(byte[] buffer1, byte[] buffer2) - { - if (buffer1.Length != buffer2.Length) - return false; + public string Username { get; set; } - for (int i = 0; i < buffer1.Length; i++) - { - if (buffer1[i] != buffer2[i]) - return false; - } + /// + /// 密码 + /// + public string Password { get; set; } - return true; - } + /// + /// 是否验证SSL证书 + /// + public bool ValidateSSL { get; set; } = true; /// - /// 带重试的文件复制操作 + /// 路径前缀 /// - private async Task CopyFileWithRetryAsync(string sourcePath, string targetPath) - { - int maxRetries = _options.MaxRetries; - int currentRetry = 0; - bool success = false; + public string PathPrefix { get; set; } = "/"; + } - while (!success && currentRetry <= maxRetries) - { - try - { - if (currentRetry > 0) - { - // 添加延迟重试 - await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, currentRetry - 1)), _cancellationToken); - ReportProgress($"重试复制文件 {Path.GetFileName(sourcePath)} (尝试 {currentRetry}/{maxRetries})", -1); - } + /// + /// 阿里云盘提供者选项 + /// + public class AliyunDriveProviderOptions : StorageProviderOptions + { + /// + /// 刷新令牌 + /// + public string RefreshToken { get; set; } - if (_options.PreserveFileTime) - { - // 保留原始时间戳 - using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan)) - using (FileStream targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan)) - { - await sourceStream.CopyToAsync(targetStream, 81920, _cancellationToken); - } + /// + /// 访问令牌 + /// + public string AccessToken { get; set; } - // 设置目标文件的时间戳与源文件相同 - File.SetCreationTimeUtc(targetPath, File.GetCreationTimeUtc(sourcePath)); - File.SetLastWriteTimeUtc(targetPath, File.GetLastWriteTimeUtc(sourcePath)); - File.SetLastAccessTimeUtc(targetPath, File.GetLastAccessTimeUtc(sourcePath)); - } - else - { - // 简单复制,不保留时间戳 - File.Copy(sourcePath, targetPath, true); - } + /// + /// 令牌类型 + /// + public string TokenType { get; set; } = "Bearer"; - success = true; - } - catch (IOException) when (currentRetry < maxRetries) - { - currentRetry++; - } - catch (Exception) - { - // 其他异常直接抛出 - throw; - } - } + /// + /// 过期时间(秒) + /// + public int ExpiresIn { get; set; } = 7200; - if (!success) - { - throw new IOException($"复制文件 {sourcePath} 到 {targetPath} 失败,已重试 {maxRetries} 次"); - } - } + /// + /// 路径前缀 + /// + public string PathPrefix { get; set; } = "/"; /// - /// 计算进度百分比 + /// 驱动类型(资源盘/备份盘) /// - private int CalculateProgress(int current, int total) - { - if (total <= 0) return 0; - return (int)((current / (double)total) * 100); - } + public string DriveType { get; set; } = "backup"; /// - /// 报告进度 + /// 是否启用秒传功能 /// - private void ReportProgress(string message, int progressPercentage) - { - if (_progress == null) return; + public bool EnableRapidUpload { get; set; } = true; - // 控制进度更新频率 - var now = DateTime.Now; - if ((now - _lastProgressUpdate).TotalMilliseconds < 100 && progressPercentage >= 0 && progressPercentage < 100) - return; + /// + /// 上传线程数 + /// + public int UploadThreads { get; set; } = 4; - _lastProgressUpdate = now; + /// + /// 下载线程数 + /// + public int DownloadThreads { get; set; } = 4; + } - double bytesPerSecond = _stopwatch.Elapsed.TotalSeconds > 0 - ? _statistics.BytesProcessed / _stopwatch.Elapsed.TotalSeconds - : 0; + /// + /// S3兼容存储提供者选项 + /// + public class S3ProviderOptions : StorageProviderOptions + { + /// + /// 终端节点URL + /// + public string Endpoint { get; set; } - double itemsPerSecond = _stopwatch.Elapsed.TotalSeconds > 0 - ? _processedItems / _stopwatch.Elapsed.TotalSeconds - : 0; + /// + /// 访问密钥ID + /// + public string AccessKeyId { get; set; } - _progress.Report(new SyncProgress - { - Message = message, - ProgressPercentage = progressPercentage, - ElapsedTime = _stopwatch.Elapsed, - ProcessedItems = _processedItems, - TotalItems = _totalItems, - BytesProcessed = _statistics.BytesProcessed, - BytesToProcess = _statistics.BytesToProcess, - BytesPerSecond = bytesPerSecond, - ItemsPerSecond = itemsPerSecond, - Statistics = _statistics - }); - } + /// + /// 访问密钥Secret + /// + public string AccessKeySecret { get; set; } /// - /// 验证同步路径 + /// 区域 /// - private void ValidatePaths() - { - // 验证源路径 - if (string.IsNullOrEmpty(_options.SourcePath)) - throw new ArgumentException("源路径不能为空"); + public string Region { get; set; } - if (!Directory.Exists(_options.SourcePath)) - throw new DirectoryNotFoundException($"源目录不存在: {_options.SourcePath}"); + /// + /// 存储桶名称 + /// + public string BucketName { get; set; } - // 验证目标路径 - if (string.IsNullOrEmpty(_options.TargetPath)) - throw new ArgumentException("目标路径不能为空"); + /// + /// 对象前缀 + /// + public string ObjectPrefix { get; set; } - // 目标路径可能不存在,需要创建 - if (!Directory.Exists(_options.TargetPath) && !_options.PreviewOnly) - { - Directory.CreateDirectory(_options.TargetPath); - } + /// + /// 是否使用HTTPS + /// + public bool UseHttps { get; set; } = true; - // 检查路径不能互相包含 - string normalizedSource = Path.GetFullPath(_options.SourcePath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; - string normalizedTarget = Path.GetFullPath(_options.TargetPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + /// + /// 是否使用路径样式访问 + /// + public bool UsePathStyle { get; set; } = false; + } - if (normalizedSource.StartsWith(normalizedTarget, StringComparison.OrdinalIgnoreCase) || - normalizedTarget.StartsWith(normalizedSource, StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException("源路径和目标路径不能互相包含"); - } - } + /// + /// SMB/CIFS提供者选项 + /// + public class SmbProviderOptions : StorageProviderOptions + { + /// + /// 服务器地址 + /// + public string Host { get; set; } /// - /// 判断操作是否为文件操作 + /// 共享名称 /// - private bool IsFileOperation(SyncActionType actionType) - { - return actionType == SyncActionType.CopyFile || - actionType == SyncActionType.UpdateFile || - actionType == SyncActionType.RenameFile || - actionType == SyncActionType.DeleteFile; - } + public string ShareName { get; set; } /// - /// 从JSON配置文件加载同步选项 + /// 域名 /// - public static SyncOptions LoadFromJsonFile(string configFilePath) - { - if (!File.Exists(configFilePath)) - throw new FileNotFoundException($"配置文件不存在: {configFilePath}"); + public string Domain { get; set; } - string json = File.ReadAllText(configFilePath); - var options = JsonSerializer.Deserialize(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() } - }); + /// + /// 用户名 + /// + public string Username { get; set; } - return options; - } + /// + /// 密码 + /// + public string Password { get; set; } /// - /// 保存同步选项到JSON配置文件 + /// 路径前缀 /// - public static void SaveToJsonFile(SyncOptions options, string configFilePath) - { - string json = JsonSerializer.Serialize(options, new JsonSerializerOptions - { - WriteIndented = true, - Converters = { new JsonStringEnumConverter() } - }); + public string PathPrefix { get; set; } = "\\"; - File.WriteAllText(configFilePath, json); - } + /// + /// SMB协议版本 + /// + public string SmbVersion { get; set; } = "3.0"; } /// - /// 同步选项类 + /// 代理类型 /// - public class SyncOptions + public enum ProxyType { /// - /// 源目录路径 + /// HTTP代理 /// - public string SourcePath { get; set; } + Http, /// - /// 目标目录路径 + /// SOCKS4代理 /// - public string TargetPath { get; set; } + Socks4, /// - /// 同步模式 + /// SOCKS5代理 /// - public SyncMode SyncMode { get; set; } = SyncMode.OneWay; + Socks5 + } + /// + /// 加密选项 + /// + public class EncryptionOptions + { /// - /// 文件比较方法 + /// 是否启用加密 /// - public CompareMethod CompareMethod { get; set; } = CompareMethod.DateTimeAndSize; + public bool Enabled { get; set; } = false; /// - /// 哈希算法类型 + /// 加密算法 /// - public EHashType? HashAlgorithm { get; set; } + public EEncryptionAlgorithm Algorithm { get; set; } = EEncryptionAlgorithm.AES256GCM; /// - /// 哈希抽样率(0.0-1.0) + /// 加密密码 /// - public double SamplingRate { get; set; } = 0.1; + public string Password { get; set; } /// - /// 最大并行操作数 + /// 加密密钥 /// - public int MaxParallelOperations { get; set; } = Math.Max(1, Environment.ProcessorCount); + public string Key { get; set; } /// - /// 是否启用并行文件操作 + /// 加密盐值 /// - public bool EnableParallelFileOperations { get; set; } = true; + public string Salt { get; set; } /// - /// 日期时间比较阈值(秒) + /// 键派生迭代次数 /// - public int DateTimeThresholdSeconds { get; set; } = 2; + public int Iterations { get; set; } = 100000; /// - /// 是否保留原始文件时间 + /// 是否加密文件名 /// - public bool PreserveFileTime { get; set; } = true; + public bool EncryptFilenames { get; set; } = false; + } + /// + /// 加密算法 + /// + public enum EEncryptionAlgorithm + { /// - /// 是否使用回收站删除文件 + /// AES-256-GCM /// - public bool UseRecycleBin { get; set; } = true; + AES256GCM, /// - /// 冲突解决策略 + /// ChaCha20-Poly1305 /// - public ConflictResolution ConflictResolution { get; set; } = ConflictResolution.Newer; + ChaCha20Poly1305 + } + /// + /// 压缩选项 + /// + public class ECompressionOptions + { /// - /// 是否跟踪符号链接 + /// 是否启用压缩 /// - public bool FollowSymlinks { get; set; } = false; + public bool Enabled { get; set; } = false; /// - /// 是否仅预览,不执行实际操作 + /// 压缩算法 /// - public bool PreviewOnly { get; set; } = false; + public ECompressionAlgorithm Algorithm { get; set; } = ECompressionAlgorithm.Zstd; /// - /// 是否在出错时继续 + /// 压缩级别 /// - public bool ContinueOnError { get; set; } = true; + public int Level { get; set; } = 3; /// - /// 最大重试次数 + /// 最小压缩文件大小(字节) /// - public int MaxRetries { get; set; } = 3; + public long MinimumSize { get; set; } = 4096; /// - /// 是否启用详细输出 + /// 不压缩的文件扩展名列表 /// - public bool Verbose { get; set; } = false; + public List ExcludeExtensions { get; set; } = new List + { + ".jpg", ".jpeg", ".png", ".gif", ".webp", ".mp4", ".avi", ".mov", + ".mp3", ".m4a", ".zip", ".rar", ".7z", ".gz", ".xz", ".bz2" + }; + } + /// + /// 压缩算法 + /// + public enum ECompressionAlgorithm + { /// - /// 要忽略的文件/目录模式 + /// Zstandard算法 /// - public IEnumerable IgnorePatterns { get; set; } = new List - { - "**/System Volume Information/**", - "**/$RECYCLE.BIN/**", - "**/Thumbs.db", - "**/*.tmp", - "**/*.temp", - "**/*.bak" - }; + Zstd, + + /// + /// LZ4算法 + /// + LZ4, + + /// + /// Snappy算法 + /// + Snappy } /// /// 同步模式 /// - public enum SyncMode + public enum ESyncMode { /// /// 单向同步:源 -> 目标 @@ -1417,7 +2656,7 @@ public enum SyncMode /// /// 文件比较方法 /// - public enum CompareMethod + public enum ESyncCompareMethod { /// /// 仅比较文件大小 @@ -1446,21 +2685,9 @@ public enum CompareMethod } /// - /// 哈希算法类型 - /// - public enum HashAlgorithmType - { - MD5, - SHA1, - SHA256, - SHA384, - SHA512 - } - - /// - /// 冲突解决策略 + /// 文件同步冲突解决策略 /// - public enum ConflictResolution + public enum ESyncConflictResolution { /// /// 源文件优先 @@ -1501,7 +2728,7 @@ public enum ConflictResolution /// /// 同步操作类型 /// - public enum SyncActionType + public enum ESyncActionType { CreateDirectory, CopyFile, @@ -1514,7 +2741,7 @@ public enum SyncActionType /// /// 同步方向 /// - public enum SyncDirection + public enum ESyncDirection { SourceToTarget, TargetToSource @@ -1523,7 +2750,7 @@ public enum SyncDirection /// /// 同步操作状态 /// - public enum SyncActionStatus + public enum ESyncActionStatus { Pending, Running, @@ -1534,7 +2761,7 @@ public enum SyncActionStatus /// /// 同步状态 /// - public enum SyncStatus + public enum ESyncStatus { NotStarted, Started, @@ -1552,7 +2779,7 @@ public class SyncAction /// /// 操作类型 /// - public SyncActionType ActionType { get; set; } + public ESyncActionType ActionType { get; set; } /// /// 源路径 @@ -1577,12 +2804,12 @@ public class SyncAction /// /// 同步方向 /// - public SyncDirection Direction { get; set; } = SyncDirection.SourceToTarget; + public ESyncDirection Direction { get; set; } = ESyncDirection.SourceToTarget; /// /// 操作状态 /// - public SyncActionStatus Status { get; set; } = SyncActionStatus.Pending; + public ESyncActionStatus Status { get; set; } = ESyncActionStatus.Pending; /// /// 错误消息 @@ -1592,7 +2819,7 @@ public class SyncAction /// /// 冲突解决策略 /// - public ConflictResolution? ConflictResolution { get; set; } + public ESyncConflictResolution? ConflictResolution { get; set; } } /// @@ -1811,12 +3038,12 @@ public class SyncResult /// /// 同步模式 /// - public SyncMode Mode { get; set; } + public ESyncMode Mode { get; set; } /// /// 同步状态 /// - public SyncStatus Status { get; set; } + public ESyncStatus Status { get; set; } /// /// 开始时间 @@ -1851,7 +3078,7 @@ public class SyncResult /// /// 是否成功完成同步 /// - public bool IsSuccessful => Status == SyncStatus.Completed; + public bool IsSuccessful => Status == ESyncStatus.Completed; /// /// 已处理的总文件数 @@ -1888,13 +3115,13 @@ public class SyncResult /// 摘要文本 public string GetSummary() { - if (Status == SyncStatus.NotStarted) + if (Status == ESyncStatus.NotStarted) return "同步尚未开始"; - if (Status == SyncStatus.Failed) + if (Status == ESyncStatus.Failed) return $"同步失败: {ErrorMessage}"; - if (Status == SyncStatus.Canceled) + if (Status == ESyncStatus.Canceled) return "同步被取消"; if (Statistics == null) @@ -1936,13 +3163,13 @@ public string GetSummary() /// /// 获取同步模式的描述 /// - private string GetSyncModeDescription(SyncMode mode) + private string GetSyncModeDescription(ESyncMode mode) { return mode switch { - SyncMode.OneWay => "单向同步", - SyncMode.Mirror => "镜像同步", - SyncMode.TwoWay => "双向同步", + ESyncMode.OneWay => "单向同步", + ESyncMode.Mirror => "镜像同步", + ESyncMode.TwoWay => "双向同步", _ => mode.ToString() }; } @@ -1950,16 +3177,16 @@ private string GetSyncModeDescription(SyncMode mode) /// /// 获取同步状态的描述 /// - private string GetStatusDescription(SyncStatus status) + private string GetStatusDescription(ESyncStatus status) { return status switch { - SyncStatus.NotStarted => "未开始", - SyncStatus.Started => "已开始", - SyncStatus.Running => "运行中", - SyncStatus.Completed => "已完成", - SyncStatus.Failed => "失败", - SyncStatus.Canceled => "已取消", + ESyncStatus.NotStarted => "未开始", + ESyncStatus.Started => "已开始", + ESyncStatus.Running => "运行中", + ESyncStatus.Completed => "已完成", + ESyncStatus.Failed => "失败", + ESyncStatus.Canceled => "已取消", _ => status.ToString() }; } @@ -2039,8 +3266,8 @@ public string GenerateReport() foreach (var action in group.OrderBy(a => a.RelativePath).Take(100)) // 限制每类显示最多100条记录 { count++; - var status = action.Status == SyncActionStatus.Completed ? "✓" : - action.Status == SyncActionStatus.Failed ? "✗" : "?"; + var status = action.Status == ESyncActionStatus.Completed ? "✓" : + action.Status == ESyncActionStatus.Failed ? "✗" : "?"; var path = action.RelativePath?.Length > 80 ? "..." + action.RelativePath.Substring(action.RelativePath.Length - 77) @@ -2048,7 +3275,7 @@ public string GenerateReport() report.AppendLine($"{status} {path ?? "-"}"); - if (action.Status == SyncActionStatus.Failed && !string.IsNullOrEmpty(action.ErrorMessage)) + if (action.Status == ESyncActionStatus.Failed && !string.IsNullOrEmpty(action.ErrorMessage)) { report.AppendLine($" 错误: {action.ErrorMessage}"); } @@ -2072,16 +3299,16 @@ public string GenerateReport() /// /// 获取操作类型的描述 /// - private string GetActionTypeDescription(SyncActionType actionType) + private string GetActionTypeDescription(ESyncActionType actionType) { return actionType switch { - SyncActionType.CreateDirectory => "创建目录", - SyncActionType.CopyFile => "复制文件", - SyncActionType.UpdateFile => "更新文件", - SyncActionType.DeleteFile => "删除文件", - SyncActionType.DeleteDirectory => "删除目录", - SyncActionType.RenameFile => "重命名文件", + ESyncActionType.CreateDirectory => "创建目录", + ESyncActionType.CopyFile => "复制文件", + ESyncActionType.UpdateFile => "更新文件", + ESyncActionType.DeleteFile => "删除文件", + ESyncActionType.DeleteDirectory => "删除目录", + ESyncActionType.RenameFile => "重命名文件", _ => actionType.ToString() }; } @@ -2113,7 +3340,7 @@ public Result ToApiResult() { return Result.Ok(dto, "同步操作已成功完成"); } - else if (this.Status == SyncStatus.Canceled) + else if (this.Status == ESyncStatus.Canceled) { return Result.Ok(dto, "同步操作被用户取消"); } @@ -2136,7 +3363,7 @@ public static SyncResult Failed(string sourcePath, string targetPath, string err { SourcePath = sourcePath, TargetPath = targetPath, - Status = SyncStatus.Failed, + Status = ESyncStatus.Failed, ErrorMessage = errorMessage, StartTime = DateTime.Now, EndTime = DateTime.Now, @@ -2159,7 +3386,7 @@ public static SyncResult Canceled(string sourcePath, string targetPath, TimeSpan { SourcePath = sourcePath, TargetPath = targetPath, - Status = SyncStatus.Canceled, + Status = ESyncStatus.Canceled, StartTime = DateTime.Now - elapsedTime, EndTime = DateTime.Now, ElapsedTime = elapsedTime, diff --git a/src/MDriveSync.Test/FileSyncHelperTests.cs b/src/MDriveSync.Test/FileSyncHelperTests.cs index bf8f12d..e79f16a 100644 --- a/src/MDriveSync.Test/FileSyncHelperTests.cs +++ b/src/MDriveSync.Test/FileSyncHelperTests.cs @@ -188,8 +188,8 @@ public async Task OneWaySync_EmptyTarget_ShouldCopyAllFiles() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Content, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Content, MaxParallelOperations = 2 }; @@ -198,7 +198,7 @@ public async Task OneWaySync_EmptyTarget_ShouldCopyAllFiles() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); Assert.Equal(3, result.Statistics.FilesCopied); // Ӧø3ļ Assert.True(result.Statistics.DirectoriesCreated >= 2); // Ӧô2Ŀ¼ @@ -240,8 +240,8 @@ public async Task OneWaySync_ExistingTarget_ShouldUpdateChangedFiles() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Content, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Content, MaxParallelOperations = 2 }; @@ -250,7 +250,7 @@ public async Task OneWaySync_ExistingTarget_ShouldUpdateChangedFiles() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); Assert.Equal(1, result.Statistics.FilesUpdated); // file1.txt Ӧñ Assert.Equal(1, result.Statistics.FilesCopied); // newfile.txt Ӧñ @@ -302,8 +302,8 @@ public async Task MirrorSync_ShouldDeleteFilesNotInSource() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.Mirror, - CompareMethod = CompareMethod.Content, + SyncMode = ESyncMode.Mirror, + CompareMethod = ESyncCompareMethod.Content, MaxParallelOperations = 2, // ʹͨɾʹûվԣ UseRecycleBin = false @@ -314,7 +314,7 @@ public async Task MirrorSync_ShouldDeleteFilesNotInSource() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); Assert.Equal(0, result.Statistics.FilesUpdated); // ûļҪ Assert.Equal(0, result.Statistics.FilesCopied); // ûļҪ @@ -368,9 +368,9 @@ public async Task TwoWaySync_WithChangesOnBothSides_ShouldResolveConflicts() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.TwoWay, - CompareMethod = CompareMethod.DateTimeAndSize, - ConflictResolution = ConflictResolution.Newer, + SyncMode = ESyncMode.TwoWay, + CompareMethod = ESyncCompareMethod.DateTimeAndSize, + ConflictResolution = ESyncConflictResolution.Newer, MaxParallelOperations = 2 }; @@ -379,7 +379,7 @@ public async Task TwoWaySync_WithChangesOnBothSides_ShouldResolveConflicts() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); // ֤ͻcommon.txt ӦʹԴ汾£ @@ -419,9 +419,9 @@ public async Task ConflictResolution_KeepBoth_ShouldRenameTarget() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.TwoWay, - CompareMethod = CompareMethod.Content, - ConflictResolution = ConflictResolution.KeepBoth, + SyncMode = ESyncMode.TwoWay, + CompareMethod = ESyncCompareMethod.Content, + ConflictResolution = ESyncConflictResolution.KeepBoth, MaxParallelOperations = 1 }; @@ -430,7 +430,7 @@ public async Task ConflictResolution_KeepBoth_ShouldRenameTarget() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); // ֤汾 @@ -480,8 +480,8 @@ public async Task HashComparison_ShouldDetectContentChanges() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Hash, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Hash, HashAlgorithm = EHashType.SHA256, SamplingRate = 0.1, // 10% MaxParallelOperations = 1 @@ -492,7 +492,7 @@ public async Task HashComparison_ShouldDetectContentChanges() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); Assert.Equal(1, result.Statistics.FilesUpdated); // ֻ largefile.bin Ӧñ Assert.Equal(0, result.Statistics.FilesCopied); @@ -535,8 +535,8 @@ public async Task PreviewMode_ShouldNotMakeChanges() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Content, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Content, PreviewOnly = true, MaxParallelOperations = 1 }; @@ -546,12 +546,12 @@ public async Task PreviewMode_ShouldNotMakeChanges() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); // ֤ƻ - Assert.Equal(1, result.Actions.Count(a => a.ActionType == SyncActionType.UpdateFile)); // file1.txt Ӧñ - Assert.Equal(1, result.Actions.Count(a => a.ActionType == SyncActionType.CopyFile)); // file2.txt Ӧñ + Assert.Equal(1, result.Actions.Count(a => a.ActionType == ESyncActionType.UpdateFile)); // file1.txt Ӧñ + Assert.Equal(1, result.Actions.Count(a => a.ActionType == ESyncActionType.CopyFile)); // file2.txt Ӧñ // ԤģʽʵļӦûб仯 var currentTargetContent = File.ReadAllText(Path.Combine(_testTargetDir, "file1.txt")); @@ -584,8 +584,8 @@ public async Task IgnorePatterns_ShouldSkipMatchingFiles() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Content, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Content, IgnorePatterns = new List { "*.tmp", @@ -600,7 +600,7 @@ public async Task IgnorePatterns_ShouldSkipMatchingFiles() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); // ֤ǺļѸ @@ -623,8 +623,8 @@ public async Task ConfigFileLoad_ShouldApplyCorrectSettings() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Hash, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Hash, HashAlgorithm = EHashType.SHA256, SamplingRate = 0.2, MaxParallelOperations = 2, @@ -650,7 +650,7 @@ public async Task ConfigFileLoad_ShouldApplyCorrectSettings() var result = await syncHelper.SyncAsync(); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); // ֤ǷȷӦ˺Թ @@ -684,8 +684,8 @@ public async Task ErrorHandling_ShouldContinueOnError() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Content, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Content, ContinueOnError = true, MaxParallelOperations = 1 }; @@ -698,7 +698,7 @@ public async Task ErrorHandling_ShouldContinueOnError() File.SetAttributes(readOnlyFile, FileAttributes.Normal); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); // Ӧɣд + Assert.Equal(ESyncStatus.Completed, result.Status); // Ӧɣд Assert.True(result.IsSuccessful); Assert.True(result.Statistics.Errors > 0); // Ӧд¼ @@ -727,8 +727,8 @@ public async Task Sync_LargeFiles_ShouldHandleThemCorrectly() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Content, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Content, MaxParallelOperations = 1 }; @@ -743,7 +743,7 @@ public async Task Sync_LargeFiles_ShouldHandleThemCorrectly() _output.WriteLine($"ͬٶ: {result.BytesPerSecond / (1024 * 1024):F2} MB/s"); // ֤ - Assert.Equal(SyncStatus.Completed, result.Status); + Assert.Equal(ESyncStatus.Completed, result.Status); Assert.True(result.IsSuccessful); Assert.Equal(1, result.Statistics.FilesCopied); @@ -772,8 +772,8 @@ public async Task FileSyncReport_ShouldContainDetailedInformation() { SourcePath = _testSourceDir, TargetPath = _testTargetDir, - SyncMode = SyncMode.OneWay, - CompareMethod = CompareMethod.Content, + SyncMode = ESyncMode.OneWay, + CompareMethod = ESyncCompareMethod.Content, MaxParallelOperations = 1 }; From a3fb78d66f5533423872f71e46026ef211ec92fb Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 10:25:55 +0800 Subject: [PATCH 74/90] cron --- src/MDriveSync.Cli/Program.cs | 83 +++--- .../Properties/launchSettings.json | 2 +- src/MDriveSync.Cli/sync.json | 4 +- .../Services/FileSyncHelper.cs | 122 +++++++- .../Services/IntervalScheduler.cs | 260 ++++++++++++++++++ .../Services/QuartzCronScheduler.cs | 7 + .../Controllers/OpenAliyunDriveController.cs | 5 +- 7 files changed, 441 insertions(+), 42 deletions(-) create mode 100644 src/MDriveSync.Core/Services/IntervalScheduler.cs diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index 1b45275..10b0173 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -1,6 +1,6 @@ using MDriveSync.Core.Services; -using MDriveSync.Infrastructure; using MDriveSync.Security.Models; +using Quartz; using Serilog; using System.Text.Json; @@ -147,12 +147,6 @@ private static async Task HandleSyncCommand(string[] args) return 1; } - Log.Information($"开始同步操作..."); - Log.Information($"源目录: {options.SourcePath}"); - Log.Information($"目标目录: {options.TargetPath}"); - Log.Information($"同步模式: {options.SyncMode}"); - Log.Information($"比较方法: {options.CompareMethod}"); - if (options.PreviewOnly) { Log.Information("预览模式:不会执行实际文件操作"); @@ -178,20 +172,10 @@ private static async Task HandleSyncCommand(string[] args) var syncHelper = new FileSyncHelper(options, progress); var result = await syncHelper.SyncAsync(); - // 显示结果 - Log.Information($"同步操作完成,状态: {result.Status}"); - Log.Information($"总耗时: {result.ElapsedTime.TotalSeconds:F2} 秒"); - - if (result.Statistics != null) + // 如果配置了定时任务,则一直执行 + if (!string.IsNullOrEmpty(options.CronExpression) || options.Interval > 0) { - Log.Information($"文件复制: {result.Statistics.FilesCopied} 个"); - Log.Information($"文件更新: {result.Statistics.FilesUpdated} 个"); - Log.Information($"文件删除: {result.Statistics.FilesDeleted} 个"); - Log.Information($"文件跳过: {result.Statistics.FilesSkipped} 个"); - Log.Information($"目录创建: {result.Statistics.DirectoriesCreated} 个"); - Log.Information($"目录删除: {result.Statistics.DirectoriesDeleted} 个"); - Log.Information($"错误数量: {result.Statistics.Errors} 个"); - Log.Information($"处理总量: {result.Statistics.BytesProcessed.FormatSize()}"); + Console.ReadKey(); } return result.Status == ESyncStatus.Completed ? 0 : 1; @@ -214,19 +198,22 @@ private static void ShowSyncHelp() Console.WriteLine(" mdrive sync [选项]"); Console.WriteLine(); Console.WriteLine("选项:"); - Console.WriteLine(" --source, -s 源目录路径 (必需)"); - Console.WriteLine(" --target, -t 目标目录路径 (必需)"); - Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); - Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); - Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); - Console.WriteLine(" --config, -f 配置文件路径, 示例: -f sync.json"); - Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); - Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); - Console.WriteLine(" --verbose, -v 显示详细日志信息 (默认: false)"); - Console.WriteLine(" --threads, -j 并行操作的最大线程数 (默认: CPU核心数)"); - Console.WriteLine(" --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true)"); - Console.WriteLine(" --preserve-time 保留原始文件时间 (默认: true)"); - Console.WriteLine(" --help 显示帮助信息"); + Console.WriteLine(" --source, -s 源目录路径 (必需)"); + Console.WriteLine(" --target, -t 目标目录路径 (必需)"); + Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); + Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); + Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); + Console.WriteLine(" --config, -f 配置文件路径, 示例: -f sync.json"); + Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); + Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); + Console.WriteLine(" --verbose, -v 显示详细日志信息 (默认: false)"); + Console.WriteLine(" --threads, -j 并行操作的最大线程数 (默认: CPU核心数)"); + Console.WriteLine(" --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true)"); + Console.WriteLine(" --preserve-time 保留原始文件时间 (默认: true)"); + Console.WriteLine(" --interval, -i 同步间隔, 单位秒"); + Console.WriteLine(" --cron, Cron表达式,设置后将优先使用Cron表达式进行调度"); + Console.WriteLine(" --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步,(默认: true)"); + Console.WriteLine(" --help 显示帮助信息"); } /// @@ -358,6 +345,36 @@ private static SyncOptions ParseSyncOptions(string[] args) else options.PreserveFileTime = true; break; + + case "--interval": + case "-i": + if (value != null && int.TryParse(value, out int interval) && interval > 0) + options.Interval = interval; + break; + + case "--cron": + { + if (value != null) + { + // 验证 Cron 表达式 + if (!CronExpression.IsValidExpression(value)) + { + Log.Error($"无效的 Cron 表达式: {value}"); + return null; + } + + options.CronExpression = value; + } + } + break; + + case "--execute-immediately": + case "-ei": + if (value != null && bool.TryParse(value, out bool executeImmediately)) + options.ExecuteImmediately = executeImmediately; + else + options.ExecuteImmediately = true; + break; } } diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 97626a1..83f5679 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - "commandLineArgs": "sync -f sync.json" + "commandLineArgs": "sync -f sync.json -i 10" } } } \ No newline at end of file diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index 2a3706c..aa7ea5a 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,6 +1,6 @@ { - "SourcePath": "E:\\_temp\\____synctest0", - "TargetPath": "E:\\_temp\\____synctest1", + "SourcePath": "E:\\_temp\\____synctest3", + "TargetPath": "E:\\_temp\\____synctest4", "SyncMode": "Mirror", "CompareMethod": "DateTimeAndSize", "HashAlgorithm": null, diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 45ba39f..3f0f9fa 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -1,5 +1,6 @@ using MDriveSync.Security; using MDriveSync.Security.Models; +using Microsoft.Extensions.Options; using Serilog; using System.Diagnostics; using System.Text; @@ -13,6 +14,9 @@ namespace MDriveSync.Core.Services /// public class FileSyncHelper { + private readonly object _lockObject = new object(); + private bool _isRunning; + private readonly SyncOptions _options; private readonly IProgress _progress; private readonly CancellationToken _cancellationToken; @@ -23,6 +27,16 @@ public class FileSyncHelper private int _totalItems = 0; private SyncStatistics _statistics = new SyncStatistics(); + /// + /// Cron 调度器 + /// + private readonly QuartzCronScheduler _quartzCronScheduler; + + /// + /// 定时器调度器 + /// + private readonly IntervalScheduler _intervalScheduler; + /// /// 初始化文件同步助手 /// @@ -34,6 +48,27 @@ public FileSyncHelper(SyncOptions options, IProgress progress = nu _options = options ?? throw new ArgumentNullException(nameof(options)); _progress = progress; _cancellationToken = cancellationToken; + + // 如果配置了 cron + if (!string.IsNullOrEmpty(_options.CronExpression)) + { + // 创建计划 + _quartzCronScheduler = new QuartzCronScheduler(_options.CronExpression, async () => + { + await SyncAsync(); + }); + _quartzCronScheduler.Start(); + } + // 如果配置了定时器 + else if (_options.Interval > 0) + { + // 启动定时器 + _intervalScheduler = new IntervalScheduler(_options.Interval, async () => + { + await SyncAsync(); + }); + _intervalScheduler.Start(); + } } /// @@ -42,8 +77,6 @@ public FileSyncHelper(SyncOptions options, IProgress progress = nu /// 同步结果 public async Task SyncAsync() { - _stopwatch.Restart(); - _statistics = new SyncStatistics(); var result = new SyncResult { StartTime = DateTime.Now, @@ -53,8 +86,40 @@ public async Task SyncAsync() Status = ESyncStatus.Started }; + // 如果配置了定时 + if (!string.IsNullOrWhiteSpace(_options.CronExpression) || _options.Interval > 0) + { + // 未开启立即执行 + if (!_options.ExecuteImmediately) + { + // 直接返回 + result.Status = ESyncStatus.NotStarted; + return result; + } + } + + lock (_lockObject) + { + if (_isRunning) + { + Log.Warning("同步操作正在进行中,请稍后再试。"); + result.Status = ESyncStatus.Running; + return result; + } + + _isRunning = true; + _stopwatch.Restart(); + _statistics = new SyncStatistics(); + } + try { + Log.Information($"开始同步操作..."); + Log.Information($"源目录: {_options.SourcePath}"); + Log.Information($"目标目录: {_options.TargetPath}"); + Log.Information($"同步模式: {_options.SyncMode}"); + Log.Information($"比较方法: {_options.CompareMethod}"); + // 初始化源目录和目标目录 ValidatePaths(); ReportProgress("正在初始化同步操作...", 0); @@ -96,8 +161,42 @@ public async Task SyncAsync() } finally { - _stopwatch.Stop(); - result.EndTime = DateTime.Now; + lock (_lockObject) + { + _stopwatch.Stop(); + result.EndTime = DateTime.Now; + + _isRunning = false; + + // 显示结果 + Log.Information($"同步操作完成,状态: {result.Status}"); + Log.Information($"总耗时: {result.ElapsedTime.TotalSeconds:F2} 秒"); + + if (result.Statistics != null) + { + Log.Information($"文件复制: {result.Statistics.FilesCopied} 个"); + Log.Information($"文件更新: {result.Statistics.FilesUpdated} 个"); + Log.Information($"文件删除: {result.Statistics.FilesDeleted} 个"); + Log.Information($"文件跳过: {result.Statistics.FilesSkipped} 个"); + Log.Information($"目录创建: {result.Statistics.DirectoriesCreated} 个"); + Log.Information($"目录删除: {result.Statistics.DirectoriesDeleted} 个"); + Log.Information($"错误数量: {result.Statistics.Errors} 个"); + Log.Information($"处理总量: {result.Statistics.BytesProcessed.FormatSize()}"); + } + + // 如果配置了 cron 或定时器,则不停止,并显示下次执行时间 + if (_quartzCronScheduler != null) + { + Log.Information($"下次执行时间: {_quartzCronScheduler.GetNextRunTime()}"); + } + else if (_intervalScheduler != null) + { + Log.Information($"下次执行时间: {_intervalScheduler.GetNextRunTime()}"); + } + + // 任务结束了,显示终止分割信息 + Log.Information(new string('-', 50)); + } } } @@ -2083,6 +2182,21 @@ public class SyncOptions "**/node_modules/**" }; + /// + /// 定时同步周期,单位秒 + /// + public int Interval { get; set; } + + /// + /// Cron 表达式,设置后将优先使用 Cron 表达式进行调度 + /// + public string CronExpression { get; set; } + + /// + /// 定时任务时,是否立即执行一次 + /// + public bool ExecuteImmediately { get; set; } = true; + /// /// 源存储提供者类型(用于跨服务同步) /// diff --git a/src/MDriveSync.Core/Services/IntervalScheduler.cs b/src/MDriveSync.Core/Services/IntervalScheduler.cs new file mode 100644 index 0000000..fc010f3 --- /dev/null +++ b/src/MDriveSync.Core/Services/IntervalScheduler.cs @@ -0,0 +1,260 @@ +using Serilog; + +namespace MDriveSync.Core.Services +{ + /// + /// 基于时间间隔的任务调度器 + /// + public class IntervalScheduler : IDisposable + { + private readonly Timer _timer; // 定时器 + private readonly TimeSpan _interval; // 执行间隔 + private readonly Action _taskToRun; // 要执行的任务 + private readonly bool _executeImmediately; // 是否立即执行 + private readonly object _lockObject = new(); // 锁对象,用于控制并发 + private readonly CancellationTokenSource _cts; // 取消令牌源 + private bool _isRunning; // 标记任务是否正在运行 + private bool _disposed; // 标记是否已释放资源 + private bool _started; // 标记是否已启动 + + // 上次执行时间 + private DateTime _lastRunTime = DateTime.MinValue; + + /// + /// 初始化基于时间间隔的任务调度器 + /// + /// 执行间隔(秒) + /// 要执行的任务 + /// 是否立即执行,默认为false + public IntervalScheduler(int intervalInSeconds, Action taskToRun, bool executeImmediately = false) + { + if (intervalInSeconds <= 0) + throw new ArgumentException("执行间隔必须大于0", nameof(intervalInSeconds)); + + _interval = TimeSpan.FromSeconds(intervalInSeconds); + _taskToRun = taskToRun ?? throw new ArgumentNullException(nameof(taskToRun)); + _executeImmediately = executeImmediately; + _timer = new Timer(TimerCallback); + _cts = new CancellationTokenSource(); + + Log.Debug("初始化定时任务调度器,间隔: {IntervalSeconds}秒,立即执行: {ExecuteImmediately}", + intervalInSeconds, executeImmediately); + } + + /// + /// 开始调度任务 + /// + public void Start() + { + lock (_lockObject) + { + if (_disposed) + throw new ObjectDisposedException(nameof(IntervalScheduler)); + + if (_started) + return; + + _started = true; + + // 记录开始时间 + _lastRunTime = DateTime.Now; + Log.Information("开始定时任务调度,当前时间: {StartTime}", _lastRunTime); + + // 设置定时器 + if (_executeImmediately) + { + // 立即执行,然后按间隔执行 + _timer.Change(TimeSpan.Zero, _interval); + Log.Debug("定时任务将立即执行一次,然后每隔 {Interval} 执行", _interval); + } + else + { + // 等待一个间隔后再执行 + _timer.Change(_interval, _interval); + Log.Debug("定时任务将在 {Interval} 后首次执行", _interval); + } + } + } + + /// + /// 获取下次执行时间 + /// + public DateTime GetNextRunTime() + { + lock (_lockObject) + { + if (_disposed) + throw new ObjectDisposedException(nameof(IntervalScheduler)); + + // 如果还没有执行过,并且设置了立即执行,返回当前时间 + if (_lastRunTime == DateTime.MinValue && _executeImmediately && !_started) + return DateTime.Now; + + // 如果还没有执行过,并且没有设置立即执行,返回启动后的第一个间隔 + if (_lastRunTime == DateTime.MinValue && !_executeImmediately && !_started) + return DateTime.Now.Add(_interval); + + // 计算下次执行时间,基于上次执行时间 + DateTime nextRunTime = _lastRunTime.Add(_interval); + + // 如果下次执行时间已经过了,则计算下一个有效的执行时间点 + if (nextRunTime < DateTime.Now) + { + // 计算已经过去的间隔数 + TimeSpan elapsed = DateTime.Now - _lastRunTime; + int intervals = (int)(elapsed.TotalSeconds / _interval.TotalSeconds) + 1; + + // 计算新的下次执行时间 + nextRunTime = _lastRunTime.AddSeconds(intervals * _interval.TotalSeconds); + } + + return nextRunTime; + } + } + + /// + /// 停止调度任务 + /// + public void Stop() + { + lock (_lockObject) + { + if (_disposed || !_started) + return; + + _started = false; + _timer.Change(Timeout.Infinite, Timeout.Infinite); + Log.Information("停止定时任务调度"); + } + } + + /// + /// 取消当前正在执行的任务和所有未来任务 + /// + public void Cancel() + { + lock (_lockObject) + { + if (_disposed) + return; + + Stop(); + try + { + _cts.Cancel(); + Log.Information("已取消定时任务"); + } + catch (ObjectDisposedException) + { + // 忽略已释放的对象异常 + } + } + } + + /// + /// 手动触发任务执行一次(不影响原有调度) + /// + public void TriggerNow() + { + if (_disposed) + throw new ObjectDisposedException(nameof(IntervalScheduler)); + + Log.Information("手动触发定时任务执行"); + Task.Run(ExecuteTask); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// 释放资源 + /// + /// 是否正在释放托管资源 + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + Stop(); + _cts.Dispose(); + _timer.Dispose(); + Log.Debug("定时任务调度器已释放资源"); + } + + _disposed = true; + } + } + + /// + /// 定时器回调方法 + /// + private void TimerCallback(object state) + { + if (_cts.IsCancellationRequested) + { + Stop(); + return; + } + + ExecuteTask(); + } + + /// + /// 执行任务 + /// + private void ExecuteTask() + { + // 检查是否已取消 + if (_cts.IsCancellationRequested) + return; + + // 使用锁确保同一时间只有一个任务在执行 + lock (_lockObject) + { + if (_isRunning || _disposed) + return; + + _isRunning = true; + } + + try + { + Log.Debug("开始执行定时任务"); + _taskToRun(); + Log.Debug("定时任务执行完成"); + } + catch (OperationCanceledException) + { + Log.Information("定时任务已取消"); + } + catch (Exception ex) + { + // 任务执行异常不应影响调度器继续工作 + Log.Error(ex, "定时任务调度器执行异常"); + } + finally + { + lock (_lockObject) + { + _isRunning = false; + _lastRunTime = DateTime.Now; + } + } + } + + /// + /// 终结器 + /// + ~IntervalScheduler() + { + Dispose(false); + } + } +} \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/QuartzCronScheduler.cs b/src/MDriveSync.Core/Services/QuartzCronScheduler.cs index 9f87f77..8108e68 100644 --- a/src/MDriveSync.Core/Services/QuartzCronScheduler.cs +++ b/src/MDriveSync.Core/Services/QuartzCronScheduler.cs @@ -72,6 +72,13 @@ private void ScheduleNextRun() _timer.Change(dueTime, Timeout.InfiniteTimeSpan); // 重设Timer,只执行一次 } + public DateTime GetNextRunTime() + { + var schedule = new CronExpression(_cronExpression); // 解析Cron表达式 + var nextRun = schedule.GetNextValidTimeAfter(DateTimeOffset.Now).GetValueOrDefault(); // 计算下一次执行时间 + return nextRun.DateTime; // 返回下一次执行时间 + } + // 停止调度器 public void Stop() { diff --git a/src/MDriveSync.Server.API/Controllers/OpenAliyunDriveController.cs b/src/MDriveSync.Server.API/Controllers/OpenAliyunDriveController.cs index 6ca62dc..9e4463f 100644 --- a/src/MDriveSync.Server.API/Controllers/OpenAliyunDriveController.cs +++ b/src/MDriveSync.Server.API/Controllers/OpenAliyunDriveController.cs @@ -1,5 +1,4 @@ -using MDriveSync.Core; -using MDriveSync.Core.Services; +using MDriveSync.Core.Services; using MDriveSync.Core.ViewModels; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; @@ -8,6 +7,8 @@ using System.Net; using System.Text.Json; +using AliyunDriveProviderOptions = MDriveSync.Core.AliyunDriveProviderOptions; + namespace MDriveSync.Server.API.Controllers { /// From cd3e4ae67a1481f8af778b727089a0619a808bdb Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 10:42:18 +0800 Subject: [PATCH 75/90] =?UTF-8?q?=E5=B9=B6=E8=A1=8C=E5=A4=84=E7=90=86=20ac?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MDriveSync.Cli/sync.json | 6 +- .../Services/FileSyncHelper.cs | 1413 +++++++---------- 2 files changed, 549 insertions(+), 870 deletions(-) diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index aa7ea5a..f6bdc64 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,7 +1,7 @@ { - "SourcePath": "E:\\_temp\\____synctest3", - "TargetPath": "E:\\_temp\\____synctest4", - "SyncMode": "Mirror", + "SourcePath": "E:\\_temp\\____synctest0", + "TargetPath": "E:\\_temp\\____synctest1", + "SyncMode": "OneWay", "CompareMethod": "DateTimeAndSize", "HashAlgorithm": null, "SamplingRate": 0.1, diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 3f0f9fa..bd86c27 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -1,7 +1,7 @@ using MDriveSync.Security; using MDriveSync.Security.Models; -using Microsoft.Extensions.Options; using Serilog; +using System.Collections.Concurrent; using System.Diagnostics; using System.Text; using System.Text.Json; @@ -245,50 +245,45 @@ private async Task ExecuteSyncByMode( } /// - /// 执行同步操作 - /// - private async Task ExecuteSyncActionsAsync(List actions) - { - // 按操作类型排序:先创建目录,然后复制/更新文件,最后删除文件和目录 - var orderedActions = actions - .OrderBy(a => GetActionPriority(a.ActionType)) - .ToList(); - - _totalItems = orderedActions.Count; - _processedItems = 0; - - ReportProgress($"开始执行同步操作,共 {_totalItems} 个任务", 0); - - if (_options.MaxParallelOperations > 1 && _options.EnableParallelFileOperations) - { - // 并行处理 - await ProcessActionsInParallelAsync(orderedActions); - } - else - { - // 串行处理 - await ProcessActionsSequentiallyAsync(orderedActions); - } - } - - /// - /// 创建单向同步操作列表(源 -> 目标) + /// 优化的单向同步创建方法,使用并行处理提高性能 /// - private List CreateOneWaySyncActionsAsync( + private List CreateOneWaySyncActionsOptimized( Dictionary sourceFiles, HashSet sourceDirs, Dictionary targetFiles, HashSet targetDirs) { - var actions = new List(); + // 预估总共需要处理的项目数 + var totalItems = sourceDirs.Count + sourceFiles.Count; + var processedItems = 0; + var lastProgressReport = DateTime.MinValue; + + ReportProgress($"正在分析同步计划:共 {sourceDirs.Count} 个目录及 {sourceFiles.Count} 个文件需要处理", 0); + + // 使用ConcurrentBag来安全地收集并行处理的结果 + var actions = new ConcurrentBag(); + + // 使用线程安全的计数器跟踪统计信息 + int filesToCopy = 0; + int filesToUpdate = 0; + int filesSkipped = 0; + long bytesToProcess = 0; + + // 使用读写锁保护进度更新 + var progressLock = new ReaderWriterLockSlim(); + + // 1. 批量处理目录创建 - 可以并行执行但通常不是瓶颈 + ReportProgress("正在分析目录结构...", 0); + var dirStartTime = DateTime.Now; + var targetDirSet = new HashSet(targetDirs, StringComparer.OrdinalIgnoreCase); - // 确保目标目录结构完整 + // 处理目录创建(目录操作通常比较少,可以顺序处理) foreach (var sourceDir in sourceDirs) { var relativePath = GetRelativePath(sourceDir, _options.SourcePath); var targetDirPath = Path.Combine(_options.TargetPath, relativePath); - if (!targetDirs.Contains(targetDirPath)) + if (!targetDirSet.Contains(targetDirPath)) { actions.Add(new SyncAction { @@ -298,297 +293,144 @@ private List CreateOneWaySyncActionsAsync( RelativePath = relativePath }); } - } - - // 比较文件 - foreach (var sourceEntry in sourceFiles) - { - var sourceFilePath = sourceEntry.Key; - var sourceFile = sourceEntry.Value; - var relativePath = GetRelativePath(sourceFilePath, _options.SourcePath); - var targetFilePath = Path.Combine(_options.TargetPath, relativePath); - - if (!targetFiles.TryGetValue(targetFilePath, out var targetFile)) - { - // 目标不存在文件,需要复制 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CopyFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length - }); - _statistics.FilesToCopy++; - _statistics.BytesToProcess += sourceFile.Length; - } - else if (NeedsUpdate(sourceFile, targetFile)) - { - // 文件需要更新 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.UpdateFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length - }); - _statistics.FilesToUpdate++; - _statistics.BytesToProcess += sourceFile.Length; - } - else - { - _statistics.FilesSkipped++; - } - } - - return actions; - } - - /// - /// 创建镜像同步操作列表(源 -> 目标,删除目标中多余内容) - /// - private List CreateMirrorSyncActionsAsync( - Dictionary sourceFiles, - HashSet sourceDirs, - Dictionary targetFiles, - HashSet targetDirs) - { - // 首先创建单向同步的操作 - var actions = CreateOneWaySyncActionsOptimized(sourceFiles, sourceDirs, targetFiles, targetDirs); - - // 查找目标中需要删除的文件和目录 - var sourceRelativePaths = sourceFiles.Keys - .Select(path => GetRelativePath(path, _options.SourcePath)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - var targetRelativePaths = targetFiles.Keys - .Select(path => GetRelativePath(path, _options.TargetPath)); - - foreach (var targetRelativePath in targetRelativePaths) - { - if (!sourceRelativePaths.Contains(targetRelativePath)) - { - var targetFilePath = Path.Combine(_options.TargetPath, targetRelativePath); - // 目标文件在源中不存在,需要删除 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.DeleteFile, - TargetPath = targetFilePath, - RelativePath = targetRelativePath, - Size = targetFiles[targetFilePath].Length - }); - _statistics.FilesToDelete++; - } - } - - // 查找目标中需要删除的目录(倒序处理,先删除子目录) - var sourceRelativeDirs = sourceDirs - .Select(path => GetRelativePath(path, _options.SourcePath)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - var targetRelativeDirs = targetDirs - .Where(d => d != _options.TargetPath) // 排除根目录 - .Select(path => GetRelativePath(path, _options.TargetPath)) - .OrderByDescending(p => p.Length); // 倒序排列,先删除深层目录 - - foreach (var targetRelativeDir in targetRelativeDirs) - { - if (!sourceRelativeDirs.Contains(targetRelativeDir)) + // 更新进度 + processedItems++; + if (ShouldReportProgress(processedItems, totalItems, ref lastProgressReport)) { - var targetDirPath = Path.Combine(_options.TargetPath, targetRelativeDir); - // 目标目录在源中不存在,需要删除 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.DeleteDirectory, - TargetPath = targetDirPath, - RelativePath = targetRelativeDir - }); - _statistics.DirectoriesToDelete++; + double progressPercent = (double)processedItems / totalItems * 100; + ReportProgress($"正在分析目录结构: {processedItems}/{totalItems} ({progressPercent:F1}%)", (int)progressPercent); } } - return actions; - } + var dirTime = DateTime.Now - dirStartTime; + ReportProgress($"目录分析完成,用时: {dirTime.TotalSeconds:F2}秒", (int)((double)processedItems / totalItems * 100)); - /// - /// 创建双向同步操作列表(源 <-> 目标,解决冲突) - /// - private List CreateTwoWaySyncActionsAsync( - Dictionary sourceFiles, - HashSet sourceDirs, - Dictionary targetFiles, - HashSet targetDirs) - { - var actions = new List(); - var processedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + // 2. 批量处理文件 - 使用并行处理提高性能 + ReportProgress($"正在分析文件: 共 {sourceFiles.Count} 个", (int)((double)processedItems / totalItems * 100)); + var fileStartTime = DateTime.Now; - // 第1步:同步目录结构 - var allSourceRelativeDirs = sourceDirs - .Select(path => GetRelativePath(path, _options.SourcePath)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); + // 准备目标文件查找表 + var targetPathLookup = PrepareTargetFileLookup(targetFiles); - var allTargetRelativeDirs = targetDirs - .Where(d => d != _options.TargetPath) - .Select(path => GetRelativePath(path, _options.TargetPath)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); + // 并行处理的参数配置 + int parallelism = _options.EnableParallelFileOperations + ? Math.Min(_options.MaxParallelOperations, Environment.ProcessorCount) + : 1; - // 在目标中创建源中存在的目录 - foreach (var sourceRelativeDir in allSourceRelativeDirs) - { - var targetDirPath = Path.Combine(_options.TargetPath, sourceRelativeDir); - if (!targetDirs.Contains(targetDirPath)) - { - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CreateDirectory, - SourcePath = Path.Combine(_options.SourcePath, sourceRelativeDir), - TargetPath = targetDirPath, - RelativePath = sourceRelativeDir - }); - } - } + // 每批处理的文件数 - 大文件集合时使用分批处理避免内存压力 + const int batchSize = 1000; + var sourceFilesList = sourceFiles.ToList(); + int batchCount = (int)Math.Ceiling(sourceFilesList.Count / (double)batchSize); - // 在源中创建目标中存在的目录 - foreach (var targetRelativeDir in allTargetRelativeDirs) - { - var sourceDirPath = Path.Combine(_options.SourcePath, targetRelativeDir); - if (!sourceDirs.Contains(sourceDirPath)) - { - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CreateDirectory, - SourcePath = targetRelativeDir, - TargetPath = sourceDirPath, - RelativePath = targetRelativeDir, - Direction = ESyncDirection.TargetToSource - }); - } - } + // 跟踪已处理的文件计数 + int currentFileIndex = 0; + object lockObject = new object(); - // 第2步:处理文件 - // 从源处理到目标 - foreach (var sourceEntry in sourceFiles) + // 批量处理文件 + for (int batchIndex = 0; batchIndex < batchCount; batchIndex++) { - var sourceFilePath = sourceEntry.Key; - var sourceFile = sourceEntry.Value; - var relativePath = GetRelativePath(sourceFilePath, _options.SourcePath); - var targetFilePath = Path.Combine(_options.TargetPath, relativePath); - - processedPaths.Add(relativePath); + var currentBatch = sourceFilesList + .Skip(batchIndex * batchSize) + .Take(batchSize) + .ToList(); - if (!targetFiles.TryGetValue(targetFilePath, out var targetFile)) - { - // 目标不存在,复制到目标 - actions.Add(new SyncAction + // 并行处理当前批次 + Parallel.ForEach( + currentBatch, + new ParallelOptions { MaxDegreeOfParallelism = parallelism }, + sourceEntry => { - ActionType = ESyncActionType.CopyFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length - }); - _statistics.FilesToCopy++; - _statistics.BytesToProcess += sourceFile.Length; - } - else - { - // 两边都存在,需要解决冲突 - var conflictResult = ResolveConflict(sourceFile, targetFile); - switch (conflictResult) - { - case ESyncConflictResolution.SourceWins: + var sourceFilePath = sourceEntry.Key; + var sourceFile = sourceEntry.Value; + var relativePath = GetRelativePath(sourceFilePath, _options.SourcePath); + var targetFilePath = Path.Combine(_options.TargetPath, relativePath); + bool needsAction = false; + + // 检查目标文件是否存在 + if (!targetPathLookup.TryGetValue(targetFilePath, out var targetFile)) + { + // 目标不存在文件,需要复制 actions.Add(new SyncAction { - ActionType = ESyncActionType.UpdateFile, + ActionType = ESyncActionType.CopyFile, SourcePath = sourceFilePath, TargetPath = targetFilePath, RelativePath = relativePath, - Size = sourceFile.Length, - ConflictResolution = conflictResult + Size = sourceFile.Length }); - _statistics.FilesToUpdate++; - _statistics.BytesToProcess += sourceFile.Length; - break; - - case ESyncConflictResolution.TargetWins: + Interlocked.Increment(ref filesToCopy); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + needsAction = true; + } + else if (NeedsUpdate(sourceFile, targetFile)) + { + // 文件需要更新 actions.Add(new SyncAction { ActionType = ESyncActionType.UpdateFile, - SourcePath = targetFilePath, - TargetPath = sourceFilePath, - RelativePath = relativePath, - Size = targetFile.Length, - Direction = ESyncDirection.TargetToSource, - ConflictResolution = conflictResult - }); - _statistics.FilesToUpdate++; - _statistics.BytesToProcess += targetFile.Length; - break; - - case ESyncConflictResolution.KeepBoth: - // 保留两个版本,重命名目标文件 - string targetNewName = GetConflictFileName(targetFilePath); - actions.Add(new SyncAction - { - ActionType = ESyncActionType.RenameFile, - SourcePath = targetFilePath, - TargetPath = targetNewName, - RelativePath = GetRelativePath(targetNewName, _options.TargetPath), - ConflictResolution = conflictResult - }); - // 然后复制源文件到目标 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CopyFile, SourcePath = sourceFilePath, TargetPath = targetFilePath, RelativePath = relativePath, Size = sourceFile.Length }); - _statistics.FilesToCopy++; - _statistics.BytesToProcess += sourceFile.Length; - break; + Interlocked.Increment(ref filesToUpdate); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + needsAction = true; + } + else + { + Interlocked.Increment(ref filesSkipped); + } - case ESyncConflictResolution.Skip: - _statistics.FilesSkipped++; - break; - } - } - } + // 更新处理进度 (使用锁以避免并发更新冲突) + lock (lockObject) + { + processedItems++; + currentFileIndex++; - // 从目标处理到源 - foreach (var targetEntry in targetFiles) - { - var targetFilePath = targetEntry.Key; - var targetFile = targetEntry.Value; - var relativePath = GetRelativePath(targetFilePath, _options.TargetPath); + if (ShouldReportProgress(processedItems, totalItems, ref lastProgressReport)) + { + double progressPercent = (double)processedItems / totalItems * 100; + + // 计算处理速度 (文件/秒) + var elapsed = DateTime.Now - fileStartTime; + double filesPerSecond = elapsed.TotalSeconds > 0 + ? currentFileIndex / elapsed.TotalSeconds + : 0; + + // 预估剩余时间 + var remainingFiles = sourceFiles.Count - currentFileIndex; + string remainingTime = filesPerSecond > 0 + ? $", 剩余时间: {TimeSpan.FromSeconds(remainingFiles / filesPerSecond):mm\\:ss}" + : ""; + + ReportProgress($"正在分析文件: {processedItems}/{totalItems} " + + $"[复制:{filesToCopy}, 更新:{filesToUpdate}, 跳过:{filesSkipped}], " + + $"速度: {filesPerSecond:F1}文件/秒{remainingTime}", + (int)progressPercent); + } + } + }); + } - // 跳过已处理的文件 - if (processedPaths.Contains(relativePath)) - continue; + var fileTime = DateTime.Now - fileStartTime; - var sourceFilePath = Path.Combine(_options.SourcePath, relativePath); + // 更新同步统计信息 + _statistics.FilesToCopy = filesToCopy; + _statistics.FilesToUpdate = filesToUpdate; + _statistics.FilesSkipped = filesSkipped; + _statistics.BytesToProcess = bytesToProcess; - // 源不存在,复制到源 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CopyFile, - SourcePath = targetFilePath, - TargetPath = sourceFilePath, - RelativePath = relativePath, - Size = targetFile.Length, - Direction = ESyncDirection.TargetToSource - }); - _statistics.FilesToCopy++; - _statistics.BytesToProcess += targetFile.Length; - } + ReportProgress($"文件分析完成,用时: {fileTime.TotalSeconds:F2}秒。需要复制: {filesToCopy}个, " + + $"需要更新: {filesToUpdate}个, 可跳过: {filesSkipped}个, 总数据量: {bytesToProcess.FormatSize()}", 100); - return actions; + // 将ConcurrentBag转换为List并按照操作类型排序返回 + return actions.OrderBy(a => GetActionPriority(a.ActionType)).ToList(); } /// - /// 优化的镜像同步创建方法 + /// 优化的镜像同步创建方法,使用并行处理提高性能 /// private List CreateMirrorSyncActionsOptimized( Dictionary sourceFiles, @@ -596,48 +438,56 @@ private List CreateMirrorSyncActionsOptimized( Dictionary targetFiles, HashSet targetDirs) { - // 优先创建一次性足够大小的列表,减少扩容 - var estimatedSize = sourceFiles.Count + sourceDirs.Count + targetFiles.Count + targetDirs.Count; - var actions = new List(estimatedSize); + // 预估操作数并报告进度 + ReportProgress($"正在分析镜像同步计划:{sourceFiles.Count}个源文件,{targetFiles.Count}个目标文件", 0); + var startTime = DateTime.Now; - // 创建单向同步操作(优化版本) - actions.AddRange(CreateOneWaySyncActionsOptimized(sourceFiles, sourceDirs, targetFiles, targetDirs)); + // 使用ConcurrentBag收集所有的操作,线程安全 + var actions = new ConcurrentBag(); - // 使用高效的 HashSet 存储相对路径,提高查找效率 - var sourceRelativePaths = new HashSet( - sourceFiles.Keys.Select(p => GetRelativePath(p, _options.SourcePath)), - StringComparer.OrdinalIgnoreCase); + // 创建单向同步操作(已经并行优化) + var oneWayActions = CreateOneWaySyncActionsOptimized(sourceFiles, sourceDirs, targetFiles, targetDirs); + foreach (var action in oneWayActions) + { + actions.Add(action); + } - var sourceRelativeDirs = new HashSet( - sourceDirs.Select(p => GetRelativePath(p, _options.SourcePath)), - StringComparer.OrdinalIgnoreCase); + // 使用线程安全的计数器记录统计信息 + int filesToDelete = 0; + int dirsToDelete = 0; - // 查找并删除目标中需要删除的文件(并行处理较多文件时) - if (targetFiles.Count > 1000 && _options.EnableParallelFileOperations) - { - var deleteFileActions = targetFiles - .AsParallel() - .Where(tf => !sourceRelativePaths.Contains(GetRelativePath(tf.Key, _options.TargetPath))) - .Select(tf => new SyncAction - { - ActionType = ESyncActionType.DeleteFile, - TargetPath = tf.Key, - RelativePath = GetRelativePath(tf.Key, _options.TargetPath), - Size = tf.Value.Length - }) - .ToList(); + // 预计算所有的相对路径并使用高效的HashSet存储,提高查找效率 + var sourceRelativePaths = new ConcurrentDictionary(); + var sourceRelativeDirs = new ConcurrentDictionary(); - actions.AddRange(deleteFileActions); - _statistics.FilesToDelete += deleteFileActions.Count; - } - else - { - // 少量文件时直接顺序处理 - foreach (var targetFile in targetFiles) + // 并行填充源路径HashSet + Parallel.ForEach(sourceFiles.Keys, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + sourcePath => + { + var relativePath = GetRelativePath(sourcePath, _options.SourcePath); + sourceRelativePaths[relativePath] = 0; + }); + + Parallel.ForEach(sourceDirs, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + sourceDir => + { + var relativePath = GetRelativePath(sourceDir, _options.SourcePath); + sourceRelativeDirs[relativePath] = 0; + }); + + ReportProgress("预处理完成,开始查找需要删除的文件...", 20); + + // 查找需要删除的文件(并行处理) + Parallel.ForEach(targetFiles, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + targetFile => { var relativePath = GetRelativePath(targetFile.Key, _options.TargetPath); - if (!sourceRelativePaths.Contains(relativePath)) + if (!sourceRelativePaths.ContainsKey(relativePath)) { + // 目标文件在源中不存在,需要删除 actions.Add(new SyncAction { ActionType = ESyncActionType.DeleteFile, @@ -645,20 +495,36 @@ private List CreateMirrorSyncActionsOptimized( RelativePath = relativePath, Size = targetFile.Value.Length }); - _statistics.FilesToDelete++; + Interlocked.Increment(ref filesToDelete); } - } - } + }); + + // 为了保证删除目录的顺序正确(子目录先删除),这部分不能完全并行化 + // 但可以先并行筛选出需要删除的目录,然后按顺序添加到操作列表 + var dirsToDeleteList = new ConcurrentBag<(string Path, string RelativePath, int Depth)>(); + + Parallel.ForEach(targetDirs.Where(d => d != _options.TargetPath), + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + targetDir => + { + var relativePath = GetRelativePath(targetDir, _options.TargetPath); + if (!sourceRelativeDirs.ContainsKey(relativePath)) + { + // 计算目录深度,用于后续排序 + int depth = relativePath.Count(c => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar); + dirsToDeleteList.Add((targetDir, relativePath, depth)); + Interlocked.Increment(ref dirsToDelete); + } + }); - // 删除目录(排序处理,确保先删除子目录) - var dirsToDelete = targetDirs - .Where(d => d != _options.TargetPath) - .Select(d => new { Path = d, RelativePath = GetRelativePath(d, _options.TargetPath) }) - .Where(d => !sourceRelativeDirs.Contains(d.RelativePath)) - .OrderByDescending(d => d.Path.Length) // 确保先删除子目录 + // 按目录深度降序排序(确保先删除深层目录) + var orderedDirsToDelete = dirsToDeleteList + .ToList() + .OrderByDescending(d => d.Depth) .ToList(); - foreach (var dir in dirsToDelete) + // 添加目录删除操作 + foreach (var dir in orderedDirsToDelete) { actions.Add(new SyncAction { @@ -666,307 +532,157 @@ private List CreateMirrorSyncActionsOptimized( TargetPath = dir.Path, RelativePath = dir.RelativePath }); - _statistics.DirectoriesToDelete++; } - return actions; + _statistics.FilesToDelete = filesToDelete; + _statistics.DirectoriesToDelete = dirsToDelete; + + var timeElapsed = DateTime.Now - startTime; + ReportProgress($"镜像同步分析完成,耗时: {timeElapsed.TotalSeconds:F2}秒,需要删除: {filesToDelete}个文件, {dirsToDelete}个目录", 100); + + // 将ConcurrentBag转换为List并按操作优先级排序 + return actions.OrderBy(a => GetActionPriority(a.ActionType)).ToList(); } /// - /// 优化的执行同步操作方法 + /// 创建优化的双向同步操作列表(源 <-> 目标,高效解决冲突),使用并行处理提高性能 /// - private async Task ExecuteSyncActionsAsyncOptimized(List actions) + private List CreateTwoWaySyncActionsOptimized( + Dictionary sourceFiles, + HashSet sourceDirs, + Dictionary targetFiles, + HashSet targetDirs) { - if (actions.Count == 0) - { - ReportProgress("没有需要执行的操作", 100); - return; - } + var startTime = DateTime.Now; + ReportProgress($"正在分析双向同步计划:{sourceFiles.Count}个源文件,{targetFiles.Count}个目标文件", 0); - // 按操作类型对操作进行分组和排序 - var actionGroups = actions - .GroupBy(a => GetActionPriority(a.ActionType)) - .OrderBy(g => g.Key) - .ToList(); + // 使用线程安全集合 + var actions = new ConcurrentBag(); + var processedPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - _totalItems = actions.Count; - _processedItems = 0; + // 线程安全的统计计数器 + int filesToCopy = 0; + int filesToUpdate = 0; + int filesSkipped = 0; + long bytesToProcess = 0; - // 优化:为大型操作列表预热文件系统缓存(创建目录操作) - var createDirActions = actionGroups - .FirstOrDefault(g => g.First().ActionType == ESyncActionType.CreateDirectory) - ?.ToList() ?? new List(); + // 并行预计算所有目录的相对路径 + var allSourceRelativeDirs = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + var allTargetRelativeDirs = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - if (createDirActions.Count > 100) - { - ReportProgress($"预热文件系统:准备创建 {createDirActions.Count} 个目录...", 0); - // 批量创建所有目录(通常很快且减少文件系统冲突) - await Task.Run(() => + Parallel.ForEach(sourceDirs, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + sourceDir => { - foreach (var action in createDirActions) - { - try - { - if (!Directory.Exists(action.TargetPath)) - { - Directory.CreateDirectory(action.TargetPath); - action.Status = ESyncActionStatus.Completed; - _statistics.DirectoriesCreated++; - } - } - catch (Exception ex) - { - action.Status = ESyncActionStatus.Failed; - action.ErrorMessage = ex.Message; - _statistics.Errors++; + var relativePath = GetRelativePath(sourceDir, _options.SourcePath); + allSourceRelativeDirs.TryAdd(relativePath, 0); + }); - if (!_options.ContinueOnError) - throw; - } - _processedItems++; - } + Parallel.ForEach(targetDirs.Where(d => d != _options.TargetPath), + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + targetDir => + { + var relativePath = GetRelativePath(targetDir, _options.TargetPath); + allTargetRelativeDirs.TryAdd(relativePath, 0); }); - // 报告进度 - ReportProgress($"目录创建完成,处理完成 {createDirActions.Count} 个目录", - CalculateProgress(_processedItems, _totalItems)); - } + Log.Information($"准备双向同步: 源目录 {allSourceRelativeDirs.Count} 个, 目标目录 {allTargetRelativeDirs.Count} 个"); + Log.Information($"源文件 {sourceFiles.Count} 个, 目标文件 {targetFiles.Count} 个"); - // 处理剩余操作组 - foreach (var group in actionGroups) - { - // 跳过已处理的目录创建组 - if (group.First().ActionType == ESyncActionType.CreateDirectory && - createDirActions.Count > 100) - continue; + ReportProgress("开始处理目录创建操作...", 10); - var groupActions = group.ToList(); - var actionType = groupActions.First().ActionType; - - ReportProgress($"处理 {GetActionTypeDescription(actionType)} 操作,共 {groupActions.Count} 项", - CalculateProgress(_processedItems, _totalItems)); + // 1. 目录创建操作(虽然目录数量通常不多,但仍使用并行可提高性能) - // 设置并行度,文件操作使用配置的并行度,目录操作单线程执行 - int parallelism = IsFileOperation(actionType) && _options.EnableParallelFileOperations - ? _options.MaxParallelOperations - : 1; - - // 批处理:每次处理一批操作以平衡内存使用和并行效率 - const int batchSize = 500; - - for (int i = 0; i < groupActions.Count; i += batchSize) + // 1.1 在目标中创建源中存在的目录 + Parallel.ForEach(allSourceRelativeDirs.Keys, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations / 2 : 1 }, + sourceRelativeDir => { - var batch = groupActions.Skip(i).Take(batchSize).ToList(); - - // 使用SemaphoreSlim控制并行度 - using var semaphore = new SemaphoreSlim(parallelism); - var tasks = new List(batch.Count); - - foreach (var action in batch) + var targetDirPath = Path.Combine(_options.TargetPath, sourceRelativeDir); + if (!targetDirs.Contains(targetDirPath)) { - // 获取信号量 - await semaphore.WaitAsync(_cancellationToken); - - tasks.Add(Task.Run(async () => + actions.Add(new SyncAction { - try - { - await ExecuteSingleActionAsync(action); - Interlocked.Increment(ref _processedItems); - } - finally - { - // 释放信号量 - semaphore.Release(); - } - }, _cancellationToken)); + ActionType = ESyncActionType.CreateDirectory, + SourcePath = Path.Combine(_options.SourcePath, sourceRelativeDir), + TargetPath = targetDirPath, + RelativePath = sourceRelativeDir + }); } + }); - // 等待当前批次完成 - await Task.WhenAll(tasks); - - // 每批次后报告进度 - ReportProgress( - $"正在处理 {GetActionTypeDescription(actionType)} 操作 ({_processedItems}/{_totalItems})", - CalculateProgress(_processedItems, _totalItems) - ); - } - } - } - - /// - /// 创建优化的双向同步操作列表(源 <-> 目标,高效解决冲突) - /// - private List CreateTwoWaySyncActionsOptimized( - Dictionary sourceFiles, - HashSet sourceDirs, - Dictionary targetFiles, - HashSet targetDirs) - { - // 预分配充足的容量来避免列表扩容 - var actions = new List(sourceFiles.Count + targetFiles.Count + sourceDirs.Count + targetDirs.Count); - - // 使用高效的 HashSet 跟踪已处理的路径 - var processedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); - - // 预计算所有目录的相对路径并缓存(避免重复计算) - var allSourceRelativeDirs = new HashSet( - sourceDirs.Select(path => GetRelativePath(path, _options.SourcePath)), - StringComparer.OrdinalIgnoreCase); - - var allTargetRelativeDirs = new HashSet( - targetDirs.Where(d => d != _options.TargetPath) - .Select(path => GetRelativePath(path, _options.TargetPath)), - StringComparer.OrdinalIgnoreCase); + // 1.2 在源中创建目标中存在的目录 + Parallel.ForEach(allTargetRelativeDirs.Keys, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations / 2 : 1 }, + targetRelativeDir => + { + var sourceDirPath = Path.Combine(_options.SourcePath, targetRelativeDir); + if (!sourceDirs.Contains(sourceDirPath)) + { + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CreateDirectory, + SourcePath = Path.Combine(_options.TargetPath, targetRelativeDir), + TargetPath = sourceDirPath, + RelativePath = targetRelativeDir, + Direction = ESyncDirection.TargetToSource + }); + } + }); - Log.Information($"准备双向同步: 源目录 {allSourceRelativeDirs.Count} 个, 目标目录 {allTargetRelativeDirs.Count} 个"); - Log.Information($"源文件 {sourceFiles.Count} 个, 目标文件 {targetFiles.Count} 个"); + ReportProgress("目录创建操作分析完成,开始预处理文件...", 20); - // 第1步:同步目录结构(批量处理目录创建操作) + // 预计算并缓存所有相对路径,使用线程安全的字典 + var sourceRelativePathMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + var targetRelativePathMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - // 1.1: 在目标中创建源中存在的目录 - foreach (var sourceRelativeDir in allSourceRelativeDirs) - { - var targetDirPath = Path.Combine(_options.TargetPath, sourceRelativeDir); - if (!targetDirs.Contains(targetDirPath)) + // 并行填充源路径映射 + Parallel.ForEach(sourceFiles, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + sourceEntry => { - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CreateDirectory, - SourcePath = Path.Combine(_options.SourcePath, sourceRelativeDir), - TargetPath = targetDirPath, - RelativePath = sourceRelativeDir - }); - } - } + var relativePath = GetRelativePath(sourceEntry.Key, _options.SourcePath); + sourceRelativePathMap.TryAdd(relativePath, (sourceEntry.Key, sourceEntry.Value)); + }); - // 1.2: 在源中创建目标中存在的目录 - foreach (var targetRelativeDir in allTargetRelativeDirs) - { - var sourceDirPath = Path.Combine(_options.SourcePath, targetRelativeDir); - if (!sourceDirs.Contains(sourceDirPath)) + // 并行填充目标路径映射 + Parallel.ForEach(targetFiles, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + targetEntry => { - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CreateDirectory, - SourcePath = Path.Combine(_options.TargetPath, targetRelativeDir), - TargetPath = sourceDirPath, - RelativePath = targetRelativeDir, - Direction = ESyncDirection.TargetToSource - }); - } - } - - // 第2步:处理文件(先缓存所有相对路径,避免重复计算) - var sourceRelativePathMap = new Dictionary(sourceFiles.Count, StringComparer.OrdinalIgnoreCase); - var targetRelativePathMap = new Dictionary(targetFiles.Count, StringComparer.OrdinalIgnoreCase); + var relativePath = GetRelativePath(targetEntry.Key, _options.TargetPath); + targetRelativePathMap.TryAdd(relativePath, (targetEntry.Key, targetEntry.Value)); + }); - // 预计算并缓存所有相对路径,提高处理效率 - foreach (var sourceEntry in sourceFiles) - { - var relativePath = GetRelativePath(sourceEntry.Key, _options.SourcePath); - sourceRelativePathMap[relativePath] = (sourceEntry.Key, sourceEntry.Value); - } + ReportProgress("文件路径映射完成,开始分析源->目标文件...", 30); - foreach (var targetEntry in targetFiles) - { - var relativePath = GetRelativePath(targetEntry.Key, _options.TargetPath); - targetRelativePathMap[relativePath] = (targetEntry.Key, targetEntry.Value); - } + // 2.1 处理源到目标的文件(将所有源文件分成多个批次并行处理) + const int batchSize = 1000; + var sourceRelativePathsList = sourceRelativePathMap.ToList(); + int batchCount = (int)Math.Ceiling(sourceRelativePathsList.Count / (double)batchSize); - // 2.1: 从源处理到目标 - foreach (var sourceEntry in sourceRelativePathMap) + for (int batchIndex = 0; batchIndex < batchCount; batchIndex++) { - var relativePath = sourceEntry.Key; - var sourceFilePath = sourceEntry.Value.FullPath; - var sourceFile = sourceEntry.Value.Info; - var targetFilePath = Path.Combine(_options.TargetPath, relativePath); - - processedPaths.Add(relativePath); - - if (!targetRelativePathMap.TryGetValue(relativePath, out var targetEntry)) - { - // 目标不存在,复制到目标 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CopyFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length - }); - _statistics.FilesToCopy++; - _statistics.BytesToProcess += sourceFile.Length; - } - else - { - var targetFile = targetEntry.Info; - - // 两边都存在,需要解决冲突 - var conflictResult = ResolveConflict(sourceFile, targetFile); + var currentBatch = sourceRelativePathsList + .Skip(batchIndex * batchSize) + .Take(batchSize) + .ToList(); - switch (conflictResult) + Parallel.ForEach(currentBatch, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + sourceEntry => { - case ESyncConflictResolution.SourceWins: - // 只有当文件实际需要更新时才添加操作 - if (NeedsUpdate(sourceFile, targetFile)) - { - actions.Add(new SyncAction - { - ActionType = ESyncActionType.UpdateFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length, - ConflictResolution = conflictResult - }); - _statistics.FilesToUpdate++; - _statistics.BytesToProcess += sourceFile.Length; - } - else - { - _statistics.FilesSkipped++; - } - break; - - case ESyncConflictResolution.TargetWins: - // 只有当文件实际需要更新时才添加操作 - if (NeedsUpdate(targetFile, sourceFile)) - { - actions.Add(new SyncAction - { - ActionType = ESyncActionType.UpdateFile, - SourcePath = targetFilePath, - TargetPath = sourceFilePath, - RelativePath = relativePath, - Size = targetFile.Length, - Direction = ESyncDirection.TargetToSource, - ConflictResolution = conflictResult - }); - _statistics.FilesToUpdate++; - _statistics.BytesToProcess += targetFile.Length; - } - else - { - _statistics.FilesSkipped++; - } - break; - - case ESyncConflictResolution.KeepBoth: - // 保留两个版本,使用时间戳创建唯一的重命名文件名 - string targetNewName = GetConflictFileName(targetFilePath); + var relativePath = sourceEntry.Key; + var sourceFilePath = sourceEntry.Value.FullPath; + var sourceFile = sourceEntry.Value.Info; + var targetFilePath = Path.Combine(_options.TargetPath, relativePath); - // 首先重命名目标文件 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.RenameFile, - SourcePath = targetFilePath, - TargetPath = targetNewName, - RelativePath = GetRelativePath(targetNewName, _options.TargetPath), - ConflictResolution = conflictResult - }); + // 标记路径已处理 + processedPaths.TryAdd(relativePath, 0); - // 然后复制源文件到目标 + if (!targetRelativePathMap.TryGetValue(relativePath, out var targetEntry)) + { + // 目标不存在,复制到目标 actions.Add(new SyncAction { ActionType = ESyncActionType.CopyFile, @@ -975,237 +691,288 @@ private List CreateTwoWaySyncActionsOptimized( RelativePath = relativePath, Size = sourceFile.Length }); + Interlocked.Increment(ref filesToCopy); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + } + else + { + var targetFile = targetEntry.Info; - _statistics.FilesToCopy++; - _statistics.BytesToProcess += sourceFile.Length; - break; + // 两边都存在,需要解决冲突 + var conflictResult = ResolveConflict(sourceFile, targetFile); - case ESyncConflictResolution.Skip: - _statistics.FilesSkipped++; - break; - } - } + switch (conflictResult) + { + case ESyncConflictResolution.SourceWins: + // 只有当文件实际需要更新时才添加操作 + if (NeedsUpdate(sourceFile, targetFile)) + { + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length, + ConflictResolution = conflictResult + }); + Interlocked.Increment(ref filesToUpdate); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + } + else + { + Interlocked.Increment(ref filesSkipped); + } + break; + + case ESyncConflictResolution.TargetWins: + // 只有当文件实际需要更新时才添加操作 + if (NeedsUpdate(targetFile, sourceFile)) + { + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = targetFilePath, + TargetPath = sourceFilePath, + RelativePath = relativePath, + Size = targetFile.Length, + Direction = ESyncDirection.TargetToSource, + ConflictResolution = conflictResult + }); + Interlocked.Increment(ref filesToUpdate); + Interlocked.Add(ref bytesToProcess, targetFile.Length); + } + else + { + Interlocked.Increment(ref filesSkipped); + } + break; + + case ESyncConflictResolution.KeepBoth: + // 生成冲突文件名是线程安全的 + string targetNewName = GetConflictFileName(targetFilePath); + + // 首先重命名目标文件 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.RenameFile, + SourcePath = targetFilePath, + TargetPath = targetNewName, + RelativePath = GetRelativePath(targetNewName, _options.TargetPath), + ConflictResolution = conflictResult + }); + + // 然后复制源文件到目标 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + + Interlocked.Increment(ref filesToCopy); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + break; + + case ESyncConflictResolution.Skip: + Interlocked.Increment(ref filesSkipped); + break; + } + } + }); + + // 报告每个批次的进度 + int progress = 30 + (int)(40 * (batchIndex + 1) / batchCount); + ReportProgress($"处理源->目标文件 批次 {batchIndex + 1}/{batchCount}, 已处理: {filesToCopy + filesToUpdate + filesSkipped}个文件", progress); } - // 2.2: 从目标处理到源(只处理源中不存在的文件) + // 2.2 处理目标到源的文件(只处理尚未处理的文件) if (targetRelativePathMap.Count > 0) { - // 只处理那些尚未处理过的目标文件 + ReportProgress("开始分析目标->源文件...", 70); + + // 筛选未处理的目标文件 var unprocessedTargetFiles = targetRelativePathMap - .Where(entry => !processedPaths.Contains(entry.Key)) + .Where(entry => !processedPaths.ContainsKey(entry.Key)) .ToList(); - // 并行处理大量文件时更高效 - if (unprocessedTargetFiles.Count > 1000 && _options.EnableParallelFileOperations) + // 对于大量文件使用并行处理 + if (unprocessedTargetFiles.Count > 0) { - var additionalActions = unprocessedTargetFiles - .AsParallel() - .Select(entry => - { - var relativePath = entry.Key; - var targetFilePath = entry.Value.FullPath; - var targetFile = entry.Value.Info; - var sourceFilePath = Path.Combine(_options.SourcePath, relativePath); - - _statistics.FilesToCopy++; - _statistics.BytesToProcess += targetFile.Length; + int unprocessedBatchCount = (int)Math.Ceiling(unprocessedTargetFiles.Count / (double)batchSize); - return new SyncAction + for (int batchIndex = 0; batchIndex < unprocessedBatchCount; batchIndex++) + { + var currentBatch = unprocessedTargetFiles + .Skip(batchIndex * batchSize) + .Take(batchSize) + .ToList(); + + Parallel.ForEach(currentBatch, + new ParallelOptions { MaxDegreeOfParallelism = _options.EnableParallelFileOperations ? _options.MaxParallelOperations : 1 }, + entry => { - ActionType = ESyncActionType.CopyFile, - SourcePath = targetFilePath, - TargetPath = sourceFilePath, - RelativePath = relativePath, - Size = targetFile.Length, - Direction = ESyncDirection.TargetToSource - }; - }) - .ToList(); + var relativePath = entry.Key; + var targetFilePath = entry.Value.FullPath; + var targetFile = entry.Value.Info; + var sourceFilePath = Path.Combine(_options.SourcePath, relativePath); - actions.AddRange(additionalActions); - } - else - { - // 对于少量文件,顺序处理更有效 - foreach (var entry in unprocessedTargetFiles) - { - var relativePath = entry.Key; - var targetFilePath = entry.Value.FullPath; - var targetFile = entry.Value.Info; - var sourceFilePath = Path.Combine(_options.SourcePath, relativePath); + actions.Add(new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = targetFilePath, + TargetPath = sourceFilePath, + RelativePath = relativePath, + Size = targetFile.Length, + Direction = ESyncDirection.TargetToSource + }); - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CopyFile, - SourcePath = targetFilePath, - TargetPath = sourceFilePath, - RelativePath = relativePath, - Size = targetFile.Length, - Direction = ESyncDirection.TargetToSource - }); + Interlocked.Increment(ref filesToCopy); + Interlocked.Add(ref bytesToProcess, targetFile.Length); + }); - _statistics.FilesToCopy++; - _statistics.BytesToProcess += targetFile.Length; + // 报告每个批次的进度 + int progress = 70 + (int)(30 * (batchIndex + 1) / unprocessedBatchCount); + ReportProgress($"处理目标->源文件 批次 {batchIndex + 1}/{unprocessedBatchCount}, 剩余: {unprocessedTargetFiles.Count - (batchIndex + 1) * batchSize}个文件", progress); } } } - // 按优先级排序操作,这样可以确保目录先创建 + // 更新统计信息 + _statistics.FilesToCopy = filesToCopy; + _statistics.FilesToUpdate = filesToUpdate; + _statistics.FilesSkipped = filesSkipped; + _statistics.BytesToProcess = bytesToProcess; + + var timeElapsed = DateTime.Now - startTime; + ReportProgress($"双向同步分析完成,耗时: {timeElapsed.TotalSeconds:F2}秒,需要复制: {filesToCopy}个, 更新: {filesToUpdate}个, 跳过: {filesSkipped}个, 总数据量: {bytesToProcess.FormatSize()}", 100); + + // 将ConcurrentBag转换为List并按操作优先级排序,确保目录先创建 return actions.OrderBy(a => GetActionPriority(a.ActionType)).ToList(); } /// - /// 优化的单向同步创建方法,减少冗余计算 + /// 优化的执行同步操作方法 /// - private List CreateOneWaySyncActionsOptimized( - Dictionary sourceFiles, - HashSet sourceDirs, - Dictionary targetFiles, - HashSet targetDirs) + private async Task ExecuteSyncActionsAsyncOptimized(List actions) { - // 预估总共需要处理的项目数 - var totalItems = sourceDirs.Count + sourceFiles.Count; - var processedItems = 0; - var lastProgressReport = DateTime.MinValue; + if (actions.Count == 0) + { + ReportProgress("没有需要执行的操作", 100); + return; + } - ReportProgress($"正在分析同步计划:共 {sourceDirs.Count} 个目录及 {sourceFiles.Count} 个文件需要处理", 0); + // 按操作类型对操作进行分组和排序 + var actionGroups = actions + .GroupBy(a => GetActionPriority(a.ActionType)) + .OrderBy(g => g.Key) + .ToList(); - var actions = new List(sourceFiles.Count + sourceDirs.Count); - var targetDirSet = new HashSet(targetDirs, StringComparer.OrdinalIgnoreCase); + _totalItems = actions.Count; + _processedItems = 0; - // 1. 批量处理目录创建 - 预先分配好容量 - ReportProgress("正在分析目录结构...", 0); - var dirStartTime = DateTime.Now; + // 优化:为大型操作列表预热文件系统缓存(创建目录操作) + var createDirActions = actionGroups + .FirstOrDefault(g => g.First().ActionType == ESyncActionType.CreateDirectory) + ?.ToList() ?? new List(); - // 1. 批量处理目录创建 - 预先分配好容量 - foreach (var sourceDir in sourceDirs) + if (createDirActions.Count > 100) { - var relativePath = GetRelativePath(sourceDir, _options.SourcePath); - var targetDirPath = Path.Combine(_options.TargetPath, relativePath); - - if (!targetDirSet.Contains(targetDirPath)) + ReportProgress($"预热文件系统:准备创建 {createDirActions.Count} 个目录...", 0); + // 批量创建所有目录(通常很快且减少文件系统冲突) + await Task.Run(() => { - actions.Add(new SyncAction + foreach (var action in createDirActions) { - ActionType = ESyncActionType.CreateDirectory, - SourcePath = sourceDir, - TargetPath = targetDirPath, - RelativePath = relativePath - }); - } + try + { + if (!Directory.Exists(action.TargetPath)) + { + Directory.CreateDirectory(action.TargetPath); + action.Status = ESyncActionStatus.Completed; + _statistics.DirectoriesCreated++; + } + } + catch (Exception ex) + { + action.Status = ESyncActionStatus.Failed; + action.ErrorMessage = ex.Message; + _statistics.Errors++; - // 更新进度 - processedItems++; - if (ShouldReportProgress(processedItems, totalItems, ref lastProgressReport)) - { - double progressPercent = (double)processedItems / totalItems * 100; - ReportProgress($"正在分析目录结构: {processedItems}/{totalItems} ({progressPercent:F1}%)", (int)progressPercent); - } + if (!_options.ContinueOnError) + throw; + } + _processedItems++; + } + }); + + // 报告进度 + ReportProgress($"目录创建完成,处理完成 {createDirActions.Count} 个目录", + CalculateProgress(_processedItems, _totalItems)); } - var dirTime = DateTime.Now - dirStartTime; - ReportProgress($"目录分析完成,用时: {dirTime.TotalSeconds:F2}秒", (int)((double)processedItems / totalItems * 100)); + // 处理剩余操作组 + foreach (var group in actionGroups) + { + // 跳过已处理的目录创建组 + if (group.First().ActionType == ESyncActionType.CreateDirectory && + createDirActions.Count > 100) + continue; - // 2. 批量处理文件 - 减少字符串操作次数 - ReportProgress($"正在分析文件: 共 {sourceFiles.Count} 个", (int)((double)processedItems / totalItems * 100)); - var fileStartTime = DateTime.Now; + var groupActions = group.ToList(); + var actionType = groupActions.First().ActionType; - // 将目标文件路径转换为哈希集合供高效查找 - var targetPathLookup = PrepareTargetFileLookup(targetFiles); + ReportProgress($"处理 {GetActionTypeDescription(actionType)} 操作,共 {groupActions.Count} 项", + CalculateProgress(_processedItems, _totalItems)); - int filesToCopy = 0; - int filesToUpdate = 0; - int filesSkipped = 0; - long bytesToProcess = 0; + // 设置并行度,文件操作使用配置的并行度,目录操作单线程执行 + int parallelism = IsFileOperation(actionType) && _options.EnableParallelFileOperations + ? _options.MaxParallelOperations + : 1; - int currentFileIndex = 0; + // 批处理:每次处理一批操作以平衡内存使用和并行效率 + const int batchSize = 500; - // 2. 批量处理文件 - 减少字符串操作次数 - foreach (var sourceEntry in sourceFiles) - { - var sourceFilePath = sourceEntry.Key; - var sourceFile = sourceEntry.Value; - var relativePath = GetRelativePath(sourceFilePath, _options.SourcePath); - var targetFilePath = Path.Combine(_options.TargetPath, relativePath); + for (int i = 0; i < groupActions.Count; i += batchSize) + { + var batch = groupActions.Skip(i).Take(batchSize).ToList(); - bool needsAction = false; + // 使用SemaphoreSlim控制并行度 + using var semaphore = new SemaphoreSlim(parallelism); + var tasks = new List(batch.Count); - // 检查目标文件是否存在 - if (!targetPathLookup.TryGetValue(targetFilePath, out var targetFile)) - { - // 目标不存在文件,需要复制 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CopyFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length - }); - filesToCopy++; - bytesToProcess += sourceFile.Length; - needsAction = true; - } - else if (NeedsUpdate(sourceFile, targetFile)) - { - // 文件需要更新 - actions.Add(new SyncAction + foreach (var action in batch) { - ActionType = ESyncActionType.UpdateFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length - }); - filesToUpdate++; - bytesToProcess += sourceFile.Length; - needsAction = true; - } - else - { - filesSkipped++; - } + // 获取信号量 + await semaphore.WaitAsync(_cancellationToken); - // 更新处理进度 - processedItems++; - currentFileIndex++; + tasks.Add(Task.Run(async () => + { + try + { + await ExecuteSingleActionAsync(action); + Interlocked.Increment(ref _processedItems); + } + finally + { + // 释放信号量 + semaphore.Release(); + } + }, _cancellationToken)); + } - if (ShouldReportProgress(processedItems, totalItems, ref lastProgressReport)) - { - double progressPercent = (double)processedItems / totalItems * 100; - string actionText = needsAction ? "需要处理" : "可以跳过"; - - // 计算处理速度 (文件/秒) - var elapsed = DateTime.Now - fileStartTime; - double filesPerSecond = elapsed.TotalSeconds > 0 - ? currentFileIndex / elapsed.TotalSeconds - : 0; - - // 预估剩余时间 - var remainingFiles = sourceFiles.Count - currentFileIndex; - string remainingTime = filesPerSecond > 0 - ? $", 剩余时间: {TimeSpan.FromSeconds(remainingFiles / filesPerSecond):mm\\:ss}" - : ""; - - ReportProgress($"正在分析文件: {processedItems}/{totalItems} " + - $"[复制:{filesToCopy}, 更新:{filesToUpdate}, 跳过:{filesSkipped}], " + - $"速度: {filesPerSecond:F1}文件/秒{remainingTime}", - (int)progressPercent); + // 等待当前批次完成 + await Task.WhenAll(tasks); + + // 每批次后报告进度 + ReportProgress( + $"正在处理 {GetActionTypeDescription(actionType)} 操作 ({_processedItems}/{_totalItems})", + CalculateProgress(_processedItems, _totalItems) + ); } } - - var fileTime = DateTime.Now - fileStartTime; - - // 更新同步统计信息 - _statistics.FilesToCopy = filesToCopy; - _statistics.FilesToUpdate = filesToUpdate; - _statistics.FilesSkipped = filesSkipped; - _statistics.BytesToProcess = bytesToProcess; - - ReportProgress($"文件分析完成,用时: {fileTime.TotalSeconds:F2}秒。需要复制: {filesToCopy}个, " + - $"需要更新: {filesToUpdate}个, 可跳过: {filesSkipped}个, 总数据量: {FormatBytes(bytesToProcess)}", 100); - - return actions; } /// @@ -1222,24 +989,6 @@ private Dictionary PrepareTargetFileLookup(Dictionary(targetFiles, StringComparer.OrdinalIgnoreCase); } - /// - /// 格式化字节大小为人类可读格式 - /// - private string FormatBytes(long bytes) - { - string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; - int i = 0; - double size = bytes; - - while (size >= 1024 && i < suffixes.Length - 1) - { - size /= 1024; - i++; - } - - return $"{size:F2} {suffixes[i]}"; - } - /// /// 判断是否应该报告进度(控制报告频率) /// @@ -1262,76 +1011,6 @@ private bool ShouldReportProgress(int processedItems, int totalItems, ref DateTi return shouldReport; } - /// - /// 并行处理同步操作 - /// - private async Task ProcessActionsInParallelAsync(List actions) - { - // 分组处理不同类型的操作 - var actionGroups = actions - .GroupBy(a => GetActionPriority(a.ActionType)) - .OrderBy(g => g.Key); - - foreach (var group in actionGroups) - { - var groupActions = group.ToList(); - var actionType = groupActions.FirstOrDefault()?.ActionType; - - ReportProgress($"处理 {GetActionTypeDescription(actionType.Value)} 操作,共 {groupActions.Count} 项", - CalculateProgress(_processedItems, _totalItems)); - - // 设置并行度,文件操作使用配置的并行度,目录操作单线程执行 - int parallelism = IsFileOperation(actionType.Value) - ? _options.MaxParallelOperations - : 1; - - await Parallel.ForEachAsync( - groupActions, - new ParallelOptions - { - MaxDegreeOfParallelism = parallelism, - CancellationToken = _cancellationToken - }, - async (action, ct) => - { - await ExecuteSingleActionAsync(action); - Interlocked.Increment(ref _processedItems); - - if (_processedItems % 10 == 0 || _processedItems == _totalItems) - { - ReportProgress( - $"正在处理 {GetActionTypeDescription(action.ActionType)} 操作 ({_processedItems}/{_totalItems})", - CalculateProgress(_processedItems, _totalItems) - ); - } - }); - } - } - - /// - /// 串行处理同步操作 - /// - private async Task ProcessActionsSequentiallyAsync(List actions) - { - for (int i = 0; i < actions.Count; i++) - { - _cancellationToken.ThrowIfCancellationRequested(); - - var action = actions[i]; - await ExecuteSingleActionAsync(action); - - _processedItems++; - - if (i % 5 == 0 || i == actions.Count - 1) - { - ReportProgress( - $"正在处理 {GetActionTypeDescription(action.ActionType)} 操作 ({_processedItems}/{_totalItems})", - CalculateProgress(_processedItems, _totalItems) - ); - } - } - } - /// /// 执行单个同步操作 /// From 8108a48fcfd7cde90b88597c4bc57cee0046f3a4 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 14:04:18 +0800 Subject: [PATCH 76/90] chunk --- src/MDriveSync.Cli/Program.cs | 63 ++- .../Properties/launchSettings.json | 2 +- src/MDriveSync.Cli/sync.json | 9 +- .../Services/FileSyncHelper.cs | 523 +++++++++++++++++- 4 files changed, 561 insertions(+), 36 deletions(-) diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index 10b0173..5ee5d6a 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -195,25 +195,29 @@ private static void ShowSyncHelp() Console.WriteLine("执行文件同步操作"); Console.WriteLine(); Console.WriteLine("用法:"); - Console.WriteLine(" mdrive sync [选项]"); + Console.WriteLine(" mdrive sync [选项]"); Console.WriteLine(); Console.WriteLine("选项:"); - Console.WriteLine(" --source, -s 源目录路径 (必需)"); - Console.WriteLine(" --target, -t 目标目录路径 (必需)"); - Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); - Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); - Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); - Console.WriteLine(" --config, -f 配置文件路径, 示例: -f sync.json"); - Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); - Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); - Console.WriteLine(" --verbose, -v 显示详细日志信息 (默认: false)"); - Console.WriteLine(" --threads, -j 并行操作的最大线程数 (默认: CPU核心数)"); - Console.WriteLine(" --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true)"); - Console.WriteLine(" --preserve-time 保留原始文件时间 (默认: true)"); - Console.WriteLine(" --interval, -i 同步间隔, 单位秒"); - Console.WriteLine(" --cron, Cron表达式,设置后将优先使用Cron表达式进行调度"); - Console.WriteLine(" --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步,(默认: true)"); - Console.WriteLine(" --help 显示帮助信息"); + Console.WriteLine(" --source, -s 源目录路径 (必需)"); + Console.WriteLine(" --target, -t 目标目录路径 (必需)"); + Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); + Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); + Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); + Console.WriteLine(" --config, -f 配置文件路径, 示例: -f sync.json"); + Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); + Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); + Console.WriteLine(" --verbose, -v 显示详细日志信息 (默认: false)"); + Console.WriteLine(" --threads, -j 并行操作的最大线程数 (默认: CPU核心数)"); + Console.WriteLine(" --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true)"); + Console.WriteLine(" --preserve-time 保留原始文件时间 (默认: true)"); + Console.WriteLine(" --interval, -i 同步间隔, 单位秒"); + Console.WriteLine(" --cron, Cron表达式,设置后将优先使用Cron表达式进行调度"); + Console.WriteLine(" --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步,(默认: true)"); + Console.WriteLine(" --chunk-size, --chunk 文件同步分块大小(MB),大于0启用分块传输"); + Console.WriteLine(" --sync-last-modified-time 同步完成后是否同步文件的最后修改时间 (默认: true)"); + Console.WriteLine(" --temp-file-suffix 临时文件后缀 (默认: .mdrivetmp)"); + Console.WriteLine(" --verify-after-copy 文件传输完成后验证文件完整性 (默认: true)"); + Console.WriteLine(" --help 显示帮助信息"); } /// @@ -375,6 +379,31 @@ private static SyncOptions ParseSyncOptions(string[] args) else options.ExecuteImmediately = true; break; + + case "--chunk-size": + case "--chunk": + if (value != null && int.TryParse(value, out int chunkSize) && chunkSize > 0) + options.ChunkSizeMB = chunkSize; + break; + + case "--sync-last-modified-time": + if (value != null && bool.TryParse(value, out bool syncTime)) + options.SyncLastModifiedTime = syncTime; + else + options.SyncLastModifiedTime = true; + break; + + case "--temp-file-suffix": + if (value != null) + options.TempFileSuffix = value; + break; + + case "--verify-after-copy": + if (value != null && bool.TryParse(value, out bool verify)) + options.VerifyAfterCopy = verify; + else + options.VerifyAfterCopy = true; + break; } } diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 83f5679..4ec6409 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - "commandLineArgs": "sync -f sync.json -i 10" + "commandLineArgs": "sync -f sync.json -i 10 --chunk 1" } } } \ No newline at end of file diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index f6bdc64..10542a1 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,7 +1,7 @@ { - "SourcePath": "E:\\_temp\\____synctest0", - "TargetPath": "E:\\_temp\\____synctest1", - "SyncMode": "OneWay", + "SourcePath": "E:\\_temp\\____synctest3", + "TargetPath": "E:\\_temp\\____synctest4", + "SyncMode": "TwoWay", "CompareMethod": "DateTimeAndSize", "HashAlgorithm": null, "SamplingRate": 0.1, @@ -27,6 +27,7 @@ "**/node_modules/*", "**/bin/*", "**/obj/*", - "**/.git/*" + "**/.git/*", + "**/.next/*" ] } \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index bd86c27..7a5788b 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -1566,13 +1566,24 @@ private bool CompareBytes(byte[] buffer1, byte[] buffer2) } /// - /// 带重试的文件复制操作 + /// 带重试的文件复制操作,支持分块传输和校验 /// private async Task CopyFileWithRetryAsync(string sourcePath, string targetPath) { int maxRetries = _options.MaxRetries; int currentRetry = 0; bool success = false; + bool useChunkedTransfer = _options.ChunkSizeMB > 0; + + // 确保目标目录存在 + string targetDir = Path.GetDirectoryName(targetPath); + if (!Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + // 临时文件路径 + string tempTargetPath = targetPath + _options.TempFileSuffix; while (!success && currentRetry <= maxRetries) { @@ -1585,35 +1596,54 @@ private async Task CopyFileWithRetryAsync(string sourcePath, string targetPath) ReportProgress($"重试复制文件 {Path.GetFileName(sourcePath)} (尝试 {currentRetry}/{maxRetries})", -1); } - if (_options.PreserveFileTime) + if (useChunkedTransfer) { - // 保留原始时间戳 - using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan)) - using (FileStream targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan)) + await CopyFileWithChunksAsync(sourcePath, tempTargetPath, targetPath); + } + else + { + await CopyFileStandardAsync(sourcePath, tempTargetPath, targetPath); + } + + // 验证复制的文件完整性 + if (_options.VerifyAfterCopy) + { + if (!await VerifyFileIntegrityAsync(sourcePath, targetPath)) { - await sourceStream.CopyToAsync(targetStream, 81920, _cancellationToken); + throw new IOException($"文件完整性验证失败: {sourcePath} -> {targetPath}"); } + } - // 设置目标文件的时间戳与源文件相同 + // 同步文件的最后修改时间 + if (_options.SyncLastModifiedTime) + { File.SetCreationTimeUtc(targetPath, File.GetCreationTimeUtc(sourcePath)); File.SetLastWriteTimeUtc(targetPath, File.GetLastWriteTimeUtc(sourcePath)); File.SetLastAccessTimeUtc(targetPath, File.GetLastAccessTimeUtc(sourcePath)); } - else - { - // 简单复制,不保留时间戳 - File.Copy(sourcePath, targetPath, true); - } success = true; } catch (IOException) when (currentRetry < maxRetries) { currentRetry++; + + // 清理可能损坏的临时文件 + if (File.Exists(tempTargetPath)) + { + try { File.Delete(tempTargetPath); } catch { } + } } - catch (Exception) + catch (Exception ex) { - // 其他异常直接抛出 + // 清理临时文件 + if (File.Exists(tempTargetPath)) + { + try { File.Delete(tempTargetPath); } catch { } + } + + // 记录错误并重新抛出 + Log.Error(ex, $"复制文件失败: {sourcePath} -> {targetPath}"); throw; } } @@ -1624,6 +1654,448 @@ private async Task CopyFileWithRetryAsync(string sourcePath, string targetPath) } } + /// + /// 标准文件复制方法 + /// + private async Task CopyFileStandardAsync(string sourcePath, string tempTargetPath, string targetPath) + { + if (_options.PreserveFileTime) + { + using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan)) + using (FileStream targetStream = new FileStream(tempTargetPath, FileMode.Create, System.IO.FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan)) + { + await sourceStream.CopyToAsync(targetStream, 81920, _cancellationToken); + } + + // 完成后,将临时文件重命名为目标文件 + if (File.Exists(targetPath)) + { + File.Delete(targetPath); + } + File.Move(tempTargetPath, targetPath); + } + else + { + // 简单复制 + File.Copy(sourcePath, targetPath, true); + } + } + + /// + /// 分块文件复制方法(修复多线程并发问题) + /// + private async Task CopyFileWithChunksAsync(string sourcePath, string tempTargetPath, string targetPath) + { + // 获取源文件信息 + var sourceFileInfo = new FileInfo(sourcePath); + long fileSize = sourceFileInfo.Length; + + // 计算分块大小和分块数量 + int chunkSizeBytes = _options.ChunkSizeMB * 1024 * 1024; + int chunksCount = (int)Math.Ceiling((double)fileSize / chunkSizeBytes); + + // 检查目标文件是否存在,如果存在则尝试使用现有块 + bool targetExists = File.Exists(targetPath); + bool[] chunkCompleted = new bool[chunksCount]; + Dictionary chunkTempFiles = new Dictionary(); + + try + { + // 检查之前的块是否已经存在并且有效 + if (targetExists && chunksCount > 1) + { + for (int i = 0; i < chunksCount; i++) + { + string chunkPath = $"{targetPath}.part{i}{_options.TempFileSuffix}"; + if (File.Exists(chunkPath)) + { + // 验证块的完整性 + bool chunkValid = await VerifyChunkIntegrityAsync(sourcePath, chunkPath, i, chunkSizeBytes); + chunkCompleted[i] = chunkValid; + + if (chunkValid) + { + // 如果块有效,记录到字典中 + chunkTempFiles[i] = chunkPath; + } + else if (File.Exists(chunkPath)) + { + // 如果块无效,删除它 + try { File.Delete(chunkPath); } catch { } + } + } + } + } + + // 如果是小文件或者只有一个块,使用标准复制方法 + if (chunksCount <= 1 || fileSize <= chunkSizeBytes) + { + await CopyFileStandardAsync(sourcePath, tempTargetPath, targetPath); + return; + } + + // 创建目标临时文件 + using (var targetStream = new FileStream(tempTargetPath, FileMode.Create, System.IO.FileAccess.Write)) + { + // 调整文件大小以预分配空间 + targetStream.SetLength(fileSize); + } + + // 创建任务列表 + var tasks = new List(); + + // 使用内存中的信号量控制并行度 + int maxParallelChunks = Math.Min(_options.MaxParallelOperations, 4); + using (var semaphore = new SemaphoreSlim(maxParallelChunks)) + { + // 处理每个块 + for (int i = 0; i < chunksCount; i++) + { + int chunkIndex = i; + + // 如果这个块已经完成,跳过它 + if (chunkCompleted[chunkIndex]) + { + ReportProgress($"跳过已完成的块 {chunkIndex + 1}/{chunksCount} - {Path.GetFileName(sourcePath)}", -1); + continue; + } + + // 等待信号量 + await semaphore.WaitAsync(_cancellationToken); + + // 为每个块创建唯一的临时文件路径 + string chunkTempPath = $"{targetPath}.part{chunkIndex}{_options.TempFileSuffix}"; + + tasks.Add(Task.Run(async () => + { + try + { + await ProcessChunkAsync(sourcePath, chunkTempPath, chunkIndex, chunkSizeBytes, fileSize); + chunkTempFiles[chunkIndex] = chunkTempPath; + } + finally + { + semaphore.Release(); + } + }, _cancellationToken)); + } + + // 等待所有任务完成 + await Task.WhenAll(tasks); + } + + // 所有块处理完成后,将块合并到临时文件 + using (var targetStream = new FileStream(tempTargetPath, FileMode.Open, FileAccess.Write, FileShare.None)) + { + for (int i = 0; i < chunksCount; i++) + { + if (chunkTempFiles.TryGetValue(i, out string chunkPath) && File.Exists(chunkPath)) + { + long startPosition = (long)i * chunkSizeBytes; + targetStream.Position = startPosition; + + using (var chunkStream = new FileStream(chunkPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + await chunkStream.CopyToAsync(targetStream, 81920, _cancellationToken); + } + } + else + { + // 如果缺少块,抛出异常 + throw new IOException($"缺少块 {i} 用于文件 {targetPath}"); + } + } + } + + // 最后将临时文件重命名为目标文件 + if (File.Exists(targetPath)) + { + File.Delete(targetPath); + } + + File.Move(tempTargetPath, targetPath); + } + finally + { + // 清理临时块文件 + foreach (var chunkPath in chunkTempFiles.Values) + { + try + { + if (File.Exists(chunkPath)) + { + File.Delete(chunkPath); + } + } + catch { /* 忽略清理错误 */ } + } + } + } + + /// + /// 处理单个文件块(修复文件锁定问题) + /// + private async Task ProcessChunkAsync(string sourcePath, string chunkTempPath, int chunkIndex, int chunkSizeBytes, long fileSize) + { + long startPosition = (long)chunkIndex * chunkSizeBytes; + long endPosition = Math.Min(startPosition + chunkSizeBytes, fileSize); + int currentChunkSize = (int)(endPosition - startPosition); + + try + { + // 步骤1: 从源文件读取指定块的数据到内存 + byte[] buffer = new byte[currentChunkSize]; + + using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + sourceStream.Position = startPosition; + await sourceStream.ReadAsync(buffer, 0, currentChunkSize, _cancellationToken); + } + + // 步骤2: 将数据直接写入块临时文件 + Directory.CreateDirectory(Path.GetDirectoryName(chunkTempPath)); + using (var chunkStream = new FileStream(chunkTempPath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + await chunkStream.WriteAsync(buffer, 0, currentChunkSize, _cancellationToken); + await chunkStream.FlushAsync(_cancellationToken); + } + + // 报告进度 + ReportProgress($"处理块 {chunkIndex + 1}/{Math.Ceiling((double)fileSize / chunkSizeBytes)} - " + + $"{Path.GetFileName(sourcePath)} - 100%", -1); + } + catch (Exception ex) + { + Log.Error(ex, $"处理文件块失败: {sourcePath}, 块 {chunkIndex}"); + throw; + } + } + + /// + /// 验证文件完整性 + /// + private async Task VerifyFileIntegrityAsync(string sourcePath, string targetPath) + { + // 如果使用哈希比较方法,我们可以利用现有哈希计算函数 + if (_options.CompareMethod == ESyncCompareMethod.Hash && _options.HashAlgorithm.HasValue) + { + using (var sourceStream = new FileStream(sourcePath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) + using (var targetStream = new FileStream(targetPath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) + { + string algorithm = _options.HashAlgorithm.ToString(); + byte[] sourceHash = HashHelper.ComputeHash(sourceStream, algorithm); + byte[] targetHash = HashHelper.ComputeHash(targetStream, algorithm); + + return CompareHash(sourceHash, targetHash); + } + } + else + { + // 对于其他比较方法,执行标准比较 + var sourceInfo = new FileInfo(sourcePath); + var targetInfo = new FileInfo(targetPath); + + // 首先比较大小 + if (sourceInfo.Length != targetInfo.Length) + return false; + + // 如果文件很小,直接比较内容 + if (sourceInfo.Length < 1024 * 1024) // 小于1MB + { + return await CompareFileContentCompletely(sourcePath, targetPath); + } + + // 对于大文件,使用抽样比较 + return await CompareFileContentSampling(sourcePath, targetPath); + } + } + + /// + /// 完全比较两个文件的内容 + /// + private async Task CompareFileContentCompletely(string file1, string file2) + { + const int bufferSize = 4096; + + using (var stream1 = new FileStream(file1, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read, bufferSize, FileOptions.SequentialScan)) + using (var stream2 = new FileStream(file2, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read, bufferSize, FileOptions.SequentialScan)) + { + byte[] buffer1 = new byte[bufferSize]; + byte[] buffer2 = new byte[bufferSize]; + + while (true) + { + int count1 = await stream1.ReadAsync(buffer1, 0, bufferSize); + int count2 = await stream2.ReadAsync(buffer2, 0, bufferSize); + + if (count1 != count2) + return false; + + if (count1 == 0) + return true; + + for (int i = 0; i < count1; i++) + { + if (buffer1[i] != buffer2[i]) + return false; + } + } + } + } + + /// + /// 使用抽样比较两个文件的内容 + /// + private async Task CompareFileContentSampling(string file1, string file2) + { + const int sampleSize = 8192; + const double samplingRate = 0.01; // 抽样比例1% + + using (var stream1 = new FileStream(file1, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) + using (var stream2 = new FileStream(file2, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) + { + long fileSize = stream1.Length; + + // 始终比较文件头部和尾部 + if (!await CompareFileRegion(stream1, stream2, 0, (int)Math.Min(sampleSize, fileSize))) + return false; + + if (fileSize > sampleSize) + { + if (!await CompareFileRegion(stream1, stream2, fileSize - sampleSize, sampleSize)) + return false; + + // 随机抽样比较中间部分 + Random random = new Random(fileSize.GetHashCode()); + int numSamples = (int)Math.Max(1, (fileSize * samplingRate) / sampleSize); + + for (int i = 0; i < numSamples; i++) + { + long position = sampleSize + (long)(random.NextDouble() * (fileSize - sampleSize * 2)); + if (!await CompareFileRegion(stream1, stream2, position, sampleSize)) + return false; + } + } + + return true; + } + } + + /// + /// 比较两个文件流指定区域的内容 + /// + private async Task CompareFileRegion(Stream stream1, Stream stream2, long position, int length) + { + byte[] buffer1 = new byte[length]; + byte[] buffer2 = new byte[length]; + + stream1.Position = position; + stream2.Position = position; + + await stream1.ReadAsync(buffer1, 0, length); + await stream2.ReadAsync(buffer2, 0, length); + + for (int i = 0; i < length; i++) + { + if (buffer1[i] != buffer2[i]) + return false; + } + + return true; + } + + /// + /// 验证文件块的完整性 + /// + private async Task VerifyChunkIntegrityAsync(string sourcePath, string chunkPath, int chunkIndex, int chunkSizeBytes) + { + if (!File.Exists(chunkPath)) + return false; + + var chunkInfo = new FileInfo(chunkPath); + var sourceInfo = new FileInfo(sourcePath); + + long startPosition = (long)chunkIndex * chunkSizeBytes; + long endPosition = Math.Min(startPosition + chunkSizeBytes, sourceInfo.Length); + int expectedChunkSize = (int)(endPosition - startPosition); + + // 首先检查大小 + if (chunkInfo.Length != expectedChunkSize) + return false; + + // 然后比较内容抽样 + using (var sourceStream = new FileStream(sourcePath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) + using (var chunkStream = new FileStream(chunkPath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) + { + sourceStream.Position = startPosition; + + // 对于小块,直接完整比较 + if (expectedChunkSize < 1024 * 1024) // 小于1MB + { + byte[] sourceBuffer = new byte[expectedChunkSize]; + byte[] chunkBuffer = new byte[expectedChunkSize]; + + await sourceStream.ReadAsync(sourceBuffer, 0, expectedChunkSize); + await chunkStream.ReadAsync(chunkBuffer, 0, expectedChunkSize); + + return CompareBytes(sourceBuffer, chunkBuffer); + } + + // 对于大块,使用抽样比较 + const int sampleSize = 8192; + + // 比较开始、中间和结束位置 + if (!await CompareStreamRegion(sourceStream, chunkStream, 0, sampleSize)) + return false; + + if (!await CompareStreamRegion(sourceStream, chunkStream, expectedChunkSize / 2, sampleSize)) + return false; + + if (!await CompareStreamRegion(sourceStream, chunkStream, expectedChunkSize - sampleSize, sampleSize)) + return false; + + return true; + } + } + + /// + /// 比较两个流指定区域的内容 + /// + private async Task CompareStreamRegion(Stream stream1, Stream stream2, long relativePosition, int length) + { + long originalPosition1 = stream1.Position; + long originalPosition2 = stream2.Position; + + try + { + stream1.Position = originalPosition1 + relativePosition; + stream2.Position = relativePosition; // 第二个流从0开始 + + byte[] buffer1 = new byte[length]; + byte[] buffer2 = new byte[length]; + + int read1 = await stream1.ReadAsync(buffer1, 0, length); + int read2 = await stream2.ReadAsync(buffer2, 0, length); + + if (read1 != read2) + return false; + + for (int i = 0; i < read1; i++) + { + if (buffer1[i] != buffer2[i]) + return false; + } + + return true; + } + finally + { + // 恢复原始位置 + stream1.Position = originalPosition1; + stream2.Position = originalPosition2; + } + } + /// /// 计算进度百分比 /// @@ -1876,6 +2348,29 @@ public class SyncOptions /// public bool ExecuteImmediately { get; set; } = true; + /// + /// 文件同步分块大小(MB),当值大于0时启用分块传输 + /// 分块传输支持断点续传,适用于大文件传输,对大文件采用固定分割,并对比 hash 的方式进行完整性校验,如果某个块一致,则不再传输 + /// 当需要传输的分块完成后,最后再合并到临时文件 .mdrivetmp 中 + /// 分块默认后缀名格式为 {文件名}.part{分块序号}.mdrivetmp + /// + public int ChunkSizeMB { get; set; } = 0; + + /// + /// 同步完成后是否同步文件的最后修改时间 + /// + public bool SyncLastModifiedTime { get; set; } = true; + + /// + /// 临时文件后缀,用于标识传输中的文件。默认为.mdrivetmp + /// + public string TempFileSuffix { get; set; } = ".mdrivetmp"; + + /// + /// 文件传输完成后验证文件完整性 + /// + public bool VerifyAfterCopy { get; set; } = true; + /// /// 源存储提供者类型(用于跨服务同步) /// From 79e40e3db8beb4dd7dc48dfc8d47a9adb6502751 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 14:33:04 +0800 Subject: [PATCH 77/90] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=86=E5=9D=97?= =?UTF-8?q?=E4=BC=A0=E8=BE=93=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Properties/launchSettings.json | 2 +- src/MDriveSync.Cli/sync.json | 2 +- .../Services/FileSyncHelper.cs | 139 +++++++++++++++--- 3 files changed, 122 insertions(+), 21 deletions(-) diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 4ec6409..5772448 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - "commandLineArgs": "sync -f sync.json -i 10 --chunk 1" + "commandLineArgs": "sync -f sync.json --chunk 64" } } } \ No newline at end of file diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index 10542a1..e3d05be 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,7 +1,7 @@ { "SourcePath": "E:\\_temp\\____synctest3", "TargetPath": "E:\\_temp\\____synctest4", - "SyncMode": "TwoWay", + "SyncMode": "OneWay", "CompareMethod": "DateTimeAndSize", "HashAlgorithm": null, "SamplingRate": 0.1, diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 7a5788b..b4a1daf 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -1619,13 +1619,17 @@ private async Task CopyFileWithRetryAsync(string sourcePath, string targetPath) { File.SetCreationTimeUtc(targetPath, File.GetCreationTimeUtc(sourcePath)); File.SetLastWriteTimeUtc(targetPath, File.GetLastWriteTimeUtc(sourcePath)); - File.SetLastAccessTimeUtc(targetPath, File.GetLastAccessTimeUtc(sourcePath)); + + // 不同步访问时间 + //File.SetLastAccessTimeUtc(targetPath, File.GetLastAccessTimeUtc(sourcePath)); } success = true; } - catch (IOException) when (currentRetry < maxRetries) + catch (IOException iex) when (currentRetry < maxRetries) { + Log.Warning(iex, "复制文件时出现IO异常: {Message}, 将重试操作", iex.Message); + currentRetry++; // 清理可能损坏的临时文件 @@ -1682,7 +1686,7 @@ private async Task CopyFileStandardAsync(string sourcePath, string tempTargetPat } /// - /// 分块文件复制方法(修复多线程并发问题) + /// 分块文件复制方法(支持断点续传和分块校验) /// private async Task CopyFileWithChunksAsync(string sourcePath, string tempTargetPath, string targetPath) { @@ -1694,37 +1698,60 @@ private async Task CopyFileWithChunksAsync(string sourcePath, string tempTargetP int chunkSizeBytes = _options.ChunkSizeMB * 1024 * 1024; int chunksCount = (int)Math.Ceiling((double)fileSize / chunkSizeBytes); - // 检查目标文件是否存在,如果存在则尝试使用现有块 - bool targetExists = File.Exists(targetPath); + // 设置使用的哈希算法 - 优先使用用户配置的算法,否则默认使用SHA256 + string hashAlgorithm = _options.HashAlgorithm?.ToString() ?? "SHA256"; + Log.Debug($"使用 {hashAlgorithm} 算法进行分块校验"); + + // 记录分块状态 bool[] chunkCompleted = new bool[chunksCount]; - Dictionary chunkTempFiles = new Dictionary(); + var chunkTempFiles = new ConcurrentDictionary(); try { // 检查之前的块是否已经存在并且有效 - if (targetExists && chunksCount > 1) + if (chunksCount > 1) { + Log.Debug($"检查分块文件是否可复用,共 {chunksCount} 个分块"); + + int reusedChunks = 0; + for (int i = 0; i < chunksCount; i++) { string chunkPath = $"{targetPath}.part{i}{_options.TempFileSuffix}"; if (File.Exists(chunkPath)) { // 验证块的完整性 - bool chunkValid = await VerifyChunkIntegrityAsync(sourcePath, chunkPath, i, chunkSizeBytes); + ReportProgress($"验证分块 {i + 1}/{chunksCount} 的完整性", -1); + bool chunkValid = await VerifyChunkIntegrityAsync(sourcePath, chunkPath, i, chunkSizeBytes, hashAlgorithm); chunkCompleted[i] = chunkValid; if (chunkValid) { // 如果块有效,记录到字典中 chunkTempFiles[i] = chunkPath; + reusedChunks++; + ReportProgress($"分块 {i + 1}/{chunksCount} 校验通过,可复用", -1); } else if (File.Exists(chunkPath)) { // 如果块无效,删除它 - try { File.Delete(chunkPath); } catch { } + try + { + File.Delete(chunkPath); + ReportProgress($"分块 {i + 1}/{chunksCount} 校验失败,已删除", -1); + } + catch (Exception ex) + { + Log.Warning(ex, $"删除无效分块文件失败: {chunkPath}"); + } } } } + + if (reusedChunks > 0) + { + Log.Information($"成功复用 {reusedChunks}/{chunksCount} 个分块文件,断点续传"); + } } // 如果是小文件或者只有一个块,使用标准复制方法 @@ -1785,6 +1812,8 @@ private async Task CopyFileWithChunksAsync(string sourcePath, string tempTargetP } // 所有块处理完成后,将块合并到临时文件 + ReportProgress($"所有分块处理完成,开始合并文件", -1); + using (var targetStream = new FileStream(tempTargetPath, FileMode.Open, FileAccess.Write, FileShare.None)) { for (int i = 0; i < chunksCount; i++) @@ -1802,11 +1831,21 @@ private async Task CopyFileWithChunksAsync(string sourcePath, string tempTargetP else { // 如果缺少块,抛出异常 - throw new IOException($"缺少块 {i} 用于文件 {targetPath}"); + throw new IOException($"缺少块 {i}/{chunksCount} 用于文件 {targetPath}"); } } } + // 最后进行完整性验证 + if (_options.VerifyAfterCopy) + { + ReportProgress($"正在验证合并后的文件完整性: {Path.GetFileName(targetPath)}", -1); + if (!await VerifyFileIntegrityAsync(sourcePath, tempTargetPath)) + { + throw new IOException($"文件完整性验证失败: {sourcePath} -> {tempTargetPath}"); + } + } + // 最后将临时文件重命名为目标文件 if (File.Exists(targetPath)) { @@ -1814,26 +1853,86 @@ private async Task CopyFileWithChunksAsync(string sourcePath, string tempTargetP } File.Move(tempTargetPath, targetPath); + ReportProgress($"文件 {Path.GetFileName(targetPath)} 传输完成", -1); } finally { - // 清理临时块文件 - foreach (var chunkPath in chunkTempFiles.Values) + // 清理临时块文件 - 如成功则清理,否则保留以便下次续传 + if (File.Exists(targetPath)) { - try + foreach (var chunkPath in chunkTempFiles.Values) { - if (File.Exists(chunkPath)) + try + { + if (File.Exists(chunkPath)) + { + File.Delete(chunkPath); + } + } + catch (Exception ex) { - File.Delete(chunkPath); + Log.Warning(ex, $"删除临时分块文件失败: {chunkPath}"); } } - catch { /* 忽略清理错误 */ } } } } /// - /// 处理单个文件块(修复文件锁定问题) + /// 验证文件块的完整性 - 支持自定义哈希算法 + /// + private async Task VerifyChunkIntegrityAsync(string sourcePath, string chunkPath, int chunkIndex, int chunkSizeBytes, string hashAlgorithm = "SHA256") + { + if (!File.Exists(chunkPath)) + return false; + + var chunkInfo = new FileInfo(chunkPath); + var sourceInfo = new FileInfo(sourcePath); + + long startPosition = (long)chunkIndex * chunkSizeBytes; + long endPosition = Math.Min(startPosition + chunkSizeBytes, sourceInfo.Length); + int expectedChunkSize = (int)(endPosition - startPosition); + + // 首先检查大小 + if (chunkInfo.Length != expectedChunkSize) + { + Log.Debug($"分块大小不匹配: 预期 {expectedChunkSize} 字节, 实际 {chunkInfo.Length} 字节"); + return false; + } + + try + { + // 使用指定的哈希算法计算源文件分块的哈希值 + byte[] sourceChunkHash; + using (var sourceStream = new FileStream(sourcePath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) + { + sourceStream.Position = startPosition; + byte[] buffer = new byte[expectedChunkSize]; + await sourceStream.ReadAsync(buffer, 0, expectedChunkSize, _cancellationToken); + sourceChunkHash = HashHelper.ComputeHash(buffer, hashAlgorithm); + } + + // 计算目标分块文件的哈希值 + byte[] targetChunkHash; + using (var chunkStream = new FileStream(chunkPath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) + { + byte[] buffer = new byte[expectedChunkSize]; + await chunkStream.ReadAsync(buffer, 0, expectedChunkSize, _cancellationToken); + targetChunkHash = HashHelper.ComputeHash(buffer, hashAlgorithm); + } + + // 比较哈希值 + return CompareHash(sourceChunkHash, targetChunkHash); + } + catch (Exception ex) + { + Log.Warning(ex, $"验证分块时发生错误: {chunkPath}"); + return false; + } + } + + /// + /// 处理单个文件块 /// private async Task ProcessChunkAsync(string sourcePath, string chunkTempPath, int chunkIndex, int chunkSizeBytes, long fileSize) { @@ -1843,7 +1942,7 @@ private async Task ProcessChunkAsync(string sourcePath, string chunkTempPath, in try { - // 步骤1: 从源文件读取指定块的数据到内存 + // 从源文件读取指定块的数据到内存 byte[] buffer = new byte[currentChunkSize]; using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read)) @@ -1852,8 +1951,10 @@ private async Task ProcessChunkAsync(string sourcePath, string chunkTempPath, in await sourceStream.ReadAsync(buffer, 0, currentChunkSize, _cancellationToken); } - // 步骤2: 将数据直接写入块临时文件 + // 确保目录存在 Directory.CreateDirectory(Path.GetDirectoryName(chunkTempPath)); + + // 将数据直接写入块临时文件 using (var chunkStream = new FileStream(chunkTempPath, FileMode.Create, FileAccess.Write, FileShare.None)) { await chunkStream.WriteAsync(buffer, 0, currentChunkSize, _cancellationToken); From c361de4bff1cd92ccccb7f3e8b7f149cb0ce1e71 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 14:44:22 +0800 Subject: [PATCH 78/90] =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=BF=BD=E7=95=A5?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MDriveSync.Cli/Program.cs | 4 ++-- src/MDriveSync.Core/Services/FileSyncHelper.cs | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index 5ee5d6a..343a362 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -410,7 +410,7 @@ private static SyncOptions ParseSyncOptions(string[] args) // 如果有排除模式,则设置到选项中 if (excludePatterns.Count > 0) { - options.IgnorePatterns = excludePatterns.ToArray(); + options.IgnorePatterns = excludePatterns.ToList(); } // 验证必需参数 @@ -685,7 +685,7 @@ private static void CreateConfigFile(FileInfo output, DirectoryInfo source, Dire "**/*.tmp", "**/*.temp", "**/*.bak" - } + }.ToList() }; // 确保目录存在 diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index b4a1daf..de3bb60 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -49,6 +49,13 @@ public FileSyncHelper(SyncOptions options, IProgress progress = nu _progress = progress; _cancellationToken = cancellationToken; + // 默认忽略临时文件 + if (!string.IsNullOrWhiteSpace(_options.TempFileSuffix) && + !_options.IgnorePatterns.Contains(_options.TempFileSuffix)) + { + _options.IgnorePatterns.Add(_options.TempFileSuffix); + } + // 如果配置了 cron if (!string.IsNullOrEmpty(_options.CronExpression)) { @@ -1704,7 +1711,7 @@ private async Task CopyFileWithChunksAsync(string sourcePath, string tempTargetP // 记录分块状态 bool[] chunkCompleted = new bool[chunksCount]; - var chunkTempFiles = new ConcurrentDictionary(); + var chunkTempFiles = new ConcurrentDictionary(); try { @@ -2416,7 +2423,7 @@ public class SyncOptions /// /// 要忽略的文件/目录模式 /// - public IEnumerable IgnorePatterns { get; set; } = new List + public List IgnorePatterns { get; set; } = new List { "**/System Volume Information/**", "**/$RECYCLE.BIN/**", From edccf7cd8439124dd1dcdc718185f207e14a8fab Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 15:07:54 +0800 Subject: [PATCH 79/90] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=8C=E5=90=91?= =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MDriveSync.Cli/MDriveSync.Cli.csproj | 4 ++++ .../Services/FileSyncHelper.cs | 21 ++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/MDriveSync.Cli/MDriveSync.Cli.csproj b/src/MDriveSync.Cli/MDriveSync.Cli.csproj index 61d8f3a..bb08ad9 100644 --- a/src/MDriveSync.Cli/MDriveSync.Cli.csproj +++ b/src/MDriveSync.Cli/MDriveSync.Cli.csproj @@ -37,4 +37,8 @@ + + + + diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index de3bb60..9262427 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -842,7 +842,8 @@ private List CreateTwoWaySyncActionsOptimized( // 报告每个批次的进度 int progress = 70 + (int)(30 * (batchIndex + 1) / unprocessedBatchCount); - ReportProgress($"处理目标->源文件 批次 {batchIndex + 1}/{unprocessedBatchCount}, 剩余: {unprocessedTargetFiles.Count - (batchIndex + 1) * batchSize}个文件", progress); + int remainingFiles = Math.Max(0, unprocessedTargetFiles.Count - (batchIndex + 1) * batchSize); + ReportProgress($"处理目标->源文件 批次 {batchIndex + 1}/{unprocessedBatchCount}, 剩余: {remainingFiles}个文件", progress); } } } @@ -1027,14 +1028,20 @@ private async Task ExecuteSingleActionAsync(SyncAction action) { _cancellationToken.ThrowIfCancellationRequested(); + //// 确定源和目标路径(考虑同步方向) + //string actualSource = action.Direction == ESyncDirection.SourceToTarget + // ? action.SourcePath + // : action.TargetPath; + + //string actualTarget = action.Direction == ESyncDirection.SourceToTarget + // ? action.TargetPath + // : action.SourcePath; + + // 确定源和目标路径(考虑同步方向) - string actualSource = action.Direction == ESyncDirection.SourceToTarget - ? action.SourcePath - : action.TargetPath; + string actualSource = action.SourcePath; - string actualTarget = action.Direction == ESyncDirection.SourceToTarget - ? action.TargetPath - : action.SourcePath; + string actualTarget = action.TargetPath; switch (action.ActionType) { From 2b6fae618188cdc1918d15f9c4d9cf233d63fa8d Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 17:11:22 +0800 Subject: [PATCH 80/90] --dev --- src/MDriveSync.Cli/Program.cs | 247 +++++++++++++----- .../Properties/launchSettings.json | 3 +- src/MDriveSync.Cli/sync.json | 13 +- .../Services/FileSyncHelper.cs | 4 +- 4 files changed, 193 insertions(+), 74 deletions(-) diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index 343a362..ca25bbe 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -2,6 +2,7 @@ using MDriveSync.Security.Models; using Quartz; using Serilog; +using System.Text; using System.Text.Json; namespace MDriveSync.Cli @@ -13,60 +14,110 @@ private static async Task Main(string[] args) // 配置日志 ConfigureLogging(); + // 检查是否为开发模式 + bool isDevMode = args.Contains("--dev"); + if (isDevMode) + { + // 从参数列表中移除 --dev 参数 + args = args.Where(arg => arg != "--dev").ToArray(); + Log.Information("正在开发模式下运行,程序将保持运行状态等待新命令,按 Ctrl+C 退出"); + } + + int exitCode = 0; try { - if (args.Length == 0 || args[0] == "--help" || args[0] == "-h") + do { - ShowHelp(); + if (isDevMode && args.Length == 0) + { + // 在开发模式下,如果没有参数,显示提示符 + Console.Write("mdrive> "); + string input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + continue; + + // 解析输入的命令行 + args = ParseCommandLine(input).ToArray(); + if (args.Length == 0) + continue; + } - // sync 参数 - ShowSyncHelp(); - Console.WriteLine(); + // 处理参数 + if (args.Length == 0 || args[0] == "--help" || args[0] == "-h") + { + ShowHelp(); - // config 参数 - ShowConfigHelp(); - Console.WriteLine(); + // sync 参数 + ShowSyncHelp(); + Console.WriteLine(); - // 示例 - Console.WriteLine("示例:"); - Console.WriteLine("mdrive sync --source C:\\Source --target D:\\Targetd"); + // config 参数 + ShowConfigHelp(); + Console.WriteLine(); - return 0; - } + // 示例 + Console.WriteLine("示例:"); + Console.WriteLine("mdrive sync --source C:\\Source --target D:\\Targetd"); + } + else + { + string command = args[0].ToLower(); - string command = args[0].ToLower(); + switch (command) + { + case "sync": + exitCode = await HandleSyncCommand(args.Skip(1).ToArray()); + break; + + case "config": + exitCode = HandleConfigCommand(args.Skip(1).ToArray()); + break; + + case "version": + ShowVersion(); + exitCode = 0; + break; + + case "exit": + case "quit": + if (isDevMode) + { + Log.Information("退出程序"); + return 0; + } + goto default; - switch (command) - { - case "sync": - return await HandleSyncCommand(args.Skip(1).ToArray()); + default: + { + Log.Error($"未知命令: {command}"); + ShowHelp(); - case "config": - return HandleConfigCommand(args.Skip(1).ToArray()); + // sync 参数 + ShowSyncHelp(); + Console.WriteLine(); - case "version": - ShowVersion(); - return 0; + // config 参数 + ShowConfigHelp(); + Console.WriteLine(); - default: - { - Log.Error($"未知命令: {command}"); - ShowHelp(); + // 示例 + Console.WriteLine("示例:"); + Console.WriteLine("mdrive sync --source C:\\Source --target D:\\Targetd"); + exitCode = 1; + } + break; + } + } - // sync 参数 - ShowSyncHelp(); - Console.WriteLine(); + // 如果不是开发模式,或者是带参数的正常调用,处理完后就退出 + if (!isDevMode || (isDevMode && args.Length > 0 && args != null)) + { + args = Array.Empty(); // 清空参数,准备下一次输入 + } - // config 参数 - ShowConfigHelp(); - Console.WriteLine(); + } while (isDevMode); // 在开发模式下循环执行 - // 示例 - Console.WriteLine("示例:"); - Console.WriteLine("mdrive sync --source C:\\Source --target D:\\Targetd"); - } - return 1; - } + return exitCode; } catch (Exception ex) { @@ -79,6 +130,63 @@ private static async Task Main(string[] args) } } + /// + /// 解析命令行字符串为参数数组 + /// + private static IEnumerable ParseCommandLine(string commandLine) + { + bool inQuotes = false; + bool isEscaping = false; + var arguments = new List(); + var currentArgument = new StringBuilder(); + + // 处理空字符串 + if (string.IsNullOrEmpty(commandLine)) + return arguments; + + foreach (char c in commandLine) + { + if (isEscaping) + { + // 转义后的字符直接添加 + currentArgument.Append(c); + isEscaping = false; + } + else if (c == '\\') + { + // 开始转义 + isEscaping = true; + } + else if (c == '"') + { + // 切换引号状态 + inQuotes = !inQuotes; + } + else if (c == ' ' && !inQuotes) + { + // 空格且不在引号内,表示一个参数结束 + if (currentArgument.Length > 0) + { + arguments.Add(currentArgument.ToString()); + currentArgument.Clear(); + } + } + else + { + // 普通字符,添加到当前参数 + currentArgument.Append(c); + } + } + + // 添加最后一个参数 + if (currentArgument.Length > 0) + { + arguments.Add(currentArgument.ToString()); + } + + return arguments; + } + /// /// 配置日志系统 /// @@ -106,12 +214,15 @@ private static void ShowHelp() Console.WriteLine("mdrive [命令] [选项]"); Console.WriteLine(); Console.WriteLine("命令:"); - Console.WriteLine(" sync 执行文件同步操作"); - Console.WriteLine(" config 管理同步配置文件"); - Console.WriteLine(" version 显示程序版本信息"); + Console.WriteLine(" sync 执行文件同步操作"); + Console.WriteLine(" config 管理同步配置文件"); + Console.WriteLine(" version 显示程序版本信息"); + Console.WriteLine(" exit 退出程序 (仅在开发模式下有效)"); + Console.WriteLine(" quit 退出程序 (仅在开发模式下有效)"); Console.WriteLine(); Console.WriteLine("选项:"); - Console.WriteLine(" --help, -h 显示帮助信息"); + Console.WriteLine(" --help, -h 显示帮助信息"); + Console.WriteLine(" --dev 开发模式,交互式运行程序"); Console.WriteLine(); Console.WriteLine("使用 'mdrive [命令] --help' 查看特定命令的帮助信息"); Console.WriteLine(); @@ -195,29 +306,29 @@ private static void ShowSyncHelp() Console.WriteLine("执行文件同步操作"); Console.WriteLine(); Console.WriteLine("用法:"); - Console.WriteLine(" mdrive sync [选项]"); + Console.WriteLine(" mdrive sync [选项]"); Console.WriteLine(); Console.WriteLine("选项:"); - Console.WriteLine(" --source, -s 源目录路径 (必需)"); - Console.WriteLine(" --target, -t 目标目录路径 (必需)"); - Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); - Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); - Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); - Console.WriteLine(" --config, -f 配置文件路径, 示例: -f sync.json"); - Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); - Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); - Console.WriteLine(" --verbose, -v 显示详细日志信息 (默认: false)"); - Console.WriteLine(" --threads, -j 并行操作的最大线程数 (默认: CPU核心数)"); - Console.WriteLine(" --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true)"); - Console.WriteLine(" --preserve-time 保留原始文件时间 (默认: true)"); - Console.WriteLine(" --interval, -i 同步间隔, 单位秒"); - Console.WriteLine(" --cron, Cron表达式,设置后将优先使用Cron表达式进行调度"); - Console.WriteLine(" --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步,(默认: true)"); - Console.WriteLine(" --chunk-size, --chunk 文件同步分块大小(MB),大于0启用分块传输"); - Console.WriteLine(" --sync-last-modified-time 同步完成后是否同步文件的最后修改时间 (默认: true)"); - Console.WriteLine(" --temp-file-suffix 临时文件后缀 (默认: .mdrivetmp)"); - Console.WriteLine(" --verify-after-copy 文件传输完成后验证文件完整性 (默认: true)"); - Console.WriteLine(" --help 显示帮助信息"); + Console.WriteLine(" --source, -s 源目录路径 (必需)"); + Console.WriteLine(" --target, -t 目标目录路径 (必需)"); + Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); + Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); + Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); + Console.WriteLine(" --config, -f 配置文件路径, 示例: -f sync.json"); + Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); + Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); + Console.WriteLine(" --verbose, -v 显示详细日志信息 (默认: false)"); + Console.WriteLine(" --threads, -j 并行操作的最大线程数 (默认: CPU核心数)"); + Console.WriteLine(" --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true)"); + Console.WriteLine(" --preserve-time 保留原始文件时间 (默认: true)"); + Console.WriteLine(" --interval, -i 同步间隔, 单位秒"); + Console.WriteLine(" --cron, Cron表达式,设置后将优先使用Cron表达式进行调度"); + Console.WriteLine(" --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步,(默认: true)"); + Console.WriteLine(" --chunk-size, --chunk 文件同步分块大小(MB),大于0启用分块传输"); + Console.WriteLine(" --sync-last-modified-time 同步完成后是否同步文件的最后修改时间 (默认: true)"); + Console.WriteLine(" --temp-file-suffix 临时文件后缀 (默认: .mdrivetmp)"); + Console.WriteLine(" --verify-after-copy 文件传输完成后验证文件完整性 (默认: true)"); + Console.WriteLine(" --help 显示帮助信息"); } /// @@ -467,13 +578,13 @@ private static void ShowConfigHelp() Console.WriteLine("管理同步配置文件"); Console.WriteLine(); Console.WriteLine("用法:"); - Console.WriteLine(" mdrivesync config [子命令] [选项]"); + Console.WriteLine(" mdrive config [子命令] [选项]"); Console.WriteLine(); Console.WriteLine("子命令:"); Console.WriteLine(" create 创建新的配置文件"); Console.WriteLine(" view 查看现有配置文件内容"); Console.WriteLine(); - Console.WriteLine("使用 'mdrivesync config [子命令] --help' 查看特定子命令的帮助信息"); + Console.WriteLine("使用 'mdrive config [子命令] --help' 查看特定子命令的帮助信息"); } /// @@ -578,7 +689,7 @@ private static void ShowConfigCreateHelp() Console.WriteLine("创建新的配置文件"); Console.WriteLine(); Console.WriteLine("用法:"); - Console.WriteLine(" mdrivesync config create [选项]"); + Console.WriteLine(" mdrive config create [选项]"); Console.WriteLine(); Console.WriteLine("选项:"); Console.WriteLine(" --output, -o 输出文件路径 (必需)"); @@ -651,7 +762,7 @@ private static void ShowConfigViewHelp() Console.WriteLine("查看现有配置文件内容"); Console.WriteLine(); Console.WriteLine("用法:"); - Console.WriteLine(" mdrivesync config view [选项]"); + Console.WriteLine(" mdrive config view [选项]"); Console.WriteLine(); Console.WriteLine("选项:"); Console.WriteLine(" --file, -f 配置文件路径 (必需)"); diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 5772448..17e2afd 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,7 +2,8 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - "commandLineArgs": "sync -f sync.json --chunk 64" + //"commandLineArgs": "sync -f sync.json --chunk 64" + "commandLineArgs": "--dev" } } } \ No newline at end of file diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index e3d05be..4c61768 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -7,7 +7,7 @@ "SamplingRate": 0.1, "MaxParallelOperations": 24, "EnableParallelFileOperations": true, - "DateTimeThresholdSeconds": 2, + "DateTimeThresholdSeconds": 0, "PreserveFileTime": true, "UseRecycleBin": true, "ConflictResolution": "Newer", @@ -15,7 +15,7 @@ "PreviewOnly": false, "ContinueOnError": true, "MaxRetries": 3, - "Verbose": true, + "Verbose": false, "IgnorePatterns": [ "**/System Volume Information/**", "**/$RECYCLE.BIN/**", @@ -29,5 +29,12 @@ "**/obj/*", "**/.git/*", "**/.next/*" - ] + ], + "Interval": 0, + "CronExpression": null, + "ExecuteImmediately": true, + "ChunkSizeMB": 64, + "SyncLastModifiedTime": true, + "TempFileSuffix": ".mdrivetmp", + "VerifyAfterCopy": true } \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 9262427..f911233 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -1335,7 +1335,7 @@ private bool CompareFileContent(FileInfo sourceFile, FileInfo targetFile) ReportProgress($"高性能扫描器失败,切换到备用扫描器", -1); // 退回到 FileFastScanner(备用扫描器) - var fastScanResult = await ScanWithFileFastScannerAsync(path); + var fastScanResult = ScanWithFileFastScannerAsync(path); return fastScanResult; } } @@ -1373,7 +1373,7 @@ private bool CompareFileContent(FileInfo sourceFile, FileInfo targetFile) /// /// 使用 FileFastScanner 扫描目录 /// - private async Task<(Dictionary, HashSet)> ScanWithFileFastScannerAsync(string path) + private (Dictionary, HashSet) ScanWithFileFastScannerAsync(string path) { var scanner = new FileFastScanner(); var result = scanner.ScanAsync( From cbc4ba7e4dbb00bc19aa413b658e37a84d7b30de Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 18:54:22 +0800 Subject: [PATCH 81/90] v1.1.0 --- src/MDriveSync.Cli/MDriveSync.Cli.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MDriveSync.Cli/MDriveSync.Cli.csproj b/src/MDriveSync.Cli/MDriveSync.Cli.csproj index bb08ad9..5db905c 100644 --- a/src/MDriveSync.Cli/MDriveSync.Cli.csproj +++ b/src/MDriveSync.Cli/MDriveSync.Cli.csproj @@ -11,7 +11,7 @@ true mdrive mdrive - 1.0.5 + 1.1.0 TrueAI.ORG 多平台文件同步命令行工具 MIT From 835da002c0399b66066006db5f877e2535c2998c Mon Sep 17 00:00:00 2001 From: trueai-org Date: Wed, 14 May 2025 19:07:08 +0800 Subject: [PATCH 82/90] readme --- src/MDriveSync.Cli/README.md | 46 +++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/MDriveSync.Cli/README.md b/src/MDriveSync.Cli/README.md index 1a47902..1e800da 100644 --- a/src/MDriveSync.Cli/README.md +++ b/src/MDriveSync.Cli/README.md @@ -56,13 +56,15 @@ MDriveSync CLI 提供了以下主要命令: mdrive [命令] [选项] 命令: - sync 执行文件同步操作 - config 管理同步配置文件 - version 显示程序版本信息 + sync 执行文件同步操作 + config 管理同步配置文件 + version 显示程序版本信息 + exit 退出程序 (仅在开发模式下有效) + quit 退出程序 (仅在开发模式下有效) 选项: - --help 显示帮助信息 - + --help, -h 显示帮助信息 + --dev 开发模式,交互式运行程序 ``` ## 同步命令 (sync) @@ -77,20 +79,26 @@ sync - 执行文件同步操作 mdrive sync [选项] 选项: - -s, --source 源目录路径 (必需) - -t, --target 目标目录路径 (必需) - -m, --mode 同步模式:OneWay(单向)、Mirror(镜像)、TwoWay(双向) [默认: OneWay] - -c, --compare 文件比较方法:Size(大小)、DateTime(修改时间)、DateTimeAndSize(时间和大小)、 - Content(内容)、Hash(哈希) [默认: DateTimeAndSize] - -h, --hash 哈希算法:MD5、SHA1、SHA256、SHA384、SHA512 [默认: SHA256] - -f, --config 配置文件路径 - -e, --exclude 排除的文件或目录模式(支持通配符,可多次指定) - -p, --preview 预览模式,不实际执行操作 - -v, --verbose 显示详细日志信息 - -j, --threads 并行操作的最大线程数 [默认: 系统处理器数量] - -r, --recycle-bin 使用回收站代替直接删除文件 [默认: true] - --preserve-time 保留原始文件时间 [默认: true] - --help 显示帮助信息 + --source, -s 源目录路径 (必需) + --target, -t 目标目录路径 (必需) + --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay) + --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize) + --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128 + --config, -f 配置文件路径, 示例: -f sync.json + --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定) + --preview, -p 预览模式,不实际执行操作 (默认: false) + --verbose, -v 显示详细日志信息 (默认: false) + --threads, -j 并行操作的最大线程数 (默认: CPU核心数) + --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true) + --preserve-time 保留原始文件时间 (默认: true) + --interval, -i 同步间隔, 单位秒 + --cron, Cron表达式,设置后将优先使用Cron表达式进行调度 + --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步,(默认: true) + --chunk-size, --chunk 文件同步分块大小(MB),大于0启用分块传输 + --sync-last-modified-time 同步完成后是否同步文件的最后修改时间 (默认: true) + --temp-file-suffix 临时文件后缀 (默认: .mdrivetmp) + --verify-after-copy 文件传输完成后验证文件完整性 (默认: true) + --help 显示帮助信息 ``` From 5ea4e6ad07cd95320fda97b6c76824690abb599b Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 15 May 2025 11:51:10 +0800 Subject: [PATCH 83/90] =?UTF-8?q?=E9=87=87=E6=A0=B7=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Properties/launchSettings.json | 4 +- src/MDriveSync.Cli/sync.json | 9 +- .../Services/FileSyncHelper.cs | 209 ++++++++++++++-- src/MDriveSync.Security/HashHelper.cs | 161 ++++++++++++ src/MDriveSync.Test/FileSyncHelperTests.cs | 1 - .../SamplingParametersTests.cs | 231 ++++++++++++++++++ 6 files changed, 585 insertions(+), 30 deletions(-) create mode 100644 src/MDriveSync.Test/SamplingParametersTests.cs diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 17e2afd..72c48ae 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,8 +2,8 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - //"commandLineArgs": "sync -f sync.json --chunk 64" - "commandLineArgs": "--dev" + "commandLineArgs": "sync -f sync.json --chunk 64" + //"commandLineArgs": "--dev" } } } \ No newline at end of file diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index 4c61768..55a533b 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -2,10 +2,11 @@ "SourcePath": "E:\\_temp\\____synctest3", "TargetPath": "E:\\_temp\\____synctest4", "SyncMode": "OneWay", - "CompareMethod": "DateTimeAndSize", - "HashAlgorithm": null, - "SamplingRate": 0.1, - "MaxParallelOperations": 24, + "CompareMethod": "Hash", + "HashAlgorithm": "SHA256", + "SamplingRate": 0.6, + "SamplingRateMinFileSize": 1048576, + "MaxParallelOperations": 4, "EnableParallelFileOperations": true, "DateTimeThresholdSeconds": 0, "PreserveFileTime": true, diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index f911233..88ff07a 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -56,6 +56,12 @@ public FileSyncHelper(SyncOptions options, IProgress progress = nu _options.IgnorePatterns.Add(_options.TempFileSuffix); } + // 抽样率文件最小限制,至少 16KB 的文件才参与抽样 + if (_options.SamplingRateMinFileSize < 16 * 1024) + { + _options.SamplingRateMinFileSize = 16 * 1024; + } + // 如果配置了 cron if (!string.IsNullOrEmpty(_options.CronExpression)) { @@ -1189,11 +1195,25 @@ private bool FilesAreEqual(FileInfo sourceFile, FileInfo targetFile) /// private bool CompareFileHash(FileInfo sourceFile, FileInfo targetFile) { - if (_options.SamplingRate > 0 && _options.SamplingRate < 1.0) + // 分块抽样 hash 计算规则 + if (_options.SamplingRate > 0 && + _options.SamplingRate < 1.0 && + sourceFile.Length > _options.SamplingRateMinFileSize) { return CompareFileHashWithSampling(sourceFile, targetFile); } + return CompareFileHashByInfo(sourceFile, targetFile); + } + + /// + /// 使用哈希算法比较文件 + /// + /// + /// + /// + private bool CompareFileHashByInfo(FileInfo sourceFile, FileInfo targetFile) + { // 全文件哈希比较 var hashAlgorithm = GetHashAlgorithm().ToString(); @@ -1215,24 +1235,121 @@ private bool CompareFileHash(FileInfo sourceFile, FileInfo targetFile) return CompareHash(sourceHash, targetHash); } + /// + /// 计算最佳块大小、块数量和抽样数量 + /// + /// 文件大小(字节) + /// 元组 (块大小, 块数量, 抽样数量) + public static (long BlockSize, int BlockCount, int SamplesToCheck) CalculateOptimalSamplingParameters(long fileSize, double rate) + { + // 常量参数定义 + const int minBlockSizeKB = 16; // 最小块大小 KB + const int maxBlockSizeMB = 128; // 最大块大小 MB + const int idealBlockSizeMB = 16; // 理想块大小 MB + const int minSampleBlocks = 1; // 最少抽样块数 + const int maxSampleBlocks = 500; // 最多抽样块数 + + // 目标抽样数量, 理想抽样数量 + int targetSampleCount = Math.Min(maxSampleBlocks, (int)Math.Max(10, 10 / rate / 10)); + + // 转换为字节单位 + long minBlockSize = minBlockSizeKB * 1024L; + long maxBlockSize = maxBlockSizeMB * 1024L * 1024L; + long idealBlockSize = idealBlockSizeMB * 1024L * 1024L; + + // 1. 计算块大小 - 目标是将文件分割成大约 targetSampleCount / rate 个块 + long calculatedBlockSize = fileSize / Math.Max(1, (int)(targetSampleCount / rate)); + + // 2. 根据文件大小自适应调整块大小 + long adaptiveBlockSize; + + if (fileSize < 10 * 1024 * 1024) // < 10MB + { + // 对小文件使用较小的块,确保至少有minSampleBlocks个块 + adaptiveBlockSize = Math.Min(calculatedBlockSize, fileSize / (minSampleBlocks * 2)); + } + else if (fileSize < 100 * 1024 * 1024) // < 100MB + { + // 对小文件使用较小的块 + adaptiveBlockSize = Math.Min(idealBlockSize / 2, calculatedBlockSize); + } + else if (fileSize < 1 * 1024 * 1024 * 1024L) // < 1GB + { + // 中等大小文件使用接近理想块大小 + adaptiveBlockSize = Math.Min(idealBlockSize, calculatedBlockSize); + } + else if (fileSize < 10 * 1024 * 1024 * 1024L) // < 10GB + { + // 大文件增加块大小 + adaptiveBlockSize = Math.Min(idealBlockSize * 2, calculatedBlockSize); + } + else // >= 10GB + { + // 超大文件使用较大块 + adaptiveBlockSize = Math.Min(idealBlockSize * 4, calculatedBlockSize); + } + + // 3. 确保块大小在合理范围内 + long finalBlockSize = Math.Max(minBlockSize, Math.Min(maxBlockSize, adaptiveBlockSize)); + + // 4. 计算块数量(向上取整,至少有1个块) + int blockCount = Math.Max(1, (int)Math.Ceiling(fileSize / (double)finalBlockSize)); + + // 5. 根据目标抽样数量和抽样率计算最终抽样数量 + int samplesToCheck = (int)Math.Ceiling(blockCount * rate); + + // 6. 如果抽样数量接近目标值的±20%范围外,调整块大小重新计算 + if (Math.Abs(samplesToCheck - targetSampleCount) > targetSampleCount * 0.2 && blockCount > minSampleBlocks * 2) + { + // 调整块大小以接近目标抽样数量 + double adjustmentFactor = (double)targetSampleCount / samplesToCheck; + finalBlockSize = (long)(finalBlockSize / adjustmentFactor); + + // 确保块大小在合理范围内 + finalBlockSize = Math.Max(minBlockSize, Math.Min(maxBlockSize, finalBlockSize)); + + // 重新计算块数量 + blockCount = Math.Max(1, (int)Math.Ceiling(fileSize / (double)finalBlockSize)); + + // 重新计算抽样数量 + samplesToCheck = (int)Math.Ceiling(blockCount * rate); + } + + // 7. 确保抽样数量在合理范围内 + samplesToCheck = Math.Max(minSampleBlocks, Math.Min(samplesToCheck, blockCount)); + samplesToCheck = Math.Min(samplesToCheck, maxSampleBlocks); + + // 8. 对于小文件特别处理,块数量少于目标抽样数时,检查所有块 + if (blockCount <= targetSampleCount) + { + samplesToCheck = blockCount; + } + + return (finalBlockSize, blockCount, samplesToCheck); + } + /// /// 使用抽样哈希比较文件 /// private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFile) { - const int headerSize = 8192; // 头部始终比较的大小 - const int randomSampleSize = 8192; // 随机抽样的块大小 + // 头部/尾部始终比较的大小 + const int headerSize = 1024 * 16; // 如果文件较小,直接全文比较 - if (sourceFile.Length < headerSize * 2) - return CompareFileHash(sourceFile, targetFile); + if (sourceFile.Length <= _options.SamplingRateMinFileSize) + return CompareFileHashByInfo(sourceFile, targetFile); + + // 如果文件较小,直接全文比较 + if (sourceFile.Length <= headerSize * 2) + return CompareFileHashByInfo(sourceFile, targetFile); var hashAlgorithm = GetHashAlgorithm().ToString(); using var sourceStream = sourceFile.OpenRead(); using var targetStream = targetFile.OpenRead(); - // 比较文件头部 + // 比较文件头部, 高效比较每个字节 byte[] sourceHeader = new byte[headerSize]; byte[] targetHeader = new byte[headerSize]; @@ -1242,7 +1359,7 @@ private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFil if (!CompareBytes(sourceHeader, targetHeader)) return false; - // 比较文件尾部 + // 比较文件尾部, 高效比较每个字节 byte[] sourceFooter = new byte[headerSize]; byte[] targetFooter = new byte[headerSize]; @@ -1255,30 +1372,68 @@ private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFil if (!CompareBytes(sourceFooter, targetFooter)) return false; - // 生成随机抽样点 - Random random = new Random(sourceFile.FullName.GetHashCode()); - int samplesCount = (int)Math.Max(1, (sourceFile.Length - headerSize * 2) * _options.SamplingRate / randomSampleSize); - long range = sourceFile.Length - headerSize * 2 - randomSampleSize; + // 优化抽样算法 + // 计算哪些块需要抽样计算 + + // 计算最佳抽样块大小和数量 + var (blockSize, blockCount, samplesToCheck) = CalculateOptimalSamplingParameters(sourceFile.Length, _options.SamplingRate); + + // 使用随机抽样 + var random = new Random(); + var blocksToCheck = new HashSet(); + while (blocksToCheck.Count < samplesToCheck && blocksToCheck.Count < blockCount) + { + // 避免重复抽样同一块 + // 随机生成块索引 + int blockIndex = random.Next(0, blockCount); + if (!blocksToCheck.Contains(blockIndex)) + { + blocksToCheck.Add(blockIndex); + } + } - for (int i = 0; i < samplesCount; i++) + try { - // 生成随机位置(避开头尾已比较的部分) - long position = headerSize + (long)(random.NextDouble() * range); + // 对每个选定的块计算并比较哈希值 + foreach (int blockIndex in blocksToCheck) + { + // 计算要比较的块的起始位置 + long startPosition = blockIndex * blockSize; - byte[] sourceSample = new byte[randomSampleSize]; - byte[] targetSample = new byte[randomSampleSize]; + // 确保不超出文件末尾 + long currentBlockSize = Math.Min(blockSize, sourceFile.Length - startPosition); - sourceStream.Seek(position, SeekOrigin.Begin); - targetStream.Seek(position, SeekOrigin.Begin); + // 直接从流读取并计算哈希值 - 无需将整个块加载到内存 + sourceStream.Position = startPosition; + targetStream.Position = startPosition; - sourceStream.Read(sourceSample, 0, randomSampleSize); - targetStream.Read(targetSample, 0, randomSampleSize); + // 使用HashHelper直接对流的指定部分计算哈希 + // 注意:我们需要限制读取长度为当前块大小 + byte[] sourceBlockHash = HashHelper.ComputeStreamHash( + sourceStream, + hashAlgorithm, + (int)currentBlockSize); // 将长度限制为当前块大小 - if (!CompareBytes(sourceSample, targetSample)) - return false; + byte[] targetBlockHash = HashHelper.ComputeStreamHash( + targetStream, + hashAlgorithm, + (int)currentBlockSize); + + if (!CompareHash(sourceBlockHash, targetBlockHash)) + return false; + } + + // 所有抽样块都匹配,认为文件相同 + return true; } + catch (Exception ex) + { + Log.Error(ex, "进行文件抽样哈希比较时出错: {SourceFile} - {TargetFile}", + sourceFile.FullName, targetFile.FullName); - return true; + // 出错时保守处理,认为文件不同 + return false; + } } /// @@ -2369,9 +2524,17 @@ public class SyncOptions /// /// 哈希抽样率(0.0-1.0) + /// 同步模式:文件哈希抽样率(按字节大小计算抽样率) + /// 备份模式:分块哈希抽样率(按分块数量计算抽样率),当文件分块时才使用,未分块的文件使用(按字节大小计算抽样率) /// public double SamplingRate { get; set; } = 0.1; + /// + /// 抽样率文件最小限制,至少 16KB 的文件才参与抽样(默认:1MB) + /// 低于最小限制的文使用完整 hash 校验 + /// + public int SamplingRateMinFileSize { get; set; } = 1024 * 1024; + /// /// 最大并行操作数 /// diff --git a/src/MDriveSync.Security/HashHelper.cs b/src/MDriveSync.Security/HashHelper.cs index 434af46..b6235a0 100644 --- a/src/MDriveSync.Security/HashHelper.cs +++ b/src/MDriveSync.Security/HashHelper.cs @@ -321,5 +321,166 @@ public static bool CompareHashes(byte[] hash1, byte[] hash2) return true; } + + /// + /// 计算流从当前位置开始指定长度的哈希值 + /// + /// 要计算哈希的流 + /// 哈希算法名称 + /// 要计算哈希的长度,如果为null则计算到流末尾 + /// 哈希值的字节数组 + public static byte[] ComputeStreamHash(Stream stream, string algorithm, int? length = null) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + // 保存原始位置以便计算结束后恢复 + long originalPosition = stream.Position; + + try + { + // 根据不同算法计算流的指定部分的哈希值 + switch (algorithm.ToUpper()) + { + case "SHA256": + using (SHA256 sha256 = SHA256.Create()) + { + return ComputeHashWithLength(stream, sha256, length); + } + + case "SHA512": + using (SHA512 sha512 = SHA512.Create()) + { + return ComputeHashWithLength(stream, sha512, length); + } + + case "BLAKE3": + if (length.HasValue) + { + // 使用临时缓冲区计算指定长度的哈希值 + byte[] buffer = new byte[length.Value]; + int bytesRead = stream.Read(buffer, 0, length.Value); + return Hasher.Hash(buffer).AsSpan().ToArray(); + } + else + { + using var blake3Stream = new Blake3Stream(stream); + return blake3Stream.ComputeHash().AsSpan().ToArray(); + } + + case "MD5": + using (MD5 md5 = MD5.Create()) + { + return ComputeHashWithLength(stream, md5, length); + } + + case "SHA1": + using (SHA1 sha1 = SHA1.Create()) + { + return ComputeHashWithLength(stream, sha1, length); + } + + case "SHA3": + case "SHA384": + using (SHA384 sha384 = SHA384.Create()) + { + return ComputeHashWithLength(stream, sha384, length); + } + + case "XXH3": + { + var hasher = new XxHash3(); + return ComputeXXHashWithLength(stream, hasher, length, 8); + } + + case "XXH128": + { + var hasher = new XxHash128(); + return ComputeXXHashWithLength(stream, hasher, length, 16); + } + + default: + throw new ArgumentException("不支持的哈希算法", nameof(algorithm)); + } + } + finally + { + // 恢复流的原始位置 + stream.Position = originalPosition; + } + } + + /// + /// 使用指定的哈希算法计算流中指定长度数据的哈希值 + /// + private static byte[] ComputeHashWithLength(Stream stream, HashAlgorithm hashAlgorithm, int? length) + { + if (!length.HasValue) + { + // 如果未指定长度,计算整个流的哈希值 + return hashAlgorithm.ComputeHash(stream); + } + + // 计算指定长度的哈希值 + int bytesRemaining = length.Value; + byte[] buffer = new byte[Math.Min(81920, bytesRemaining)]; // 使用最多80KB的缓冲区 + + hashAlgorithm.Initialize(); + + while (bytesRemaining > 0) + { + int bytesToRead = Math.Min(buffer.Length, bytesRemaining); + int bytesRead = stream.Read(buffer, 0, bytesToRead); + + if (bytesRead == 0) + break; // 流结束 + + hashAlgorithm.TransformBlock(buffer, 0, bytesRead, buffer, 0); + bytesRemaining -= bytesRead; + } + + // 完成哈希计算 + hashAlgorithm.TransformFinalBlock(Array.Empty(), 0, 0); + return hashAlgorithm.Hash; + } + + /// + /// 使用XXHash算法计算流中指定长度数据的哈希值 + /// + private static byte[] ComputeXXHashWithLength(Stream stream, T hasher, int? length, int resultSize) where T : NonCryptographicHashAlgorithm + { + int totalBytesRead = 0; + int bytesToProcess = length ?? int.MaxValue; + byte[] buffer = new byte[81920]; // 80KB缓冲区 + + while (totalBytesRead < bytesToProcess) + { + int bytesToRead = Math.Min(buffer.Length, bytesToProcess - totalBytesRead); + int bytesRead = stream.Read(buffer, 0, bytesToRead); + + if (bytesRead == 0) + break; // 流结束 + + hasher.Append(buffer.AsSpan(0, bytesRead)); + totalBytesRead += bytesRead; + } + + // 根据XXHash类型生成相应的哈希值 + byte[] result = new byte[resultSize]; + + if (hasher is XxHash3 xxh3) + { + ulong hashValue = xxh3.GetCurrentHashAsUInt64(); + BinaryPrimitives.WriteUInt64BigEndian(result, hashValue); + } + else if (hasher is XxHash128 xxh128) + { + var hashValue = xxh128.GetCurrentHashAsUInt128(); + BinaryPrimitives.WriteUInt128BigEndian(result, hashValue); + } + + return result; + } + } } \ No newline at end of file diff --git a/src/MDriveSync.Test/FileSyncHelperTests.cs b/src/MDriveSync.Test/FileSyncHelperTests.cs index e79f16a..f9a5329 100644 --- a/src/MDriveSync.Test/FileSyncHelperTests.cs +++ b/src/MDriveSync.Test/FileSyncHelperTests.cs @@ -483,7 +483,6 @@ public async Task HashComparison_ShouldDetectContentChanges() SyncMode = ESyncMode.OneWay, CompareMethod = ESyncCompareMethod.Hash, HashAlgorithm = EHashType.SHA256, - SamplingRate = 0.1, // 10% MaxParallelOperations = 1 }; diff --git a/src/MDriveSync.Test/SamplingParametersTests.cs b/src/MDriveSync.Test/SamplingParametersTests.cs new file mode 100644 index 0000000..c9886f2 --- /dev/null +++ b/src/MDriveSync.Test/SamplingParametersTests.cs @@ -0,0 +1,231 @@ +using MDriveSync.Core.Services; +using Xunit; +using Xunit.Abstractions; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace MDriveSync.Test +{ + /// + /// 哈希采样参数计算优化测试 + /// + public class SamplingParametersTests : BaseTests + { + private readonly ITestOutputHelper _output; + + public SamplingParametersTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [InlineData(100 * 1024, 0.1)] // 100KB, 10%采样率 + [InlineData(1 * 1024 * 1024, 0.1)] // 1MB, 10%采样率 + [InlineData(10 * 1024 * 1024, 0.1)] // 10MB, 10%采样率 + [InlineData(100 * 1024 * 1024, 0.1)] // 100MB, 10%采样率 + [InlineData(1 * 1024 * 1024 * 1024L, 0.1)] // 1GB, 10%采样率 + [InlineData(10 * 1024 * 1024, 0.05)] // 10MB, 5%采样率 + [InlineData(10 * 1024 * 1024, 0.2)] // 10MB, 20%采样率 + [InlineData(10 * 1024 * 1024, 0.5)] // 10MB, 50%采样率 + [InlineData(10 * 1024 * 1024, 0.01)] // 10MB, 1%采样率 + public void CalculateOptimalSamplingParameters_ReturnsReasonableValues(long fileSize, double rate) + { + // Act + var (blockSize, blockCount, samplesToCheck) = FileSyncHelper.CalculateOptimalSamplingParameters(fileSize, rate); + + // 计算实际采样率 + double actualSamplingRate = (double)samplesToCheck / blockCount; + + // 计算覆盖率 + double coveragePercentage = (double)(blockSize * samplesToCheck) / fileSize * 100; + + // 输出结果 + _output.WriteLine($"文件大小: {FormatSize(fileSize)}"); + _output.WriteLine($"目标采样率: {rate:P2}"); + _output.WriteLine($"计算结果:"); + _output.WriteLine($" 块大小: {FormatSize(blockSize)}"); + _output.WriteLine($" 块数量: {blockCount}"); + _output.WriteLine($" 抽样数: {samplesToCheck}"); + _output.WriteLine($" 实际采样率: {actualSamplingRate:P2}"); + _output.WriteLine($" 数据覆盖率: {coveragePercentage:F2}%"); + _output.WriteLine(""); + + // Assert + // 1. 采样数量应在合理范围内 + Assert.InRange(samplesToCheck, 2, 100); // 最小2个样本,最大100个样本 + + // 2. 块大小应满足最小块限制 + Assert.True(blockSize >= 64 * 1024, "块大小应大于等于64KB"); + + // 3. 块数量应该合理 + Assert.True(blockCount > 0, "块数量应大于0"); + + // 4. 块大小 * 块数量应该接近文件大小 + Assert.True(blockSize * blockCount >= fileSize, "总块大小应覆盖整个文件"); + + // 5. 实际采样率应接近目标采样率 + if (fileSize > 1 * 1024 * 1024) // 对于大于1MB的文件做此检查 + { + // 对于量较小的文件,允许较大的偏差 + double tolerance = rate < 0.1 ? 1.0 : 0.5; // 非常低的采样率可能有较大偏差 + Assert.True(actualSamplingRate >= rate / tolerance && actualSamplingRate <= rate * tolerance, + $"实际采样率 {actualSamplingRate:P2} 应接近目标采样率 {rate:P2}"); + } + } + + [Fact] + public void CalculateOptimalSamplingParameters_PerformanceBenchmark() + { + // 准备测试数据 + var fileSizes = new List + { + 16 * 1024, // 16KB + 64 * 1024, // 64KB + 128 * 1024, // 128KB + 1 * 1024 * 1024, // 1MB + 10 * 1024 * 1024, // 10MB + 20 * 1024 * 1024, // 20MB + 50 * 1024 * 1024, // 100MB + 100 * 1024 * 1024, // 100MB + 200 * 1024 * 1024, // 200MB + 300 * 1024 * 1024, // 300MB + 800 * 1024 * 1024, // 800MB + 1 * 1024 * 1024 * 1024L, // 1GB + 10 * 1024 * 1024 * 1024L, // 10GB + 50 * 1024 * 1024 * 1024L // 50GB + }; + + var samplingRates = new List { 0.01, 0.05, 0.1, 0.2, 0.5, 0.6, 0.8, 0.9, 1 }; + + var results = new List<(long FileSize, double Rate, long BlockSize, int BlockCount, int SamplesCount, + double ActualRate, double Coverage, double ElapsedMs)>(); + + // 执行基准测试 + foreach (var fileSize in fileSizes) + { + foreach (var rate in samplingRates) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + // 重复调用以获得更准确的性能测量 + const int iterations = 1000; + var blockSizeSum = 0L; + var blockCountSum = 0; + var samplesSum = 0; + + for (int i = 0; i < iterations; i++) + { + var (blockSize, blockCount, samples) = FileSyncHelper.CalculateOptimalSamplingParameters(fileSize, rate); + blockSizeSum += blockSize; + blockCountSum += blockCount; + samplesSum += samples; + } + + stopwatch.Stop(); + + // 计算平均值 + var avgBlockSize = blockSizeSum / iterations; + var avgBlockCount = blockCountSum / (double)iterations; + var avgSamples = samplesSum / (double)iterations; + + // 计算实际采样率和覆盖率 + double actualRate = avgSamples / avgBlockCount; + double coverage = (avgBlockSize * avgSamples) / (double)fileSize * 100; + + // 记录结果 + results.Add((fileSize, rate, avgBlockSize, (int)avgBlockCount, (int)avgSamples, + actualRate, coverage, stopwatch.Elapsed.TotalMilliseconds / iterations)); + } + } + + // 输出结果 + var sb = new StringBuilder(); + sb.AppendLine("## 采样参数计算性能基准测试"); + sb.AppendLine("| 文件大小 | 目标采样率 | 块大小 | 块数量 | 抽样数 | 实际采样率 | 覆盖率 (%) | 计算时间 (μs) |"); + sb.AppendLine("|----------|------------|--------|--------|--------|------------|------------|--------------|"); + + foreach (var result in results) + { + // 修复字符串格式化错误,将对齐说明符放在格式说明符之前 + var str = $"| {FormatSize(result.FileSize),-8} | {result.Rate,-10:P2} | " + + $"{FormatSize(result.BlockSize),-6} | {result.BlockCount,-6} | {result.SamplesCount,-6} | " + + $"{result.ActualRate,-10:P2} | {result.Coverage,-10:F2} | {result.ElapsedMs * 1000,-12:F2} |"; + sb.AppendLine(str); + } + // 保存到文件以便查看完整结果 + string reportPath = Path.Combine(Directory.GetCurrentDirectory(), "采样参数性能测试报告.md"); + + // 使用 _output.ToString() 获取 Xunit 输出到内存的内容 + File.WriteAllText(reportPath, sb.ToString(), Encoding.UTF8); + _output.WriteLine(sb.ToString()); + _output.WriteLine($"\n报告已保存至: {reportPath}"); + + // 简单断言以确保测试运行完成 + Assert.True(results.Count == fileSizes.Count * samplingRates.Count, "应该测试了所有文件大小和采样率组合"); + } + + [Fact] + public void CalculateOptimalSamplingParameters_EdgeCases() + { + // 测试极端情况 + + // 1. 非常小的文件 + var smallResult = FileSyncHelper.CalculateOptimalSamplingParameters(1024, 0.1); + _output.WriteLine($"非常小的文件 (1KB):"); + _output.WriteLine($" 块大小: {FormatSize(smallResult.BlockSize)}"); + _output.WriteLine($" 块数量: {smallResult.BlockCount}"); + _output.WriteLine($" 抽样数: {smallResult.SamplesToCheck}"); + + // 2. 非常大的文件 + var largeResult = FileSyncHelper.CalculateOptimalSamplingParameters(100L * 1024 * 1024 * 1024, 0.1); + _output.WriteLine($"\n非常大的文件 (100GB):"); + _output.WriteLine($" 块大小: {FormatSize(largeResult.BlockSize)}"); + _output.WriteLine($" 块数量: {largeResult.BlockCount}"); + _output.WriteLine($" 抽样数: {largeResult.SamplesToCheck}"); + + // 3. 非常低的采样率 + var lowRateResult = FileSyncHelper.CalculateOptimalSamplingParameters(100 * 1024 * 1024, 0.001); + _output.WriteLine($"\n非常低的采样率 (0.1%):"); + _output.WriteLine($" 块大小: {FormatSize(lowRateResult.BlockSize)}"); + _output.WriteLine($" 块数量: {lowRateResult.BlockCount}"); + _output.WriteLine($" 抽样数: {lowRateResult.SamplesToCheck}"); + + // 4. 非常高的采样率 + var highRateResult = FileSyncHelper.CalculateOptimalSamplingParameters(100 * 1024 * 1024, 0.9); + _output.WriteLine($"\n非常高的采样率 (90%):"); + _output.WriteLine($" 块大小: {FormatSize(highRateResult.BlockSize)}"); + _output.WriteLine($" 块数量: {highRateResult.BlockCount}"); + _output.WriteLine($" 抽样数: {highRateResult.SamplesToCheck}"); + + // 断言 + Assert.True(smallResult.BlockCount >= 1, "即使是小文件也应至少有1个块"); + Assert.True(smallResult.SamplesToCheck >= 1, "即使是小文件也应至少有1个样本"); + + Assert.True(largeResult.BlockSize <= 64 * 1024 * 1024, "即使是大文件块大小也应该有上限"); + Assert.True(largeResult.SamplesToCheck <= 100, "样本数应该有上限"); + + Assert.True(lowRateResult.SamplesToCheck >= 2, "即使采样率很低也应至少有2个样本"); + + Assert.True(highRateResult.SamplesToCheck < highRateResult.BlockCount || + highRateResult.BlockCount <= 10, "高采样率不应导致不必要的全部抽样"); + } + + /// + /// 格式化数据大小显示 + /// + private string FormatSize(long bytes) + { + string[] suffixes = { "B", "KB", "MB", "GB", "TB", "PB" }; + int i = 0; + double size = bytes; + while (size >= 1024 && i < suffixes.Length - 1) + { + size /= 1024; + i++; + } + return $"{size:0.##} {suffixes[i]}"; + } + } +} From 3206d3aedea49a6fc14a53254d9daf9e6423580e Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 15 May 2025 15:00:36 +0800 Subject: [PATCH 84/90] =?UTF-8?q?=E6=8A=BD=E6=A0=B7=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MDriveSync.Cli/sync.json | 6 +- .../Services/FileSamplingCalculator.cs | 175 ++++++++++++++++++ .../Services/FileSyncHelper.cs | 93 +--------- .../SamplingParametersTests.cs | 25 ++- 4 files changed, 207 insertions(+), 92 deletions(-) create mode 100644 src/MDriveSync.Core/Services/FileSamplingCalculator.cs diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index 55a533b..ca5ebca 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,10 +1,10 @@ { - "SourcePath": "E:\\_temp\\____synctest3", - "TargetPath": "E:\\_temp\\____synctest4", + "SourcePath": "E:\\_temp\\____synctest1", + "TargetPath": "E:\\_temp\\____synctest2", "SyncMode": "OneWay", "CompareMethod": "Hash", "HashAlgorithm": "SHA256", - "SamplingRate": 0.6, + "SamplingRate": 0.05, "SamplingRateMinFileSize": 1048576, "MaxParallelOperations": 4, "EnableParallelFileOperations": true, diff --git a/src/MDriveSync.Core/Services/FileSamplingCalculator.cs b/src/MDriveSync.Core/Services/FileSamplingCalculator.cs new file mode 100644 index 0000000..c1d875e --- /dev/null +++ b/src/MDriveSync.Core/Services/FileSamplingCalculator.cs @@ -0,0 +1,175 @@ +namespace MDriveSync.Core.Services +{ + public class SamplingResult + { + /// + /// 总块数 + /// + public long TotalChunks { get; set; } + + /// + /// 块大小 + /// + public long ChunkSize { get; set; } + + /// + /// 抽样块数 + /// + public int SampledChunks { get; set; } + + /// + /// 实际覆盖率 + /// + public double Coverage { get; set; } + } + + /// + /// 文件采样计算器,抽样块数尽可能少 + /// + public class FileSamplingCalculator + { + private const long MinChunkSizeBytes = 16 * 1024; // 16KB + + private const long MaxChunkSizeBytes = 1024 * 1024 * 1024 * 1024L; // 1TB + + /// + /// 文件采样计算器,抽样块数尽可能少 + /// + /// 文件大小 + /// 抽样率 0-1 + /// + /// 最大抽样块数(1-16) + /// 当系数为 2 时,抽样率大于 0.65 时,将始终抽样。 + /// 当系数为 4 时,抽样率大于 0.80 时,将始终抽样。 + /// ... + /// 当系数为 4 时,抽样率大于 0.90 时,将始终抽样。 + /// + /// + public static SamplingResult CalculateSampling(long fileSize, double sampleRate, int maxSampledChunks = 4) + { + if (fileSize < MinChunkSizeBytes) + return FullCoverageResult(fileSize); + + if (sampleRate <= 0 || sampleRate >= 1) + return FullCoverageResult(fileSize); + + // 核心算法 + var bestResult = FindOptimalSampling(fileSize, sampleRate, maxSampledChunks); + return bestResult ?? AdjustEdgeCase(fileSize, sampleRate, maxSampledChunks); + } + + private static SamplingResult FindOptimalSampling(long fileSize, double sampleRate, int maxSampledChunks) + { + SamplingResult best = null; + double minError = double.MaxValue; + + // 遍历所有可能的抽样块数(优先小值) + for (int k = 1; k <= maxSampledChunks; k++) + { + // 计算理论分块数 + double exactN = k / sampleRate; + var n = (long)Math.Floor(exactN); + + // 有效性检查 + if (n < k || n == 0) continue; + + // 计算块尺寸 + long chunkSize = fileSize / n; + + // 块尺寸合法性检查 + if (chunkSize < MinChunkSizeBytes || chunkSize > MaxChunkSizeBytes) + continue; + + // 计算实际覆盖率 + double actualRate = (double)k / n; + double error = Math.Abs(actualRate - sampleRate); + + // 择优策略:误差更小,或误差相同但块数更少 + if (error < minError || (Math.Abs(error - minError) < 1e-9 && k < best.SampledChunks)) + { + best = new SamplingResult + { + TotalChunks = n, + ChunkSize = chunkSize, + SampledChunks = k, + Coverage = actualRate + }; + minError = error; + } + } + return best; + } + + // 边界情况调整(当无法找到合法分块时) + private static SamplingResult AdjustEdgeCase(long fileSize, double sampleRate, int maxSampledChunks) + { + // 判断需要调整的方向 + bool isOversized = (fileSize / (long)(1 / sampleRate)) > MaxChunkSizeBytes; + + if (isOversized) + return AdjustForMaxChunkSize(fileSize, sampleRate, maxSampledChunks); + else + return AdjustForMinChunkSize(fileSize, sampleRate, maxSampledChunks); + } + + // 调整到最小块尺寸 + private static SamplingResult AdjustForMinChunkSize(long fileSize, double sampleRate, int maxSampledChunks) + { + long n = fileSize / MinChunkSizeBytes; + n = Math.Max(n, 1); // 确保至少1个块 + + // 在允许范围内寻找最优k值 + int bestK = FindBestK(n, sampleRate, maxSampledChunks); + return new SamplingResult + { + TotalChunks = n, + ChunkSize = fileSize / n, + SampledChunks = bestK, + Coverage = (double)bestK / n + }; + } + + // 调整到最大块尺寸 + private static SamplingResult AdjustForMaxChunkSize(long fileSize, double sampleRate, int maxSampledChunks) + { + long n = (long)Math.Ceiling((double)fileSize / MaxChunkSizeBytes); + n = Math.Max(n, 1); // 确保至少1个块 + + // 在允许范围内寻找最优k值 + int bestK = FindBestK(n, sampleRate, maxSampledChunks); + return new SamplingResult + { + TotalChunks = n, + ChunkSize = fileSize / n, + SampledChunks = bestK, + Coverage = (double)bestK / n + }; + } + + // 在给定总块数n时寻找最佳k + private static int FindBestK(long n, double targetRate, int maxSampledChunks) + { + int bestK = 1; + double minError = double.MaxValue; + + for (int k = 1; k <= Math.Min(maxSampledChunks, n); k++) + { + double error = Math.Abs((double)k / n - targetRate); + if (error < minError || (Math.Abs(error - minError) < 1e-9 && k > bestK)) + { + bestK = k; + minError = error; + } + } + return bestK; + } + + private static SamplingResult FullCoverageResult(long fileSize) => new SamplingResult + { + TotalChunks = 1, + ChunkSize = fileSize, + SampledChunks = 1, + Coverage = 1 + }; + } +} \ No newline at end of file diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 88ff07a..5fb43af 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -1043,7 +1043,6 @@ private async Task ExecuteSingleActionAsync(SyncAction action) // ? action.TargetPath // : action.SourcePath; - // 确定源和目标路径(考虑同步方向) string actualSource = action.SourcePath; @@ -1236,96 +1235,14 @@ private bool CompareFileHashByInfo(FileInfo sourceFile, FileInfo targetFile) } /// - /// 计算最佳块大小、块数量和抽样数量 + /// 计算采样点数量,尽可能减少采样点数量 /// /// 文件大小(字节) /// 元组 (块大小, 块数量, 抽样数量) public static (long BlockSize, int BlockCount, int SamplesToCheck) CalculateOptimalSamplingParameters(long fileSize, double rate) { - // 常量参数定义 - const int minBlockSizeKB = 16; // 最小块大小 KB - const int maxBlockSizeMB = 128; // 最大块大小 MB - const int idealBlockSizeMB = 16; // 理想块大小 MB - const int minSampleBlocks = 1; // 最少抽样块数 - const int maxSampleBlocks = 500; // 最多抽样块数 - - // 目标抽样数量, 理想抽样数量 - int targetSampleCount = Math.Min(maxSampleBlocks, (int)Math.Max(10, 10 / rate / 10)); - - // 转换为字节单位 - long minBlockSize = minBlockSizeKB * 1024L; - long maxBlockSize = maxBlockSizeMB * 1024L * 1024L; - long idealBlockSize = idealBlockSizeMB * 1024L * 1024L; - - // 1. 计算块大小 - 目标是将文件分割成大约 targetSampleCount / rate 个块 - long calculatedBlockSize = fileSize / Math.Max(1, (int)(targetSampleCount / rate)); - - // 2. 根据文件大小自适应调整块大小 - long adaptiveBlockSize; - - if (fileSize < 10 * 1024 * 1024) // < 10MB - { - // 对小文件使用较小的块,确保至少有minSampleBlocks个块 - adaptiveBlockSize = Math.Min(calculatedBlockSize, fileSize / (minSampleBlocks * 2)); - } - else if (fileSize < 100 * 1024 * 1024) // < 100MB - { - // 对小文件使用较小的块 - adaptiveBlockSize = Math.Min(idealBlockSize / 2, calculatedBlockSize); - } - else if (fileSize < 1 * 1024 * 1024 * 1024L) // < 1GB - { - // 中等大小文件使用接近理想块大小 - adaptiveBlockSize = Math.Min(idealBlockSize, calculatedBlockSize); - } - else if (fileSize < 10 * 1024 * 1024 * 1024L) // < 10GB - { - // 大文件增加块大小 - adaptiveBlockSize = Math.Min(idealBlockSize * 2, calculatedBlockSize); - } - else // >= 10GB - { - // 超大文件使用较大块 - adaptiveBlockSize = Math.Min(idealBlockSize * 4, calculatedBlockSize); - } - - // 3. 确保块大小在合理范围内 - long finalBlockSize = Math.Max(minBlockSize, Math.Min(maxBlockSize, adaptiveBlockSize)); - - // 4. 计算块数量(向上取整,至少有1个块) - int blockCount = Math.Max(1, (int)Math.Ceiling(fileSize / (double)finalBlockSize)); - - // 5. 根据目标抽样数量和抽样率计算最终抽样数量 - int samplesToCheck = (int)Math.Ceiling(blockCount * rate); - - // 6. 如果抽样数量接近目标值的±20%范围外,调整块大小重新计算 - if (Math.Abs(samplesToCheck - targetSampleCount) > targetSampleCount * 0.2 && blockCount > minSampleBlocks * 2) - { - // 调整块大小以接近目标抽样数量 - double adjustmentFactor = (double)targetSampleCount / samplesToCheck; - finalBlockSize = (long)(finalBlockSize / adjustmentFactor); - - // 确保块大小在合理范围内 - finalBlockSize = Math.Max(minBlockSize, Math.Min(maxBlockSize, finalBlockSize)); - - // 重新计算块数量 - blockCount = Math.Max(1, (int)Math.Ceiling(fileSize / (double)finalBlockSize)); - - // 重新计算抽样数量 - samplesToCheck = (int)Math.Ceiling(blockCount * rate); - } - - // 7. 确保抽样数量在合理范围内 - samplesToCheck = Math.Max(minSampleBlocks, Math.Min(samplesToCheck, blockCount)); - samplesToCheck = Math.Min(samplesToCheck, maxSampleBlocks); - - // 8. 对于小文件特别处理,块数量少于目标抽样数时,检查所有块 - if (blockCount <= targetSampleCount) - { - samplesToCheck = blockCount; - } - - return (finalBlockSize, blockCount, samplesToCheck); + var res = FileSamplingCalculator.CalculateSampling(fileSize, rate); + return (res.ChunkSize, (int)res.TotalChunks, res.SampledChunks); } /// @@ -1375,7 +1292,7 @@ private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFil // 优化抽样算法 // 计算哪些块需要抽样计算 - // 计算最佳抽样块大小和数量 + // 计算采样点数量,尽可能减少采样点数量 var (blockSize, blockCount, samplesToCheck) = CalculateOptimalSamplingParameters(sourceFile.Length, _options.SamplingRate); // 使用随机抽样 @@ -1421,6 +1338,8 @@ private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFil if (!CompareHash(sourceBlockHash, targetBlockHash)) return false; + + Log.Information($"{sourceFile.Name}, 比较块 {blockIndex + 1}/{blockCount} 完成"); } // 所有抽样块都匹配,认为文件相同 diff --git a/src/MDriveSync.Test/SamplingParametersTests.cs b/src/MDriveSync.Test/SamplingParametersTests.cs index c9886f2..0184b04 100644 --- a/src/MDriveSync.Test/SamplingParametersTests.cs +++ b/src/MDriveSync.Test/SamplingParametersTests.cs @@ -96,7 +96,28 @@ public void CalculateOptimalSamplingParameters_PerformanceBenchmark() 50 * 1024 * 1024 * 1024L // 50GB }; - var samplingRates = new List { 0.01, 0.05, 0.1, 0.2, 0.5, 0.6, 0.8, 0.9, 1 }; + // 随机填充 1b - 1TB + for (int i = 0; i < 10; i++) + { + fileSizes.Add((long)(new Random().NextDouble() * 1024 * 1024 * 1024 * 1024)); + } + + // 随机填充 1b - 1G + for (int i = 0; i < 10; i++) + { + fileSizes.Add((long)(new Random().NextDouble() * 1024 * 1024 * 1024)); + } + + // 排序 + fileSizes.Sort(); + + var samplingRates = new List { 0.01, 0.05, 0.1, 0.2, 0.5, 0.6, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1 }; + // 随机填充 + for (int i = 0; i < 10; i++) + { + samplingRates.Add(new Random().NextDouble()); + } + samplingRates.Sort(); var results = new List<(long FileSize, double Rate, long BlockSize, int BlockCount, int SamplesCount, double ActualRate, double Coverage, double ElapsedMs)>(); @@ -141,7 +162,7 @@ public void CalculateOptimalSamplingParameters_PerformanceBenchmark() } // 输出结果 - var sb = new StringBuilder(); + var sb = new StringBuilder(); sb.AppendLine("## 采样参数计算性能基准测试"); sb.AppendLine("| 文件大小 | 目标采样率 | 块大小 | 块数量 | 抽样数 | 实际采样率 | 覆盖率 (%) | 计算时间 (μs) |"); sb.AppendLine("|----------|------------|--------|--------|--------|------------|------------|--------------|"); From 33b1559f82643eae7cda1bf5c9da721ea68084d3 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 15 May 2025 15:05:54 +0800 Subject: [PATCH 85/90] readme --- src/MDriveSync.Cli/Properties/launchSettings.json | 2 +- src/MDriveSync.Cli/sync.json | 2 +- src/MDriveSync.Core/Services/FileSyncHelper.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 72c48ae..56c7830 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - "commandLineArgs": "sync -f sync.json --chunk 64" + "commandLineArgs": "sync -f sync.json --chunk 64 -v true" //"commandLineArgs": "--dev" } } diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index ca5ebca..66c0485 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -4,7 +4,7 @@ "SyncMode": "OneWay", "CompareMethod": "Hash", "HashAlgorithm": "SHA256", - "SamplingRate": 0.05, + "SamplingRate": 0.1, "SamplingRateMinFileSize": 1048576, "MaxParallelOperations": 4, "EnableParallelFileOperations": true, diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 5fb43af..008344c 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -2443,8 +2443,8 @@ public class SyncOptions /// /// 哈希抽样率(0.0-1.0) - /// 同步模式:文件哈希抽样率(按字节大小计算抽样率) - /// 备份模式:分块哈希抽样率(按分块数量计算抽样率),当文件分块时才使用,未分块的文件使用(按字节大小计算抽样率) + /// 同步模式:文件哈希抽样率(按文件大小计算) + /// 备份模式:分块哈希抽样率(按分块数量计算),当文件分块时才使用,未分块的文件使用(按文件大小计算) /// public double SamplingRate { get; set; } = 0.1; From fec404c413c790d1c5242080af3c5aa4f5bac145 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 15 May 2025 15:24:02 +0800 Subject: [PATCH 86/90] 1.2.0 --- src/MDriveSync.Cli/MDriveSync.Cli.csproj | 2 +- src/MDriveSync.Cli/Program.cs | 111 ++++++++++++++---- .../Properties/launchSettings.json | 4 +- src/MDriveSync.Cli/README.md | 12 +- 4 files changed, 101 insertions(+), 28 deletions(-) diff --git a/src/MDriveSync.Cli/MDriveSync.Cli.csproj b/src/MDriveSync.Cli/MDriveSync.Cli.csproj index 5db905c..4bb6608 100644 --- a/src/MDriveSync.Cli/MDriveSync.Cli.csproj +++ b/src/MDriveSync.Cli/MDriveSync.Cli.csproj @@ -11,7 +11,7 @@ true mdrive mdrive - 1.1.0 + 1.2.0 TrueAI.ORG 多平台文件同步命令行工具 MIT diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index ca25bbe..64f8279 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -314,6 +314,10 @@ private static void ShowSyncHelp() Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); + Console.WriteLine(" --sampling-rate 哈希抽样率 (0.0-1.0之间的小数,默认: 0.1)"); + Console.WriteLine(" --sampling-min-size 参与抽样的最小文件大小 (字节,默认: 1MB)"); + Console.WriteLine(" --date-threshold 修改时间比较阈值 (秒,默认: 0)"); + Console.WriteLine(" --parallel 是否启用并行文件操作 (默认: true)"); Console.WriteLine(" --config, -f 配置文件路径, 示例: -f sync.json"); Console.WriteLine(" --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定)"); Console.WriteLine(" --preview, -p 预览模式,不实际执行操作 (默认: false)"); @@ -321,14 +325,20 @@ private static void ShowSyncHelp() Console.WriteLine(" --threads, -j 并行操作的最大线程数 (默认: CPU核心数)"); Console.WriteLine(" --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true)"); Console.WriteLine(" --preserve-time 保留原始文件时间 (默认: true)"); + Console.WriteLine(" --continue-on-error 发生错误时是否继续执行 (默认: true)"); + Console.WriteLine(" --retry 操作失败时的最大重试次数 (默认: 3)"); + Console.WriteLine(" --conflict 冲突解决策略: SourceWins, TargetWins, KeepBoth, Skip, Newer(默认), Older, Larger"); + Console.WriteLine(" --follow-symlinks 是否跟踪符号链接 (默认: false)"); Console.WriteLine(" --interval, -i 同步间隔, 单位秒"); - Console.WriteLine(" --cron, Cron表达式,设置后将优先使用Cron表达式进行调度"); - Console.WriteLine(" --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步,(默认: true)"); + Console.WriteLine(" --cron Cron表达式,设置后将优先使用Cron表达式进行调度"); + Console.WriteLine(" --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步 (默认: true)"); Console.WriteLine(" --chunk-size, --chunk 文件同步分块大小(MB),大于0启用分块传输"); Console.WriteLine(" --sync-last-modified-time 同步完成后是否同步文件的最后修改时间 (默认: true)"); Console.WriteLine(" --temp-file-suffix 临时文件后缀 (默认: .mdrivetmp)"); Console.WriteLine(" --verify-after-copy 文件传输完成后验证文件完整性 (默认: true)"); + Console.WriteLine(); Console.WriteLine(" --help 显示帮助信息"); + Console.WriteLine(); } /// @@ -367,6 +377,7 @@ private static SyncOptions ParseSyncOptions(string[] args) switch (arg) { + // 基本路径选项 case "--source": case "-s": if (value != null) options.SourcePath = value; @@ -377,6 +388,7 @@ private static SyncOptions ParseSyncOptions(string[] args) if (value != null) options.TargetPath = value; break; + // 同步模式和方法 case "--mode": case "-m": if (value != null && Enum.TryParse(value, true, out var mode)) @@ -389,12 +401,7 @@ private static SyncOptions ParseSyncOptions(string[] args) options.CompareMethod = compare; break; - case "--hash": - case "-h": - if (value != null && Enum.TryParse(value, true, out var hash)) - options.HashAlgorithm = hash; - break; - + // 配置文件加载 case "--config": case "-f": if (value != null) @@ -424,6 +431,44 @@ private static SyncOptions ParseSyncOptions(string[] args) } break; + // 哈希相关选项 + case "--hash": + case "-h": + if (value != null && Enum.TryParse(value, true, out var hash)) + options.HashAlgorithm = hash; + break; + + case "--sampling-rate": + if (value != null && double.TryParse(value, out double samplingRate) && samplingRate >= 0.0 && samplingRate <= 1.0) + options.SamplingRate = samplingRate; + break; + + case "--sampling-min-size": + if (value != null && int.TryParse(value, out int minSize) && minSize > 0) + options.SamplingRateMinFileSize = minSize; + break; + + // 时间比较选项 + case "--date-threshold": + if (value != null && int.TryParse(value, out int threshold) && threshold >= 0) + options.DateTimeThresholdSeconds = threshold; + break; + + // 并行处理选项 + case "--parallel": + if (value != null && bool.TryParse(value, out bool enableParallel)) + options.EnableParallelFileOperations = enableParallel; + else + options.EnableParallelFileOperations = true; + break; + + case "--threads": + case "-j": + if (value != null && int.TryParse(value, out int threads) && threads > 0) + options.MaxParallelOperations = threads; + break; + + // 文件处理方式选项 case "--exclude": case "-e": if (value != null) @@ -440,12 +485,6 @@ private static SyncOptions ParseSyncOptions(string[] args) options.Verbose = true; break; - case "--threads": - case "-j": - if (value != null && int.TryParse(value, out int threads) && threads > 0) - options.MaxParallelOperations = threads; - break; - case "--recycle-bin": case "-r": if (value != null && bool.TryParse(value, out bool useRecycle)) @@ -461,6 +500,34 @@ private static SyncOptions ParseSyncOptions(string[] args) options.PreserveFileTime = true; break; + // 错误处理选项 + case "--continue-on-error": + if (value != null && bool.TryParse(value, out bool continueOnError)) + options.ContinueOnError = continueOnError; + else + options.ContinueOnError = true; + break; + + case "--retry": + if (value != null && int.TryParse(value, out int retries) && retries >= 0) + options.MaxRetries = retries; + break; + + // 符号链接选项 + case "--follow-symlinks": + if (value != null && bool.TryParse(value, out bool followSymlinks)) + options.FollowSymlinks = followSymlinks; + else + options.FollowSymlinks = true; + break; + + // 冲突处理选项 + case "--conflict": + if (value != null && Enum.TryParse(value, true, out var conflict)) + options.ConflictResolution = conflict; + break; + + // 定时任务选项 case "--interval": case "-i": if (value != null && int.TryParse(value, out int interval) && interval > 0) @@ -468,18 +535,15 @@ private static SyncOptions ParseSyncOptions(string[] args) break; case "--cron": + if (value != null) { - if (value != null) + // 验证 Cron 表达式 + if (!CronExpression.IsValidExpression(value)) { - // 验证 Cron 表达式 - if (!CronExpression.IsValidExpression(value)) - { - Log.Error($"无效的 Cron 表达式: {value}"); - return null; - } - - options.CronExpression = value; + Log.Error($"无效的 Cron 表达式: {value}"); + return null; } + options.CronExpression = value; } break; @@ -491,6 +555,7 @@ private static SyncOptions ParseSyncOptions(string[] args) options.ExecuteImmediately = true; break; + // 文件传输选项 case "--chunk-size": case "--chunk": if (value != null && int.TryParse(value, out int chunkSize) && chunkSize > 0) diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 56c7830..6e343f2 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,8 +2,8 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - "commandLineArgs": "sync -f sync.json --chunk 64 -v true" - //"commandLineArgs": "--dev" + //"commandLineArgs": "sync -f sync.json --chunk 64 -v true" + "commandLineArgs": "--dev" } } } \ No newline at end of file diff --git a/src/MDriveSync.Cli/README.md b/src/MDriveSync.Cli/README.md index 1e800da..3c4785f 100644 --- a/src/MDriveSync.Cli/README.md +++ b/src/MDriveSync.Cli/README.md @@ -84,6 +84,10 @@ sync - 执行文件同步操作 --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay) --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize) --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128 + --sampling-rate 哈希抽样率 (0.0-1.0之间的小数,默认: 0.1) + --sampling-min-size 参与抽样的最小文件大小 (字节,默认: 1MB) + --date-threshold 修改时间比较阈值 (秒,默认: 0) + --parallel 是否启用并行文件操作 (默认: true) --config, -f 配置文件路径, 示例: -f sync.json --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定) --preview, -p 预览模式,不实际执行操作 (默认: false) @@ -91,9 +95,13 @@ sync - 执行文件同步操作 --threads, -j 并行操作的最大线程数 (默认: CPU核心数) --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true) --preserve-time 保留原始文件时间 (默认: true) + --continue-on-error 发生错误时是否继续执行 (默认: true) + --retry 操作失败时的最大重试次数 (默认: 3) + --conflict 冲突解决策略: SourceWins, TargetWins, KeepBoth, Skip, Newer(默认), Older, Larger + --follow-symlinks 是否跟踪符号链接 (默认: false) --interval, -i 同步间隔, 单位秒 - --cron, Cron表达式,设置后将优先使用Cron表达式进行调度 - --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步,(默认: true) + --cron Cron表达式,设置后将优先使用Cron表达式进行调度 + --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步 (默认: true) --chunk-size, --chunk 文件同步分块大小(MB),大于0启用分块传输 --sync-last-modified-time 同步完成后是否同步文件的最后修改时间 (默认: true) --temp-file-suffix 临时文件后缀 (默认: .mdrivetmp) From cb220581c19d669e8720122e8626033b63b4b35e Mon Sep 17 00:00:00 2001 From: trueai-org Date: Thu, 15 May 2025 17:44:26 +0800 Subject: [PATCH 87/90] 1.2.1 --- src/MDriveSync.Cli/MDriveSync.Cli.csproj | 2 +- src/MDriveSync.Cli/Program.cs | 32 ++++++++++++++----- src/MDriveSync.Cli/README.md | 2 +- .../Services/FileSyncHelper.cs | 2 +- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/MDriveSync.Cli/MDriveSync.Cli.csproj b/src/MDriveSync.Cli/MDriveSync.Cli.csproj index 4bb6608..db854e7 100644 --- a/src/MDriveSync.Cli/MDriveSync.Cli.csproj +++ b/src/MDriveSync.Cli/MDriveSync.Cli.csproj @@ -11,7 +11,7 @@ true mdrive mdrive - 1.2.0 + 1.2.1 TrueAI.ORG 多平台文件同步命令行工具 MIT diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index 64f8279..395dccb 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -37,7 +37,7 @@ private static async Task Main(string[] args) continue; // 解析输入的命令行 - args = ParseCommandLine(input).ToArray(); + args = input.Split(' ').ToArray(); // ParseCommandLine(input).ToArray(); if (args.Length == 0) continue; } @@ -144,22 +144,33 @@ private static IEnumerable ParseCommandLine(string commandLine) if (string.IsNullOrEmpty(commandLine)) return arguments; - foreach (char c in commandLine) + // 逐字符处理命令行 + for (int i = 0; i < commandLine.Length; i++) { + char c = commandLine[i]; + if (isEscaping) { - // 转义后的字符直接添加 + // 转义字符后面的字符直接添加 currentArgument.Append(c); isEscaping = false; } else if (c == '\\') { - // 开始转义 - isEscaping = true; + // 检查是否是转义引号的情况 + if (i + 1 < commandLine.Length && commandLine[i + 1] == '"') + { + currentArgument.Append('"'); + i++; // 跳过下一个字符(引号) + } + else + { + isEscaping = true; + } } else if (c == '"') { - // 切换引号状态 + // 切换引号状态,但不将引号加入参数 inQuotes = !inQuotes; } else if (c == ' ' && !inQuotes) @@ -184,6 +195,12 @@ private static IEnumerable ParseCommandLine(string commandLine) arguments.Add(currentArgument.ToString()); } + // 检查是否有未闭合的引号 + if (inQuotes) + { + Log.Warning("命令行中存在未闭合的引号"); + } + return arguments; } @@ -313,7 +330,7 @@ private static void ShowSyncHelp() Console.WriteLine(" --target, -t 目标目录路径 (必需)"); Console.WriteLine(" --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay)"); Console.WriteLine(" --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize)"); - Console.WriteLine(" --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); + Console.WriteLine(" --hash 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128"); Console.WriteLine(" --sampling-rate 哈希抽样率 (0.0-1.0之间的小数,默认: 0.1)"); Console.WriteLine(" --sampling-min-size 参与抽样的最小文件大小 (字节,默认: 1MB)"); Console.WriteLine(" --date-threshold 修改时间比较阈值 (秒,默认: 0)"); @@ -433,7 +450,6 @@ private static SyncOptions ParseSyncOptions(string[] args) // 哈希相关选项 case "--hash": - case "-h": if (value != null && Enum.TryParse(value, true, out var hash)) options.HashAlgorithm = hash; break; diff --git a/src/MDriveSync.Cli/README.md b/src/MDriveSync.Cli/README.md index 3c4785f..f17db55 100644 --- a/src/MDriveSync.Cli/README.md +++ b/src/MDriveSync.Cli/README.md @@ -83,7 +83,7 @@ sync - 执行文件同步操作 --target, -t 目标目录路径 (必需) --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay) --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize) - --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128 + --hash 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128 --sampling-rate 哈希抽样率 (0.0-1.0之间的小数,默认: 0.1) --sampling-min-size 参与抽样的最小文件大小 (字节,默认: 1MB) --date-threshold 修改时间比较阈值 (秒,默认: 0) diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 008344c..7ccaa41 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -1261,7 +1261,7 @@ private bool CompareFileHashWithSampling(FileInfo sourceFile, FileInfo targetFil if (sourceFile.Length <= headerSize * 2) return CompareFileHashByInfo(sourceFile, targetFile); - var hashAlgorithm = GetHashAlgorithm().ToString(); + var hashAlgorithm = GetHashAlgorithm()?.ToString() ?? "SHA256"; using var sourceStream = sourceFile.OpenRead(); using var targetStream = targetFile.OpenRead(); From f58cc6e89702fac70503bd61a341a17ab3c96ec2 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 16 May 2025 09:35:44 +0800 Subject: [PATCH 88/90] =?UTF-8?q?=E5=8F=8C=E5=90=91=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Properties/launchSettings.json | 4 +- src/MDriveSync.Cli/sync.json | 11 +- .../Services/FileSyncHelper.cs | 278 ++++++++++++------ 3 files changed, 203 insertions(+), 90 deletions(-) diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 6e343f2..56c7830 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,8 +2,8 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - //"commandLineArgs": "sync -f sync.json --chunk 64 -v true" - "commandLineArgs": "--dev" + "commandLineArgs": "sync -f sync.json --chunk 64 -v true" + //"commandLineArgs": "--dev" } } } \ No newline at end of file diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index 66c0485..fe9db33 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,7 +1,7 @@ { - "SourcePath": "E:\\_temp\\____synctest1", - "TargetPath": "E:\\_temp\\____synctest2", - "SyncMode": "OneWay", + "SourcePath": "E:\\_temp\\____synctest3", + "TargetPath": "E:\\_temp\\____synctest4", + "SyncMode": "TwoWay", "CompareMethod": "Hash", "HashAlgorithm": "SHA256", "SamplingRate": 0.1, @@ -10,7 +10,7 @@ "EnableParallelFileOperations": true, "DateTimeThresholdSeconds": 0, "PreserveFileTime": true, - "UseRecycleBin": true, + "UseRecycleBin": false, "ConflictResolution": "Newer", "FollowSymlinks": false, "PreviewOnly": false, @@ -29,7 +29,8 @@ "**/bin/*", "**/obj/*", "**/.git/*", - "**/.next/*" + "**/.next/*", + "**/.vs/*" ], "Interval": 0, "CronExpression": null, diff --git a/src/MDriveSync.Core/Services/FileSyncHelper.cs b/src/MDriveSync.Core/Services/FileSyncHelper.cs index 7ccaa41..b613d8a 100644 --- a/src/MDriveSync.Core/Services/FileSyncHelper.cs +++ b/src/MDriveSync.Core/Services/FileSyncHelper.cs @@ -713,85 +713,160 @@ private List CreateTwoWaySyncActionsOptimized( // 两边都存在,需要解决冲突 var conflictResult = ResolveConflict(sourceFile, targetFile); - - switch (conflictResult) + if (conflictResult == ESyncConflictResolution.Skip) { - case ESyncConflictResolution.SourceWins: - // 只有当文件实际需要更新时才添加操作 - if (NeedsUpdate(sourceFile, targetFile)) - { - actions.Add(new SyncAction - { - ActionType = ESyncActionType.UpdateFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length, - ConflictResolution = conflictResult - }); - Interlocked.Increment(ref filesToUpdate); - Interlocked.Add(ref bytesToProcess, sourceFile.Length); - } - else - { - Interlocked.Increment(ref filesSkipped); - } - break; - - case ESyncConflictResolution.TargetWins: - // 只有当文件实际需要更新时才添加操作 - if (NeedsUpdate(targetFile, sourceFile)) - { - actions.Add(new SyncAction - { - ActionType = ESyncActionType.UpdateFile, - SourcePath = targetFilePath, - TargetPath = sourceFilePath, - RelativePath = relativePath, - Size = targetFile.Length, - Direction = ESyncDirection.TargetToSource, - ConflictResolution = conflictResult - }); - Interlocked.Increment(ref filesToUpdate); - Interlocked.Add(ref bytesToProcess, targetFile.Length); - } - else + Interlocked.Increment(ref filesSkipped); + } + else + { + // 只有当文件实际需要更新时才添加操作 + if (NeedsUpdate(sourceFile, targetFile)) + { + switch (conflictResult) { - Interlocked.Increment(ref filesSkipped); + case ESyncConflictResolution.SourceWins: + { + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length, + ConflictResolution = conflictResult + }); + Interlocked.Increment(ref filesToUpdate); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + } + break; + + case ESyncConflictResolution.TargetWins: + { + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = targetFilePath, + TargetPath = sourceFilePath, + RelativePath = relativePath, + Size = targetFile.Length, + Direction = ESyncDirection.TargetToSource, + ConflictResolution = conflictResult + }); + Interlocked.Increment(ref filesToUpdate); + Interlocked.Add(ref bytesToProcess, targetFile.Length); + } + break; + + case ESyncConflictResolution.KeepBoth: + { + // 生成冲突文件名是线程安全的 + var (targetNewName, sourceNewName) = GetConflictFileName(targetFilePath, sourceFilePath); + + // 首先重命名目标文件 + var pa = new SyncAction + { + ActionType = ESyncActionType.RenameFile, + SourcePath = targetFilePath, + TargetPath = targetNewName, + RelativePath = GetRelativePath(targetNewName, _options.TargetPath), + ConflictResolution = conflictResult + }; + + // 然后复制源文件到目标 + pa.SubActions.Add(new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = sourceFilePath, + TargetPath = targetFilePath, + RelativePath = relativePath, + Size = sourceFile.Length + }); + + // 最后将已经重命名的目标文件复制到源 + pa.SubActions.Add(new SyncAction + { + ActionType = ESyncActionType.CopyFile, + SourcePath = targetNewName, + TargetPath = sourceNewName, + RelativePath = GetRelativePath(sourceNewName, _options.SourcePath), + Size = targetFile.Length + }); + actions.Add(pa); + + Interlocked.Increment(ref filesToCopy); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + } + break; + + case ESyncConflictResolution.Newer: + { + // 判断哪个文件较新 + var newerFile = sourceFile.LastWriteTime > targetFile.LastWriteTime ? sourceFile : targetFile; + + // 较新的文件优先 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = newerFile == sourceFile ? sourceFilePath : targetFilePath, + TargetPath = newerFile == sourceFile ? targetFilePath : sourceFilePath, + RelativePath = relativePath, + Size = newerFile.Length, + ConflictResolution = conflictResult + }); + Interlocked.Increment(ref filesToUpdate); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + } + break; + + case ESyncConflictResolution.Older: + { + // 判断哪个文件较旧 + var olderFile = sourceFile.LastWriteTime < targetFile.LastWriteTime ? sourceFile : targetFile; + // 较旧的文件优先 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = olderFile == sourceFile ? sourceFilePath : targetFilePath, + TargetPath = olderFile == sourceFile ? targetFilePath : sourceFilePath, + RelativePath = relativePath, + Size = olderFile.Length, + ConflictResolution = conflictResult + }); + Interlocked.Increment(ref filesToUpdate); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + } + break; + + case ESyncConflictResolution.Larger: + { + // 判断哪个文件较大 + var largerFile = sourceFile.Length > targetFile.Length ? sourceFile : targetFile; + // 较大的文件优先 + actions.Add(new SyncAction + { + ActionType = ESyncActionType.UpdateFile, + SourcePath = largerFile == sourceFile ? sourceFilePath : targetFilePath, + TargetPath = largerFile == sourceFile ? targetFilePath : sourceFilePath, + RelativePath = relativePath, + Size = largerFile.Length, + ConflictResolution = conflictResult + }); + + Interlocked.Increment(ref filesToUpdate); + Interlocked.Add(ref bytesToProcess, sourceFile.Length); + } + break; + + case ESyncConflictResolution.Skip: + default: + Interlocked.Increment(ref filesSkipped); + break; } - break; - - case ESyncConflictResolution.KeepBoth: - // 生成冲突文件名是线程安全的 - string targetNewName = GetConflictFileName(targetFilePath); - - // 首先重命名目标文件 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.RenameFile, - SourcePath = targetFilePath, - TargetPath = targetNewName, - RelativePath = GetRelativePath(targetNewName, _options.TargetPath), - ConflictResolution = conflictResult - }); - - // 然后复制源文件到目标 - actions.Add(new SyncAction - { - ActionType = ESyncActionType.CopyFile, - SourcePath = sourceFilePath, - TargetPath = targetFilePath, - RelativePath = relativePath, - Size = sourceFile.Length - }); - - Interlocked.Increment(ref filesToCopy); - Interlocked.Add(ref bytesToProcess, sourceFile.Length); - break; - - case ESyncConflictResolution.Skip: + } + else + { Interlocked.Increment(ref filesSkipped); - break; + } } } }); @@ -967,6 +1042,16 @@ await Task.Run(() => try { await ExecuteSingleActionAsync(action); + + // 子操作 + if (action.SubActions?.Count > 0) + { + foreach (var subAction in action.SubActions) + { + await ExecuteSingleActionAsync(subAction); + } + } + Interlocked.Increment(ref _processedItems); } finally @@ -1512,15 +1597,35 @@ private ESyncConflictResolution ResolveConflict(FileInfo sourceFile, FileInfo ta /// /// 获取用于解决冲突的新文件名 + /// 同时确保源目录和目录目录都不存在重命名的文件 /// - private string GetConflictFileName(string originalPath) + private (string targetNewName, string sourceNewName) GetConflictFileName(string targetFilePath, string sourceFilePath) { - string dir = Path.GetDirectoryName(originalPath); - string fileName = Path.GetFileNameWithoutExtension(originalPath); - string ext = Path.GetExtension(originalPath); - string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + var targetNewName = ""; + var sourceNewName = ""; + var count = 0; + do + { + string targetDir = Path.GetDirectoryName(targetFilePath); + + string fileName = Path.GetFileNameWithoutExtension(targetFilePath); + string ext = Path.GetExtension(targetFilePath); + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - return Path.Combine(dir, $"{fileName} ({timestamp}){ext}"); + if (count > 0) + { + timestamp += $"_{count}"; + } + + targetNewName = Path.Combine(targetDir, $"{fileName} ({timestamp}){ext}"); + + string sourceDir = Path.GetDirectoryName(sourceFilePath); + sourceNewName = Path.Combine(sourceDir, $"{fileName} ({timestamp}){ext}"); + + count++; + } while (File.Exists(targetNewName) || File.Exists(sourceNewName)); + + return (targetNewName, sourceNewName); } /// @@ -1661,7 +1766,9 @@ private async Task CopyFileWithRetryAsync(string sourcePath, string targetPath) int maxRetries = _options.MaxRetries; int currentRetry = 0; bool success = false; - bool useChunkedTransfer = _options.ChunkSizeMB > 0; + + var sourceInfo = new FileInfo(sourcePath); + bool useChunkedTransfer = _options.ChunkSizeMB > 0 && sourceInfo.Length > _options.ChunkSizeMB * 1024 * 1024; // 确保目标目录存在 string targetDir = Path.GetDirectoryName(targetPath); @@ -3170,7 +3277,7 @@ public enum ESyncCompareMethod } /// - /// 文件同步冲突解决策略 + /// 文件同步冲突解决策略(用于双向同步) /// public enum ESyncConflictResolution { @@ -3305,6 +3412,11 @@ public class SyncAction /// 冲突解决策略 /// public ESyncConflictResolution? ConflictResolution { get; set; } + + /// + /// 子操作(用于当前操作完成后的操作,用于双向同步业务) + /// + public List SubActions { get; set; } = new List(); } /// From 13436ccb0fe2553b1019ede5ad20a2ced62f2e69 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Fri, 16 May 2025 09:48:50 +0800 Subject: [PATCH 89/90] =?UTF-8?q?=E8=B7=AF=E5=BE=84=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8D=95=E5=BC=95=E5=8F=B7=E5=92=8C=E5=8F=8C=E5=BC=95=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MDriveSync.Cli/Program.cs | 118 +++++++++++++----- .../Properties/launchSettings.json | 4 +- src/MDriveSync.Cli/sync.json | 2 +- 3 files changed, 89 insertions(+), 35 deletions(-) diff --git a/src/MDriveSync.Cli/Program.cs b/src/MDriveSync.Cli/Program.cs index 395dccb..0d9fc1f 100644 --- a/src/MDriveSync.Cli/Program.cs +++ b/src/MDriveSync.Cli/Program.cs @@ -37,7 +37,7 @@ private static async Task Main(string[] args) continue; // 解析输入的命令行 - args = input.Split(' ').ToArray(); // ParseCommandLine(input).ToArray(); + args = ParseCommandLine(input).ToArray(); if (args.Length == 0) continue; } @@ -129,64 +129,88 @@ private static async Task Main(string[] args) Log.CloseAndFlush(); } } - /// /// 解析命令行字符串为参数数组 /// private static IEnumerable ParseCommandLine(string commandLine) { - bool inQuotes = false; - bool isEscaping = false; + if (string.IsNullOrEmpty(commandLine)) + return new List(); + var arguments = new List(); var currentArgument = new StringBuilder(); + bool inQuotes = false; + char quoteChar = '"'; // 默认使用双引号 + bool isEscaping = false; - // 处理空字符串 - if (string.IsNullOrEmpty(commandLine)) - return arguments; - - // 逐字符处理命令行 for (int i = 0; i < commandLine.Length; i++) { char c = commandLine[i]; if (isEscaping) { - // 转义字符后面的字符直接添加 + // 处理转义字符 currentArgument.Append(c); isEscaping = false; + continue; } - else if (c == '\\') + + // 处理转义符 + if (c == '\\') { - // 检查是否是转义引号的情况 - if (i + 1 < commandLine.Length && commandLine[i + 1] == '"') + // 处理转义下一个字符 + if (i + 1 < commandLine.Length) { - currentArgument.Append('"'); - i++; // 跳过下一个字符(引号) + char nextChar = commandLine[i + 1]; + // 仅当下一个字符是引号或反斜杠时才视为转义 + if (nextChar == '"' || nextChar == '\'' || nextChar == '\\') + { + isEscaping = true; + continue; + } + } + + // 普通反斜杠 + currentArgument.Append(c); + continue; + } + + // 处理引号(支持单引号和双引号) + if (c == '"' || c == '\'') + { + if (!inQuotes) + { + // 开始引号 + inQuotes = true; + quoteChar = c; + } + else if (c == quoteChar) + { + // 结束引号,必须匹配开始的引号类型 + inQuotes = false; } else { - isEscaping = true; + // 在一种引号内出现另一种引号,当作普通字符处理 + currentArgument.Append(c); } + continue; } - else if (c == '"') - { - // 切换引号状态,但不将引号加入参数 - inQuotes = !inQuotes; - } - else if (c == ' ' && !inQuotes) + + // 处理空格 + if (c == ' ' && !inQuotes) { - // 空格且不在引号内,表示一个参数结束 + // 参数结束 if (currentArgument.Length > 0) { arguments.Add(currentArgument.ToString()); currentArgument.Clear(); } + continue; } - else - { - // 普通字符,添加到当前参数 - currentArgument.Append(c); - } + + // 处理普通字符 + currentArgument.Append(c); } // 添加最后一个参数 @@ -195,10 +219,10 @@ private static IEnumerable ParseCommandLine(string commandLine) arguments.Add(currentArgument.ToString()); } - // 检查是否有未闭合的引号 + // 检查未闭合的引号 if (inQuotes) { - Log.Warning("命令行中存在未闭合的引号"); + Log.Warning($"命令行中存在未闭合的{(quoteChar == '"' ? "双" : "单")}引号"); } return arguments; @@ -397,12 +421,42 @@ private static SyncOptions ParseSyncOptions(string[] args) // 基本路径选项 case "--source": case "-s": - if (value != null) options.SourcePath = value; + if (value != null) + { + // 处理兼容单引号与双引号 + if (value.StartsWith("'") && value.EndsWith("'")) + { + value = value[1..^1]; + } + else if (value.StartsWith("\"") && value.EndsWith("\"")) + { + value = value[1..^1]; + } + else + { + options.SourcePath = value; + } + } break; case "--target": case "-t": - if (value != null) options.TargetPath = value; + if (value != null) + { + // 处理兼容单引号与双引号 + if (value.StartsWith("'") && value.EndsWith("'")) + { + value = value[1..^1]; + } + else if (value.StartsWith("\"") && value.EndsWith("\"")) + { + value = value[1..^1]; + } + else + { + options.TargetPath = value; + } + } break; // 同步模式和方法 diff --git a/src/MDriveSync.Cli/Properties/launchSettings.json b/src/MDriveSync.Cli/Properties/launchSettings.json index 56c7830..6e343f2 100644 --- a/src/MDriveSync.Cli/Properties/launchSettings.json +++ b/src/MDriveSync.Cli/Properties/launchSettings.json @@ -2,8 +2,8 @@ "profiles": { "MDriveSync.Cli": { "commandName": "Project", - "commandLineArgs": "sync -f sync.json --chunk 64 -v true" - //"commandLineArgs": "--dev" + //"commandLineArgs": "sync -f sync.json --chunk 64 -v true" + "commandLineArgs": "--dev" } } } \ No newline at end of file diff --git a/src/MDriveSync.Cli/sync.json b/src/MDriveSync.Cli/sync.json index fe9db33..d01eb5b 100644 --- a/src/MDriveSync.Cli/sync.json +++ b/src/MDriveSync.Cli/sync.json @@ -1,5 +1,5 @@ { - "SourcePath": "E:\\_temp\\____synctest3", + "SourcePath": "E:\\_temp\\____synctest3 1", "TargetPath": "E:\\_temp\\____synctest4", "SyncMode": "TwoWay", "CompareMethod": "Hash", From d5562b284d371e668b0fa552c5c0988b1b144d21 Mon Sep 17 00:00:00 2001 From: trueai-org Date: Sat, 17 May 2025 17:30:21 +0800 Subject: [PATCH 90/90] 1.2.2 --- README.md | 81 ++++++++++++++++++++++-- src/MDriveSync.Cli/MDriveSync.Cli.csproj | 2 +- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 94f49fc..1135775 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,6 @@ -![作业](/docs/screenshots/job.gif) -![挂载](/docs/screenshots/mount.png) -![macOS](/docs/screenshots/macOS.png) - ## 同步助手 MDriveSync CLI 是一个功能强大的命令行工具,用于执行多平台文件同步操作。该工具提供了灵活的配置选项,支持多种同步模式、文件比较方法和哈希算法,适用于各种文件同步和备份场景。 @@ -46,6 +42,83 @@ mdrive sync -s /a -t /b mdirve -h ``` +```bash +mdrive - 多平台文件同步工具 v1.2.0 + +用法: +mdrive [命令] [选项] + +命令: + sync 执行文件同步操作 + config 管理同步配置文件 + version 显示程序版本信息 + exit 退出程序 (仅在开发模式下有效) + quit 退出程序 (仅在开发模式下有效) + +选项: + --help, -h 显示帮助信息 + --dev 开发模式,交互式运行程序 + +使用 'mdrive [命令] --help' 查看特定命令的帮助信息 + +执行文件同步操作 + +用法: + mdrive sync [选项] + +选项: + --source, -s 源目录路径 (必需) + --target, -t 目标目录路径 (必需) + --mode, -m 同步模式: OneWay(单向), Mirror(镜像), TwoWay(双向) (默认: OneWay) + --compare, -c 比较方法: Size(大小), DateTime(修改时间), DateTimeAndSize(时间和大小), Content(内容), Hash(哈希) (默认: DateTimeAndSize) + --hash, -h 哈希算法: MD5, SHA1, SHA256(默认), SHA3, SHA384, SHA512, BLAKE3, XXH3, XXH128 + --sampling-rate 哈希抽样率 (0.0-1.0之间的小数,默认: 0.1) + --sampling-min-size 参与抽样的最小文件大小 (字节,默认: 1MB) + --date-threshold 修改时间比较阈值 (秒,默认: 0) + --parallel 是否启用并行文件操作 (默认: true) + --config, -f 配置文件路径, 示例: -f sync.json + --exclude, -e 排除的文件或目录模式 (支持通配符,可多次指定) + --preview, -p 预览模式,不实际执行操作 (默认: false) + --verbose, -v 显示详细日志信息 (默认: false) + --threads, -j 并行操作的最大线程数 (默认: CPU核心数) + --recycle-bin, -r 使用回收站代替直接删除文件 (默认: true) + --preserve-time 保留原始文件时间 (默认: true) + --continue-on-error 发生错误时是否继续执行 (默认: true) + --retry 操作失败时的最大重试次数 (默认: 3) + --conflict 冲突解决策略: SourceWins, TargetWins, KeepBoth, Skip, Newer(默认), Older, Larger + --follow-symlinks 是否跟踪符号链接 (默认: false) + --interval, -i 同步间隔, 单位秒 + --cron Cron表达式,设置后将优先使用Cron表达式进行调度 + --execute-immediately, -ei 配置定时执行时,是否立即执行一次同步 (默认: true) + --chunk-size, --chunk 文件同步分块大小(MB),大于0启用分块传输 + --sync-last-modified-time 同步完成后是否同步文件的最后修改时间 (默认: true) + --temp-file-suffix 临时文件后缀 (默认: .mdrivetmp) + --verify-after-copy 文件传输完成后验证文件完整性 (默认: true) + + --help 显示帮助信息 + + +管理同步配置文件 + +用法: + mdrive config [子命令] [选项] + +子命令: + create 创建新的配置文件 + view 查看现有配置文件内容 + +使用 'mdrive config [子命令] --help' 查看特定子命令的帮助信息 + +示例: +mdrive sync --source C:\Source --target D:\Targetd +``` + +## 截图 + +![作业](/docs/screenshots/job.gif) +![挂载](/docs/screenshots/mount.png) +![macOS](/docs/screenshots/macOS.png) + ## 安装与使用 ### 快速启动 diff --git a/src/MDriveSync.Cli/MDriveSync.Cli.csproj b/src/MDriveSync.Cli/MDriveSync.Cli.csproj index db854e7..cb087f8 100644 --- a/src/MDriveSync.Cli/MDriveSync.Cli.csproj +++ b/src/MDriveSync.Cli/MDriveSync.Cli.csproj @@ -11,7 +11,7 @@ true mdrive mdrive - 1.2.1 + 1.2.2 TrueAI.ORG 多平台文件同步命令行工具 MIT