上傳大文件就需要一段一段的上傳,主要是先在客戶端獲取文件的大小,例如想一次傳256kb,那就按照256kb分割。分割後又兩種上傳方式。
(1)逐個數據段讀取,然後調用API上傳,把數據追加到文件上。上傳完這一段,接着傳下一段,直到上傳完畢。
(2)方式與(1)類似,只是可以好幾段可以並行上傳,上傳後,會把每段按照索引命名,在服務器上保存成臨時文件。判斷都上傳完畢後,再調用合併命令,把這些小的碎文件,合併成大的目標文件。
我一般喜歡用第一種方式,主要是因爲簡單明瞭。缺點就是,因爲必須等上一段完成後,再傳下一段,所以無法並行上傳,導致速度會受到影響。
但我參與的項目基本上都是在局域網運行並且使用人數有限,所以這種傳大文件的方式是可以滿足系統要求。
下面的實現用到了定義的AttachedFileEntity和HttpClientEx,這兩個類的定義可參考 005 Controller上傳小文件。
開始上傳API的目的是爲了獲取文件在服務器上的存儲路徑,代碼如下。
/// <summary> /// 開始大上傳文件 /// </summary> /// <returns></returns> [HttpPost] [Route("StartUploadBigFile")] public IActionResult StartUploadBigFile(AttachedFileEntity pEntity, string pFileEx) { string myServerFilePath = DateTime.Now.ToString("yyyy_MM_dd") + "\\" + Guid.NewGuid().ToString() + pFileEx; pEntity.ServerPath = myServerFilePath; return this.Ok(pEntity); }
分段上傳文件,接收到後,追加到現有的文件上。
/// <summary> /// 上傳大文件 /// </summary> /// <param name="pServerPath"></param> /// <returns></returns> [HttpPost] [Route("UploadBigFile")] [DisableRequestSizeLimit] public IActionResult UploadBigFile(string pServerPath) { var myFile = Request.Form.Files[0]; //創建目錄 string myFullServerPath = AppDomain.CurrentDomain.BaseDirectory + "\\Files\\" + pServerPath; string myFullFolder = Path.GetDirectoryName(myFullServerPath)!; if (Directory.Exists(myFullFolder) == false) { Directory.CreateDirectory(myFullFolder); } //寫入文件 Stream? myStream = null; FileStream? myFileStream = null; BinaryWriter? myBinaryWriter = null; try { myStream = myFile.OpenReadStream(); byte[] myBytes = new byte[myStream.Length]; myStream.Read(myBytes, 0, myBytes.Length); myStream.Seek(0, SeekOrigin.Begin); myFileStream = new FileStream(myFullServerPath, FileMode.Append); myBinaryWriter = new BinaryWriter(myFileStream); myBinaryWriter.Write(myBytes); } catch (Exception ex) { return this.BadRequest("上傳大文件失敗," + ex.Message); } finally { myBinaryWriter?.Close(); myFileStream?.Close(); myStream?.Close(); } return this.Ok(); }
最後,結束上傳,把文件的信息記錄到數據庫中。
/// <summary> /// 結束上傳大文件 /// </summary> /// <returns></returns> [HttpPost] [Route("FinishUploadBigFile")] public IActionResult FinishUploadBigFile(AttachedFileEntity pEntity) { if (string.IsNullOrEmpty(pEntity.GUID)) { pEntity.GUID = Guid.NewGuid().ToString(); } //記錄到數據庫中 //代碼略 return this.Ok(pEntity); }
3、客戶端調用
客戶端如果用的C#,代碼中沒有加入進度信息,進度信息可以傳入一個ProcessInfo對象,傳一段數據後,就更新下進度信息。
調用的代碼入下所示。
private void Init_BigFileUpLoad_UIs() { this.UI_BigFile_Button.Click += (x, y) => { var myOpenFileDialog = new OpenFileDialog { Filter = ".*|*.*" }; var myIsOK = myOpenFileDialog.ShowDialog(); if (myIsOK != true) { return; } this.UI_BigFile_TextBox.Text = myOpenFileDialog.FileName; }; this.UI_BigFileUpLoad_Button.Click += async (x, y) => { var myFilePath = this.UI_BigFile_TextBox.Text.Trim(); if (myFilePath.Length == 0) { MessageBox.Show("請選擇一個文件。"); return; } if (File.Exists(myFilePath) == false) { MessageBox.Show("文件不存在,請重新選擇。"); return; } //定義AttachedFileEntity var myFileEntity = new AttachedFileEntity() { GUID = Guid.NewGuid().ToString(), Name = "用戶頭像", KeyWord = "UserProfilePhoto", Description = "", EntityGUID = "AAAA" }; //打開上傳的文件 var myFileStream = new FileStream(myFilePath, FileMode.Open); myFileEntity.FileSize = (int)myFileStream.Length; var myFileName = Path.GetFileName(myFilePath); //每次上傳256kb int myChunkSize = 1024 * 256; int myChunkCount = (int)Math.Ceiling(myFileStream.Length / (double)myChunkSize); //調用開始上傳 var myHttpClient = new HttpClient(); var myHttpClientEx = new HttpClientEx(myHttpClient) { Url = "http://localhost:5000/api/AttachedFile/StartUploadBigFile", HttpContent = JsonContent.Create(myFileEntity) }; myHttpClientEx.ParameterDictionary.Add("pFileEx", Path.GetExtension(myFileName)); await myHttpClientEx.PostAsync(); if (myHttpClientEx.IsSuccess == false) { myFileStream.Close(); MessageBox.Show("上傳文件失敗," + myHttpClientEx.ResponseContenString); return; } myFileEntity = myHttpClientEx.GetResponseObject<AttachedFileEntity>(); if (myFileEntity == null) { myFileStream.Close(); MessageBox.Show("上傳文件失敗,返回的AttachedFileEntity爲null。"); return; } //循環上傳文件 for (int i = 0; i < myChunkCount; i++) { //組織數據 int myByteArraySize = myChunkSize; if (i == myChunkCount - 1) { myByteArraySize = (int)(myFileStream.Length % myChunkSize); } byte[] myBytes = new byte[myByteArraySize]; myFileStream.Position = myChunkSize * i; myFileStream.Read(myBytes, 0, myBytes.Length); var myMemoryStream = new MemoryStream(myBytes); //請求服務 myHttpClientEx = new HttpClientEx(myHttpClient) { Url = "http://localhost:5000/api/AttachedFile/UploadBigFile", HttpContent = new MultipartFormDataContent { {new StreamContent(myMemoryStream),"pFile",myFileName} } }; myHttpClientEx.ParameterDictionary.Add("pServerPath", myFileEntity!.ServerPath); await myHttpClientEx.PostAsync(); //解析結果 if (myHttpClientEx.IsSuccess == false) { myFileStream.Close(); MessageBox.Show("上傳文件失敗," + myHttpClientEx.ResponseContenString); return; } } //結束上傳 myHttpClientEx = new HttpClientEx(myHttpClient) { Url = "http://localhost:5000/api/AttachedFile/FinishUploadBigFile", HttpContent = JsonContent.Create<AttachedFileEntity>(myFileEntity) }; await myHttpClientEx.PostAsync(); if (myHttpClientEx.IsSuccess == false) { myFileStream.Close(); MessageBox.Show("上傳文件失敗," + myHttpClientEx.ResponseContenString); return; } myFileStream.Close(); myFileEntity = myHttpClientEx.GetResponseObject<AttachedFileEntity>(); var myEntityJosnString = JsonSerializer.Serialize<AttachedFileEntity>(myFileEntity); MessageBox.Show(myEntityJosnString); }; }
如果客戶端是js,代碼如下。
on(myButton, "change", function (e) { var myFileReader = new FileReader(); var myFileName = ""; myFileReader.onloadend = function () { var myFileResult = myFileReader.result; var myFileLength = myFileResult.byteLength; var myPerLength = 1024 * 256; var myCount = Math.ceil(myFileLength / myPerLength); var myFileEntity = new Object() { ServerPath: "" }; //調用開始上傳StartUploadBigFile,具體代碼略。 var myK = 0; Upload(); function Upload() { var myByteArray = myFileResult.slice(myPerLength * myK, myPerLength * (myK + 1)); var myBlob = new Blob([myByteArray]); var myFile = new File([myBlob], myFileName); var myFormData = new FormData(); myFormData.append("file", myFile) request.post(myUrl + "?pServerFile=" + myFileEntity.ServerPath +, { data: myFormData }).then(function (data) { myFileEntity = json.parse(data); myK++; if (myK < myCount) { Upload(); } else { alert("上傳大文件結束。"); alert(json.stringify(myFileEntity)); //結束,post FinishUploadBigFile } }, function (err) { alert(err); return; }); } } myFileName = this.files[0].name; myFileReader.readAsArrayBuffer(this.files[0]); });
Js代碼沒有實際測試,只是一個思路。