System.IO.FileSystemWatcher的坑
System.IO
命名空間下面有一個FileSystemWatcher
,這個東西可以實現文件變動的提醒。需要監控文件夾變化(比如FTP伺服器)的情形非常適用。
需要監控文件新建時,我們可以這麼寫:
_fileSystemWatcher.Path = path;
_fileSystemWatcher.IncludeSubdirectories = true;
_fileSystemWatcher.Created += _fileSystemWatcher_Created;
_fileSystemWatcher.EnableRaisingEvents = true;
protected async void _fileSystemWatcher_Created(object sender, FileSystemEventArgs e)
{
Console.WriteLine(e.FullPath);
}
感覺還是挺方便的吧?接下來就是坑了。
傳輸延遲問題
FileSystemWatcher
只要發現文件創建就觸發了,大文件或者FTP等需要一段時間才能完成傳輸的情況下,直接在時間處理程式中處理文件會由於文件不完整導致錯誤。可惜的是,FileSystemWatcher
並沒有內建任何機制可以保障文件傳輸完成再觸發Created
事件,我們只能靠自己程式碼保障。
以下程式碼運行於.NET 6,Windows 11,Rocky Linux 9
Windows only方案
-
FileSystemWatcher
除了Created,還提供了Changed事件,我們可以先監聽Created事件,然後再監控Changed的情況,當文件屬性不在變化時,認為是傳輸完畢了。
這種方案可行,不過感覺有點太麻煩了,我需要監聽兩個事件,還需要處理先後順序,其實我只想知道創建而已… -
在Created事件中,使用排他性的文件打開操作
在File.Open()函數中,有重載可以提供獨佔的訪問,訪問不成功,文件會彈出錯誤。
//防止文件上傳時間過長,導致無法正常識別
if (!File.Exists(e.FullPath)) return;
var accessable = false;
for (int i = 0; i < 5; i++)
{
try
{
using (File.Open(e.FullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
Console.WriteLine("Break");
accessable = true;
break;
}
}
catch (Exception)
{
Console.WriteLine("Loop" + i);
}
await Task.Delay(3000);
}
//文件超時無法讀取,失敗。
if (!accessable) return;
//後續程式碼
運行可以看見這樣的輸出,說明方案可行。
Linux與Windows通用方案
上面的方案似乎已經解決了我們的問題,我興緻勃勃地部署到Linux機器上時卻死活無法正常工作,Debug發現Open()
這個方法居然可以一次直接通過,看來Linux下的Share不能正常獨佔這個文件,還得換一個方法。
protected async void _fileSystemWatcher_Created(object sender, FileSystemEventArgs e)
{
//防止文件上傳時間過長,導致無法正常識別
if (!File.Exists(e.FullPath)) return;
var accessable = false;
for (int i = 0; i < 5; i++)
{
await Task.Delay(3000);
Console.WriteLine("loop" + i);
var time1 = File.GetLastWriteTimeUtc(e.FullPath);
await Task.Delay(1000);
var time2 = File.GetLastWriteTimeUtc(e.FullPath);
if (time1 == time2)
{
accessable = true;
break;
}
}
//文件超時無法讀取,失敗。
if (!accessable) return;
//後續程式碼
}
我們可以在程式中定時檢查文件的最後修改時間,如果相隔一段時間的兩次最後修改時間一致的話,那說明文件已經完成了傳輸,這種方式不依賴於打開操作,並且可以在Windows和Linux下運行。
為了防止無限循環,設置了超時,如果在指定的時間內無法完成,那麼程式直接跳出。