using System.Collections.Concurrent;
using System.Diagnostics;
// 发布命令 (不包含调试信息)
// dotnet publish .\CopyFile\CopyFile.csproj -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true /p:DebugType=None -o .\Publish\CopyFile\V1.0.0
// 发布命令(包含调试信息)
// dotnet publish .\CopyFile\CopyFile.csproj -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true -o .\Publish\CopyFile\V2.0.0
// 命令行运行:
// set "scriptPath=%cd%"
// dotnet run --project ./CopyFile/CopyFile.csproj -- "%scriptPath%"
public class Program
{
private static string SourceDirectory = string.Empty;
private static string DstFolderPath = string.Empty;
private static string SourceDirectoryBakPath = string.Empty;
private static string DirectorySuffix = string.Empty;
private static readonly DateTime Now = DateTime.Now;
private static readonly List<string> ExcludedEqualDirectories = new List<string>
{
"bin",
"obj",
"npm_modules",
".git",
"Publish",
};
// ⭐ 优化点 1:使用 BlockingCollection 代替 ConcurrentQueue
private static readonly BlockingCollection<CopyFileTask> BackupCollection =
new BlockingCollection<CopyFileTask>();
private static volatile bool IsProducingComplete = false;
private static readonly ConcurrentDictionary<string, object> DirectoryLocks =
new ConcurrentDictionary<string, object>();
private static readonly CancellationTokenSource CancellationTokenSource =
new CancellationTokenSource();
static void Main(string[] args)
{
try
{
// 扫描源目录并准备移动任务
Console.WriteLine("正在扫描文件...");
System.Console.WriteLine("输入文件夹后缀:");
DirectorySuffix = System.Console.ReadLine() ?? string.Empty;
if (args.Length == 0)
{
System.Console.WriteLine("请传入文件夹路径");
return;
}
SourceDirectory = args[0];
// 检查源目录是否存在
if (!Directory.Exists(SourceDirectory))
{
Console.WriteLine($"错误: 源目录 {SourceDirectory} 不存在。");
return;
}
System.Console.WriteLine("请输入目标文件夹:");
DstFolderPath = System.Console.ReadLine() ?? string.Empty;
if (DstFolderPath == string.Empty)
return;
SourceDirectoryBakPath =
Path.GetFileName(SourceDirectory) + $"_{Now:yyyyMMdd_HHmmss}_{DirectorySuffix}";
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var producerTask = Task.Run(() =>
{
try
{
var options = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount * 2,
};
// 使用 Parallel.ForEach 对顶层目录进行并行递归扫描
Parallel.ForEach(
Directory.EnumerateDirectories(SourceDirectory),
options,
(dirPath) =>
{
ScanDirectory(dirPath);
}
);
// 单独处理根目录下的文件
GetFiles(SourceDirectory);
}
finally
{
// ⭐ 关键:当所有生产者都完成后,标记集合已“完成添加”
// 这样消费者就知道不会再有新任务了
BackupCollection.CompleteAdding();
IsProducingComplete = true;
}
});
Console.WriteLine("扫描已启动,开始并行处理文件...");
// 启动多个工作线程
const int maxThreads = 8;
var backupTasks = new Task[maxThreads];
for (int i = 0; i < maxThreads; i++)
{
backupTasks[i] = Task.Run(() => CodeBackup(CancellationTokenSource.Token));
}
// 等待所有任务完成
Task.WaitAll(backupTasks);
stopwatch.Stop();
Console.WriteLine($"处理完成,共耗时 {stopwatch.Elapsed.TotalSeconds} s");
}
catch (Exception ex)
{
Console.WriteLine($"\n发生致命错误: {ex.Message}");
}
finally
{
CancellationTokenSource.Cancel();
}
Console.WriteLine("============备份完成============");
}
private static bool FilterFolder(string dirPath)
{
var dirName = Path.GetFileName(dirPath);
var excludedSet = new HashSet<string>(
ExcludedEqualDirectories,
StringComparer.OrdinalIgnoreCase
);
return !excludedSet.Contains(dirName);
}
private static void ScanDirectory(string currentPath)
{
if (!FilterFolder(currentPath))
return;
try
{
// 处理当前目录的文件
GetFiles(currentPath);
var options = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount * 2,
};
// ⭐ 关键:对子目录的遍历也使用并行方式
Parallel.ForEach(
Directory.EnumerateDirectories(currentPath),
options,
(subDirectory) =>
{
ScanDirectory(subDirectory); // 递归调用
}
);
}
catch (IOException ex)
{
Console.WriteLine($"IOException ex: {ex.Message}");
}
catch (UnauthorizedAccessException ex)
{ /* 忽略无权限目录 */
System.Console.WriteLine($"UnauthorizedAccessException ex: {ex.Message} ");
}
catch (Exception ex)
{
Console.WriteLine($"\n扫描目录时出错 ({currentPath}): {ex.Message}");
}
}
private static void GetFiles(string sourcePath)
{
var files = Directory.GetFiles(sourcePath, "*", SearchOption.TopDirectoryOnly);
foreach (string filePath in files)
{
string relativePath = Path.GetRelativePath(SourceDirectory, filePath);
string dstCodePath = Path.Combine(DstFolderPath, SourceDirectoryBakPath, relativePath);
EnsureDirectoryExists(Path.GetDirectoryName(dstCodePath)!);
var task = new CopyFileTask { CodePath = filePath, DstCodePath = dstCodePath };
BackupCollection.Add(task);
}
}
private static void CodeBackup(CancellationToken cancellationToken)
{
try
{
// GetConsumingEnumerable 会阻塞线程直到有新项目或集合被标记为完成
foreach (var copyFileTask in BackupCollection.GetConsumingEnumerable(cancellationToken))
{
try
{
CopyFile(copyFileTask);
}
catch (Exception ex)
{
Console.WriteLine($"\n处理文件时出错 ({copyFileTask.CodePath}): {ex.Message}");
}
}
}
catch (OperationCanceledException)
{
// 如果取消操作被请求,循环会正常退出
System.Console.WriteLine("\n文件处理已取消。");
}
}
private static void CopyFile(CopyFileTask code2mdTask, int maxRetries = 3)
{
int retries = 0;
while (true)
{
try
{
string codePath = code2mdTask.CodePath;
string dstCodePath = code2mdTask.DstCodePath;
File.Copy(codePath, dstCodePath, false);
System.Console.WriteLine(codePath + "=>" + dstCodePath);
// 验证文件是否成功移动
if (!File.Exists(dstCodePath))
{
throw new IOException("文件复制后不存在于目标位置");
}
break;
}
catch (Exception ex)
{
retries++;
if (retries > maxRetries)
{
Console.WriteLine($"文件移动失败 ({code2mdTask.CodePath} : {ex.Message}");
throw;
}
// 等待一段时间后重试
Thread.Sleep(500 * retries);
}
}
}
// ⭐ 新增:一个执行绪安全的目录建立辅助方法
private static void EnsureDirectoryExists(string directoryPath)
{
if (Directory.Exists(directoryPath))
return;
// 为目录路径取得或新增一个锁
object dirLock = DirectoryLocks.GetOrAdd(directoryPath, _ => new object());
if (!Directory.Exists(directoryPath))
{
if (Directory.Exists(directoryPath))
return;
lock (dirLock)
{
Directory.CreateDirectory(directoryPath);
}
}
}
}
public class CopyFileTask
{
public string CodePath { get; set; }
public string DstCodePath { get; set; }
}