1. 程式人生 > >.NET Core的檔案系統[3]:由PhysicalFileProvider構建的物理檔案系統

.NET Core的檔案系統[3]:由PhysicalFileProvider構建的物理檔案系統

ASP.NET Core應用中使用得最多的還是具體的物理檔案,比如配置檔案、View檔案以及網頁上的靜態檔案,物理檔案系統的抽象通過PhysicalFileProvider這個FileProvider來實現,該型別定義在NuGet包“Microsoft.Extensions.FileProviders.Physical”中。我們知道System.IO名稱空間下定義了一整套針操作物理目錄和檔案的API,實際上PhysicalFileProvider最終也是通過呼叫這些API來完成相關的IO操作的。[ 本文已經同步到《ASP.NET Core框架揭祕》之中]

目錄
一、PhysicalFileProvider
二、PhysicalFileInfo
三、PhysicalDirectoryInfo
四、針對物理檔案的監控
五、總結

一、PhysicalFileProvider

如下所示的程式碼片段展示PhysicalFileProvider型別的定義。

   1: public class PhysicalFileProvider : IFileProvider, IDisposable
   2: {   
   3:     public PhysicalFileProvider(string root);   
   4:      
   5:     public IFileInfo GetFileInfo(string subpath);  
   6:     public IDirectoryContents GetDirectoryContents(string
subpath);
   7:     public IChangeToken Watch(string filter);
   8:  
   9:     public void Dispose();   
  10: }

二、PhysicalFileInfo

一個PhysicalFileProvider物件總是對映到某個具體的物理目錄下,被對映的目錄所在的路徑通過建構函式的引數root來提供,該目錄將作為PhysicalFileProvider的根目錄。GetFileInfo方法返回的FileInfo物件代表指定路徑對應的檔案,這是一個型別為PhysicalFileInfo的物件,如下所示的程式碼片段展示了該型別的完整定義。一個物理檔案可以通過一個System.IO.FileInfo

物件來表示,一個PhysicalFileInfo物件實際上就是對這個一個FileInfo物件的封裝,定義在PhysicalFileInfo的所有屬性都來源於這個FileInfo物件。對於建立讀取檔案輸出流的CreateReadStream方法來說,它返回的是一個根據物理檔案絕對路徑建立的FileStream物件。

   1: public class PhysicalFileInfo : IFileInfo
   2: {
   3:     ...
   4:     public PhysicalFileInfo(FileInfo info);    
   5: }

對於PhysicalFileProvider的GetFile方法來說,即使我們指定的路徑指向一個具體的物理檔案,它並不總是會返回一個PhysicalFileInfo物件。具體來說,PhysicalFileProvider會將如下幾種場景視為“目標檔案不存在”,並讓GetFile返回一個NotFoundFileInfo物件。顧名思義,NotFoundFileInfo表示的正式一個“不存在”的檔案,即它的Exists屬性總是返回False,而其他的屬性則變得沒有任何意義。當我們呼叫它的CreateReadStream試圖讀取一個根本不存在的檔案內容時,會丟擲一個FileNotFoundException型別的異常。

  • 確實沒有一個物理檔案與指定的路徑相匹配。
  • 如果指定的是一個絕對路徑(比如“c:\foobar”),即Path.IsPathRooted返回返回True。
  • 如果指定的路徑指向一個隱藏檔案。

三、PhysicalDirectoryInfo

對於PhysicalFileProvider來說,它利用PhysicalFileInfo物件來描述某個具體的物理檔案,針對目錄的描述則通過一個型別為PhysicalDirectoryInfo的物件。既然PhysicalFileInfo是對一個System.IO.FileInfo物件的封裝,那麼我們應該想得到PhysicalDirectoryInfo封裝的自然就是表示目錄的DirectoryInfo物件。如下面的程式碼片段所示,我們需要在建立一個PhysicalDirectoryInfo物件時提供這個DirectoryInfo物件,PhysicalDirectoryInfo實現的所有屬性的返回值都來源於這個DirectoryInfo物件。由於CreateReadStream方法的目的是讀取檔案的內容,所以當我們呼叫一個PhysicalDirectoryInfo物件的這個方法的時候,會丟擲一個InvalidOperationException型別的異常。

   1: public class PhysicalDirectoryInfo : IFileInfo
   2: {   
   3:     ...
   4:     public PhysicalDirectoryInfo(DirectoryInfo info);
   5: }

