[ApiController]
public class FileController : ControllerBase
{
/// <summary>
/// 請求上傳文件
/// </summary>
/// <param name="requestFile">請求上傳參數實體</param>
/// <returns></returns>
[HttpPost, Route("RequestUpload")]
public MessageEntity RequestUploadFile([FromBody]RequestFileUploadEntity requestFile)
{
}

/// <summary>
/// 文件上傳
/// </summary>
/// <returns></returns>
[HttpPost, Route("Upload")]
public async Task<MessageEntity> FileSave()
{
}

/// <summary>
/// 文件合并
/// </summary>
/// <param name="fileInfo">文件參數信息[name]</param>
/// <returns></returns>
[HttpPost, Route("Merge")]
public async Task<MessageEntity> FileMerge([FromBody]Dictionary<string, object> fileInfo)
{
}
}

如果直接復制的朋友,這里肯定是滿眼紅彤彤,這里主要用了兩個類,一個請求實體RequestFileUploadEntity,一個回調實體MessageEntity,這兩個我們到Util工程創建(當然也可以放到Entity工程,這里為什么放到Util呢,因為我覺得放到這里公用比較好,畢竟還是有復用的價值的)。

創建完成寫好之后我們在紅的地方Alt+Enter,哪里爆紅點哪里(so easy),好了,不扯犢子了,每個接口的方法如下。

RequestUploadFile

public MessageEntity RequestUploadFile([FromBody]RequestFileUploadEntity requestFile)
{
LogUtil.Debug($"RequestUploadFile 接收參數:{JsonConvert.SerializeObject(requestFile)}");
MessageEntity message = new MessageEntity();
if (requestFile.size <= 0 || requestFile.count <= 0 || string.IsNullOrEmpty(requestFile.filedata))
{
message.Code = -1;
message.Msg = "參數有誤";
}
else
{
//這里需要記錄文件相關信息,并返回文件guid名,后續請求帶上此參數
string guidName = Guid.NewGuid().ToString("N");

//前期單臺服務器可以記錄Cache,多臺后需考慮redis或數據庫
CacheUtil.Set(guidName, requestFile, new TimeSpan(0, 10, 0), true);

message.Code = 0;
message.Msg = "";
message.Data = new { filename = guidName };
}
return message;
}

FileSave

public async Task<MessageEntity> FileSave()
{
var files = Request.Form.Files;
long size = files.Sum(f => f.Length);
string fileName = Request.Form["filename"];

int fileIndex = 0;
int.TryParse(Request.Form["fileindex"], out fileIndex);
LogUtil.Debug($"FileSave開始執行獲取數據:{fileIndex}_{size}");
MessageEntity message = new MessageEntity();
if (size <= 0 || string.IsNullOrEmpty(fileName))
{
message.Code = -1;
message.Msg = "文件上傳失敗";
return message;
}

if (!CacheUtil.Exists(fileName))
{
message.Code = -1;
message.Msg = "請重新請求上傳文件";
return message;
}

long fileSize = 0;
string filePath = $".{AprilConfig.FilePath}{DateTime.Now.ToString("yyyy-MM-dd")}/{fileName}";
string saveFileName = $"{fileName}_{fileIndex}";
string dirPath = Path.Combine(filePath, saveFileName);
if (!Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}

foreach (var file in files)
{
//如果有文件
if (file.Length > 0)
{
fileSize = 0;
fileSize = file.Length;

using (var stream = new FileStream(dirPath, FileMode.OpenOrCreate))
{
await file.CopyToAsync(stream);
}
}
}
message.Code = 0;
message.Msg = "";
return message;
}

FileMerge

public async Task<MessageEntity> FileMerge([FromBody]Dictionary<string, object> fileInfo)
{
MessageEntity message = new MessageEntity();
string fileName = string.Empty;
if (fileInfo.ContainsKey("name"))
{
fileName = fileInfo["name"].ToString();
}
if (string.IsNullOrEmpty(fileName))
{
message.Code = -1;
message.Msg = "文件名不能為空";
return message;
}

//最終上傳完成后,請求合并返回合并消息
try
{
RequestFileUploadEntity requestFile = CacheUtil.Get<RequestFileUploadEntity>(fileName);
if (requestFile == null)
{
message.Code = -1;
message.Msg = "合并失敗";
return message;
}
string filePath = $".{AprilConfig.FilePath}{DateTime.Now.ToString("yyyy-MM-dd")}/{fileName}";
string fileExt = requestFile.fileext;
string fileMd5 = requestFile.filedata;
int fileCount = requestFile.count;
long fileSize = requestFile.size;

LogUtil.Debug($"獲取文件路徑:{filePath}");
LogUtil.Debug($"獲取文件類型:{fileExt}");

string savePath = filePath.Replace(fileName, "");
string saveFileName = $"{fileName}{fileExt}";
var files = Directory.GetFiles(filePath);
string fileFinalName = Path.Combine(savePath, saveFileName);
LogUtil.Debug($"獲取文件最終路徑:{fileFinalName}");
FileStream fs = new FileStream(fileFinalName, FileMode.Create);
LogUtil.Debug($"目錄文件下文件總數:{files.Length}");

LogUtil.Debug($"目錄文件排序前:{string.Join(",", files.ToArray())}");
LogUtil.Debug($"目錄文件排序后:{string.Join(",", files.OrderBy(x => x.Length).ThenBy(x => x))}");
byte[] finalBytes = new byte[fileSize];
foreach (var part in files.OrderBy(x => x.Length).ThenBy(x => x))
{
var bytes = System.IO.File.ReadAllBytes(part);
await fs.WriteAsync(bytes, 0, bytes.Length);
bytes = null;
System.IO.File.Delete(part);//刪除分塊
}
fs.Close();
//這個地方會引發文件被占用異常
fs = new FileStream(fileFinalName, FileMode.Open);
string strMd5 = GetCryptoString(fs);
LogUtil.Debug($"文件數據MD5:{strMd5}");
LogUtil.Debug($"文件上傳數據:{JsonConvert.SerializeObject(requestFile)}");
fs.Close();
Directory.Delete(filePath);
//如果MD5與原MD5不匹配,提示重新上傳
if (strMd5 != requestFile.filedata)
{
LogUtil.Debug($"上傳文件md5:{requestFile.filedata},服務器保存文件md5:{strMd5}");
message.Code = -1;
message.Msg = "MD5值不匹配";
return message;
}
CacheUtil.Remove(fileInfo["name"].ToString());
message.Code = 0;
message.Msg = "";
}
catch (Exception ex)
{
LogUtil.Error($"合并文件失敗,文件名稱:{fileName},錯誤信息:{ex.Message}");
message.Code = -1;
message.Msg = "合并文件失敗,請重新上傳";
}
return message;
}

