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] 扩展文件系统构建一个简易版“云盘”

.NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”的更多相关文章

  1. .NET CORE学习笔记系列(2)——依赖注入[4]: 创建一个简易版的DI框架[上篇]

    原文https://www.cnblogs.com/artech/p/net-core-di-04.html 本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章从 ...

  2. 【MEF】构建一个WPF版的ERP系统

    原文:[MEF]构建一个WPF版的ERP系统 引言 MEF是微软的一个扩展性框架,遵循某种约定将各个部件组合起来.而ERP系统的一大特点是模块化,它们两者的相性很好,用MEF构建一个ERP系统是相当合 ...

  3. .NET CORE学习笔记系列(2)——依赖注入[5]: 创建一个简易版的DI框架[下篇]

    为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在上篇中我们介绍了Cat的基本编程模式,接下来我们就来聊聊Cat的 ...

  4. php扩展开发-实现一个简易的哈希表

    从一个简易的哈希表入手,会让你更好的理解php的哈希表,他们的本质是一样的,只是php的哈希表做了更多的功能扩展,php的哈希表是php语言的一个重要核心,大量的内核代码使用到哈希表. #includ ...

  5. 用Akka构建一个简易的分布式文件系统

    本来初期打算用Hadoop 2,可是后来有限的服务器部署了Solr Cloud,各种站点,发现资源不够了,近10T的文件,已经几乎把服务器的磁盘全部用光.想来想去,由于目前架构基于Scala的,所以还 ...

  6. net core体系-web应用程序-1VS2017构建一个简单的web

    使用vs2017,添加一个新项目-asp.net core web应用程序. 结构如图, wwwroot放了网站的静态资源如css.js.image文件: appsetting.json是应用程序的配 ...

  7. docker构建一个简易镜像

    一 下载centos镜像 docker pull centos 二 启动镜像 [root@Centos-node3 ~]# docker run -it --name my_ng centos bas ...

  8. AIX扩展文件系统的大小

    由于AIX系统空间不够需要增加硬盘,希望增加文件系统的空间,折腾了好几天怎么都不能扩展文件系统的空间,原来是把硬盘加错了卷组 首先,确定文件系统所在的LV /dev/datalv      270.0 ...

  9. aix 扩展文件系统

    今天发现公司的oracle测试 数据库不能启动,检查警告日志日志,提示归档空间不足,不能归档,于是扩展文件系统: 1.检查rootvg卷组的剩余空间[p2704u]:[/dsg/oracle11]$ ...

随机推荐

  1. CoreCLR源码探索(一) Object是什么

    .Net程序员们每天都在和Object在打交道 如果你问一个.Net程序员什么是Object,他可能会信誓旦旦的告诉你"Object还不简单吗,就是所有类型的基类" 这个答案是对的 ...

  2. web前端基础知识

    #HTML    什么是HTML,和他ML...    网页可以比作一个装修好了的,可以娶媳妇的房子.    房子分为:毛坯房,精装修    毛坯房的修建: 砖,瓦,水泥,石头,石子....    精 ...

  3. .NET平台开源项目速览(17)FluentConsole让你的控制台酷起来

    从该系列的第一篇文章 .NET平台开源项目速览(1)SharpConfig配置文件读写组件 开始,不知不觉已经到第17篇了.每一次我们都是介绍一个小巧甚至微不足道的.NET平台的开源软件,或者学习,或 ...

  4. iOS逆向工程之Theos

    如果你对iOS逆向工程有所了解,那么你对Tweak并不陌生.那么由Tweak我们又会引出Theos, 那么什么是Theos呢,简单一句话,Theos是一个越狱开发工具包,Theos是越狱开发工具的首先 ...

  5. Javascript实用方法二

    承接上一篇, Object keys object的keys方法能够获取一个给定对象的所有键(key/属性名)并以数组的形式返回.这个方法可以用于键的筛选.匹配等. var basket = { st ...

  6. JS实现页面进入、返回定位到具体位置

    最为一个刚入职不久的小白...慢慢磨练吧... JS实现页面返回定位到具体位置 其实浏览器也自带了返回的功能,也就是说,自带了返回定位的功能.正常的跳转,返回确实可以定位,但是有些特殊场景就不适用了. ...

  7. 从阿里巴巴笔试题看Java加载顺序

    一.阿里巴巴笔试题: public class T implements Cloneable { public static int k = 0; public static T t1 = new T ...

  8. 防线修建 bzoj 2300

    防线修建(1s 512MB)defense [问题描述] 近来A国和B国的矛盾激化,为了预防不测,A国准备修建一条长长的防线,当然修建防线的话,肯定要把需要保护的城市修在防线内部了.可是A国上层现在还 ...

  9. PHP设计模式(六)原型模式(Prototype For PHP)

    原型设计模式: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象. 原型设计模式简单的来说,顾名思义, 不去创建新的对象进而保留原型的一种设计模式. 缺点:原型设计模式是的最主要的缺点就 ...

  10. AlloyTouch实战--60行代码搞定QQ看点资料卡

    原文链接:https://github.com/AlloyTeam/AlloyTouch/wiki/kandian 先验货 访问DEMO你也可以点击这里 源代码可以点击这里 如你体验所见,流程的滚动的 ...