當我們呼叫PhysicalFileProvider的GetDirectoryContents方法時,如果指定的路徑指向一個具體的目錄,那麼該方法會返回一個型別為EnumerableDirectoryContents的物件,不過EnumerableDirectoryContents僅僅是一個在程式設計過程中不可見的內部型別。EnumerableDirectoryContents是一個FileInfo物件的集合,該集合中會包括所有描述子目錄的PhysicalDirectoryInfo物件和描述檔案的PhysicalFileInfo物件。至於EnumerableDirectoryContents的Exists屬性,它總是返回True。如果指定的路徑並不指向一個存在目錄,或者指定的是一個絕對路徑,這個方法都會返回一個Exsits屬性總是返回False的NotFoundDirectoryContents物件。

四、針對物理檔案的監控

我們接著來談談PhysicalFileProvider的Watch方法。當我們呼叫該方法的時候,PhysicalFileProvider會通過解析我們提供的篩選表示式確定我們期望監控的檔案,然後利用FileSystemWatcher物件來對這些檔案試試監控。針對這些檔案的變化(建立、修改、重新命名和刪除)都會實時地反映到Watch方法返回的ChangeToken上。 值得一提的是,FileSystemWatcher型別實現IDisposable介面,PhysicalFileProvider也實現了相同的介面,PhysicalFileProvider的Dispose方法的唯一使命就是釋放這個FileSystemWatcher物件

