開發過檔案儲存那塊業務的小夥伴或多或少都應該瞭解過諸如:FastDFS、Minio、MongDb GridFS,通過這些第三方元件可以應用於我們的檔案儲存系統。
之前有用過Minio,效能很高而且部署起來非常簡單,有興趣的同學可以嘗試一下。?
同樣,在.Net Core中我們一樣可以處理靜態檔案的讀取與寫入,我們一般將靜態檔案都放置於wwwroot資料夾下,但是我們可以自行擴充套件配置,將對外的URL對映到自定義的目錄達到外部對其檔案訪問的目的。
於是我自己用.Net Core整合了一個檔案服務,對外可提供檔案的讀寫操作。特出此文,希望能幫助到大家,僅供參考!?
我們以微軟的官方文件為主,官方文件是最準確,最可靠的。✌️ https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/static-files?view=aspnetcore-5.0
專案的整體結構如下(因為沒有涉及過多業務層面的內容,所以分的層也比較簡單,但是也易於後期擴充套件):
如果要使用靜態檔案的話,我們需要在Configure方法中註冊UseStaticFiles中介軟體,這裡話我寫了一個擴充套件方法,如下:
/// <summary> /// 使用靜態檔案 /// </summary> public static class StaticFilesMildd { public static void UseStaticFilesMildd(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); var staticfile = new StaticFileOptions(); staticfile.ServeUnknownFileTypes = true; var provider = new FileExtensionContentTypeProvider(); #region 手動設定對應的 MIME TYPE =>下載 provider.Mappings[".log"] = "application/x-msdownload"; provider.Mappings[".xls"] = "application/x-msdownload"; provider.Mappings[".doc"] = "application/x-msdownload"; provider.Mappings[".pdf"] = "application/x-msdownload"; provider.Mappings[".docx"] = "application/x-msdownload"; provider.Mappings[".xlsx"] = "application/x-msdownload"; provider.Mappings[".ppt"] = "application/x-msdownload"; provider.Mappings[".pptx"] = "application/x-msdownload"; provider.Mappings[".zip"] = "application/x-msdownload"; provider.Mappings[".rar"] = "application/x-msdownload"; provider.Mappings[".dwg"] = "application/x-msdownload"; #endregion staticfile.ContentTypeProvider = provider; //app.UseStaticFiles(staticfile); // 使用預設資料夾 wwwroot 僅僅使wwwroot對外可見 app.UseStaticFiles(new StaticFileOptions() {
//這裡的路徑寫部署的主機的某個資料夾的路徑 FileProvider = new PhysicalFileProvider(@"/data/Files"), }); } }
這裡面說一下FileExtensionContentTypeProvider類,這個類裡面包含了381種檔案的型別。我們可以通過這個類對其檔案型別進行刪除或替換,比如上面我就把這些檔案的MIME 內容型別
給替換掉了,當客戶端訪問的時候就都為下載。假設我們上傳的是一張jpg圖片,當我們用其URL進行訪問時,則可以在瀏覽器上直接顯示,因為其Content-Type是image/jpeg。如果設定
檔案的MIME內容型別是application/x-msdownload這種,那麼就會下載。歸根結底是Response-Header裡面的Content-Type指示瀏覽器這是什麼型別,而不是通過網址字尾去判斷的
我們還需要對外提供的上傳檔案的介面,主要程式碼如下,Controller層的程式碼我就不貼出來了,都很簡單?
抽象介面層主要程式碼:
public interface IUploadBusiness { Task<string> uploadFile(FileDataBase64 file); Task<List<string>> uploadFile(FormData data); Task<string> uploadFile(FileDataStream file); Task<List<string>> uploadFile(FormDataStream data); Task<string> uploadFile(FileDataByte file); Task<List<string>> uploadFile(FormDataByte data); }
業務實現層主要程式碼:
public class UploadBusiness : IUploadBusiness { private readonly FileHelper _helper; private readonly UtilConvert _utilConvert; public UploadBusiness(FileHelper helper, UtilConvert utilConvert) { _helper = helper; _utilConvert = utilConvert; } /// <summary> /// 上傳單個檔案 /// </summary> /// Base64 /// <param name="files"></param> /// <returns></returns> public async Task<string> uploadFile(FileDataBase64 file) { return await _helper.uploadFileHelper(file, fileType.base64); } /// <summary> /// 上傳多個檔案 /// </summary> /// Base64 /// <param name="files"></param> /// <returns></returns> public async Task<List<string>> uploadFile(FormData data) { List<string> picStrArray = new(); foreach (var item in data.files) { picStrArray.Add(await _helper.uploadFileHelper(new { suffix = item.suffix, FileByBase64 = item.FileByBase64 }, fileType.base64)); } return picStrArray; } /// <summary> /// 上傳單個檔案 /// </summary> /// Stream /// <param name="files"></param> /// <returns></returns> public async Task<string> uploadFile(FileDataStream file) { return await _helper.uploadFileHelper(new { suffix = file.suffix, fileStream = file.FileByStream.OpenReadStream() }, fileType.stream); } /// <summary> /// 上傳多個檔案 /// </summary> /// Stream /// <param name="files"></param> /// <returns></returns> public async Task<List<string>> uploadFile(FormDataStream data) { List<string> picStrArray = new(); foreach (var item in data.files) { picStrArray.Add(await _helper.uploadFileHelper(new { suffix = item.suffix, fileStream = item.FileByStream.OpenReadStream() }, fileType.stream)); } return picStrArray; } /// <summary> /// 上傳單個檔案 /// </summary> /// Byte /// <param name="files"></param> /// <returns></returns> public async Task<string> uploadFile(FileDataByte file) { FileDataStream fileData = new(); var fileByte = await _utilConvert.StringByByte(file.FileByByte); return await _helper.uploadFileHelper(new { suffix = file.suffix, fileStream = await _utilConvert.BytesToStream(fileByte) }, fileType.stream); } /// <summary> /// 上傳多個檔案 /// </summary> /// Byte /// <param name="files"></param> /// <returns></returns> public async Task<List<string>> uploadFile(FormDataByte data) { List<string> picStrArray = new(); foreach (var item in data.files) { var fileByte = await _utilConvert.StringByByte(item.FileByByte); picStrArray.Add(await _helper.uploadFileHelper(new { suffix = item.suffix, fileStream = await _utilConvert.BytesToStream(fileByte) }, fileType.stream)); } return picStrArray; } }
統一的上傳公共方法層主要程式碼:
public class FileHelper { private readonly IHostingEnvironment _hostingEnvironment; private readonly IConfiguration _configuration; public FileHelper(IHostingEnvironment hostingEnvironment, IConfiguration configuration) { _hostingEnvironment = hostingEnvironment; _configuration = configuration; } public async Task<string> uploadFileHelper(dynamic data, fileType fileType) { string path = string.Empty; var picUrl = string.Empty; string Mainpath = string.Empty; if (CommonClass.suffixList.Contains(data.suffix.ToLower())) Mainpath = "/Pictures/" + DateTime.Now.ToString("yyyy-MM-dd") + "/"; else Mainpath = "/OtherFiles/" + DateTime.Now.ToString("yyyy-MM-dd") + "/"; string FileName = DateTime.Now.ToString("yyyyMMddHHmmssfff"); string FilePath = $@"/data/Files" + Mainpath; string type = data.suffix; DirectoryInfo di = new DirectoryInfo(FilePath); path = Mainpath + FileName + type; picUrl = $"{_configuration["machine-ip"]}{path}"; if (!di.Exists) { di.Create(); } if (fileType == fileType.base64) { byte[] arr = Convert.FromBase64String(data.FileByBase64); using (Stream stream = new MemoryStream(arr)) { using (FileStream fs = System.IO.File.Create(FilePath + FileName + type)) { // 複製檔案 stream.CopyTo(fs); // 清空緩衝區資料 fs.Flush(); } } } else { using (FileStream fs = System.IO.File.Create(FilePath + FileName + type)) { // 複製檔案 data.fileStream.CopyTo(fs); // 清空緩衝區資料 fs.Flush(); } } return picUrl; } }
這裡說一下,主機ip我寫在了配置檔案中,每上傳一個檔案都對其進行路徑拼接。可以上傳Base64,Stream或Byte都行。?
只有讀取、寫入只是完成了一部分,我們要管控起我們的服務,鑑權、日誌、限流、健康檢查、備份、叢集這些都要需要加入進來。
授權與鑑權
鑑權是指對我們檔案上傳的介面進行鑑權,校驗其令牌的合法性。授權的話就很簡單,只需在控制器或方法上加一個特性[Authorize]即可
這裡的話我使用JWT,它可以做到跨伺服器驗證,只要金鑰和演算法相同,不同伺服器程式生成的Token可以互相驗證。如果是微服務架構的話需要介入基於 OpenID Connect 和 OAuth 2.0 認證框架IdentityServer4.
主要程式碼如下:
//新增jwt驗證: .AddJwtBearer(options => { options.SaveToken = true; options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, ClockSkew = TimeSpan.FromSeconds(15), ValidateAudience = true, ValidAudience = "Static-Service", ValidIssuer = "localhost", ValidateIssuerSigningKey = true,//是否驗證SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:key"]))//拿到SecurityKey }; options.Events = new JwtBearerEvents { //此處為許可權驗證失敗後觸發的事件 OnChallenge = context => { context.HandleResponse(); //自定義自己想要返回的資料結果 var payload = JsonConvert.SerializeObject(new { code = 401, success = false, msg = "很抱歉,您無權訪問該介面。" }); //自定義返回的資料型別 context.Response.ContentType = "application/json"; context.Response.StatusCode = StatusCodes.Status200OK; //輸出Json資料結果 context.Response.WriteAsync(payload); return Task.FromResult(0); } }; });
SecurityKey不能太短了,要在16位以上。這裡當鑑權不成功時我就自定義返回響應的資料,以供前端好拿到資料,不然就會返回401Unauthorized,這樣不太友好。
如果專案有閘道器的加入,那麼閘道器就作為了鑑權的入口,如下:
日誌
日誌的記錄我這裡使用的是Serilog,同時利用Filter將每次請求與異常都寫入了DataBase,藉助Elasticsearch和Kibana可以快速檢視我們的日誌資訊。
具體關於Serilog的引入我不做太多介紹,請看此篇文章:https://www.cnblogs.com/zhangnever/p/12459399.html
DataBase中的日誌表,如下:
CREATE TABLE `system_log` ( `id` VARCHAR(36) NOT NULL COMMENT '主鍵', `ip_address` VARCHAR(100) NOT NULL COMMENT '請求ip', `oper_type` VARCHAR(50) NOT NULL COMMENT '操作型別:檢視、新增、修改、刪除、匯入、匯出', `oper_describe` VARCHAR(500) NOT NULL COMMENT '操作詳情', `level` VARCHAR(50) NOT NULL COMMENT '事件', `exception` VARCHAR(500) NULL DEFAULT NULL COMMENT 'ErrorMessage', `create_op_id` VARCHAR(36) NOT NULL COMMENT '建立人id', `create_op_name` VARCHAR(50) NOT NULL COMMENT '建立人', `create_op_date` DATETIME NOT NULL COMMENT '建立時間', `edit_op_id` VARCHAR(36) NULL DEFAULT NULL COMMENT '修改人id', `edit_op_name` VARCHAR(50) NULL DEFAULT NULL COMMENT '修改人', `edit_op_date` DATETIME NULL DEFAULT NULL COMMENT '修改時間', PRIMARY KEY (`id`) ) COMMENT='系統日誌' COLLATE='utf8_general_ci' ENGINE=InnoDB ;
如果需要將日誌寫入ElasticSearch需要安裝包Serilog.Sinks.Elasticsearch
主要程式碼:
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200/")) { AutoRegisterTemplate = true, ModifyConnectionSettings = connectionSettings => {
//訪問的使用者名稱與密碼 connectionSettings.BasicAuthentication("username", "password"); return connectionSettings; } })
在Kibana中進行檢視:
另外對於異常資訊,我這裡還加入了郵件傳送的功能:
//... ...
var apiUrl = _accessor.HttpContext.Request.Path.ToString(); _context.db.StringIncrement(apiUrl); var result = _context.db.StringGet(apiUrl); //Content mailMessage.Body = $"【Static-Service】當前請求{apiUrl}有異常,異常資訊為{exception}。該API已累計異常了{result}次!";
//... ...
限流
對於請求的限流我們可以使用AspNetCoreRateLimit,它是根據IP進行限流,在管道中進行攔截,
原始碼在此:https://github.com/stefanprodan/AspNetCoreRateLimit
這裡的話需要引入兩個包:AspNetCoreRateLimit與Microsoft.Extensions.Caching.Redis.
對於不同的Api可配置不同的限流規則,如下:
//... ...
//api規則 "GeneralRules": [ { "Endpoint": "*:/api/*", "Period": "1m", "Limit": 500 }, { "Endpoint": "*/api/*", "Period": "1s", "Limit": 3 }, { "Endpoint": "*/api/*", "Period": "1m", "Limit": 30 }, { "Endpoint": "*/api/*", "Period": "12h", "Limit": 500 } ]
其它程式碼我就不羅列出來了,感興趣的同學可以把我的程式碼down下來看看。如果是微服務專案的話,限流就在閘道器(Ocelot)處理了,還有就是對外暴露一個介面用作健康檢查。
檔案備份
對於檔案的備份我採用rsync+inotify的方式,inotity用來監控檔案或目錄的變化,rsync用來遠端資料的同步。
我這裡有兩臺伺服器,分別是客戶端訪問的伺服器(192.168.2.121)和要同步的遠端伺服器(192.168.2.122)
一、首先在要同步的遠端伺服器上做如下操作:
1.安裝工具(rsync):yum -y install xinetd rsync
安裝完成之後可檢視版本看其是否安裝成功:rsync --version
2.設定與客戶端伺服器同步的目錄,配置檔案在etc/rsyncd.conf中.
[backup] path =/data/Files #同步的目錄 comment = Rsync share test auth users = root #使用者名稱 read only = no #只讀設為no hosts allow = 192.168.2.121 #客戶端伺服器的ip hosts deny = *
3.設定訪問的使用者名稱與密碼,在/etc/rsync_pass中配置,如下:
root:******
4.重啟服務 service xinetd restart
5.檢測873埠是否已啟動 netstat -nultp
二、客戶端訪問的伺服器上做如下配置:
1.安裝inotify-toolwget
連結:http://downloads.sourceforge.net/project/inotify-tools/inotify-tools/3.13/inotify-tools-3.13.tar.gz
解壓:tar -zxvf inotify-tools-3.13.tar.gz
進入inotify-tools-3.13目錄,依次執行 ./configure,make&make install
2.安裝工具(rsync):yum -y install xinetd rsync
安裝完成之後可檢視版本看其是否安裝成功:rsync --version
3.設定要同步過去的遠端伺服器的密碼
在/etc/rsync_pass文字中寫入即可
4.設定指令碼,如下: #!/bin/bash inotify_rsync_fun () { dir=`echo $1 | awk -F"," '{print $1}'` ip=`echo $1 | awk -F"," '{print $2}'` des=`echo $1 | awk -F"," '{print $3}'` user=`echo $1 | awk -F"," '{print $4}'` inotifywait -mr --timefmt '%d/%m/%y %H:%M' --format '%T %w %f' -e modify,delete,create,attrib ${dir} | while read DATE TIME DIR FILE do FILECHAGE=${DIR}${FILE} /usr/bin/rsync -av --progress --delete --password-file=/etc/rsync_pass ${dir} ${user}@${ip}::${des} && echo "At ${TIME} on ${DATE}, <br> file $FILECHAGE was backed up via rsync" >> /var/log/rsyncd.log done } count=1 # localdir,host,rsync_module,user of rsync_module, sync1="/data/Files/,192.168.2.122,backup,root" ############################################################# #main i=0 while [ ${i} -lt ${count} ] do i=`expr ${i} + 1` tmp="sync"$i eval "sync=\$$tmp" inotify_rsync_fun "$sync" & done
部署
部署的話我先執行dotnet *.dll,然後用nginx做了一個代理轉發.
1.先檢視是否安裝了dotnet環境:dotnet --info
2.將釋出好的專案Copy到伺服器上,切入到專案根目錄執行:nohup dotnet Static-Application.dll --urls="http://*:85" --ip="127.0.0.1" --port=85 >output 2>&1 &
這裡需要說一下在命令中加入nohup可以在你退出帳戶/關閉終端之後繼續執行相應的程式.
3.配置nginx,在/etc/nginx/conf.d檔案中新增static-nginx.conf檔案。配置的語句如下: server { listen 86; server_name localhost; proxy_set_header X-Real-IP $remote_addr; location /index.html { proxy_pass http://localhost:85/swagger/index.html; } location /api/{ proxy_pass http://localhost:85; } location /{ proxy_pass http://localhost:85/swagger/; } location /swagger/{ proxy_pass http://localhost:85; } }
4.檢查語句是否正確:nginx -t 重啟nginx:nginx -s reload
開啟谷歌瀏覽器檢視一下
GoAccess
這裡使用了nginx,可以用實時網路日誌分析器GoAccess來統計和展示日誌資訊。
1.安裝GoAccess,可看官網:https://goaccess.io/download
2.配置nginx,可在瀏覽器端訪問. server { listen 9000; server_name localhost; location /report.html { alias /home/zopen/nginx/html/report.html; } }
3.執行GoAccess關聯到nginx的日誌 goaccess /var/log/nginx/access.log -o /home/zopen/nginx/html/report.html --real-time-html --time-format='%H:%M:%S' --date-format='%d/%b/%Y' --log-format=COMBINED
在谷歌瀏覽器中開啟檢視
文中可能有疏漏,如有不當,望諒解!?
但凡可以幫到各位一點點,那就沒白忙活.
程式碼我已上傳至我的GitHub:https://github.com/Davenever/static-service.git
朝看花開滿樹紅,暮看花落樹還空?