搭建Wpf框架(17) ——大文件上传与下载
AIStudio框架汇总及介绍
先上效果图:
大文件上传
1.客户端需要按照块拆成一块一块,先计算大小,然后计算块的个数,然后按块逐个上传,代码如下:
public async Task<UploadResult> UploadFileChunck(string path, Action<double> progressAction)
{
try
{
var fStream = File.OpenRead(path);
int chunckSize = 2097152;//2MB
int totalChunks = (int)(fStream.Length / chunckSize);
if (fStream.Length % chunckSize != 0)
{
totalChunks++;
}
double progress = 0d;
progressAction?.Invoke(progress);
var tempDirectory = Guid.NewGuid().ToString("N");
UploadResult result = null;
for (int i = 0; i < totalChunks; i++)
{
long positon = (i * (long)chunckSize);
int toRead = (int)Math.Min(fStream.Length - positon, chunckSize);
byte[] buffer = new byte[toRead];
await fStream.ReadAsync(buffer, 0, buffer.Length);
using (MultipartFormDataContent data = new MultipartFormDataContent())
{
data.Add(new StringContent(tempDirectory ?? ""), "tempDirectory");
data.Add(new StringContent(i.ToString()), "index");
data.Add(new StringContent(totalChunks.ToString()), "total");
data.Add(new ByteArrayContent(buffer), "file", Path.GetFileName(path));
var content = await PostAsync(string.Format("{0}/Base_Manage/Upload/UploadFileChunck", Url), data, TimeOut, Header.GetHeader());
result = JsonConvert.DeserializeObject<AjaxResult<UploadResult>>(content)?.Data;
progress += 1d / totalChunks;
progressAction?.Invoke(progress);
}
}
fStream.Close();
return result;
}
catch (Exception ex)
{
return new UploadResult() { status = ex.Message };
}
}
注:加了一个Action用于通知进度 2.服务端需要一块一块接送,直接最后一块接收后,把文件合并,删除块文件,结束,代码如下:
#region 大文件上传
/// <summary>
/// 上传附件
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<ActionResult> UploadFileChunck(IFormFile file, string tempDirectory, int index, int total)
{
if (file == null)
{
return BadRequest("请选择上传文件");
}
string fileUploadPath = GetUploadPath();
string tmp = Path.Combine(fileUploadPath, tempDirectory) + "/";//临时保存分块的目录
if (index == 0)
{
if (Directory.Exists(tmp))
{
Directory.Delete(tmp);
}
}
try
{
using (var stream = file.OpenReadStream())
{
var strmd5 = GetMD5Value(stream);
//if (md5 == strmd5)//校验MD5值
//{
//}
if (await Save(stream, tmp, index.ToString()))
{
bool mergeOk = false;
string path = "";
string physicPath = "";
if (total - 1 == index)
{
path = $"/Upload/{Guid.NewGuid().ToString("N")}/{file.FileName}";
physicPath = GetAbsolutePath($"~{path}");
string dir = Path.GetDirectoryName(physicPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
mergeOk = await FileMerge(tmp, physicPath);
if (mergeOk)
{
_logger.LogInformation($"文件上传成功:{physicPath}");
}
}
string url = $"{AppSettingsConfig.webUrl}{path}";
var res = new
{
index = index,
name = file.FileName,
status = mergeOk == true ? "done" :"part",
thumbUrl = url,
url = url
};
return AjaxResultActionFilter.Success(res);
}
else
{
return AjaxResultActionFilter.Error("上传失败");
}
}
}
catch (Exception ex)
{
Directory.Delete(tmp);//删除文件夹
_logger.LogError($"文件上传异常:{ex.Message}");
return AjaxResultActionFilter.Error("上传失败");
}
}
/// <summary>
/// 合并文件
/// </summary>
/// <param name="tmpDirectory">临时上传目录</param>
/// <param name="path">上传目录</param>
/// <param name="saveFileName">保存之后新文件名</param>
/// <returns></returns>
private async Task<bool> FileMerge(string tmpDirectory, string saveName)
{
try
{
var files = Directory.GetFiles(tmpDirectory);
using (var fs = new FileStream(saveName, FileMode.Create))
{
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();
Directory.Delete(tmpDirectory);//删除临时目录
return true;
}
}
catch (Exception ex)
{
_logger.LogError($"文件合并异常:{ex.Message}");
return false;
}
}
#endregion
/// <summary>
/// 文件保存到本地
/// </summary>
/// <param name="stream"></param>
/// <param name="path"></param>
/// <param name="saveName"></param>
/// <returns></returns>
private async Task<bool> Save(Stream stream, string path, string saveName)
{
try
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
await Task.Run(() =>
{
FileStream fs = new FileStream(path + saveName, FileMode.Create);
stream.Position = 0;
stream.CopyTo(fs);
fs.Close();
});
return true;
}
catch (Exception ex)
{
_logger.LogError($"文件保存异常:{ex.Message}");
return false;
}
}
/// <summary>
/// 计算文件的MD5值
/// </summary>
/// <param name="obj">类型只能为string or stream,否则将会抛出错误</param>
/// <returns>文件的MD5值</returns>
private string GetMD5Value(object obj)
{
MD5 md5Hash = MD5.Create();
byte[] data = null;
switch (obj)
{
case string str:
data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(str));
break;
case Stream stream:
data = md5Hash.ComputeHash(stream);
break;
case null:
throw new ArgumentException("参数不能为空");
default:
throw new ArgumentException("参数类型错误");
}
return BitConverter.ToString(data).Replace("-", "");
}
/// <summary>
/// 获取路径
/// </summary>
/// <param name="virtualPath"></param>
/// <returns></returns>
protected string GetAbsolutePath(string virtualPath)
{
string path = virtualPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
if (path[0] == '~')
path = path.Remove(0, 2);
string rootPath = HttpContext.RequestServices.GetService<IWebHostEnvironment>().WebRootPath;
return Path.Combine(rootPath, path);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected string GetUploadPath()
{
string rootPath = HttpContext.RequestServices.GetService<IWebHostEnvironment>().WebRootPath;
return Path.Combine(rootPath, "Upload");
}
示例实现的比较简单,您还可以进行优化,比如所有块拆分后同时长传,同时上传完毕后,客户在发起合并请求,另外如果丢了一块,其实也是可以进行检查,补上传的。
大文件下载
使用开源框架Downloader,大家自己去GitHub看吧,链接https://github.com/bezzad/Downloader,下图为官网例子图
支持分块同时下载,非常不错哟。
最后老规矩,上源码地址
前台 https://gitee.com/akwkevin/aistudio.-wpf.-aclient
后台 https://gitee.com/akwkevin/AIStudio.Blazor.App