1、上传大文件的方式
上传大文件就需要一段一段的上传,主要是先在客户端获取文件的大小,例如想一次传256kb,那就按照256kb分割。分割后又两种上传方式。
(1)逐个数据段读取,然后调用API上传,把数据追加到文件上。上传完这一段,接着传下一段,直到上传完毕。
(2)方式与(1)类似,只是可以好几段可以并行上传,上传后,会把每段按照索引命名,在服务器上保存成临时文件。判断都上传完毕后,再调用合并命令,把这些小的碎文件,合并成大的目标文件。
我一般喜欢用第一种方式,主要是因为简单明了。缺点就是,因为必须等上一段完成后,再传下一段,所以无法并行上传,导致速度会受到影响。
但我参与的项目基本上都是在局域网运行并且使用人数有限,所以这种传大文件的方式是可以满足系统要求。
2、上传大文件API实现
下面的实现用到了定义的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代码没有实际测试,只是一个思路。