程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET Core的文件系統[5]:擴展文件系統構建一個簡易版“雲盤”,core雲盤

.NET Core的文件系統[5]:擴展文件系統構建一個簡易版“雲盤”,core雲盤

編輯:關於.NET

.NET Core的文件系統[5]:擴展文件系統構建一個簡易版“雲盤”,core雲盤


FileProvider構建了一個抽象文件系統,作為它的兩個具體實現,PhysicalFileProvider和EmbeddedFileProvider則分別為我們構建了一個物理文件系統和程序集內嵌文件系統。總的來說,它們針對的都是“本地”文件,接下來我們通過自定義FileProvider構建一個“遠程”文件系統,我們可以將它視為一個只讀的“雲盤”。由於文件系統的目錄結構和文件內容都是通過HTTP請求的方式讀取的,所以我們將這個自定義的FileProvider命名為HttpFileProvider。[ 本文已經同步到《ASP.NET Core框架揭秘》之中]

上圖基本上體現了以HttpFileProvider的遠程文件系統的設計和實現原理。真實的文件保存在文件服務器上,客戶端可以通過公布出來的Web API得到指定路徑所在的目錄結構,以及目錄和文件描述信息,甚至可以讀取指定文件的內容。文件服務器中的每一個目錄都對應著一個URL,客戶端可以指定相應的URL將某一個目錄作為本地文件系統的根。如圖7所示,服務器上的文件系統實際是直接通過指向“c:\test”目錄的PhysicalFileProvider來表示的,這個根目錄通過“http://server/files/”表示。對於兩個客戶端的“本地文件系統來說”,它們的根分別指向文件服務器上的目錄“c:\dir1”和“c:\dir1\foobar”(對應的URL分別是“http://server/files/dir1”和“ http://server/files/dir1/foobar”)。

 

目錄
一、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:     public bool               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 content = _httpClient.GetStringAsync(url).Result;
  24:         HttpFileDescriptor descriptor = JsonConvert.DeserializeObject<HttpFileDescriptor>(content);
  25:         return descriptor.ToFileInfo(_httpClient);
  26:     }
  27:  
  28:     public IChangeToken Watch(string filter)
  29:     {
  30:         return NullChangeToken.Singleton;
  31:     }
  32: }

由於文件系統由服務器托管,目錄內容和目錄與文件的描述信息都只能通過發送HTTP請求的形式來獲取,HttpFileProvider利用一個HttpClient對象來獲取這些遠程資源。HttpFileProvider建立的本地文件系統的根目錄可以指向文件服務器上任意一個目錄,我們將指向這個目錄的URL成為“基地址”,對應著它的字段_baseAddress。對於任何一個目錄或者文件來說,它對應的URL通過這個基地址和相對地址合並而成。

不論是GetFileInfo方法還是GetDirectoryContents,HttpFileProvider發送HTTP請求的地址都是所在目錄或者文件對應的URL,但是它們返回的內容是不同的。前者返回的是目錄或者文件的描述信息,後者返回的目錄內容的描述信息。為此我們采用相應的查詢字符串來區分這兩種具有相同路徑的HTTP請求,它們采用的查詢字符串名稱分別是“ ?file-meta”和“?dir-meta”。

對於HttpFileProvider實現的GetDirectoryContents和GetFileInfo方法,它根據指定的相對路徑解析出對應的URL,然後利用HttpClient針對這個地址發送HTTP請求,響應的內容利用JsonConvert反序列成一個HttpDirectoryContentsDescriptor或者HttpFileDescriptor對象,然後在據此創建並返回一個HttpDirectoryContents或者HttpFileInfo對象。

三、FileProviderMiddleware

作為文件服務器的其實就是一個簡單的ASP.NET Core應用,HttpFileProvider調用的Web API則是通過一個類型為FileProviderMiddleware的中間件實現的。具體來說,這個FileProviderMiddleware需要處理如下三種類型的HTTP請求:

  • 讀取文件內容: 地址指向目標文件,不含任何查詢字符串,比如“/files/dir1/foobar/foo.txt”。
  • 讀取文件或目錄的描述:地址指向目標目錄或文件,采用“?file-meta”作為查詢字符串,比如“/files/dir1/foobar?file-meta”或者“ /files/dir1/foobar/foo.txt?file-meta”。
  • 讀取目錄內容:地址指向目標目錄,采用“?dir-meta”作為查詢字符串,比如“/files/dir1/foobar?dir-meta”。

