.NET Core的檔案系統[5]:擴充套件檔案系統構建一個簡易版“雲盤”
FileProvider構建了一個抽象檔案系統,作為它的兩個具體實現,PhysicalFileProvider和EmbeddedFileProvider則分別為我們構建了一個物理檔案系統和程式集內嵌檔案系統。總的來說,它們針對的都是“本地”檔案,接下來我們通過自定義FileProvider構建一個“遠端”檔案系統,我們可以將它視為一個只讀的“雲盤”。由於檔案系統的目錄結構和檔案內容都是通過HTTP請求的方式讀取的,所以我們將這個自定義的FileProvider命名為HttpFileProvider。[ 本文已經同步到《ASP.NET Core框架揭祕》之中]
上圖基本上體現了以HttpFileProvider的遠端檔案系統的設計和實現原理。真實的檔案儲存在檔案伺服器上,客戶端可以通過公佈出來的Web API得到指定路徑所在的目錄結構,以及目錄和檔案描述資訊,甚至可以讀取指定檔案的內容。檔案伺服器中的每一個目錄都對應著一個URL,客戶端可以指定相應的URL將某一個目錄作為本地檔案系統的根。如圖7所示,伺服器上的檔案系統實際是直接通過指向“c:\test”目錄的PhysicalFileProvider來表示的,這個根目錄通過“http://server/files/
目錄
一、HttpFileInfo與HttpDirectoryContents
二、HttpFileProvider
三、FileProviderMiddleware
四、遠端檔案系統的應用
一、HttpFileInfo與HttpDirectoryContents
在以HttpFileProvider為核心的檔案系統中,我們通過HttpFileInfo來表示目錄和檔案,包含子目錄和檔案的目錄內容則通過另一個HttpDirectoryContents型別來表示。不過在這之前,我們需要介紹兩個對應的描述型別,它們分別是描述檔案和目錄的HttpFileDescriptor和描述目錄內容的HttpDirectoryContentsDescriptor。
如下面的程式碼片段所示,HttpFileDescriptor的屬性成員基本上是根據IFileInfo這個介面來定義的,並且這些屬性的值本身就來源於在構造時指定的FileInfo物件。由於真實的目錄或檔案存在於檔案伺服器上,所以HttpFileDescriptor的PhysicalPath屬性表示的實際上是對應的URL,這個URL是通過構造時指定的委託物件計算出來的。
1: public class HttpFileDescriptor
2: {
3: public bool Exists { get; set; }
4: publicbool IsDirectory { get; set; }
5: public DateTimeOffset LastModified { get; set; }
6: public long Length { get; set; }
7: public string Name { get; set; }
8: public string PhysicalPath { get; set; }
9:
10: public HttpFileDescriptor()
11: { }
12:
13: public HttpFileDescriptor(IFileInfo fileInfo, Func<string, string> physicalPathResolver)
14: {
15: this.Exists = fileInfo.Exists;
16: this.IsDirectory = fileInfo.IsDirectory;
17: this.LastModified = fileInfo.LastModified;
18: this.Length = fileInfo.Length;
19: this.Name = fileInfo.Name;
20: this.PhysicalPath = physicalPathResolver(fileInfo.Name);
21: }
22:
23: public IFileInfo ToFileInfo(HttpClient httpClient)
24: {
25: return this.Exists
26: ? new HttpFileInfo(this, httpClient)
27: : (IFileInfo)new NotFoundFileInfo(this.Name);
28: }
29: }
用於描述檔案或者目錄HttpFileDescriptor物件實際上可以視為是對一個FileInfo物件的封裝,而用來描述目錄內容的HttpDirectoryContentsDescriptor則是對一個DirectoryContents物件的封裝。如下面的程式碼片段所示,HttpDirectoryContentsDescriptor具有一個名為FileDescriptors的屬性返回一組HttpFileDescriptor物件的集合,集合中的每個HttpFileDescriptor物件對應著當前目錄下的某個子目錄或者檔案。
1: public class HttpDirectoryContentsDescriptor
2: {
3: public bool Exists { get; set; }
4: public IEnumerable<HttpFileDescriptor> FileDescriptors { get; set; }
5:
6: public HttpDirectoryContentsDescriptor()
7: {
8: this.FileDescriptors = new HttpFileDescriptor[0];
9: }
10:
11: public HttpDirectoryContentsDescriptor(IDirectoryContents directoryContents, Func<string, string> physicalPathResolver)
12: {
13: this.Exists = directoryContents.Exists;
14: this.FileDescriptors = directoryContents.Select(_ => new HttpFileDescriptor(_, physicalPathResolver));
15: }
16: }
從前面的程式碼片段可以看到HttpFileDescriptor具有一個ToFileInfo方法將自己轉換成一個FileInfo物件,這個物件的型別就是我們上面提到過的HttpFileInfo。由於HttpFileInfo是通過一個HttpFileDescriptor物件創建出來的,所以它的所有屬性最初都來源於這個物件。由於FileInfo除了提供目錄或者檔案的描述資訊之外,它還通過自身的CreateReadStream方法承載著讀取檔案內容的職責。由於真正的檔案儲存在伺服器上,所以我們需要利用構建時提供的HttpClient物件向目標檔案所在的URL傳送HTTP請求的方式來讀取檔案內容,
1: public class HttpFileInfo: IFileInfo
2: {
3: private HttpClient _httpClient;
4:
5: public bool Exists { get; private set; }
6: public bool IsDirectory { get; private set; }
7: public DateTimeOffset LastModified { get; private set; }
8: public long Length { get; private set; }
9: public string Name { get; private set; }
10: public string PhysicalPath { get; private set; }
11:
12: public HttpFileInfo(HttpFileDescriptor descriptor, HttpClient httpClient)
13: {
14: this.Exists = descriptor.Exists;
15: this.IsDirectory = descriptor.IsDirectory;
16: this.LastModified = descriptor.LastModified;
17: this.Length = descriptor.Length;
18: this.Name = descriptor.Name;
19: this.PhysicalPath = descriptor.PhysicalPath;
20: _httpClient = httpClient;
21: }
22:
23: public Stream CreateReadStream()
24: {
25: HttpResponseMessage message = _httpClient.GetAsync(this.PhysicalPath).Result;
26: return message.Content.ReadAsStreamAsync().Result;
27: }
28: }
表示目錄內容的HttpDirectoryContents具有如下的定義。與HttpFileInfo類似,HttpDirectoryContents物件依然是根據對應的描述物件(一個HttpDirectoryContentsDescriptor物件)建立的。HttpDirectoryContents本質上就是一個FileInfo物件的集合,集合中的每個元素都是一個根據HttpFileDescriptor物件建立的HttpFileInfo物件。
1: public class HttpDirectoryContents : IDirectoryContents
2: {
3: private IEnumerable<IFileInfo> _fileInfos;
4: public bool Exists { get; private set; }
5:
6: public HttpDirectoryContents(HttpDirectoryContentsDescriptor descriptor, HttpClient httpClient)
7: {
8: this.Exists = descriptor.Exists;
9: _fileInfos = descriptor.FileDescriptors.Select(file => file.ToFileInfo(httpClient));
10: }
11:
12: public IEnumerator<IFileInfo> GetEnumerator() => _fileInfos.GetEnumerator();
13: IEnumerator IEnumerable.GetEnumerator() => _fileInfos.GetEnumerator();
14: }
二、HttpFileProvider
接下來我們來介紹作為核心的HttpFileProvider型別的實現。我們知道FileProvider承載著三項職責,即通過GetDirectoryContents方法得到指定目錄的內容,通過GetFileInfo得到指定目錄或者檔案的描述,以及通過Watch方法監控目錄或者檔案的變化。雖然我們可以採用某種技術手段實現從服務端向客戶端傳送通知,但是針對遠端檔案的監控意義不大,所以HttpFileProvider只提供前面兩種基本的功能。
1: public class HttpFileProvider : IFileProvider
2: {
3: private readonly string _baseAddress;
4: private HttpClient _httpClient;
5:
6: public HttpFileProvider(string baseAddress)
7: {
8: _baseAddress = baseAddress.TrimEnd('/');
9: _httpClient = new HttpClient();
10: }
11:
12: public IDirectoryContents GetDirectoryContents(string subpath)
13: {
14: string url = $"{_baseAddress}/{subpath.TrimStart('/')}?dir-meta";
15: string content = _httpClient.GetStringAsync(url).Result;
16: HttpDirectoryContentsDescriptor descriptor = JsonConvert.DeserializeObject<HttpDirectoryContentsDescriptor>(content);
17: return new HttpDirectoryContents(descriptor, _httpClient);
18: }
19:
20: public IFileInfo GetFileInfo(string subpath)
21: {
22: string url = $"{_baseAddress}/{subpath.TrimStart('/')}?file-meta";
23: string conten