大文件上传源码

前端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
    <input type="file" id="fileInput"/>
    <progress
        value="0"
        max="100"
        id="progress"
    ></progress>
    <button onClick={upload()}>
  上传文件
</button>
</body>
<script>
    // 计算文件哈希值的函数
    const computeFileHash = (file) => { //传入文件内容  对文件内容进行哈希值的计算
        return new Promise((resolve, reject) => {
            const chunkSize = 1 * 1024 * 1024; // 1MB
            const fileReader = new FileReader() // 创建一个FileReader对象,用于读取文件内容
            const spark = new SparkMD5.ArrayBuffer(); // 创建一个SparkMD5对象,用于计算文件哈希值
            let currentChunk = 0; // 当前处理的分片索引

            // 文件读取成功时的回调函数
            fileReader.onload = function(e) {
                spark.append(e.target.result); //将文件块的数据添加到哈希计算中
                currentChunk++;

                if(currentChunk < totalChunks){
                    loadNextChunk(); //继续加载下一个文件块
                }else{
                    const hash = spark.end(); //完成哈希值计算
                    resolve(hash); //返回计算得到的哈希值
                }
            };

            // 文件读取失败时的回调函数
            fileReader.onerror = function (e) {
                reject(e.target.error); // 返回读取错误
            };

            // 加载下一个文件块
            function loadNextChunk() {
                const start = currentChunk * chunkSize; //当前文件块的起始位置
                const end = Math.min(start + chunkSize, file.size); //当前文件块的结束位置
                const chunk = file.slice(start, end); // 提取当前文件块的数据
                fileReader.readAsArrayBuffer(chunk); // 以ArrayBuffer形式读取文件块的数据
            }

            const totalChunks = Math.ceil(file.size / chunkSize); // 总的文件块数量
            loadNextChunk(); // 开始加载第一个块
        })
    }
    async function upload(){
        const fileInput = document.getElementById('fileInput');
        const file = fileInput.files[0];

        const chunkSize = 1 * 1024 * 1024; // 设置每个分片的大小为1MB
        const totalChunks = Math.ceil(file.size / chunkSize); // 计算文件总分片数
        let currentChunk = 0; // 当前处理的分片索引

        const resumeKey = file.name + '-currentChunk'; // 用于断点续传的本地存储键名
        const resumeIndex = localStorage.getItem(resumeKey); // 当前的索引值
        if (resumeIndex != null) {
            currentChunk = parseInt(resumeIndex); // 如果存在已处理的分片索引,则更新currentChunk
        } else {
            localStorage.removeItem(resumeKey); // 否则移除本地存储的键值对
        }

        // 计算文件哈希值
        const fileHash = await computeFileHash(file);

        // 检查服务器是否已存在相同的文件
        try{
            const response = await axios.head(
                'http://localhost:3000/check-file?filehash=' + fileHash
            );
            if(response.status === 200){
                // 文件已存在,直接完成上传
                console.log('文件已存在,秒传成功');
                return;
            }
        }catch(error){
            console.log('检查文件失败',error)
        }

        while (currentChunk < totalChunks) {
            const start = currentChunk * chunkSize; //计算当前块的起始位置
            const end = Math.min(start + chunkSize, file.size); //计算当前块的结束位置
            const chunk = file.slice(start, end);  //切割文件为当前块

            
            const formData = new FormData();
            formData.append('file', chunk); //添加当前块到FormData对象
            formData.append('filename', file.name); //添加文件名到FormData对象
            formData.append('totalChunks', totalChunks); //添加总块数到FormData对象
            formData.append('currentChunk', currentChunk); //添加当前块数到FormData对象

            try{
                const res = await axios.post('http://localhost:3000/upload',formData,{
                    headers:{
                        'Content-Type':'multipart/form-data',
                    },
                }); //发送当前块的上传请求

                const { progress } = res.data; //获取当前块的上传进度
                document.getElementById('progress').value = progress; //更新进度

                localStorage.setItem(resumeKey,currentChunk);
            }catch(error){
                console.error(error);
                return;
            }
            currentChunk++; //增加当前块数,继续下一块的上传
        }

        localStorage.removeItem(resumeKey);

        try{
            const postData = { filename:file.name,totalChunks:totalChunks,fileHash:fileHash }; //构造合并请求的数据
            await http.post('http://localhost:3000/merge', postData,{
                headers: {
                'Content-Type': 'application/json'
                }
            }); //发送合并请求
        }catch(error){
            console.error(error);
        }
    }
</script>
</html>

后端:

const express = require('express');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const upload = multer({dest:'uploads/'});
const bodyParser = require('body-parser');
const app = express();

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:false}));

// 存储文件哈希值的对象
const fileHashes = {};

app.post('/upload',upload.single('file'),(req,res) => {
    const file = req.file; // 获取上传的文件对象
    const filename = req.body.filename; // 获取文件名
    const totalChunks = parseInt(req.body.totalChunks); // 获取总块数
    const currentChunk = parseInt(req.body.currentChunk); //获取当前块数
    const chunkPath = path.join(
        "uploads/",
        `${filename}-chunk-${currentChunk}`
    ); //生成当前存储路径

    const chunkStream = fs.createReadStream(file.path); //创建读取文件块的可读流
    const writeStream = fs.createWriteStream(chunkPath); //创建写入当前块的可写流

    chunkStream.pipe(writeStream); //将读取的文件块内容通过管道写入当前块的文件

    chunkStream.on("end", () => {
        fs.unlinkSync(file.path); //读取文件块的流结束后,删除临时文件
        const progress = ((currentChunk + 1) / totalChunks) * 100;
        res.json({ progress }); //相应上传成功的状态
    });
});


router.post("/merge", (req, res) => {
    const filename = req.body.filename; //获取文件名
    const totalChunks = parseInt(req.body.totalChunks);  //获取总块数
    const fileHash = req.body.fileHash //接收到哈希值

    const mergedPath = path.join("uploads", filename); //生成合并后文件的存储路径
    const writeStream = fs.createWriteStream(mergedPath); //创建写入合并后文件的可写流

    const mergeChunks = (index) => {
      if (index === totalChunks) {
        writeStream.end(); //所有块都合并完成后,关闭写入流
        res.sendStatus(200); //响应合并成功的状态
        return;
      }

      const chunkPath = path.join("uploads", `${filename}-chunk-${index}`); //获取当前块的存储路径
      const chunk = fs.readFileSync(chunkPath); //同步读取当前块的内容
      fs.unlinkSync(chunkPath); //删除已合并的块文件

      // 存储文件哈希值,以便进行响应式的检测操作
      fileHashes[fileHash] = true;

      writeStream.write(chunk,() => {
        mergeChunks(index + 1); //递归合并下一块
      });
    };

    mergeChunks(0); //从第一块开始合并
});

// 检查文件是否已存在
app.head('/check-file',(req,res) => {
  const fileHash = req.query.filehash;
  console.log(fileHash,fileHashes);
  if(fileHashes[fileHash]){
    res.sendStatus(200); //文件已存在
  }else{
    res.sendStatus(404); //文件不存在
  }
})



app.listen(3000,() => {
    console.log('Server started on port 3000');
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值