netcore3.0 IFileProvider 文件系统
Nuget包:以Microsoft.Extensins.FileProviders开头的包中
Github地址:https://github.com/dotnet/extensions/tree/master/src/FileProviders
一、PhysicalFileProvider
/// <summary> /// Represents a file on a physical filesystem /// </summary> public class PhysicalFileInfo : IFileInfo { private readonly FileInfo _info; /// <summary> /// Initializes an instance of <see cref="PhysicalFileInfo"/> that wraps an instance of <see cref="System.IO.FileInfo"/> /// </summary> /// <param name="info">The <see cref="System.IO.FileInfo"/></param> public PhysicalFileInfo(FileInfo info) { _info = info; } /// <inheritdoc /> public bool Exists => _info.Exists; /// <inheritdoc /> public long Length => _info.Length; /// <inheritdoc /> public string PhysicalPath => _info.FullName; /// <inheritdoc /> public string Name => _info.Name; /// <inheritdoc /> public DateTimeOffset LastModified => _info.LastWriteTimeUtc; /// <summary> /// Always false. /// </summary> public bool IsDirectory => false; /// <inheritdoc /> public Stream CreateReadStream() { // We are setting buffer size to 1 to prevent FileStream from allocating it's internal buffer // 0 causes constructor to throw var bufferSize = 1; return new FileStream( PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan); } }
/// <summary> /// Looks up files using the on-disk file system /// </summary> /// <remarks> /// When the environment variable "DOTNET_USE_POLLING_FILE_WATCHER" is set to "1" or "true", calls to /// <see cref="Watch(string)" /> will use <see cref="PollingFileChangeToken" />. /// </remarks> public class PhysicalFileProvider : IFileProvider, IDisposable { private const string PollingEnvironmentKey = "DOTNET_USE_POLLING_FILE_WATCHER"; private static readonly char[] _pathSeparators = new[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}; private readonly ExclusionFilters _filters; private readonly Func<PhysicalFilesWatcher> _fileWatcherFactory; private PhysicalFilesWatcher _fileWatcher; private bool _fileWatcherInitialized; private object _fileWatcherLock = new object(); private bool? _usePollingFileWatcher; private bool? _useActivePolling; /// <summary> /// Initializes a new instance of a PhysicalFileProvider at the given root directory. /// </summary> /// <param name="root">The root directory. This should be an absolute path.</param> public PhysicalFileProvider(string root) : this(root, ExclusionFilters.Sensitive) { } /// <summary> /// Initializes a new instance of a PhysicalFileProvider at the given root directory. /// </summary> /// <param name="root">The root directory. This should be an absolute path.</param> /// <param name="filters">Specifies which files or directories are excluded.</param> public PhysicalFileProvider(string root, ExclusionFilters filters) { if (!Path.IsPathRooted(root)) { throw new ArgumentException("The path must be absolute.", nameof(root)); } var fullRoot = Path.GetFullPath(root); // When we do matches in GetFullPath, we want to only match full directory names. Root = PathUtils.EnsureTrailingSlash(fullRoot); if (!Directory.Exists(Root)) { throw new DirectoryNotFoundException(Root); } _filters = filters; _fileWatcherFactory = () => CreateFileWatcher(); } /// <summary> /// Gets or sets a value that determines if this instance of <see cref="PhysicalFileProvider"/> /// uses polling to determine file changes. /// <para> /// By default, <see cref="PhysicalFileProvider"/> uses <see cref="FileSystemWatcher"/> to listen to file change events /// for <see cref="Watch(string)"/>. <see cref="FileSystemWatcher"/> is ineffective in some scenarios such as mounted drives. /// Polling is required to effectively watch for file changes. /// </para> /// <seealso cref="UseActivePolling"/>. /// </summary> /// <value> /// The default value of this property is determined by the value of environment variable named <c>DOTNET_USE_POLLING_FILE_WATCHER</c>. /// When <c>true</c> or <c>1</c>, this property defaults to <c>true</c>; otherwise false. /// </value> public bool UsePollingFileWatcher { get { if (_fileWatcher != null) { throw new InvalidOperationException($"Cannot modify {nameof(UsePollingFileWatcher)} once file watcher has been initialized."); } if (_usePollingFileWatcher == null) { ReadPollingEnvironmentVariables(); } return _usePollingFileWatcher.Value; } set => _usePollingFileWatcher = value; } /// <summary> /// Gets or sets a value that determines if this instance of <see cref="PhysicalFileProvider"/> /// actively polls for file changes. /// <para> /// When <see langword="true"/>, <see cref="IChangeToken"/> returned by <see cref="Watch(string)"/> will actively poll for file changes /// (<see cref="IChangeToken.ActiveChangeCallbacks"/> will be <see langword="true"/>) instead of being passive. /// </para> /// <para> /// This property is only effective when <see cref="UsePollingFileWatcher"/> is set. /// </para> /// </summary> /// <value> /// The default value of this property is determined by the value of environment variable named <c>DOTNET_USE_POLLING_FILE_WATCHER</c>. /// When <c>true</c> or <c>1</c>, this property defaults to <c>true</c>; otherwise false. /// </value> public bool UseActivePolling { get { if (_useActivePolling == null) { ReadPollingEnvironmentVariables(); } return _useActivePolling.Value; } set => _useActivePolling = value; } internal PhysicalFilesWatcher FileWatcher { get { return LazyInitializer.EnsureInitialized( ref _fileWatcher, ref _fileWatcherInitialized, ref _fileWatcherLock, _fileWatcherFactory); } set { Debug.Assert(!_fileWatcherInitialized); _fileWatcherInitialized = true; _fileWatcher = value; } } internal PhysicalFilesWatcher CreateFileWatcher() { var root = PathUtils.EnsureTrailingSlash(Path.GetFullPath(Root)); return new PhysicalFilesWatcher(root, new FileSystemWatcher(root), UsePollingFileWatcher, _filters) { UseActivePolling = UseActivePolling, }; } private void ReadPollingEnvironmentVariables() { var environmentValue = Environment.GetEnvironmentVariable(PollingEnvironmentKey); var pollForChanges = string.Equals(environmentValue, "1", StringComparison.Ordinal) || string.Equals(environmentValue, "true", StringComparison.OrdinalIgnoreCase); _usePollingFileWatcher = pollForChanges; _useActivePolling = pollForChanges; } /// <summary> /// Disposes the provider. Change tokens may not trigger after the provider is disposed. /// </summary> public void Dispose() => Dispose(true); /// <summary> /// Disposes the provider. /// </summary> /// <param name="disposing"><c>true</c> is invoked from <see cref="IDisposable.Dispose"/>.</param> protected virtual void Dispose(bool disposing) { _fileWatcher?.Dispose(); } /// <summary> /// Destructor for <see cref="PhysicalFileProvider"/>. /// </summary> ~PhysicalFileProvider() => Dispose(false); /// <summary> /// The root directory for this instance. /// </summary> public string Root { get; } private string GetFullPath(string path) { if (PathUtils.PathNavigatesAboveRoot(path)) { return null; } string fullPath; try { fullPath = Path.GetFullPath(Path.Combine(Root, path)); } catch { return null; } if (!IsUnderneathRoot(fullPath)) { return null; } return fullPath; } private bool IsUnderneathRoot(string fullPath) { return fullPath.StartsWith(Root, StringComparison.OrdinalIgnoreCase); } /// <summary> /// Locate a file at the given path by directly mapping path segments to physical directories. /// </summary> /// <param name="subpath">A path under the root directory</param> /// <returns>The file information. Caller must check <see cref="IFileInfo.Exists"/> property. </returns> public IFileInfo GetFileInfo(string subpath) { if (string.IsNullOrEmpty(subpath) || PathUtils.HasInvalidPathChars(subpath)) { return new NotFoundFileInfo(subpath); } // Relative paths starting with leading slashes are okay subpath = subpath.TrimStart(_pathSeparators); // Absolute paths not permitted. if (Path.IsPathRooted(subpath)) { return new NotFoundFileInfo(subpath); } var fullPath = GetFullPath(subpath); if (fullPath == null) { return new NotFoundFileInfo(subpath); } var fileInfo = new FileInfo(fullPath); if (FileSystemInfoHelper.IsExcluded(fileInfo, _filters)) { return new NotFoundFileInfo(subpath); } return new PhysicalFileInfo(fileInfo); } /// <summary> /// Enumerate a directory at the given path, if any. /// </summary> /// <param name="subpath">A path under the root directory. Leading slashes are ignored.</param> /// <returns> /// Contents of the directory. Caller must check <see cref="IDirectoryContents.Exists"/> property. <see cref="NotFoundDirectoryContents" /> if /// <paramref name="subpath" /> is absolute, if the directory does not exist, or <paramref name="subpath" /> has invalid /// characters. /// </returns> public IDirectoryContents GetDirectoryContents(string subpath) { try { if (subpath == null || PathUtils.HasInvalidPathChars(subpath)) { return NotFoundDirectoryContents.Singleton; } // Relative paths starting with leading slashes are okay subpath = subpath.TrimStart(_pathSeparators); // Absolute paths not permitted. if (Path.IsPathRooted(subpath)) { return NotFoundDirectoryContents.Singleton; } var fullPath = GetFullPath(subpath); if (fullPath == null || !Directory.Exists(fullPath)) { return NotFoundDirectoryContents.Singleton; } return new PhysicalDirectoryContents(fullPath, _filters); } catch (DirectoryNotFoundException) { } catch (IOException) { } return NotFoundDirectoryContents.Singleton; } /// <summary> /// <para>Creates a <see cref="IChangeToken" /> for the specified <paramref name="filter" />.</para> /// <para>Globbing patterns are interpreted by <seealso cref="Microsoft.Extensions.FileSystemGlobbing.Matcher" />.</para> /// </summary> /// <param name="filter"> /// Filter string used to determine what files or folders to monitor. Example: **/*.cs, *.*, /// subFolder/**/*.cshtml. /// </param> /// <returns> /// An <see cref="IChangeToken" /> that is notified when a file matching <paramref name="filter" /> is added, /// modified or deleted. Returns a <see cref="NullChangeToken" /> if <paramref name="filter" /> has invalid filter /// characters or if <paramref name="filter" /> is an absolute path or outside the root directory specified in the /// constructor <seealso cref="PhysicalFileProvider(string)" />. /// </returns> public IChangeToken Watch(string filter) { if (filter == null || PathUtils.HasInvalidFilterChars(filter)) { return NullChangeToken.Singleton; } // Relative paths starting with leading slashes are okay filter = filter.TrimStart(_pathSeparators); return FileWatcher.CreateFileChangeToken(filter); } }
可以看到PhysicalFileProvider的构造行数,需要传递两个参数,root目录和ExclusionFilters类型的filters(过滤一些文件)
PhysicalFileProvider类的GetFileInfo会根据传递的文件名或子目录获取对应的FileInfo对象,并返回PhysicalFileInfo对象
FileProvider的简单使用:
在项目下创建如下目录结构
class Program { static void Main(string[] args) { var fileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "files")); var fileInfo = fileProvider.GetFileInfo("data.txt"); using (var streamReader = new StreamReader(fileInfo.CreateReadStream())) { var result = streamReader.ReadToEnd(); Console.WriteLine(result); } var contents = fileProvider.GetDirectoryContents("sub"); foreach (var item in contents) { Console.WriteLine(item.Name); } Console.Read(); } }
二、 ManifestEmbeddedFileProvider
/// <summary> /// Represents a file embedded in an assembly. /// </summary> public class EmbeddedResourceFileInfo : IFileInfo { private readonly Assembly _assembly; private readonly string _resourcePath; private long? _length; /// <summary> /// Initializes a new instance of <see cref="EmbeddedFileProvider"/> for an assembly using <paramref name="resourcePath"/> as the base /// </summary> /// <param name="assembly">The assembly that contains the embedded resource</param> /// <param name="resourcePath">The path to the embedded resource</param> /// <param name="name">An arbitrary name for this instance</param> /// <param name="lastModified">The <see cref="DateTimeOffset" /> to use for <see cref="LastModified" /></param> public EmbeddedResourceFileInfo( Assembly assembly, string resourcePath, string name, DateTimeOffset lastModified) { _assembly = assembly; _resourcePath = resourcePath; Name = name; LastModified = lastModified; } /// <summary> /// Always true. /// </summary> public bool Exists => true; /// <summary> /// The length, in bytes, of the embedded resource /// </summary> public long Length { get { if (!_length.HasValue) { using (var stream = _assembly.GetManifestResourceStream(_resourcePath)) { _length = stream.Length; } } return _length.Value; } } /// <summary> /// Always null. /// </summary> public string PhysicalPath => null; /// <summary> /// The name of embedded file /// </summary> public string Name { get; } /// <summary> /// The time, in UTC, when the <see cref="EmbeddedFileProvider"/> was created /// </summary> public DateTimeOffset LastModified { get; } /// <summary> /// Always false. /// </summary> public bool IsDirectory => false; /// <inheritdoc /> public Stream CreateReadStream() { var stream = _assembly.GetManifestResourceStream(_resourcePath); if (!_length.HasValue) { _length = stream.Length; } return stream; } }
/// <summary> /// An embedded file provider that uses a manifest compiled in the assembly to /// reconstruct the original paths of the embedded files when they were embedded /// into the assembly. /// </summary> public class ManifestEmbeddedFileProvider : IFileProvider { private readonly DateTimeOffset _lastModified; /// <summary> /// Initializes a new instance of <see cref="ManifestEmbeddedFileProvider"/>. /// </summary> /// <param name="assembly">The assembly containing the embedded files.</param> public ManifestEmbeddedFileProvider(Assembly assembly) : this(assembly, ManifestParser.Parse(assembly), ResolveLastModified(assembly)) { } /// <summary> /// Initializes a new instance of <see cref="ManifestEmbeddedFileProvider"/>. /// </summary> /// <param name="assembly">The assembly containing the embedded files.</param> /// <param name="root">The relative path from the root of the manifest to use as root for the provider.</param> public ManifestEmbeddedFileProvider(Assembly assembly, string root) : this(assembly, root, ResolveLastModified(assembly)) { } /// <summary> /// Initializes a new instance of <see cref="ManifestEmbeddedFileProvider"/>. /// </summary> /// <param name="assembly">The assembly containing the embedded files.</param> /// <param name="root">The relative path from the root of the manifest to use as root for the provider.</param> /// <param name="lastModified">The LastModified date to use on the <see cref="IFileInfo"/> instances /// returned by this <see cref="IFileProvider"/>.</param> public ManifestEmbeddedFileProvider(Assembly assembly, string root, DateTimeOffset lastModified) : this(assembly, ManifestParser.Parse(assembly).Scope(root), lastModified) { } /// <summary> /// Initializes a new instance of <see cref="ManifestEmbeddedFileProvider"/>. /// </summary> /// <param name="assembly">The assembly containing the embedded files.</param> /// <param name="root">The relative path from the root of the manifest to use as root for the provider.</param> /// <param name="manifestName">The name of the embedded resource containing the manifest.</param> /// <param name="lastModified">The LastModified date to use on the <see cref="IFileInfo"/> instances /// returned by this <see cref="IFileProvider"/>.</param> public ManifestEmbeddedFileProvider(Assembly assembly, string root, string manifestName, DateTimeOffset lastModified) : this(assembly, ManifestParser.Parse(assembly, manifestName).Scope(root), lastModified) { } internal ManifestEmbeddedFileProvider(Assembly assembly, EmbeddedFilesManifest manifest, DateTimeOffset lastModified) { if (assembly == null) { throw new ArgumentNullException(nameof(assembly)); } if (manifest == null) { throw new ArgumentNullException(nameof(manifest)); } Assembly = assembly; Manifest = manifest; _lastModified = lastModified; } /// <summary> /// Gets the <see cref="Assembly"/> for this provider. /// </summary> public Assembly Assembly { get; } internal EmbeddedFilesManifest Manifest { get; } /// <inheritdoc /> public IDirectoryContents GetDirectoryContents(string subpath) { var entry = Manifest.ResolveEntry(subpath); if (entry == null || entry == ManifestEntry.UnknownPath) { return NotFoundDirectoryContents.Singleton; } if (!(entry is ManifestDirectory directory)) { return NotFoundDirectoryContents.Singleton; } return new ManifestDirectoryContents(Assembly, directory, _lastModified); } /// <inheritdoc /> public IFileInfo GetFileInfo(string subpath) { var entry = Manifest.ResolveEntry(subpath); switch (entry) { case null: return new NotFoundFileInfo(subpath); case ManifestFile f: return new ManifestFileInfo(Assembly, f, _lastModified); case ManifestDirectory d when d != ManifestEntry.UnknownPath: return new NotFoundFileInfo(d.Name); } return new NotFoundFileInfo(subpath); } /// <inheritdoc /> public IChangeToken Watch(string filter) { if (filter == null) { throw new ArgumentNullException(nameof(filter)); } return NullChangeToken.Singleton; } private static DateTimeOffset ResolveLastModified(Assembly assembly) { var result = DateTimeOffset.UtcNow; if (!string.IsNullOrEmpty(assembly.Location)) { try { result = File.GetLastWriteTimeUtc(assembly.Location); } catch (PathTooLongException) { } catch (UnauthorizedAccessException) { } } return result; } }
可以读取嵌入资源的文件
在项目中添加user.txt,修改生成操作为嵌入资源
class Program { static void Main(string[] args) { var fileProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly()); var fileInfo = fileProvider.GetFileInfo("user.txt"); using (var streamReader = new StreamReader(fileInfo.CreateReadStream())) { Console.WriteLine(streamReader.ReadToEnd()); } Console.Read(); } }
三、CompositeFileProvider
/// <summary> /// Looks up files using a collection of <see cref="IFileProvider"/>. /// </summary> public class CompositeFileProvider : IFileProvider { private readonly IFileProvider[] _fileProviders; /// <summary> /// Initializes a new instance of the <see cref="CompositeFileProvider" /> class using a collection of file provider. /// </summary> /// <param name="fileProviders">The collection of <see cref="IFileProvider" /></param> public CompositeFileProvider(params IFileProvider[] fileProviders) { _fileProviders = fileProviders ?? new IFileProvider[0]; } /// <summary> /// Initializes a new instance of the <see cref="CompositeFileProvider" /> class using a collection of file provider. /// </summary> /// <param name="fileProviders">The collection of <see cref="IFileProvider" /></param> public CompositeFileProvider(IEnumerable<IFileProvider> fileProviders) { if (fileProviders == null) { throw new ArgumentNullException(nameof(fileProviders)); } _fileProviders = fileProviders.ToArray(); } /// <summary> /// Locates a file at the given path. /// </summary> /// <param name="subpath">The path that identifies the file. </param> /// <returns>The file information. Caller must check Exists property. This will be the first existing <see cref="IFileInfo"/> returned by the provided <see cref="IFileProvider"/> or a not found <see cref="IFileInfo"/> if no existing files is found.</returns> public IFileInfo GetFileInfo(string subpath) { foreach (var fileProvider in _fileProviders) { var fileInfo = fileProvider.GetFileInfo(subpath); if (fileInfo != null && fileInfo.Exists) { return fileInfo; } } return new NotFoundFileInfo(subpath); } /// <summary> /// Enumerate a directory at the given path, if any. /// </summary> /// <param name="subpath">The path that identifies the directory</param> /// <returns>Contents of the directory. Caller must check Exists property. /// The content is a merge of the contents of the provided <see cref="IFileProvider"/>. /// When there is multiple <see cref="IFileInfo"/> with the same Name property, only the first one is included on the results.</returns> public IDirectoryContents GetDirectoryContents(string subpath) { var directoryContents = new CompositeDirectoryContents(_fileProviders, subpath); return directoryContents; } /// <summary> /// Creates a <see cref="IChangeToken"/> for the specified <paramref name="pattern"/>. /// </summary> /// <param name="pattern">Filter string used to determine what files or folders to monitor. Example: **/*.cs, *.*, subFolder/**/*.cshtml.</param> /// <returns>An <see cref="IChangeToken"/> that is notified when a file matching <paramref name="pattern"/> is added, modified or deleted. /// The change token will be notified when one of the change token returned by the provided <see cref="IFileProvider"/> will be notified.</returns> public IChangeToken Watch(string pattern) { // Watch all file providers var changeTokens = new List<IChangeToken>(); foreach (var fileProvider in _fileProviders) { var changeToken = fileProvider.Watch(pattern); if (changeToken != null) { changeTokens.Add(changeToken); } } // There is no change token with active change callbacks if (changeTokens.Count == 0) { return NullChangeToken.Singleton; } return new CompositeChangeToken(changeTokens); } /// <summary> /// Gets the list of configured <see cref="IFileProvider" /> instances. /// </summary> public IEnumerable<IFileProvider> FileProviders => _fileProviders; }
CompositeFileProvider 可以看成是一种组合模式,接收多个IFileProvider
获取IFileInfo的时候遍历IFileProvider获取
class Program { static void Main(string[] args) { var services = new ServiceCollection(); var provider1 = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "files")); var provider2 = new EmbeddedFileProvider(Assembly.GetEntryAssembly()); provider1.Watch("data.txt").RegisterChangeCallback(state => { Console.WriteLine("Change"); }, ""); services.AddTransient<IFileProvider>(p => provider1); services.AddTransient<IFileProvider>(p => provider2); services.AddTransient<CompositeFileProvider>(); var provider = services.BuildServiceProvider(); var fileProvider = provider.GetService<CompositeFileProvider>(); var fileInfo = fileProvider.GetFileInfo("data.txt"); using (var streamReader = new StreamReader(fileInfo.CreateReadStream())) { var result = streamReader.ReadToEnd(); Console.WriteLine(result); } var contents = fileProvider.GetDirectoryContents("sub"); foreach (var item in contents) { Console.WriteLine(item.Name); } Console.Read(); } }
IFileProvider有Watch方法,可以监听某个文件,当文件被修改时,会触发相应的操作
总结:了解了netcore文件系统的原理,可以自定义一个自己的文件提供器。比如获取阿里云Oss的文件