Watch方法中指定的篩選表示式必須是針對當前PhysicalFileProvider根目錄的相對路徑,可以使用“/”或者“./”字首,也可以不採用任何字首。一旦我們使用了絕對路徑(比如“c:\test\*.txt”)或者“../”字首(比如“../test/*.txt”),不論解析出來的檔案是否存在於PhysicalFileProvider的根目錄下,這些檔案都不會被監控。除此之外,如果我們沒有指定任何篩選條件,也不會有任何的檔案會被監控。

監控檔案變化的真正目的在於讓應用程式能夠及時感知到資料來源的改變,進而自動執行某些預先註冊的回掉操作。回撥的註冊可以直接通過呼叫ChangeToken的RegisterChangeCallback方法來完成,註冊的回撥通過一個型別為Action<object>的委託物件來表示。對於在第一節演示的檔案監控的例項,相應的程式“照理說”可以改寫成如下的形式。

   1: IFileProvider fileProvider = new PhysicalFileProvider(@"c:\test");
   2: fileProvider.Watch("data.txt").RegisterChangeCallback(_ = >LoadFileAsync(fileProvider), null);
   3: while (true)
   4: {
   5:     File.WriteAllText(@"c:\test\data.txt", DateTime.Now.ToString());
   6:     Task.Delay(5000).Wait();
   7: }
   8:  
   9: public static async void LoadFileAsync(IFileProvider fileProvider)
  10: {
  11:     Stream stream = fileProvider.GetFileInfo("data.txt").CreateReadStream();
  12:     {
  13:         byte[] buffer = new byte[stream.Length];
  14:         await stream.ReadAsync(buffer, 0, buffer.Length);
  15:         Console.WriteLine(Encoding.ASCII.GetString(buffer));
  16:     }
  17: }

如果執行上面這段程式,我們會發現只有第一個針對檔案的更新能夠被感知,後續的檔案更新操作將自動被忽略。導致這個問題的根源在於,單個ChangeToken物件的使命在於當繫結的資料來源第一次發生變換時對外發送相應的訊號,而不具有持續傳送資料變換的能力。其實這一點從IChangeToken介面的定義就可以看出來,我們知道它具有一個HasChanged屬性表示資料是否已經發生變化,而並沒有提供一個讓這個屬性“復位”的方法。所以當我們需要對某個檔案進行持續監控的時候,我們需要在註冊的回撥中重新呼叫FileProvider的Watch方法,並利用生成ChangeToken再次註冊回撥。除此之外,考慮到ChangeToken的RegisterChangeCallback方法以一個IDisposable物件的形式返回回撥註冊物件,我們應該在對回撥實施二次註冊時呼叫第一次返回的回撥註冊物件的Dispose方法將其釋放掉。如下所示的程式才能達到對檔案試試持續監控的目的。

   1: IFileProvider fileProvider = new PhysicalFileProvider(@"c:\test");
   2: Action<object> callback = null;
   3: IDisposable regiser = null;
   4: callback = _ =>
   5: {
   6:     regiser.Dispose();
   7:     LoadFileAsync(fileProvider);
   8:     fileProvider.Watch("data.txt").RegisterChangeCallback(callback, null);
   9: };
  10:  
  11: regiser = fileProvider.Watch("data.txt").RegisterChangeCallback(callback, null);

不過這樣的程式設計方式不但看起來比較繁瑣,很多對ChangeToken缺乏認識的人甚至對這樣的程式設計方式無法理解。為了解決這個問題,我們可以使用定義在ChangeToken型別中如下兩個方法OnChange方法來註冊資料發生改變時自動執行的回撥。這兩個方法具有兩個引數,前者是一個用於建立ChangeToken物件的Func<IChangeToken>物件,後者則是代表回撥操作的Action<object>/Action<TState>物件。實際上第一節的例項演示中我們就是呼叫的這個OnChange方法。

   1: public static class ChangeToken
   2: {
   3:     public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
   4:     {        
   5:         Action<object> callback = null;
   6:         callback = delegate (object s) {
   7:             changeTokenConsumer();
   8:             changeTokenProducer().RegisterChangeCallback(callback, null);
   9:         };
  10:         return changeTokenProducer().RegisterChangeCallback(callback, null);
  11:     }
  12:  
  13:     public static IDisposable OnChange<TState>(Func<IChangeToken> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
  14:     {
  15:         Action<object> callback = null;
  16:         callback = delegate (object s) {
  17:             changeTokenConsumer((TState) s);
  18:             changeTokenProducer().RegisterChangeCallback(callback, s);
  19:         };
  20:         return changeTokenProducer().RegisterChangeCallback(callback, state);
  21:     }
  22: }

如果改用這個OnChange方法來替換掉原來手工呼叫ChangeToken的RegisterChangeCallback方法進行回撥註冊的方式,原本顯得相對繁瑣的程式可以通過如下兩句程式碼來替換。實際上在《讀取並監控檔案的變化》中,我們呼叫的正是這個OnChange方法。

   1: IFileProvider fileProvider = new PhysicalFileProvider(@"c:\test");
   2: ChangeToken.OnChange(() => fileProvider.Watch("data.txt"), () => LoadFileAsync(fileProvider));

五、總結

我們藉助下圖所示的UML來對由PhysicalFileProvider構建物理檔案系統的整體設計做一個簡單的總結。首先,該檔案系統下用於描述目錄和檔案的分別是一個PhysicalDirectoryInfo和PhysicalFileInfo物件,它們分別是對一個DirectoryInfo和FileInfo(System.IO.FileInfo)物件的封裝。PhysicalFileProvider的GetDirectoryContents方法返回一個EnumerableDirectoryContents物件(如果指定的目錄存在),組成該物件的分別是根據其所有子目錄和檔案建立的PhysicalDirectoryInfo和PhysicalFileInfo物件。當我們呼叫PhysicalFileProvider的GetFileInfo方法時,如果指定的檔案存在,返回的是描述該檔案的PhysicalFileInfo物件。至於PhysicalFileProvider的Watch方法,它最終利用了FileSystemWatcher來監控指定檔案的變化。

3