敲黑板:大文件上传是需要后端支持的
因为我是前端,我用的是node来说明后端怎么操作。前端自己看懂以后,方便前端工程师与Java后端沟通。
首先,大文件上传分为以下几点考量:
(1)秒传:服务器已经有该文件了,直接显示上传成功。
(2)分片上传:大文件分片传给后端,后端自己组合成一个完整的文件。
(3)断点续传:分片上传一半没成功,然后从断点处继续接着上传。
(1)秒传
a、前端思路:
- 用户上传文件
- 计算文件的hash值,传递给后端,后端利用hash值判断文件是否已经存在。
- 如果存在,提示上传成功,上传结束。
- 如果不存在,采用分片上传的方式。
前端代码: 计算hash值
使用插件:spark-md5
主要代码片段:
import SparkMD5 from 'spark-md5
// 计算hash值
const fileReader = new FileReader()//文件读取器
fileReader.onload = function(){
const spark =new SparkMD5.ArrayBuffer()//构建hash值对象
spark.append(fileReader.result)//添加文件二进制内容
const hash=spark.end()//计算hash值console.log(hash)
fileReader.readAsArrayBuffer(file)
b、后端思路:
- 接收前端传递的hash值。
- 查询数据库是否已经存在hash对应的文件。
- 如果存在,返回数据,否则返回code告知前端。
后端代码片段
const mongoose =require('mongoose')
const Schema = mongoose.Schema
// 定义文件资源的数据结构
const fileSchema = new Schema({
name:{ type: String,required: true },
hash:{ type: String, required: true },
size:{ type: Number,required: true },
type:{ type: String, required: true },
createTime:{type:Date,default:Date.now },
updateTime:{ type: Date,default:Date.now }
})
// 定义文件资源模型
const File = mongoose.model('File', fileschema)
//查询是否存在该hash值对应的文件
File.findone({ hash: req.query.hash },(err, file)=>{if(err){
console.error(err)res.status(500).json({ message:'服务器端错误'})
} else {
if(file){
res.status(200).json({ message: '文件已存在', url: file.url })
} else {
res.status(404).json({ message:'文件不存在'})
}
}
})
(2)分片上传
a、前端思路:
- 将文件进行分片,使用File.slice()。
- 依次上传每一个分片,使用Promise.all()确保所有分片上传成功,使用axios进行数据上传。
- 同时计算上传进度,展示给用户。
- 上传所有分片以后,由后端将各个分片进行合并。
前端分片代码
const slicesize=1024*1024//1MB的分片大小
const chunks = Math.ceil(filesize /slicesize)// 文件分片数
const requests=[]//分片请求的promise列表
for(leti=0:i<chunks;i++){
const start=i*sliceSize // 当前分片在文件中的起始位置
const end = Math.min(start +sliceSize,filesize)// 当前分片在文件中的终止位置
const chunk=file.slice(start,end)//获取当前分片
const formData=new FormData()//构建formdata用于上传文件
formData.append('chunk', chunk)formData.append('hash', hash)
formData.append('name', file.name)formData.append('chunkIndex',i)
formData.append('chunks',chunks)
const config={
headers:{'Content-Type':'multipart/form-data'}
},
onUploadProgress:progressEvent=>{
const uploaded = start + progressEvent.loaded//已上传的大小
const total=filesize//文件总大小
const percentCompleted = Math.floor((uploaded /total)*100)// 计算上传进
度
// 更新当前分片的上传进度
this.$set(this.chunks,i, percentCompleted)// 更新已上传总大小
this.uploadedsize += progressEvent.loaded11 更新已上传总进度
this.totalPercentCompleted = Math.floor((this.uploadedsize this.filesize)*100)
}
}
const request = axios.post(`http://localhost:3000/upload`, formData, config)
requests.push(request)
}
Promise.all(requests)//所有分片上传完成
b、后端思路
- 分片上传以后先存储下来,包括分片的索引、分片总数、文件的hash值。
- 等所有分片上传成功,在进行文件合并。
- 将文件存储到数据库。
const formidable = require('formidable')
const fs = require('fs')
const path = require('path')
//存储分片文件
const fileDir = path.resolve('./uploads')
if(!fs.existssync(fileDir))fs.mkdirsync(fileDir)
form.on('field',(name,value)=>{
if(name ==='chunkIndex'){
chunkInfo.index = Number(value)
else if(name ==='chunks'){
chunkInfo.total = Number(value)
}
}
form.on('file',(_,file)=>{
const chunkPath = path.join(fileDir,${hash}_${chunkInfo.index})
fs.renameSync(file.path,chunkPath)//将分片文件存入指定目录下
)
form.on('end',()=>{
// 如果所有分片上传完成
if(chunkInfo.total-1=== chunkInfo.index){
const file = fs.createWriteStream(path.join(fileDir,name))// 创建新文件流
for(leti=0;i<chunkInfo.total;i++){
const chunkPath=path.join(fileDir,${hash}_${i})
const chunk=fs.readFileSync(chunkPath)//读取分片内容
file.write(chunk)//将分片内容写入新文件流
fs.unlinkSync(chunkPath)//删除该分片
}
file.end()
// 将文件信息存入数据库
const newFile = new File({
name: name,
hash: hash,
size: size,
type: type,
url:/uploads/${name}
})
newFile.save((err,file)=>{
if(err){
console.error(err)
res.status(500).json({ message:'服务器端错误' })
}else {
res.status(200).json({ message: '上传成功', url: file.url })
}
})
}else {
res.status(200).json({ message:'文件块已经上传'})
}
})
(3)断点续传
a、前端思路
- 使用FileReader 对象读取文件内容
- 将读取内容分成多个片段,将片段上传到后端。
- 上传中断,下次上传直接从上次上传成功的片段之后的片段进行上传,使用Promise 和async/await实现。
前端代码:
async uploadFilechunk(file,start,end){
const chunk=file.slice(start,end)//获取当前分片
const formData =new FormData()//构建formdata用于上传文件
formData.append('chunk',chunk)
formData.append('hash', this.hash)
formData.append('name', file.name)
formData.append('start', start)
const config={
headers:{'Content-Type':'multipart/form-data'}
}
try {
const response = await axios.post( http://localhost:3000/upload`,formData, config)
if(response.data.message ==='Chunk uploaded'){
//如果分片上传成功
this.uploadFile(this.file,end)
//上传完当前分片后,继续上传下一个分片
}else if(response.data.message ==='Upload successful'){
// 如果整个文件上传成功
this.uploading = false
this.$emit('uploadFinish',response.data.url)
}
}
catch(error){
console.error(error)
this.uploading = false
}
}
async uploadFile(file,start=0){
const end=start+ this.chunksize // 当前分片的终止位置
await this.uploadFileChunk(file,start,end)//上传当前分片
}
b、后端思路
- 通过文件的hash值判断是否文件已存在。
- 如果文件在服务器上不存在,就创建一个新的文件,
- 如果存在,就继续上传
- 后端将上传的分片存在服务器的磁盘上,基础分片在整个文件的起始位置,最后合并文件
后端代码:
const formidable = require('formidable')
const fs = require('fs')
const path = require('path')
const fileDir = path.resolve('./uploads')
if(!fs.existsSync(fileDir))fs.mkdirSync(fileDir)
let start=0// 文件上传的起始位置
form.on('field',(name,value)=>{
if(name === 'hash'){
hash = value
}else if(name === 'name'){
name = value
}else if(name ==='start'){
start = Number(value)
}
})
form.on('file',(_,file)=>{
const filePath = path.join(fileDir, name)
const stream = fs.createWriteStream(filePath, { start, flags: 'a' })
fs.createReadstream(file.path).pipe(stream)// 将当前分片写入指定文件
stream.on('close',()=>{
res.status(200).json({ message:'chunk uploaded'})
})
})
form.on('end',()=>{
if(fs.statSync(path.join(fileDir,name)).size === size){
// 如果文件已上传完成
//将文件信息存入数据库
const newFile = new File({} else {
res.status(200).json({ message: '分块上传成功' })
}
name: name ,
hash: hash,
size: size,
type: type,
url:/uploads/${name}
})
newFile.save((err,file)=>{
if(err){
console.error(err)
res.status(500).json({ message:"服务器端错误' })
}else {
res.status(200).json({ message: '文件上传成功', url: file.url })
}
})
}
})