.NET Core的文件系统[3]:由PhysicalFileProvider构建的物理文件系统
ASP.NET Core應(yīng)用中使用得最多的還是具體的物理文件,比如配置文件、View文件以及網(wǎng)頁(yè)上的靜態(tài)文件,物理文件系統(tǒng)的抽象通過(guò)PhysicalFileProvider這個(gè)FileProvider來(lái)實(shí)現(xiàn),該類型定義在NuGet包“Microsoft.Extensions.FileProviders.Physical”中。我們知道System.IO命名空間下定義了一整套針操作物理目錄和文件的API,實(shí)際上PhysicalFileProvider最終也是通過(guò)調(diào)用這些API來(lái)完成相關(guān)的IO操作的。[ 本文已經(jīng)同步到《ASP.NET Core框架揭秘》之中]
目錄
一、PhysicalFileProvider
二、PhysicalFileInfo
三、PhysicalDirectoryInfo
四、針對(duì)物理文件的監(jiān)控
五、總結(jié)
一、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
一個(gè)PhysicalFileProvider對(duì)象總是映射到某個(gè)具體的物理目錄下,被映射的目錄所在的路徑通過(guò)構(gòu)造函數(shù)的參數(shù)root來(lái)提供,該目錄將作為PhysicalFileProvider的根目錄。GetFileInfo方法返回的FileInfo對(duì)象代表指定路徑對(duì)應(yīng)的文件,這是一個(gè)類型為PhysicalFileInfo的對(duì)象,如下所示的代碼片段展示了該類型的完整定義。一個(gè)物理文件可以通過(guò)一個(gè)System.IO.FileInfo對(duì)象來(lái)表示,一個(gè)PhysicalFileInfo對(duì)象實(shí)際上就是對(duì)這個(gè)一個(gè)FileInfo對(duì)象的封裝,定義在PhysicalFileInfo的所有屬性都來(lái)源于這個(gè)FileInfo對(duì)象。對(duì)于創(chuàng)建讀取文件輸出流的CreateReadStream方法來(lái)說(shuō),它返回的是一個(gè)根據(jù)物理文件絕對(duì)路徑創(chuàng)建的FileStream對(duì)象。
1: public class PhysicalFileInfo : IFileInfo 2: { 3: ... 4: public PhysicalFileInfo(FileInfo info); 5: }對(duì)于PhysicalFileProvider的GetFile方法來(lái)說(shuō),即使我們指定的路徑指向一個(gè)具體的物理文件,它并不總是會(huì)返回一個(gè)PhysicalFileInfo對(duì)象。具體來(lái)說(shuō),PhysicalFileProvider會(huì)將如下幾種場(chǎng)景視為“目標(biāo)文件不存在”,并讓GetFile返回一個(gè)NotFoundFileInfo對(duì)象。顧名思義,NotFoundFileInfo表示的正式一個(gè)“不存在”的文件,即它的Exists屬性總是返回False,而其他的屬性則變得沒(méi)有任何意義。當(dāng)我們調(diào)用它的CreateReadStream試圖讀取一個(gè)根本不存在的文件內(nèi)容時(shí),會(huì)拋出一個(gè)FileNotFoundException類型的異常。
- 確實(shí)沒(méi)有一個(gè)物理文件與指定的路徑相匹配。
- 如果指定的是一個(gè)絕對(duì)路徑(比如“c:\foobar”),即Path.IsPathRooted返回返回True。
- 如果指定的路徑指向一個(gè)隱藏文件。
三、PhysicalDirectoryInfo
對(duì)于PhysicalFileProvider來(lái)說(shuō),它利用PhysicalFileInfo對(duì)象來(lái)描述某個(gè)具體的物理文件,針對(duì)目錄的描述則通過(guò)一個(gè)類型為PhysicalDirectoryInfo的對(duì)象。既然PhysicalFileInfo是對(duì)一個(gè)System.IO.FileInfo對(duì)象的封裝,那么我們應(yīng)該想得到PhysicalDirectoryInfo封裝的自然就是表示目錄的DirectoryInfo對(duì)象。如下面的代碼片段所示,我們需要在創(chuàng)建一個(gè)PhysicalDirectoryInfo對(duì)象時(shí)提供這個(gè)DirectoryInfo對(duì)象,PhysicalDirectoryInfo實(shí)現(xiàn)的所有屬性的返回值都來(lái)源于這個(gè)DirectoryInfo對(duì)象。由于CreateReadStream方法的目的是讀取文件的內(nèi)容,所以當(dāng)我們調(diào)用一個(gè)PhysicalDirectoryInfo對(duì)象的這個(gè)方法的時(shí)候,會(huì)拋出一個(gè)InvalidOperationException類型的異常。
1: public class PhysicalDirectoryInfo : IFileInfo 2: { 3: ... 4: public PhysicalDirectoryInfo(DirectoryInfo info); 5: }當(dāng)我們調(diào)用PhysicalFileProvider的GetDirectoryContents方法時(shí),如果指定的路徑指向一個(gè)具體的目錄,那么該方法會(huì)返回一個(gè)類型為EnumerableDirectoryContents的對(duì)象,不過(guò)EnumerableDirectoryContents僅僅是一個(gè)在編程過(guò)程中不可見(jiàn)的內(nèi)部類型。EnumerableDirectoryContents是一個(gè)FileInfo對(duì)象的集合,該集合中會(huì)包括所有描述子目錄的PhysicalDirectoryInfo對(duì)象和描述文件的PhysicalFileInfo對(duì)象。至于EnumerableDirectoryContents的Exists屬性,它總是返回True。如果指定的路徑并不指向一個(gè)存在目錄,或者指定的是一個(gè)絕對(duì)路徑,這個(gè)方法都會(huì)返回一個(gè)Exsits屬性總是返回False的NotFoundDirectoryContents對(duì)象。
四、針對(duì)物理文件的監(jiān)控
我們接著來(lái)談?wù)凱hysicalFileProvider的Watch方法。當(dāng)我們調(diào)用該方法的時(shí)候,PhysicalFileProvider會(huì)通過(guò)解析我們提供的篩選表達(dá)式確定我們期望監(jiān)控的文件,然后利用FileSystemWatcher對(duì)象來(lái)對(duì)這些文件試試監(jiān)控。針對(duì)這些文件的變化(創(chuàng)建、修改、重命名和刪除)都會(huì)實(shí)時(shí)地反映到Watch方法返回的ChangeToken上。 值得一提的是,FileSystemWatcher類型實(shí)現(xiàn)IDisposable接口,PhysicalFileProvider也實(shí)現(xiàn)了相同的接口,PhysicalFileProvider的Dispose方法的唯一使命就是釋放這個(gè)FileSystemWatcher對(duì)象。
Watch方法中指定的篩選表達(dá)式必須是針對(duì)當(dāng)前PhysicalFileProvider根目錄的相對(duì)路徑,可以使用“/”或者“./”前綴,也可以不采用任何前綴。一旦我們使用了絕對(duì)路徑(比如“c:\test\*.txt”)或者“../”前綴(比如“../test/*.txt”),不論解析出來(lái)的文件是否存在于PhysicalFileProvider的根目錄下,這些文件都不會(huì)被監(jiān)控。除此之外,如果我們沒(méi)有指定任何篩選條件,也不會(huì)有任何的文件會(huì)被監(jiān)控。
監(jiān)控文件變化的真正目的在于讓應(yīng)用程序能夠及時(shí)感知到數(shù)據(jù)源的改變,進(jìn)而自動(dòng)執(zhí)行某些預(yù)先注冊(cè)的回掉操作。回調(diào)的注冊(cè)可以直接通過(guò)調(diào)用ChangeToken的RegisterChangeCallback方法來(lái)完成,注冊(cè)的回調(diào)通過(guò)一個(gè)類型為Action<object>的委托對(duì)象來(lái)表示。對(duì)于在第一節(jié)演示的文件監(jiān)控的實(shí)例,相應(yīng)的程序“照理說(shuō)”可以改寫成如下的形式。
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: }如果執(zhí)行上面這段程序,我們會(huì)發(fā)現(xiàn)只有第一個(gè)針對(duì)文件的更新能夠被感知,后續(xù)的文件更新操作將自動(dòng)被忽略。導(dǎo)致這個(gè)問(wèn)題的根源在于,單個(gè)ChangeToken對(duì)象的使命在于當(dāng)綁定的數(shù)據(jù)源第一次發(fā)生變換時(shí)對(duì)外發(fā)送相應(yīng)的信號(hào),而不具有持續(xù)發(fā)送數(shù)據(jù)變換的能力。其實(shí)這一點(diǎn)從IChangeToken接口的定義就可以看出來(lái),我們知道它具有一個(gè)HasChanged屬性表示數(shù)據(jù)是否已經(jīng)發(fā)生變化,而并沒(méi)有提供一個(gè)讓這個(gè)屬性“復(fù)位”的方法。所以當(dāng)我們需要對(duì)某個(gè)文件進(jìn)行持續(xù)監(jiān)控的時(shí)候,我們需要在注冊(cè)的回調(diào)中重新調(diào)用FileProvider的Watch方法,并利用生成ChangeToken再次注冊(cè)回調(diào)。除此之外,考慮到ChangeToken的RegisterChangeCallback方法以一個(gè)IDisposable對(duì)象的形式返回回調(diào)注冊(cè)對(duì)象,我們應(yīng)該在對(duì)回調(diào)實(shí)施二次注冊(cè)時(shí)調(diào)用第一次返回的回調(diào)注冊(cè)對(duì)象的Dispose方法將其釋放掉。如下所示的程序才能達(dá)到對(duì)文件試試持續(xù)監(jiān)控的目的。
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: 9: }; 10:? 11: regiser = fileProvider.Watch("data.txt").RegisterChangeCallback(callback, null);不過(guò)這樣的編程方式不但看起來(lái)比較繁瑣,很多對(duì)ChangeToken缺乏認(rèn)識(shí)的人甚至對(duì)這樣的編程方式無(wú)法理解。為了解決這個(gè)問(wèn)題,我們可以使用定義在ChangeToken類型中如下兩個(gè)方法OnChange方法來(lái)注冊(cè)數(shù)據(jù)發(fā)生改變時(shí)自動(dòng)執(zhí)行的回調(diào)。這兩個(gè)方法具有兩個(gè)參數(shù),前者是一個(gè)用于創(chuàng)建ChangeToken對(duì)象的Func<IChangeToken>對(duì)象,后者則是代表回調(diào)操作的Action<object>/Action<TState>對(duì)象。實(shí)際上第一節(jié)的實(shí)例演示中我們就是調(diào)用的這個(gè)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: }如果改用這個(gè)OnChange方法來(lái)替換掉原來(lái)手工調(diào)用ChangeToken的RegisterChangeCallback方法進(jìn)行回調(diào)注冊(cè)的方式,原本顯得相對(duì)繁瑣的程序可以通過(guò)如下兩句代碼來(lái)替換。實(shí)際上在《讀取并監(jiān)控文件的變化》中,我們調(diào)用的正是這個(gè)OnChange方法。
1: IFileProvider fileProvider = new PhysicalFileProvider(@"c:\test"); 2: ChangeToken.OnChange(() => fileProvider.Watch("data.txt"), () => LoadFileAsync(fileProvider));五、總結(jié)
我們借助下圖所示的UML來(lái)對(duì)由PhysicalFileProvider構(gòu)建物理文件系統(tǒng)的整體設(shè)計(jì)做一個(gè)簡(jiǎn)單的總結(jié)。首先,該文件系統(tǒng)下用于描述目錄和文件的分別是一個(gè)PhysicalDirectoryInfo和PhysicalFileInfo對(duì)象,它們分別是對(duì)一個(gè)DirectoryInfo和FileInfo(System.IO.FileInfo)對(duì)象的封裝。PhysicalFileProvider的GetDirectoryContents方法返回一個(gè)EnumerableDirectoryContents對(duì)象(如果指定的目錄存在),組成該對(duì)象的分別是根據(jù)其所有子目錄和文件創(chuàng)建的PhysicalDirectoryInfo和PhysicalFileInfo對(duì)象。當(dāng)我們調(diào)用PhysicalFileProvider的GetFileInfo方法時(shí),如果指定的文件存在,返回的是描述該文件的PhysicalFileInfo對(duì)象。至于PhysicalFileProvider的Watch方法,它最終利用了FileSystemWatcher來(lái)監(jiān)控指定文件的變化。
總結(jié)
以上是生活随笔為你收集整理的.NET Core的文件系统[3]:由PhysicalFileProvider构建的物理文件系统的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。