import SparkMD5 from 'spark-md5';
import api from '../../api/file';
import React, { useState, useRef } from 'react';
import { Button, message, Progress } from 'antd';
import { PlayCircleFilled, PauseCircleFilled } from '@ant-design/icons';
const SIZE = 1024 * 1024 * 2;
const UploadingPanel = (props: any) => {
const [abort, setAbort] = useState<boolean>(false);
const [uploadProgress, setUploadProgress] = useState<number>(0); // 上传进度
const [uploadedIndex, setUploadedIndex] = useState(0); // 上传进度
const [infoMsg, setInfoMsg] = useState<string>(''); //
const [fileInfo, setFileInfo] = useState<any>({});
const InputRef = useRef<any>(null);
const hash = useRef<any>(null); // 文件hash值
const partList = useRef<any>([]); // 分片后的文件
const abortRef = useRef<any>(false);
// 上传文件
async function uploadFile() {
setUploadProgress(0);
setInfoMsg('文件分解中');
const file = InputRef.current.files[0];
await createChunkFile(file);
}
// 3. 文件分片,根据设定的每片数据的大小,计算出当前文件可以分成多少片。将分片后的数据保存在一个数据中。
async function createChunkFile(file: any) {
if (!file) return;
const buffer = await fileParse(file);
const spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
hash.current = spark.end();
const suffix = /\.([0-9a-zA-Z]+)$/i.exec(file.name)[1];
const list = [];
const count = Math.ceil(file.size / SIZE); // 向上取整
const partSize = file.size / count; // 每次上传的大小
console.log('file', file)
let cur = 0;
for (let i = 0; i < count; i++) {
let item = {
chunk: file.slice(cur, cur + partSize),
filename: `${hash.current}_${i}.${suffix}`,
};
cur += partSize;
list.push(item);
}
console.log('list=>', list)
partList.current = list;
getLoadingFiles();
}
//转换文件类型(解析为BUFFER数据)
function fileParse(file: any) {
return new Promise((resolve) => {
const fileRead = new FileReader();
fileRead.readAsArrayBuffer(file);
fileRead.onload = (ev: any) => {
resolve(ev.target.result);
};
});
}
// 4. 确定上传索引值,到这里我们已经得到了一个文件分片后的数据列表,但现在还不能开始上传。有可能此文件在之前已经上传了一些内容,那么只需要上传剩下的内容即可。因此需要用 MD5 值查询一下应该从第几片开始上传。
async function getLoadingFiles() {
const params = {
hash: hash.current
}
api.getUploadedCount(params)
.then((res: any) => {
if (res.code === 1) {
const count = res.data.count;
setInfoMsg('文件上传中');
setUploadProgress(Number((count * 100 / partList.current.length).toFixed(2)));
uploadFn(count);
}
})
}
// 现在可以正式开始上传文件了
async function uploadFn(startIndex: number = 0) {
if (partList.current.length === 0) return;
abortRef.current = false;
const requestList: any[] = [];
partList.current.forEach((item: any, index: number) => {
const fn = () => {
let formData = new FormData();
formData.append('chunk', item.chunk);
formData.append('filename', item.filename);
return api
.uploadFile(formData)
.then((res: any) => {
const data = res.data;
if (res.code === 1) {
setUploadedIndex(index);
setUploadProgress((data.index + 1) * 100 / partList.current.length);
}
})
.catch(function () {
setAbort(true);
message.error('上传失败')
});
};
requestList.push(fn);
});
uploadSend(startIndex, requestList);
}
// 上传单个切片
async function uploadSend(index: number, requestList: any) {
if (abortRef.current) return;
if (index >= requestList.length) {
uploadComplete();
return;
}
requestList[index] ? await requestList[index]() : setInfoMsg('');
uploadSend( ++index, requestList);
}
// 上传完成
async function uploadComplete() {
const params = {
hash: hash.current
}
let result: any = await api.uploadComplete(params);
if (result.code === 1) {
message.success('上传成功');
setInfoMsg('上传完成');
}
}
// 选择文件
function changeFile() {
const file = InputRef.current.files[0];
setFileInfo(file || {});
}
return (
<div className="uploading-panel">
<input type="file" onChange={changeFile} ref={InputRef}/>
{infoMsg && (
<span>【{infoMsg}】span>
)}
<Button type="primary" onClick={uploadFile}>
上传
Button>
<Button
type="primary"
shape="circle"
icon={abort ? /> : <PauseCircleFilled />}
onClick={() => {
abortRef.current = !abort;
abort && uploadFn(uploadedIndex + 1)
setAbort(!abort)
}}
/>
<div>
<Progress percent={uploadProgress} status="active"/>
div>
div>
);
};
export default UploadingPanel;
对应接口实现
查询某个文件已经上传多少片了
router.get('/uploaded/count', async (ctx, next) => {
const {
hash
} = ctx.query;
const filePath = `${uploadDir}${hash}`;
const fileList = (fs.existsSync(filePath) && fs.readdirSync(filePath)) || [];
ctx.body = {
code: 1,
data: {
count: fileList.length
}
}
})
接收上传的文件
router.post('/upload', async (ctx, next) => {
const file = ctx.request.files.chunk // 获取上传文件
const {
filename,
} = ctx.request.body;
const reader = fs.createReadStream(file.path);
const [hash, suffix] = filename.split('_');
const folder = uploadDir + hash;
!fs.existsSync(folder) && fs.mkdirSync(folder);
const filePath = `${folder}/${filename}`;
const upStream = fs.createWriteStream(filePath);
reader.pipe(upStream);
ctx.body = await new Promise((resolve, reject) => {
reader.on('error', () => {
reject({
code: 0,
massage: '上传失败',
})
})
reader.on('close', () => {
resolve({
code: 1,
massage: '上传成功',
data: {
hash,
index: Number(suffix.split('.')[0])
}
})
})
})
})
上传完成,合并文件
router.post('/upload', async (ctx, next) => {
const file = ctx.request.files.chunk // 获取上传文件
const {
filename,
} = ctx.request.body;
const reader = fs.createReadStream(file.path);
const [hash, suffix] = filename.split('_');
const folder = uploadDir + hash;
!fs.existsSync(folder) && fs.mkdirSync(folder);
const filePath = `${folder}/${filename}`;
const upStream = fs.createWriteStream(filePath);
reader.pipe(upStream);
ctx.body = await new Promise((resolve, reject) => {
reader.on('error', () => {
reject({
code: 0,
massage: '上传失败',
})
})
reader.on('close', () => {
resolve({
code: 1,
massage: '上传成功',
data: {
hash,
index: Number(suffix.split('.')[0])
}
})
})
})
})