如何确保从网络驱动器复制的文件完成写入后再执行读取操作?
这个场景我太熟悉了——跨组件处理文件时,FileSystemWatcher经常会“迫不及待”触发Created事件,加上网络驱动器的缓存特性,很容易导致读取半写完的文件。你目前用的isFileLocked方法有个致命漏洞:它只尝试以只读方式打开文件,但很多写进程(包括PowerShell的Copy-Item)在写入时会允许读共享,这时候你的检测会误以为文件已经就绪,但实际上内容还在传输中。
下面给你几个从根源解决问题的方案,按可靠性排序:
1. 临时文件+原子重命名(首推方案)
这是业界公认最可靠的做法,核心思路是:先把文件复制到目标目录的临时文件,复制完成后再重命名为最终文件名。因为文件重命名是原子操作(操作系统保证要么完成要么不做),FileSystemWatcher只会在重命名完成后触发Created事件,这时候文件肯定是完整的。
PowerShell复制组件修改:
# 生成临时文件名(比如加.tmp后缀) $tmpFileName = $currentfile.Name + ".tmp" $tmpDestination = Join-Path $destfolder $tmpFileName # 先复制到临时文件 Copy-Item $currentfile.FullName -Destination $tmpDestination –Force -ErrorAction Stop # 复制完成后,原子重命名为最终文件 Rename-Item $tmpDestination -Destination (Join-Path $destfolder $currentfile.Name) -Force -ErrorAction Stop
C#监控组件配合:
可以给FileSystemWatcher设置过滤规则,忽略.tmp后缀的文件:
fileSystemWatcher.Filter = "*.*"; // 或者更具体的后缀 fileSystemWatcher.Created += (sender, e) => { if (Path.GetExtension(e.FullPath).Equals(".tmp", StringComparison.OrdinalIgnoreCase)) { // 跳过临时文件 return; } // 处理正式文件 ProcessFile(e.FullPath); };
这个方案完全避免了半写文件被读取的可能,唯一的代价是需要修改两边的代码,但绝对值得。
2. 改进文件就绪检测逻辑
如果你暂时不想修改复制流程,可以强化isFileLocked的检测逻辑,不仅检查文件是否被锁定,还要验证文件大小是否稳定(避免网络缓存导致的“假存在”):
private bool IsFileFullyWritten(string filePath) { if (!File.Exists(filePath)) return false; FileInfo fileInfo = new FileInfo(filePath); long initialSize = fileInfo.Length; // 先尝试以独占方式打开文件(确保没有写入进程占用) try { using (FileStream fs = fileInfo.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { // 打开成功,说明文件没有被锁定 } } catch (IOException) { return false; } // 再检查文件大小是否稳定(应对网络缓存延迟) Thread.Sleep(100); // 等待100毫秒,可根据文件大小调整 return fileInfo.Length == initialSize; }
调用这个方法时,建议循环等待几次(比如最多等5秒),如果始终返回false就放弃或者记录日志:
int retryCount = 0; while (retryCount < 50) // 最多等5秒(50*100ms) { if (IsFileFullyWritten(filePath)) break; Thread.Sleep(100); retryCount++; } if (retryCount >= 50) { // 处理文件无法就绪的情况,比如记录日志、跳过 return; } // 开始读取文件
3. 调整FileSystemWatcher的通知规则
默认的FileSystemWatcher会监控很多文件属性变化(比如LastWrite时间),有时候文件还在写入,但LastWrite已经更新,导致Created事件提前触发。你可以缩小监控范围,只关注文件名变化,或者结合Size属性:
fileSystemWatcher.NotifyFilter = NotifyFilter.FileName | NotifyFilter.Size;
不过这个只是辅助手段,不能完全替代前面的方案,因为网络文件系统的缓存可能还是会导致Size属性提前更新。
为什么你的原方法会失效?
你用的File.OpenRead会请求FileShare.ReadWrite权限,而PowerShell的Copy-Item在写入时默认允许读共享,所以即使文件还在被写入,OpenRead也能成功,导致你的方法误判文件已就绪。而用FileAccess.ReadWrite + FileShare.None的方式,会强制检查是否有其他进程在写入,更准确。
内容的提问来源于stack exchange,提问作者Chris

