基于React实现大文件断点续传

断点续传,指的是在上传/下载的过程中,由于网络或其他原因导致上传/下载终断。可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传/下载。用户可以节省时间,提高速度

实现流程

在这里插入图片描述

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 ? <PlayCircleFilled /> : <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])
        }
      })
    })
  })
  
})

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值