月色真美

月色真美

大文件分段上传以及续传、秒传功能实现

13
2021-02-22

dotNet在上传方面,默认上限是30M,即超过30M的文件上传,你需要同时修改Web服务组件的配置和dotNet自身的上传限制,才允许上传更多的。

此外,对于超大文件上传,即时是修改配置,也会有很多疑难问题,因为这里讲解一下如何在DotNet上使用大文件分段上传,以及续传、秒传功能的实现。

  1. 上传后台部分(为表达顺序逻辑,代码未进行封装处理)

FileModel file;
//第一个分段文件,创建基础文件信息
if (model.Index == 0)
{
	file = new FileModel
	{
		Name = model.FileName,
		Code = Guid.NewGuid().ToString("N"),
		Extension = Path.GetExtension(model.FileName),
		Size = model.Size,
		SHA256 = model.SHA256?.ToUpper(),
		Status = "L"
	};
	//加入到数据库
	file = Add(file);
	if (file != null)
	{
		model.Code = file.Code;
	}
	else
	{
		return (0, "保存文件信息出错", null, true);
	}
}
else
{
	//从数据库查询文件信息
	file = Query<FileModel>(model.Code);
}
//保存文件信息
var localFileName = file.AbsolutePath + $"{model.Index.ToString().PadLeft(model.Count.ToString().Length, '0')}";
var saveResult = await model.File.SaveAsync(localFileName, true, true);
if (!saveResult.status)
	return (0, saveResult.msg, null, true);
//最后一个,合并文件
if (model.Count == model.Index + 1)
{
	var folder = Path.GetDirectoryName(file.AbsolutePath);
	var baseFileName = Path.GetFileName(file.AbsolutePath);
	var files = new DirectoryInfo(folder).GetFiles().Where(t => t.Name.StartsWith(baseFileName)).OrderBy(t => t.Name).ToList();
	//强制删除同名文件
	if (File.Exists(file.AbsolutePath))
		File.Delete(file.AbsolutePath);
	//合并文件
	using var fs = new FileStream(file.AbsolutePath, FileMode.Create);
	foreach (var part in files)
	{
		var reader = new FileStream(part.FullName, FileMode.Open);
		var bytes = new byte[reader.Length];
		reader.Read(bytes, 0, (int)reader.Length);
		reader.Close();
		await fs.WriteAsync(bytes, 0, bytes.Length);
	}
	//校验SHA256
	using (SHA256 mySHA256 = SHA256.Create())
	{
		try
		{
			using FileStream fileStream = new FileStream(file.AbsolutePath, FileMode.Open);
			fileStream.Position = 0;
			byte[] hashValue = mySHA256.ComputeHash(fileStream);
			var enText = new StringBuilder();
			foreach (byte encryptbyte in hashValue)
			{
				enText.AppendFormat("{0:x2}", encryptbyte);
			}
			var sha256 = enText.ToString().ToUpper();
			if (sha256 == file.SHA256?.ToUpper())
			{
				//删除分段文件
				foreach (var item in files)
				{
					item.Delete();
				}
				return (1, "成功", file, true);
			}
			return (0, $"校验SHA256不一致({sha256})", null, true);
		}
		catch (IOException e)
		{
			return (0, $"IO异常,{e.Message}", null, true);
		}
		catch (UnauthorizedAccessException e)
		{
			return (0, $"读写权限异常,{e.Message}", null, true);
		}
	}
}
//数据库同步上传状态index
......
return (1, "成功", file, false);
  1. 上传前后台检查部分

var file = XSharp.Queryable<FileModel>().FirstOrDefault(t => t.SHA256 == sha256);
if (file == null || file.Status != "L")
  return file;
if (上传者是上次同一个人)
{
  var folder = Path.GetDirectoryName(file.AbsolutePath);
  var baseFileName = Path.GetFileName(file.AbsolutePath);
  //检查历史是否完整,并删除记录索引之后的未完成文件
  var count = 0;
  foreach (var item in new DirectoryInfo(folder).GetFiles().Where(t => t.Name.StartsWith(baseFileName)))
  {
	var name = item.Name.Replace(baseFileName, "").ToInt();
	if (file.Index <= name)
	  item.Delete();
	else
	  count++;
  }
  if (file.Index  == count)
	return file;
  else
  {
	foreach (var item in new DirectoryInfo(folder).GetFiles().Where(t => t.Name.StartsWith(baseFileName)))
	{
	  item.Delete();
	}
  }
}
return null;
  1. 前端代码
    3.1.需要的依赖

CryptoJS

sha256

3.2.代码部分(因为懒,未进行有效封装)