如下所示的代碼片段體現了FileProviderMiddleware這個中間件的完整定義。我們可以看出它直接使用一個PhysicalFileProvider來作為自身的文件系統,對應的根目錄直接在構造函數中指定。針對上述這三種HTTP請求的處理實現在Invoke方法中,具體的實現邏輯其實很簡單:如果請求地址攜帶查詢字符串“dir-meta”,則根據請求目標目錄創建一個HttpDirectoryContentsDescriptor對象,將利用JsonConvert將其序列化後寫入響應;如果請求地址攜帶查詢字符串“file-meta”,則根據請求的目錄或者文件創建一個HttpFileDescriptor對象,並采用相同的方式序列化後寫入響應;如果請求地址不具有如上兩個查詢字符串,則直接讀取目標文件的內容並寫入響應。

   1: public class FileProviderMiddleware
   2: {
   3:     private readonly RequestDelegate     _next;
   4:     private readonly IFileProvider       _fileProvider;
   5:  
   6:     public FileProviderMiddleware(RequestDelegate next, string root)
   7:     {
   8:         _next             = next;
   9:         _fileProvider     = new PhysicalFileProvider(root);
  10:     }
  11:  
  12:     public async Task Invoke(HttpContext context)
  13:     {
  14:         if (context.Request.Query.ContainsKey("dir-meta"))
  15:         {
  16:             var dirContents = _fileProvider.GetDirectoryContents(context.Request.Path);
  17:             var dirDecriptor = new HttpDirectoryContentsDescriptor(dirContents, CreatePhysicalPathResolver(context, true));
  18:             await context.Response.WriteAsync(JsonConvert.SerializeObject(dirDecriptor));
  19:         }
  20:         else if (context.Request.Query.ContainsKey("file-meta"))
  21:         {
  22:             var fileInfo = _fileProvider.GetFileInfo(context.Request.Path);
  23:             var fileDescriptor = new HttpFileDescriptor(fileInfo, CreatePhysicalPathResolver(context, false));
  24:             await context.Response.WriteAsync(JsonConvert.SerializeObject(fileDescriptor));
  25:         }
  26:         else
  27:         {
  28:             await context.Response.SendFileAsync(_fileProvider.GetFileInfo(context.Request.Path));
  29:         }
  30:     }
  31:  
  32:     private Func<string, string> CreatePhysicalPathResolver(HttpContext context, bool isDirRequest)        
  33:     {
  34:         string schema       = context.Request.IsHttps ? "https" : "http";
  35:         string host         = context.Request.Host.Host;
  36:         int port            = context.Request.Host.Port ?? 8080;
  37:         string pathBase     = context.Request.PathBase.ToString().Trim('/');
  38:         string path         = context.Request.Path.ToString().Trim('/');
  39:  
  40:         pathBase     = string.IsNullOrEmpty(pathBase) ? string.Empty : $"/{pathBase}";
  41:         path         = string.IsNullOrEmpty(path) ? string.Empty : $"/{path}";
  42:  
  43:         return isDirRequest
  44:             ? (Func<string, string>)(name => $"{schema}://{host}:{port}{pathBase}{path}/{name}")
  45:             : name => $"{schema}://{host}:{port}{pathBase}{path}";
  46:     }
  47: }

四、遠程文件系統的應用

整個文件系統由FileProviderMiddleware和HttpFileProvider這兩個核心對象組成,我們可以利用前者創建一個ASP.NET Core應用來作為文件服務器,客戶端則利用後者在本地建立一個虛擬的文件系統。接下來我們就來演示如何在一個具體的實例使用它們。我們首先創建一個控制台應用來承載作為文件服務器的ASP.NET Core應用。在添加必要NuGet包依賴之後,我們只需要編寫如下幾行簡單程序即可。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseUrls("http://localhost:3721/files")
   8:             .Configure(app => app.UseMiddleware<FileProviderMiddleware>(@"c:\test"))
   9:             .Build()
  10:             .Run();
  11:     }
  12: }

FileProviderMiddleware這個中間件類型直接通過調用WebHostBuilder的擴展方法Configure進行注冊,我們在注冊的同時指定了根目錄的路徑。接下來我們直接利用在《讀取並監控文件的變化》創建的實例來演示如何利用HttpFileProvider來展示指定的目錄結構和遠程讀取文件內容,為此我們對之前的程序進行了如下的改寫。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         IFileManager fileManager = new ServiceCollection()
   6:             .AddSingleton<IFileProvider>(new HttpFileProvider("http://localhost:3721/files/dir1"))
   7:             .AddSingleton<IFileManager, FileManager>()
   8:             .BuildServiceProvider()
   9:             .GetService<IFileManager>();
  10:  
  11:         fileManager.ShowStructure((layer, name) => Console.WriteLine($"{new string('\t', layer)}{name}"));
  12:     }
  13: }

如上面的代碼片段所示,我們創建了並注冊了一個HttpFileProvider,而指定的作為根目錄的URL為“http://localhost:3721/files/dir1”。由於文件服務器和客戶端所處同一台主機,所以通過HttpFileProvider建立的本地文件系統的根目錄實際上指向“C:\test\dir1”這個目錄。當我們調用FileManager的ShowStructure方法之後,控制台上會以如下圖所示的形式呈現出本地文件系統的虛擬結構。

我們依然可以直接調用FileManager的ReadAllTextAsync方法讀取遠程地讀取某個文件的內容。如下面的代碼片段所示,我們調用這個方法讀取的文件路徑為“foobar/foo.txt”,由於HttpFileProvider采用的基地址為“/files/dir1”,所以讀取的這個文件在本地的路徑為“c:\test\dir1\foobar\foo.txt”。如下所示的調試斷言表明利用HttpFileProvider讀取的文件就是這個物理文件。

   1: public static void Main()
   2: {
   3:     IFileManager fileManager = new ServiceCollection()
   4:         .AddSingleton<IFileProvider>(new HttpFileProvider("http://localhost:3721/files/dir1"))
   5:         .AddSingleton<IFileManager, FileManager>()
   6:         .BuildServiceProvider()
   7:         .GetService<IFileManager>();
   8:  
   9:     string content1 = fileManager.ReadAllTextAsync("foobar/foo.txt").Result;
  10:     string content2 = File.ReadAllText(@"c:\test\dir1\foobar\foo.txt");
  11:     Debug.Assert(content1 == content2);
  12: }

 


[1] 取並監控文件的變化
[2] FileProvider是個什麼東西?
[3] 由PhysicalFileProvider構建的物理文件系統
[4] 由EmbeddedFileProvider構建的內嵌(資源)文件系統
[5] 擴展文件系統構建一個簡易版“雲盤”

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved