在很多實際的開發場景中,我們都需要上傳文件,比如圖片,文檔,pdf等等。而除了圖片相對比較小以外(現在很多智能手機拍出的圖片,都是幾M的大小),其它文檔相對來說都比較大。
而我們在上傳文件的時候,一般服務器都是對上傳文件有大小限制的,就算我們把服務器的上傳文件大小改到1G,我們在上傳的時候,也難免出現接口超時,界面卡死等現象。
所以這裏我用net core 3.0 + vue-antd-pro的上傳插件,實現了一個大文件分片上傳的功能,比如100M的文件,我們把它分割成50個文件,每個文件2M(分割文件的大小,可以自定義)。這樣我們上傳的時候就不會經常出現超時,界面卡死的問題。
先看看效果圖:
net core 3.0大文件分片上傳的代碼如下:
using JuCheap.Core.Infrastructure.Extentions;
using JuCheap.Core.Model;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace JuCheap.Core.WebApi.Controllers
{
/// <summary>
/// 文件上傳api
/// </summary>
[Route("api/file")]
public class FileUploadController : BaseApiController
{
private readonly ILogger<FileUploadController> _logger;
private readonly IWebHostEnvironment _webHostEnvironment;
/// <summary>
/// 文件存放的根路徑
/// </summary>
private readonly string _baseUploadDir;
/// <summary>
/// ctor
/// </summary>
public FileUploadController(ILogger<FileUploadController> logger,
IWebHostEnvironment hostEnvironment)
{
_logger = logger;
_webHostEnvironment = hostEnvironment;
_baseUploadDir = string.Concat(_webHostEnvironment.ContentRootPath, "\\uploads\\");
}
/// <summary>
/// 分片上傳文件
/// </summary>
[HttpPost]
[Route("upload")]
public async Task<ActionResult<FileResponseDto>> Upload()
{
var fileName = Request.Form["name"];
//當前分塊序號
var index = Request.Form["chunk"].ToString().ToInt();
//所有塊數
var maxChunk = Request.Form["maxChunk"].ToString().ToInt();
//前端傳來的GUID號
var guid = Request.Form["guid"];
//臨時保存分塊的目錄
var dir = Path.Combine(_baseUploadDir, guid);
dir.CreateDirectoryIfNotExists();
//分塊文件名爲索引名,更嚴謹一些可以加上是否存在的判斷,防止多線程時併發衝突
var filePath = Path.Combine(dir, index.ToString());
//表單中取得分塊文件
var file = Request.Form.Files["file"];
//獲取文件擴展名
//var extension = file.FileName.Substring(file.FileName.LastIndexOf(".") + 1, (file.FileName.Length - file.FileName.LastIndexOf(".") - 1));
var filePathWithFileName = string.Concat(filePath, fileName);
using (var stream = new FileStream(filePathWithFileName, FileMode.Create))
{
await file.CopyToAsync(stream);
}
//如果是最後一個分塊, 則合併文件
var fileResponseDto = new FileResponseDto();
if (index == maxChunk - 1)
{
await MergeFileAsync(fileName, guid);
fileResponseDto.Completed = true;
}
return Ok(fileResponseDto);
}
/// <summary>
/// 合併分片的文件
/// </summary>
/// <param name="fileName">文件名稱</param>
/// <param name="guid">文件guid</param>
private async Task MergeFileAsync(string fileName, string guid)
{
//臨時文件夾
var dir = Path.Combine(_baseUploadDir, guid);
//獲得下面的所有文件
var files = Directory.GetFiles(dir);
var yearMonth = DateTime.Now.ToString("yyyyMM");
//最終的文件名(demo中保存的是它上傳時候的文件名,實際操作肯定不能這樣)
var finalDir = Path.Combine(_baseUploadDir, yearMonth, guid);
finalDir.CreateDirectoryIfNotExists();
var finalPath = Path.Combine(finalDir, fileName);
using (var fs = new FileStream(finalPath, FileMode.Create))
{
//排一下序,保證從0-N Write
var fileParts = files.OrderBy(x => x.Length).ThenBy(x => x);
foreach (var part in fileParts)
{
var bytes = await System.IO.File.ReadAllBytesAsync(part);
await fs.WriteAsync(bytes, 0, bytes.Length);
bytes = null;
//刪除分塊
System.IO.File.Delete(part);
}
await fs.FlushAsync();
fs.Close();
//刪除臨時文件夾和分片文件
Directory.Delete(dir);
}
}
}
}
前臺頁面js代碼如下:
<template>
<div>
<a-card :loading="loading" title="說明">
<p>1.支持自定義上傳方式</p>
<p>2.支持大文件上傳, 把一個大文件,分割成若干個小文件,上傳到服務</p>
<p>3.支持顯示上傳進度</p>
</a-card>
<p></p>
<a-upload-dragger
name="file"
:multiple="true"
action="/api/file/upload"
@change="handleChange"
:customRequest="customRequest"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">點擊或者拖拽文件到此區域,上傳文件</p>
<p class="ant-upload-hint">
支持單個文件或批量上傳文件。嚴禁上傳公司機密資料
</p>
</a-upload-dragger>
</div>
</template>
<script>
import defaultSettings from '@/config/defaultSettings'
import ProcessHelper from '@/utils/helper/ProcessHelper'
const rootUrl = () => {
if (ProcessHelper.isProduction() || ProcessHelper.isPreview()) {
return defaultSettings.publishRootUrl
} else {
return defaultSettings.localRootUrl
}
}
export default {
data() {
return {
//已上傳的文件
fileList:[],
loading: true
}
},
mounted(){
this.loading = false
},
methods:{
handleChange(info) {
let me = this
const status = info.file.status
if (status !== 'uploading') {//removed
console.log(info.file, info.fileList);
}
if (status === 'removed') {
me.fileList.forEach(function(item, index){
if(item.uid === info.file.uid){
me.fileList.splice(index, 1)
}
})
}
if (status === 'done') {
} else if (status === 'error') {
}
},
customRequest(option){
console.log(option)
let url = rootUrl() + option.action
let createGuid = function() {
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1)
}
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4())
}
this.fileUpload(url, option, 0, createGuid())
},
//上傳文件
fileUpload(uploadUrl, option, chunk, guid) {
let file = option.file
//每次上傳文件的大小
let chunkSize = 1024 * 1024
//最大上傳大小 默認100M
let maxSize = 1024 * 1024 * 100
let maxChunk = Math.ceil((file.size / chunkSize))
let formData = new FormData()
//將文件進行分段
let fileSize = file.size
if (fileSize > maxSize) {
this.$message.error("文件大小不能超過" + (maxSize / 1024 / 1024) + "M")
return
}
//當前上傳進度
let currentPercent = parseInt((chunk / maxChunk) * 100)
option.onProgress({ percent: currentPercent })
formData.append('file', file.slice(chunk * chunkSize, (chunk + 1) * chunkSize))
formData.append('name', file.name)
formData.append('chunk', chunk)
formData.append('maxChunk', maxChunk)
formData.append('guid', guid)
this.$http.post(uploadUrl, formData).then(resJson => {
this.confirmLoading = false
if (resJson.success) {
if(!resJson.data.completed){
this.fileUpload(uploadUrl, option, ++chunk, guid)
}else{
this.$message.success("文件上傳成功")
option.onProgress({ percent: 100 })
option.onSuccess()
}
} else {
this.$message.error(resJson.message)
}
})
}
}
}
</script>
效果查看地址:http://core.jucheap.com
賬號:jucheap 密碼:qwaszx