function sliceUpload() {
  element.render();
  //注册上传
  upload.render({
	elem: '#uploadFile',
	url: 'api',
	accept: 'file',
	auto: false,
	choose: function (obj) {
	  //预读本地文件,如果是多文件,则会遍历。(不支持ie8/9)
	  obj.preview(function (index, file, result) {
		loadFile(file, (progress) => {
		  //进度
		  progress = (progress * 0.3 + 10.00).toFixed(2);
		}, (sha256) => {
		  //对比文件hash
		  admin.req(api.fileCheck(), { sha256: sha256 }, function (dataResult) {
			if (0 === dataResult.errcode && dataResult.data && dataResult.data.status == "A") {
			  layer.msg("文件秒传成功", { icon: 1 });
			  element.progress('uploadFileProgress', '100%');
			  $('#uploadFile').attr('fileId', dataResult.data.id);
			} else {
			  //切片上传
			  doSliceUpload(file, sha256, (dataResult.data ? dataResult.data.index : 0), (dataResult.data ? dataResult.data.code : 0), (progress) => {
				progress = (progress * 0.5 + 50.00).toFixed(2);
			  }, (result) => {
				if (result && result.result) {
				  $('#uploadFile').attr('fileId', result.data.data.id);
				  layer.msg("文件上传成功", { icon: 1 });
				}
				else {
				  layer.msg("文件上传失败,请重试", { icon: 2 });
				}
			  });
			}
		  }, 'post');
		});
		//obj.upload(index, file); //对上传失败的单个文件重新上传
	  });
	}
  });
}
function loadFile(contractFile, progress, complete) {
  var reader = new FileReader();
  var blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice;
  // 指定文件分块大小(2M)
  var chunkSize = 2 * 1024 * 1024;
  // 计算文件分块总数
  var chunks = Math.ceil(contractFile.size / chunkSize);
  // 指定当前块指针
  var currentChunk = 0;
  // 创建SHA256
  var hasher = CryptoJS.algo.SHA256.create();
  hasher.reset();
  // FileReader分片式读取文件
  // 计算开始读取的位置
  var start = currentChunk * chunkSize;
  // 计算结束读取的位置
  var end = start + chunkSize >= contractFile.size ? contractFile.size : start + chunkSize;
  reader.readAsArrayBuffer(blobSlice.call(contractFile, start, end));
  reader.onload = function (evt) {
	var tmpWordArray = CryptoJS.lib.WordArray.create(evt.target.result);
	hasher.update(tmpWordArray);
	progress((currentChunk * chunkSize * 100 / (contractFile.size * 1.00)).toFixed(2) * 1.00);
	currentChunk += 1;
	tmpWordArray = null;
	// 判断文件是否都已经读取完
	if (currentChunk < chunks) {
	  // 计算开始读取的位置
	  var start = currentChunk * chunkSize;
	  // 计算结束读取的位置
	  var end = start + chunkSize >= contractFile.size ? contractFile.size : start + chunkSize;
	  reader.readAsArrayBuffer(blobSlice.call(contractFile, start, end));
	}
  };
  reader.onloadend = function () {
	if (currentChunk >= chunks) {
	  progress(100.00);
	  var hash = hasher.finalize();
	  complete(hash.toString());
	  hasher = null;
	  blobSlice = null;
	  reader = null;
	  hash = null;
	}
  };
}
function doSliceUpload(file, sha256, index, code, progress, complete) {
  var blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice;
  // 指定文件分块大小(4M)
  var chunkSize = 4 * 1024 * 1024;
  // 计算文件分块总数
  var chunks = Math.ceil(file.size / chunkSize);
  // 计算开始读取的位置
  var currentChunk = 0;
  var start = currentChunk * chunkSize;
  // 计算结束读取的位置
  var end = start + chunkSize >= file.size ? file.size : start + chunkSize;
  progress(0.00);
  var formData = new FormData();
  formData.append("file", blobSlice.call(file, start, end));
  formData.append("fileName", file.name);
  formData.append("code", code);
  formData.append("index", currentChunk);
  formData.append("SHA256", sha256);
  formData.append("size", file.size);
  formData.append("count", chunks);
  sliceUploadRequest(file, blobSlice, index, formData, currentChunk, chunkSize, chunks, progress, complete);
}
function sliceUploadRequest(file, blobSlice, index, formData, currentChunk, chunkSize, chunks, progress, complete) {
  if (index > currentChunk) {
	currentChunk++;
	// 计算开始读取的位置
	var start = currentChunk * chunkSize;
	// 计算结束读取的位置
	var end = start + chunkSize >= file.size ? file.size : start + chunkSize;
	progress((currentChunk * chunkSize * 100 / (file.size * 1.00)).toFixed(2) * 1.00);
	formData.set('index', currentChunk);
	formData.set('file', blobSlice.call(file, start, end));
	sliceUploadRequest(file, blobSlice, index, formData, currentChunk, chunkSize, chunks, progress, complete);
	return;
  }
  var loadUpload = new Promise((resolve, reject) => {
	$.ajax({
	  type: "POST",
	  url: api.sliceUpload(),
	  data: formData,
	  contentType: false,
	  processData: false,
	  async: true,
	  dataType: "json",
	  beforeSend: function (request) {
	  },
	  success: function (dataResult) {
		if (dataResult && dataResult.data) {
		  formData.set('code', dataResult.data.code);
		  resolve({ result: true, data: dataResult });
		} else {
		  resolve({ result: false });
		}
	  },
	  error: function (xhr, status, errorData) {
		resolve({ result: false });
	  }
	});
  });
  loadUpload.then((result) => {
	currentChunk++;
	if (currentChunk >= chunks) {
	  complete(result);
	} else {
	  // 计算开始读取的位置
	  var start = currentChunk * chunkSize;
	  // 计算结束读取的位置
	  var end = start + chunkSize >= file.size ? file.size : start + chunkSize;
	  progress((currentChunk * chunkSize * 100 / (file.size * 1.00)).toFixed(2) * 1.00);
	  formData.set('index', currentChunk);
	  formData.set('file', blobSlice.call(file, start, end));
	  sliceUploadRequest(file, blobSlice, index, formData, currentChunk, chunkSize, chunks, progress, complete);
	}
  });
}