這里說明下,在Merge的時候,主要校驗md5值,用到了一個方法,我這里沒有放到Util(其實是因為懶),代碼如下:

/// 文件流加密
/// </summary>
/// <param name="fileStream"></param>
/// <returns></returns>
private string GetCryptoString(Stream fileStream)
{
MD5 md5 = new MD5CryptoServiceProvider();
byte[] cryptBytes = md5.ComputeHash(fileStream);
return GetCryptoString(cryptBytes);
}

private string GetCryptoString(byte[] cryptBytes)
{
//加密的二進制轉為string類型返回
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cryptBytes.Length; i++)
{
sb.Append(cryptBytes[i].ToString("x2"));
}
return sb.ToString();
}

測試

方法寫好了之后,我們需不需要測試呢,那不是廢話么,自己的代碼不過一遍等著讓測試人員搞你呢。

再說個編碼習慣,就是自己的代碼自己最起碼常規的過一遍,也不說跟大廠一樣什么KPI啊啥的影響,自己的東西最起碼拿出手讓人一看知道用心了就行,不說什么測試全覆蓋,就是1+1=2這種基本的正常就OK。

程序運行之后,我這里寫了個簡單的測試界面,運行之后發現提示OPTIONS,果斷跨域錯誤,還記得我們之前提到的跨域問題,這里給出解決方法。

跨域

跨域,就是我在這個區域,想跟另一個區域聯系的時候,我們會碰到墻,這堵墻的目的就是,禁止不同區域的人私下交流溝通,但是現在我們就是不要這堵墻或者說要開幾個門的話怎么做呢,net core有專門設置的地方,我們回到Startup這里。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
//…之前的代碼忽略
services.AddCors(options =>
{
options.AddPolicy("AllowAll", p =>
{
p.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
services.AddAspectCoreContainer();
return services.BuildAspectInjectorProvider();
}

AddCors來添加一個跨域處理方式,AddPolicy就是加個巡邏官,看看符合規則的放進來,不符合的直接趕出去。

我們來看新增的代碼:

這里我使用的是允許所有,可以根據自身業務需要來調整,比如只允許部分域名訪問,部分請求方式,部分Header:

//只是示例,具體根據自身需要
services.AddCors(options =>
{
options.AddPolicy("AllowSome", p =>
{
p.WithOrigins("https://www.baidu.com")
.WithMethods("GET", "POST")
.WithHeaders(HeaderNames.ContentType, "x-custom-header");
});
});

寫好之后我們在Configure中聲明注冊使用哪個巡邏官。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//…之前的
app.UseCors("AllowAll");
app.UseHttpsRedirection();
app.UseMvc();
}

好了,設置好跨域之后我們再來執行下上傳操作。

我們看到這個提示之后,是不是能想起來什么

在appsettings.json添加上接口白名單。

"AllowUrl": "/api/Values,/api/File/RequestUpload,/api/File/Upload,/api/File/Merge"

設置好之后,我們繼續上傳,這次總算是可以了(文件后綴這個忽略,測試使用,js就是做了個簡單的substring)。

我們來查看上傳文件記錄的日志信息。

再來我們看下文件存儲的位置,這個位置我們在appsettings里面已經設置過,可以根據自己業務需要調整。

打開文件看下是否有損壞,壓縮包很容易看出來是否正常,只要能打開基本上(當然可能會有問題)沒問題。

解壓出來如果正常那肯定就是沒問題了吧(壓縮這個玩意兒真是牛逼,節省了多少的存儲空間,雖說硬盤白菜價)。

小結

在整理文件上傳這篇剛好捎帶著把跨域也簡單了過了一遍,下來需要再折騰的東西就是大文件的分片下載,大致的思路與文件上傳一致,畢竟都是一個大蛋糕,切成好幾塊,你一塊,剩下的都是我的。

文章轉自微信公眾號@DotNet

上一篇:

實例利用gin搭建一個API框架

下一篇:

.NET Core遷移指南:WebApi升級與優化技巧
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費