FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文件系统。总的来说,它们针对的都是“本地”文件,接下来我们通过自定义FileProvider构建一个“远程”文件系统,我们可以将它视为一个只读的“云盘”。由于文件系统的目录结构和文件内容都是通过HTTP请求的方式读取的,所以我们将这个自定义的FileProvider命名为HttpFileProvider。
上图基本上体现了以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 四、远程文件系统的应用
在以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类型的实现。我们知道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对象。
作为文件服务器的其实就是一个简单的ASP.NET Core应用,HttpFileProvider调用的Web API则是通过一个类型为FileProviderMiddleware的中间件实现的。具体来说,这个FileProviderMiddleware需要处理如下三种类型的HTTP请求:
如下所示的代码片段体现了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: }
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句