在桌面应用或后台服务中,经常需要把某个文件夹的内容备份到移动硬盘或另一块磁盘。如果每次都整体复制,不仅耗时还会写坏存储介质。利用C#比对文件的修改日期,只同步发生变化的文件,是简单且足够可靠的增量方案。

一、增量同步的基本思路
增量同步的本质是找出源目录和目标目录之间的差别,然后把差别弥补上。对于本地文件来说,最直观的差别就是修改日期。当源文件的最后写入时间晚于目标文件,或者目标文件根本不存在,就认为这个文件需要被同步。
这种基于修改日期的策略实现成本极低,不需要引入第三方库,也不会因为计算哈希占用大量CPU。在日常备份、开发环境镜像等场景下,修改日期的精度(通常是秒或毫秒级)已经能够满足需求。当然它也有局限,例如人为把系统时间改早可能导致漏同步,因此关键业务可辅以文件大小或哈希校验。
1.1 为什么选择LastWriteTime
在C#中,System.IO.FileInfo 提供了 LastWriteTime、CreationTime 和 LastAccessTime 三个时间属性。其中 LastWriteTime 表示文件内容最后一次被写入的时间,最能体现文件是否发生实质性变化。
相比之下,LastAccessTime 会在杀毒软件扫描或读取时更新,容易造成误判;CreationTime 在复制文件时经常被重置为当前时间,也不适合做差异判断。因此增量同步应优先使用 LastWriteTime 进行比对。
二、核心代码实现
下面给出一个完整的控制台程序示例,它会递归遍历源目录,将修改日期较新或缺失的文件复制到目标目录,并自动创建子目录结构。
using System;
using System.IO;
class LocalSync
{
// 源目录与目标目录
static string sourceRoot = @"C:WorkSrc";
static string targetRoot = @"D:BackupDst";
static void Main()
{
// 确保目标根目录存在
if (!Directory.Exists(targetRoot))
{
Directory.CreateDirectory(targetRoot);
}
SyncDirectory(sourceRoot, targetRoot);
Console.WriteLine("同步完成");
}
// 递归同步文件夹
static void SyncDirectory(string srcDir, string dstDir)
{
// 复制当前目录下的文件
foreach (string filePath in Directory.GetFiles(srcDir))
{
FileInfo srcFile = new FileInfo(filePath);
string dstFile = Path.Combine(dstDir, srcFile.Name);
FileInfo dstInfo = new FileInfo(dstFile);
// 目标不存在,或源修改时间更新,则复制
if (!dstInfo.Exists || srcFile.LastWriteTime > dstInfo.LastWriteTime)
{
File.Copy(filePath, dstFile, true);
Console.WriteLine("已同步: " + filePath);
}
}
// 递归处理子目录
foreach (string subDir in Directory.GetDirectories(srcDir))
{
string dirName = Path.GetFileName(subDir);
string newDst = Path.Combine(dstDir, dirName);
if (!Directory.Exists(newDst))
{
Directory.CreateDirectory(newDst);
}
SyncDirectory(subDir, newDst);
}
}
}
上述代码中,SyncDirectory 方法先处理当前目录的文件,再递归进入子目录。通过 FileInfo.LastWriteTime 的比较,只复制真正有变化的文件。参数 true 表示允许覆盖已有文件。
需要注意的是,在Windows下文件路径建议使用 @"..." 逐字字符串,避免反斜杠被转义。如果目标文件正在被其他程序占用,File.Copy 会抛出 IOException,实际项目中应包裹 try-catch 并记录日志。
2.1 提升健壮性的改进
基础版本没有处理异常和文件锁。我们可以在复制前增加异常捕获,并跳过无法访问的文件,保证同步过程不中断。
static void SafeCopy(string src, string dst)
{
try
{
File.Copy(src, dst, true);
}
catch (IOException ex)
{
// 文件被占用或权限不足时记录后跳过
Console.WriteLine("跳过 " + src + " 原因: " + ex.Message);
}
}
把原来的 File.Copy 替换为 SafeCopy 调用,程序就能在部分文件无法读取时继续完成其余同步任务。对于定时备份服务,这种容错能力非常重要。
三、结合文件大小做二次校验
单纯比对修改日期在极少数情况下会漏掉内容相同但时间被外部工具更新的文件,也可能误同步时间变新但内容未改的文件。加入文件长度判断可以减少不必要的写入。
3.1 修改比对条件
只有当源文件时间更新且大小不一致时才复制,能进一步降低磁盘损耗。示例判断如下:
if (!dstInfo.Exists
|| srcFile.LastWriteTime > dstInfo.LastWriteTime
&& srcFile.Length != dstInfo.Length)
{
SafeCopy(filePath, dstFile);
}
这段代码表示:目标不存在一定同步;若存在,则要求源时间更新并且文件大小不同才同步。由于获取 Length 不需要读取文件内容,性能开销可以忽略。
如果希望更严谨,可以对大于一定阈值的文件计算SHA256哈希,但那会显著增加CPU和IO占用,一般本地备份无需做到这一步。依据实际数据敏感度权衡即可。
四、总结与实践建议
使用C#实现本地文件增量同步,核心就是借助 FileInfo.LastWriteTime 做差异识别,再配合递归目录遍历完成复制。该方法代码量少、依赖少,非常适合写工具脚本。
建议在正式使用前,先对小批量文件做多次测试,确认时间比对逻辑符合预期;若同步目标为可移动磁盘,注意在复制完成后调用 File.SetAttributes 还原只读属性,防止备份盘文件被误删。掌握这套思路后,你还可以将其扩展为带配置文件的定时同步服务。