大文件上传
前端实现
使用vue+elementui进行前端开发, 实现在dialog中 带进度条的上传大文件页面
<el-form :model="ruleForm" ref="ruleForm" :label-width="formLabelWidth" :rules="theRules" >
<el-form-item prop="jar" :label-width="formLabelWidth">
<label slot="lable" style="font-weight: lighter">上传文件</label>
<el-upload
ref="upload"
action=""
:http-request="handleFile"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:on-change="handleChange"
:multiple="false"
:limit="1"
:file-list="fileList"
accept=".tar">
<el-button slot="trigger" size="small" type="primary" :disabled="fileButtonDisabled">选择应用包</el-button>
<el-button style="margin-left: 10px" size="small" type="success" @click="uploadFile" :disabled="fileButtonDisabled">上传</el-button>
</el-upload>
</el-form-item>
<el-form-item>
<label slot="lable" style="font-weight: lighter"></label>
<el-progress :text-inside="true" :stroke-width="15" :percentage="filePercentage"></el-progress>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose('ruleForm')">取 消</el-button>
<el-button type="primary" @click="submitForm('ruleForm')">确 定</el-button>
</div>
这里使用axios有一个坑需要注意一下,必须是这个指定的header,且必须用Promise包起来。
data: function () {
return {
ruileForms: {
file: '',
jar: ''
}
theRules: {
//jar:[{required:true, message:"请上传tar包", trigger:'blur'}]
}
fileList:[],
filePercentage: 0, // 文件上传进度条
fileLocation: '', // 文件在后台方式的位置
fileCancelUpload: false, // 取消文件分片上传
fileButtonDisabled: false, //文件上传按钮禁用
}
}
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.fileLocation == "") {
this.$message({message:"请上传文件", type:'fail'})
}
let formData = new FormData()
formData.append("fileLocation", this.fileLocation)
// 设置header头
let config = {
headers: {
'Content-Type':'multipart/form-data',
}
}
Axios.post('/api/fileUpload', formData, config)
.then((response) => {
if (response.data.result == true) {
this.$message({message:"成功", type:'success'})
this.resetForm('ruleForm')
}
})
.catch((err) => {
console.log(err)
})
}
})
},
handleFile() {
// 空方法
},
handleChange(file, fileList) {
// 文件改变时
this.fileList = fileList
},
handleRemove(file, fileList) {
this.fileCancelUpload = true
this.filePercentage = 0 //进度条置空
this.fileList = []
this.fileButtonDisabled = false // 上传可点击
},
beforeRemove(file, fileList) {
return this.$confirm('确定移除 ${file.name} ?');
},
//上传文件
uploadFile() {
let file = this.fileList[0] ? this.fileList[0].raw : ""
if (file == "") {//判断文件是否存在
this.$message({message:"未选择文件", type:'fail'})
return;
}
if (file.size > 50 * 1024 * 1024) {
//判断文件大小
this.$message({message:"文件不能大于50M", type:'fail'})
return;
}
//判断文件类型
if (file.type.indexOf("tar") == "-1") { //application/x-tar
//判断文件大小
this.$message({message:"文件必须为tar包", type:'fail'})
return;
}
if (file.name.length > 30) {
//判断文件大小
this.$message({message:"文件名大于30个字符", type:'fail'})
return;
}
this.fileButtonDisabled = true
// 唯一标识
var uiqueIdentifier = this.Id + '-' + parseInt(new Data().getTime() / 1000)
console.log(uiqueIdentifier)
this.uploadBySplit(file, uiqueIdentifier, 0)
},
//分片上传
uploadBySplit(file, identifier, i) {
//如果取消上传直接初始化为最初状态
if (this.fileCancelUpload) {
this.fileCancelUpload = false
this.filePercentage = 0
this.fileList = []
this.fileButtonDisabled = false
}
var chunkSize = 1024 * 1024 * 1; //分片大小1M
var size = file.size; //总大小
var totalChunks = Math.ceil(size/chunkSize);
//分片停止条件
if (i == totalChunks) {
this.$message({message:"上传成功", type:'success'})
return;
}
//计算每一篇的起始位置和结束位置
var start = i * chunkSize;
var end = Math.min(size, start + chunkSize);
var fileData= file.slice(start, end)
//文件分块上传
var reader = new FileReader();
reader.readAsBinaryString(fileData);
reader.onload = function(e) {
let formData = new FormData();
formData.append('chunkNumber', i+1); //当前第几片,从0开始,文件下表从1计算。
formData.append('chunkSize', chunkSize); //当前分片大小
formData.append('currentChunkSize', fileData.size); //当前块大小
formData.append('totalSize', size) //总的大小
formData.append('identifier', identifier) //唯一标识
formData.append('filename', file.name) //文件名
formData.append('type', file.type) //文件类型
// formData.append('relativePath', "/") //相对路径,暂时没用
formData.append('totalChunks', totalChunks) //总片数
formData.append('file', fileData) //总片数
// 必须用这个Promise包起来axios,不然有问题
return new Promise((resolve, reject) => {
// 必须用这个类型的头,并且要包括boundary
let config = {
headers: {
'Content-Type':'multipart/form-data; charset=utf-8; boundary="another cool boundary";',
}
}
Axios.post('/api/upload', formData, config)
.then((response) => {
if (response.data.result == true) {
if (response.data.data != "") {
this.fileLocation=response.data.data//合并后文件放置的位置
}
//上传进度
var process =Math.random(end/ size*1000);
this.fileLocation = process
i++;
this.uploadBySplit(file, identifier, i);
resolve(response.data)
}else {
this.$message({message:"分片上传失败", type:'fail'})
reject(err)
}
})
.catch((err) => {
console.log(err)
})
})
}
}
}
后端实现 java版
@ResponseBody
@RequestMapping(value="/upload", method = RequestMethod.Post)
public JsonResult upload(HttpServletRequest request, Chunk chunk) {
try {
boolen isMutipart = ServletFileUpload.isMutipartContent(request);
if(isMutipart) {
MultipartFile file = chunk.getFile();
if(file == null) {
return JsonResult.failure("文件为空");
}
File outFile = new File(generatePath(chunk));
InputStream inputStream = file.getInputStream();
FileUtils.copyInputStreamToFile(inputStream, outFile);
//判断所有分片是否全部上传完成,完成就合并
File dir = new File(generateFileDir(chunk));
File[] files = dir.listFiles();
if (files.length == chunk.getTotalChunks()) {
String filePath = mergeFile(chunk); //合并文件
return JsonResult.success(filePath);
}
}
return JsonResult.success();
} catch (Exception e) {
log.error(e)
return JsonResult.failure("系统错误");
}
}
// 获取上传文件路径
public String generatePath(Chunk chunk) {
StringBuilder sb = new StringBuilder();
sb.append(uploadDir).append("/").append(chunk.getIdentifier());
if (!Files.isWritable(Paths.get(sb.toString()))) {
log.info("path not exist, create path:" , sb.toString());
}
try {
Files.createDiretories(Paths.get(sb.toString()));
} catch (IOExeception e){
log.error(e)
}
return sb.append("/").append(chunk.getFilename()).append("-").append(chunk.getChunkNumber().toString());
}
//获取切片路径
public String getnerateFileDir(Chunk chunk) {
StringBuilder sb = new StringBuilder();
sb.append(uploadDir).append("/")/append(chunk.getIdentifier());
return sb.toString();
}
//合并文件
public String mergeFile(Chunk chunk) {
String filename = chunk.getFilename(); //文件名
String folder = generateFileDir(chunk); //文件路径
String file = folder + File.separator + filename; //生成文件名
merge(file, folder, filename); //合并
return chunk.getIdentifier() + + File.separator + filename; //返回相对路径
}
//合并
public static void merge(String targetFile, String folder) {
try {
Files.createFile(Paths.get(targetFile));
Files.list(Paths.get(folder))
.filter(path -> path.getFileName().toString().contains("-"))
.sorted((o1, o2) -> {
String p1 = o1.getFileName().toString();
String p2 = o2.getFileName().toString();
int i1 = p1.lastIndexOf("-");
int i2 = p2.lastIndexOf("-");
return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
})
.forEach(path -> {
try {
//以追加的形式写入文件
Files.write(Paths.get(targetFile), Files.readAllBytes(path), StandardOpenOption.APPEND);
//合并后删除该块
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
chunk 块结构
@Data
@Entity
public class Chunk implements Serializable {
@Id
@GeneratedValue
private Long id;
/**
* 当前文件块,从1开始
*/
@Column(nullable = false)
private Integer chunkNumber;
/**
* 分块大小
*/
@Column(nullable = false)
private Long chunkSize;
/**
* 当前分块大小
*/
@Column(nullable = false)
private Long currentChunkSize;
/**
* 总大小
*/
@Column(nullable = false)
private Long totalSize;
/**
* 文件标识
*/
@Column(nullable = false)
private String identifier;
/**
* 文件名
*/
@Column(nullable = false)
private String filename;
/**
* 相对路径
*/
@Column(nullable = false)
private String relativePath;
/**
* 总块数
*/
@Column(nullable = false)
private Integer totalChunks;
/**
* 文件类型
*/
@Column
private String type;
@Transient
private MultipartFile file;
}
参考
SpringBoot+Vue.js前后端分离实现大文件分块上传
来源:oschina
链接:https://my.oschina.net/solate/blog/4313345