(极速秒传)利用md5判断上传的文件是否存在
MD5信息摘要算法,一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。
每一个文件都会生成一个不同的md5编码 例:【3a94b8ca53dea8524d16c4e39dd43a69】
优点
服务器中不会出现重复文件
极速秒传
传输文件先校验md5,减小服务器压力
思路是
在文件上传到服务器前,将文件进行MD5转换,然后将转换完后的MD5码传给服务器,服务器判断当前MD5码是否存在,如果存在就代表着服务器上已经有了跟该文件相同的文件,不再需要上传文件
// 获取md5
import SparkMD5 from 'spark-md5';
const getMd5 = async (file: File) => {
let text = await file.text(); // 将文件转换成text文本
return SparkMD5.hashBinary(text); // 通过插件进行MD5加密
}
分片上传
分片上传就是把一个大文件,按照一定的大小,分割成多个小文件分别进行上传,文件上传结束后,服务器对所有的文件进行合并(前端负责拆分、后端负责整合)
优点(分片上传/断点续传)
上传速度快
上传稳定性强
降低传输断开风险
前端拆分文件的方法
前端拿到【file】文件后,可以调用file.slice方法(Blob的方法)对文件进行拆分
const BIGSIZE = 1024 * 1024 * 2 // 2mb
// 3. 切割文件
const getFileList = (file: File): FileList[] => {
let cur = 0; // 当前分割到的位置
const fileChunkList: any[] = [] // 最终的blob数组
while (cur < file.size) {
let chunk = file.slice(cur, cur + BIGSIZE)
fileChunkList.push({
chunk: chunk,
index: fileChunkList.length
})
cur += BIGSIZE;
}
return fileChunkList
}
服务器合并文件的方法(node示例)
filesNameList = ['分片文件a','分片文件b',...] // 文件路径
// 创建可写流
let writeStream = fs.createWriteStream('最终合并后的文件路径', {
flags: 'w+',
encoding: 'base64',
})
// 合并文件
for (let i = 0; i < filesName.length; i++) {
// 读取文件
let val = await getFile(filesNameList[i])
// 写入文件
writeStream.write(val)
}
writeStream.end();
// 读取文件函数
const getFile = (path) => {
return new Promise(resolve => {
// 创建可读流
const readStream = fs.createReadStream(path, {
flags: 'r',
encoding: 'base64',
});
var count = 0;
var str = '';
readStream.on('data', (data) => { //监听
str += data
count++
})
readStream.on('end', () => { //读取结束
resolve(str)
})
});
}
断点续传
断点续传就是在分片上传逻辑的基础上,增加断开后继续上传的逻辑
一种实现思路是:可以将分片的文件命名为对应的索引,在文件断开后,根据索引判断有多少分片已经上传成功了,然后继续传输没有上传成功的分片
附代码
前端:
bigUpFile(file)
/***********************样式与提示***********************/
// 大文件上传中
const bigFileLoading = ref(false)
const loadingTextArr = ref<string[]>([])
const addText = (str: string, number: undefined | number = undefined, type = 'default') => {
loadingTextArr.value.push(str)
if (number || number === 0) {
if (type === 'add') {
progressNumber.value += number
} else {
progressNumber.value = number
}
}
}
/***********************上传代码***********************/
// 开始函数
const bigUpFile = async (file: File) => {
addText('大文件上传中...', 0)
bigFileLoading.value = true
// 1 获取文件的md5
addText('获取文件md5...', 5)
const fileMD5 = await getMd5(file) // md5
// 2. 极速秒传
addText('判断是否可以进行极速秒传...', 10)
let upEnd = await fastUpFile(file, fileMD5)
if (upEnd) {
addText('极速秒传成功!', 100)
message.success('极速秒传成功!')
emit("getList");
return
}
addText('开始分割文件...', 15)
// 3. 切割文件
let fileChunkList = await getFileList(file)
let fileNumber = fileChunkList.length // 文件分段总数
addText('校验断点续传...', 18)
// 4. 校验大文件分片上传文件数量
let execFileList = await getExecFileList(fileMD5)
if (execFileList && execFileList?.length) {
let fl: FileList[] = []
fileChunkList.forEach(item => {
if (!execFileList.includes(item.index) && !execFileList.includes(item.index + '')) {
fl.push(item)
}
})
fileChunkList = fl
addText('校验断点续传成功,继续传输...', 18)
}
addText('开始上传文件...', 20)
// 5. 开始上传任务
await createUpBigFile(fileChunkList, fileMD5, file.name, fileNumber)
addText('结束...', 100)
}
/**********************************************/
// 1 获取文件的md5
const getMd5 = async (file: File | Blob) => {
let buffer = await file.text(); // 获取buffer
return SparkMD5.hashBinary(buffer);
}
// 2. 极速秒传
const fastUpFile = async (file: File, fileMD5: string) => {
// 发送md5到服务器判断是否已经存在
const res = await api.upPage.execFile({
md5: fileMD5
});
// md5已存在,更新标签
if (res) {
await api.upPage.updateMD5({
md5: fileMD5,
name: formState.name,
})
// 极速秒传成功
return true
}
}
// 3. 切割文件
const getFileList = (file: File): FileList[] => {
let cur = 0;
const fileChunkList: any[] = [] // blob数组
while (cur < file.size) {
let chunk = file.slice(cur, cur + BIGSIZE)
fileChunkList.push({
chunk: chunk,
index: fileChunkList.length
})
cur += BIGSIZE;
}
return fileChunkList
}
// 4. 校验大文件分片上传文件数量
const getExecFileList = async (fileMD5: string): Promise<string | number[]> => {
let res = await api.upPage.execFileList({
md5: fileMD5
});
console.log('res', res)
return res
}
// 5. 开始上传任务
const createUpBigFile = async (fileList: any[], fileMD5: string, fileName: string, fileNumber: number) => {
console.log('---fileList', fileList)
let promiseList: any[] = []
fileList.forEach(async (item, i) => {
promiseList.push(up(item.chunk, item.index, fileMD5))
})
let num = Math.floor(70 / promiseList.length)
promiseList.forEach(p => p.then(res => {
addText('上传成功1个分片...', num, 'add')
return res
}))
await Promise.all(promiseList)
// 5.2 合并文件
await mergeFile(fileMD5, fileNumber, fileName, fileList)
}
// 5.1 上传
const up = async (file, i, fileMD5) => {
let formData = new FormData();
formData.append("fileName", fileMD5);
formData.append("fileIndex", `${i}`);
formData.append("file", file);
let res = await api.upPage.saveBigFile(formData);
}
// 5.2 合并文件
const mergeFile = async (fileMD5: string, fileNumber: number, fileName: string, fileList: any[]) => {
addText('合并文件中...', 90)
let res = await api.upPage.mergeFile({
fileNumber,
md5: fileMD5,
name: formState.name,
fileName,
});
console.log('res', res)
if (res.type === 'add') {
let indexList = res.fileList
let fl: any[] = []
fileList.forEach(item => {
if (!indexList.includes(item.index) && !indexList.includes(item.index + '')) {
fl.push(item)
}
})
createUpBigFile(fl, fileMD5, fileName, fileNumber)
}
}
nodejs
const filePath = path.join(__dirname, './static') // 文件上传后保存的路径
// 方法函数:添加文件到服务器
const addFileToServer = (file, filePath) => {
// 转成文件流
var _file = fs.createReadStream(file.filepath)
// 存储文件
_file.pipe(fs.createWriteStream(filePath))
}
// 方法函数:获取文件可读流
const getFile = (path) => {
return new Promise(resolve => {
const readStream = fs.createReadStream(path, {
flags: 'r',
encoding: 'base64',
});
var count = 0;
var str = '';
readStream.on('data', (data) => { //监听
str += data
count++
})
readStream.on('end', () => { //读取结束
resolve(str)
})
});
}
// 返回参数格式化
cosnt sendVal = (val) => return {...,val}
/*************************************************************************/
// 校验大文件分片上传文件数量
router.post('/execFileList', async (req, res) => {
const data = getReq(req) // 获取传参
try {
// 文件路径
let dirPath = path.join(filePath, data.md5)
// 判断是否有该文件夹
if (fs.existsSync(dirPath)) {
// 列出文件夹中文件名称
const filesName = fs.readdirSync(dirPath);
return res.send(sendVal(filesName))
} else {
return res.send(sendVal(false))
}
} catch (error) {
return res.send(sendErr(error))
}
})
// 大文件上传
router.post('/saveBigFile', async (req, res) => {
try {
var form = new formidable.IncomingForm();
form.encoding = 'utf-8';
form.keepExtensions = true;//保留后缀
form.parse(req, async (err, fields, files) => {
if (err) {
return res.send(sendErr(err))
}
let _filePath = path.join(filePath, fields.fileName)
createPath(_filePath) // 如果没有该路径,创建路径
// 添加文件到服务器
addFileToServer(files.file, `${_filePath}/${fields.fileIndex}`)
return res.send(sendVal(1))
})
} catch (error) {
return res.send(sendErr(error))
}
})
// 大文件合并
router.post('/mergeFile', async (req, res) => {
const data = getReq(req) // 获取参数
// 字段校验
let field = fieldVerification({
md5: 'string',
name: 'string',
fileName: 'string',
fileNumber: 'number'
}, data)
if (field) return res.send(sendErr(3, field))
// 登陆权限校验+获取用户信息
const userInfo = validateErr(req, "imgUp")
if (typeof (userInfo) !== 'object') return res.send(sendErr(userInfo))
try {
// 获取后缀
let extensions = data.fileName.replace(/^.*(\..*?)$/, '$1')
// 文件路径
let dirPath = path.join(filePath, data.md5)
let fileName = path.join(filePath, `${data.md5}${extensions}`)
const filesName = fs.readdirSync(dirPath);
// 校验文件完整性
if (filesName?.length !== data.fileNumber) {
return res.send(sendVal({
fileList: filesName,
type: 'add'
}))
}
// 创建合并流
let writeStream = fs.createWriteStream(fileName, {
flags: 'w+',
encoding: 'base64',
})
writeStream.on('finish', async () => {
console.log('成功');
await addFileToSQL(data.md5, data.name, data.fileName, extensions, true)
res.send(sendVal({
type: 'success'
}))
})
// 合并文件
for (let i = 0; i < filesName.length; i++) {
let _filePath = path.join(dirPath, `${i}`)
// 读取文件
let val = await getFile(_filePath)
// 写入文件
writeStream.write(val)
}
writeStream.end();
} catch (error) {
return res.send(sendErr(error))
}
})