跟我一起學.NetCore之檔案系統應用及核心淺析

Code綜藝圈發表於2020-09-14

前言

在開發過程中,肯定避免不了讀取檔案操作,比如讀取配置檔案、上傳和下載檔案、Web中html、js、css、圖片等靜態資源的訪問;在配置檔案讀取章節中有說到,針對不同配置源資料讀取由對應的IConfigurationProvider進行讀取,其實讀取檔案也是一樣,針對於不同型別(物理檔案、嵌入檔案、雲端檔案等)檔案,就由對應的IFileProvider的實現進行讀取,下面詳細說說;

正文

由於通過IFileProvider將目錄檔案進行抽象化,統一規範讀取操作,使得讀取不同地方的檔案就顯得更加方便,如物理檔案、嵌入檔案,只要有對應的實現即可;而框架針對物理檔案和嵌入檔案已經進行了具體實現,如下:

  • PhysicalFileProvider:物理檔案提供程式,用來讀取物理檔案,就是平時使用的檔案,不管是副檔名是什麼;
  • EmbeddedFileProvider:嵌入檔案提供程式,用來讀取嵌入檔案,就是程式編譯時嵌入到程式集內部的檔案,就像資原始檔一樣;
  • CompositeFileProvider:組合提供程式,同時可以讀取物理檔案和嵌入檔案,就是可以指定多種資料來源,這樣的好處就是像操作同一個資料來源一樣;後續也可以與自定義的提供程式進行組合;

為了避免直接扒程式碼懵圈,先來個控制檯例子,體驗一下以上xxxProvider的使用:

img

執行結果:

img

讀取物理檔案是不是很簡單,其實就是建立了一個PhysicalFileProvider物件時指定了一個路徑,然後就能很方便的獲取到對應目錄下的資訊;

嵌入檔案也是如此,只需指定對應程式集即可(因為嵌入檔案已經編譯到程式集中),如下優化程式碼:

img

執行結果如下:

img

同樣也是使用很簡單,只是在建立EmbeddedFileProvider物件時指定一下對應的程式集即可,後續便可以用統一的方式進行檔案和目錄操作;

組合提供程式的目的就是將不同提供程式整合,就像使用同一個源一樣,如下:

img

當然,按老套路走,不能用用就行了,繼續扒扒程式碼,先看看IFileProvider:

namespace Microsoft.Extensions.FileProviders
{
    // IFileProvider定義的三個方法其實就是其對應的三大功能
    public interface IFileProvider
    {
        // 獲取指定檔案的資訊,之後可以檔案進行讀取操作
        IFileInfo GetFileInfo(string subpath);
        // 獲取指定目錄下所有內容
        IDirectoryContents GetDirectoryContents(string subpath);
        // 用於監聽檔案改變
        IChangeToken Watch(string filter);
    }
}

再來看看返回的IFileInfo和IDirectoryContents :

namespace Microsoft.Extensions.FileProviders
{
    public interface IFileInfo
    {
        // 標識是否存在
        bool Exists
        {
            get;
        }
        // 檔案大小,如果不存在或是目錄,這個值就是-1
        long Length
        {
            get;
        }
        // 對應的物理路徑,其實就是檔案的實際路徑
        string PhysicalPath
        {
            get;
        }
        // 檔名字
        string Name
        {
            get;
        }
        // 檔案最後的修改時間
        DateTimeOffset LastModified
        {
            get;
        }
        // 標識是否是目錄
        bool IsDirectory
        {
            get;
        }
        // 返回的留可以進行檔案讀取
        Stream CreateReadStream();
    }
    
    // 其他資訊繼承了IFileInfo資訊
    public interface IDirectoryContents : IEnumerable<IFileInfo>, IEnumerable
    {
        // 標識指定目錄是否存在
        bool Exists
        {
            get;
        }
    }
}

IChangeToken 之前在配置檔案監聽的時候有提到過,是用來監聽到檔案改變時進行傳送通知的,這裡就不深入了,感興趣的小夥伴可以研究研究;

PhysicalFileProvider和EmbeddedFileProvider兩個挑PhysicalFileProvider這個看看,後者小夥伴私下去扒吧:

namespace Microsoft.Extensions.FileProviders
{
    // 這裡只挑了幾個關鍵方法說明,其他屬性和方法刪除
    public class PhysicalFileProvider : IFileProvider, IDisposable
    {
        // 判斷路徑是否在指定的根路徑下
        private bool IsUnderneathRoot(string fullPath)
        {
            return fullPath.StartsWith(Root, StringComparison.OrdinalIgnoreCase);
        }
        // 獲取指定路徑檔案的FileInfo資訊
        public IFileInfo GetFileInfo(string subpath)
        {
            // 判斷路徑是否處匹配
            if (string.IsNullOrEmpty(subpath) || PathUtils.HasInvalidPathChars(subpath))
            {
                return new NotFoundFileInfo(subpath);
            }
            // 判斷指定的路徑是否是在根目錄下
            subpath = subpath.TrimStart(_pathSeparators);
            if (Path.IsPathRooted(subpath))
            {
                return new NotFoundFileInfo(subpath);
            }
            // 獲取全路徑,因為一般在外面操作是根據相對路徑進行操作
            string fullPath = GetFullPath(subpath);
            if (fullPath == null)
            {
                return new NotFoundFileInfo(subpath);
            }
            // 構建了一個檔案資訊,包含檔案的的操作和屬性;
            FileInfo fileInfo = new FileInfo(fullPath);
            if (FileSystemInfoHelper.IsExcluded(fileInfo, _filters))
            {
                return new NotFoundFileInfo(subpath);
            }
            // 封裝成PhysicalFileInfo物件
            return new PhysicalFileInfo(fileInfo);
        }
        // 獲取指定目錄下的所有內容
        public IDirectoryContents GetDirectoryContents(string subpath)
        {
            try
            {  
                // 路徑校驗和上面一樣
                if (subpath == null || PathUtils.HasInvalidPathChars(subpath))
                {
                    return NotFoundDirectoryContents.Singleton;
                }

                subpath = subpath.TrimStart(_pathSeparators);
                if (Path.IsPathRooted(subpath))
                {
                    return NotFoundDirectoryContents.Singleton;
                }

                string fullPath = GetFullPath(subpath);
                if (fullPath == null || !Directory.Exists(fullPath))
                {
                    return NotFoundDirectoryContents.Singleton;
                }
                // 封裝為PhysicalDirectoryContents物件
                return new PhysicalDirectoryContents(fullPath, _filters);
            }
            catch (DirectoryNotFoundException)
            {
            }
            catch (IOException)
            {
            }

            return NotFoundDirectoryContents.Singleton;
        }
        // 用監聽檔案改變的,通過檔案匹配模式來指定需要監控的檔案
        public IChangeToken Watch(string filter)
        {
            if (filter == null || PathUtils.HasInvalidFilterChars(filter))
            {
                return NullChangeToken.Singleton;
            }

            filter = filter.TrimStart(_pathSeparators);
            return FileWatcher.CreateFileChangeToken(filter);
        }
    }
}

以上GetDirectoryContents和GetFileInfo分別返回的PhysicalDirectoryContents和PhysicalFileInfo才是關鍵,進去瞅瞅:

public class PhysicalDirectoryContents : IDirectoryContents, IEnumerable<IFileInfo>, IEnumerable
{
    // 用於存放指定目錄下的全部內容的
    private IEnumerable<IFileInfo> _entries;
    // 判斷指定目錄是否存在
    public bool Exists => Directory.Exists(_directory);
    // 讀取目錄內容的關鍵方法
    private void EnsureInitialized()
    {
        try
        {
            // 根據指定的目錄,獲取目錄下的所有內容,將其儲存在集合中
            _entries = new DirectoryInfo(_directory).EnumerateFileSystemInfos().Where((Func<FileSystemInfo, bool>)((FileSystemInfo info) => !FileSystemInfoHelper.IsExcluded(info, _filters))).Select((Func<FileSystemInfo, IFileInfo>)delegate (FileSystemInfo info)
            {
                // 將取到的內容封裝為PhysicalFileInfo物件
                FileInfo fileInfo = info as FileInfo;
                if (fileInfo != null)
                {
                    return new PhysicalFileInfo(fileInfo);
                }
                // 將取到的內容封裝為PhysicalFileInfo物件
                DirectoryInfo directoryInfo = info as DirectoryInfo;
                if (directoryInfo != null)
                {
                    return new PhysicalDirectoryInfo(directoryInfo);
                }

                throw new InvalidOperationException("Unexpected type of FileSystemInfo");
            });
        }
        catch (Exception ex) when (ex is DirectoryNotFoundException || ex is IOException)
        {
            _entries = Enumerable.Empty<IFileInfo>();
        }
    }
}

PhysicalFileInfo

// 其實裡面就是封裝了IO檔案操作的相關屬性和操作
public class PhysicalFileInfo : IFileInfo
{
    // 檔案資訊,就是平時我們們直接讀取到檔案的那些資訊
    private readonly FileInfo _info;
    // 是否存在
    public bool Exists => _info.Exists;
    // 檔案大小
    public long Length => _info.Length;
    // 檔案的全路徑
    public string PhysicalPath => _info.FullName;
    // 檔名稱
    public string Name => _info.Name;
    // 檔案的最後修改時間
    public DateTimeOffset LastModified => _info.LastWriteTimeUtc;
    // 預設就是false,所以這裡只能對檔案有效
    public bool IsDirectory => false;
    public PhysicalFileInfo(FileInfo info)
    {
        _info = info;
    }
    // 獲取檔案流,並設定了只讀許可權
    public Stream CreateReadStream()
    {
        int bufferSize = 1;
        // 這裡就熟悉了,平時直接讀取檔案就是這樣的
        return new FileStream(PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize, FileOptions.SequentialScan | FileOptions.Asynchronous);
    }
}

好了,到這其實差不多就明白了,至少知道為什麼IFileInfo只能獲取到檔案件資訊,目錄資訊獲取不到;至少在寫檔案的時候不再懵逼的在想:為什麼不能寫檔案了,如果直接用返回的流進行檔案寫操作,就會報以下錯:

img

總結

框架只是實現了本地讀取的兩個IFileProvider,如果針對於雲端檔案、FTP檔案等有統一的讀取需求,則就需要自己實現了;所以原始碼是不錯的參考,封裝之後,結合組合提供程式,後續使用就能像使用本地檔案一樣簡便;

加上這篇,總共十五篇,把.NetCore中比較關鍵的核心都過了一遍,其中包含了啟動流程、依賴注入、配置、選項、日誌、中介軟體、檔案,在每個章節中都會針對對應的核心型別進行原始碼分析,雖然只是淺讀,但也能明白其中緣由;後續的文章將會偏應用,比如靜態檔案目錄配置、API的最佳實現、JWT使用、IdentityServer4的整合等等一堆元件的應用;

同時,後續將同步開啟另一個專題:跟我一起學Redis,歡迎一起來學習;

------------------------------------------------

CSDN:Code綜藝圈

知乎:Code綜藝圈

掘金:Code綜藝圈

部落格園:Code綜藝圈

bilibili:Code綜藝圈

------------------------------------------------

一個被程式搞醜的帥小夥,關注"Code綜藝圈",識別關注跟我一起學~~~

擼文不易,莫要白瞟,三連走起~~~~

相關文章