nodejs:实现大文件的分段上传

问:

再打文件上传过程中遇到了问题:

javascript 复制代码
下面是我的前端代码:
<script setup lang='ts'>
import { ref } from "vue";
import api from '@/plugins/axios.js';
import { BASE_URL } from '../plugins/baseUrl'; // 确保路径正确

const selectedFile = ref(null);
const responseMessage = ref('');
const chunkSize = 1024 * 1024; // 1 MB per chunk

const handleFileChange = (event) => {
    selectedFile.value = event.target.files[0]; // 获取选中的文件
};

const uploadFile = async () => {
    console.log('进入了uploadFile函数');
    if (!selectedFile.value) {
        alert('请选择一个文件!');
        return;
    }

    const totalChunks = Math.ceil(selectedFile.value.size / chunkSize);
    let currentChunk = 0;

    while (currentChunk < totalChunks) {
        const start = currentChunk * chunkSize;
        const end = Math.min(start + chunkSize, selectedFile.value.size);
        const fileChunk = selectedFile.value.slice(start, end); // 获取当前分片

        const formData = new FormData();
        formData.append('file', fileChunk); // 将分片添加到 FormData 对象
        formData.append('chunkNumber', currentChunk + 1); // 当前分片编号
        formData.append('totalChunks', totalChunks); // 总分片数

        try {
            console.log(`上传第 ${currentChunk + 1} 个分片`);
            const response = await api.post(`${BASE_URL}/upload`, formData, {
                headers: {
                    'Content-Type': 'multipart/form-data',
                },
            });
            console.log(response, 'response');
            
            if (response.data.success) {
                currentChunk++; // 仅在当前分片成功上传后增加计数
            } else {
                throw new Error('上传失败');
            }
        } catch (error) {
            responseMessage.value = '上传失败: ' + error.response?.data?.message || error.message;
            return; // 在发生错误时停止上传
        }
    }

    responseMessage.value = '上传成功!';
};
</script>

<template>
    <div>
        <h2>文件上传示例</h2>
        <input type="file" @change="handleFileChange" accept="image/*" required />
        <button @click="uploadFile">上传文件</button>
        <div v-if="responseMessage">{{ responseMessage }}</div>
    </div>
</template>

<style scoped lang='less'></style>
下面是我的后端node代码:
// 定义上传路由
app.post('/upload', upload.single('file'), (req, res) => {
    console.log('接收到文件:', req.file);
    console.log('分片编号:', req.body.chunkNumber);
    console.log('总分片数:', req.body.totalChunks);

    if (!req.file) {
        return res.status(400).json({ message: '未选择文件' });
    }

    const totalChunks = parseInt(req.body.totalChunks);
    const originalName = req.file.originalname;

    if (parseInt(req.body.chunkNumber) === totalChunks) {
        const mergedFilePath = path.join('uploads', originalName);
        const writeStream = fs.createWriteStream(mergedFilePath);

        for (let i = 1; i <= totalChunks; i++) {
            const chunkFilePath = path.join('uploads', `${originalName}.part${i}`);
            console.log(`检查分片文件: ${chunkFilePath}`);

            // 检查分片文件是否存在
            if (fs.existsSync(chunkFilePath)) {
                const readStream = fs.createReadStream(chunkFilePath);
                readStream.pipe(writeStream, { end: false });

                readStream.on('end', () => {
                    fs.unlinkSync(chunkFilePath);
                });
            } else {
                console.error(`分片文件不存在: ${chunkFilePath}`);
                return res.status(400).json({ message: `分片文件不存在: ${chunkFilePath}` });
            }
        }

        writeStream.on('finish', () => {
            res.json({ message: '文件上传成功', filename: originalName });
        });

        writeStream.on('error', (err) => {
            console.error(err);
            res.status(500).json({ message: '合并分片失败' });
        });
    } else {
        res.json({ message: '分片上传成功', chunkNumber: req.body.chunkNumber });
    }
});
现在调用接口后参数:
file: (二进制)
chunkNumber: 1
totalChunks: 1
调用接口返回400:
{
    "message": "分片文件不存在: uploads\\blob.part1"
}
控制台报错信息:
进入了uploadFile函数
UploadFIle.vue:35 上传第 1 个分片
axios.js:42 
        
        
       POST http://localhost:3001/upload 400 (Bad Request)
结合我的前后端代码分析哪里导致的

回答:

javascript 复制代码
你的问题出在后端代码,具体来说是文件保存和合并的逻辑。

**问题分析:**

1. **分片文件命名冲突:** 你在上传每个分片时,直接使用 `originalName.part${i}` 作为文件名保存。当 `totalChunks` 为 1 时,也就是只有一个分片的情况下,这个文件名和最终合并后的文件名 `originalName` 是一样的。
2. **合并逻辑错误:** 在 `totalChunks` 为 1 的情况下,你的代码仍然会进入分片合并的逻辑,尝试读取 `originalName.part1` 文件,但此时这个文件已经被 `multer` 中间件保存成了 `originalName`,所以会报 \来保存,例如使用 UUID 或者时间戳。
2. **优化合并逻辑:**  在 `totalChunks` 为 1 的情况下,可以直接将上传的单个文件作为最终文件,不需要进行分片合并操作。

**修改后的后端代码:**

const fs = require('fs');
const path = require('path');
const multer = require('multer');
const upload = multer({ dest: 'uploads/' }); // 使用 uploads/ 目录作为临时存储目录

// ...

app.post('/upload', upload.single('file'), (req, res) => {
  // ... (获取参数部分代码不变)

  const totalChunks = parseInt(req.body.totalChunks);
  const originalName = req.file.originalname;

  // 使用临时文件名保存分片
  const chunkFilePath = path.join('uploads', `${Date.now()}-${req.file.originalname}.part${req.body.chunkNumber}`);
  fs.renameSync(req.file.path, chunkFilePath);

  if (parseInt(req.body.chunkNumber) === totalChunks) {
    // 当只有一个分片时,直接使用上传的文件
    if (totalChunks === 1) {
      const finalFilePath = path.join('uploads', originalName);
      fs.renameSync(chunkFilePath, finalFilePath);
      return res.json({ message: '文件上传成功', filename: originalName });
    }

    // ... (多个分片合并逻辑,注意修改分片文件路径)
  } else {
    res.json({ message: '分片上传成功', chunkNumber: req.body.chunkNumber });
  }
});

**修改说明:**

1.  **临时文件名:** 使用 `Date.now()}-${req.file.originalname}.part${req.body.chunkNumber}` 作为临时文件名,确保唯一性。
2.  **分片合并逻辑:** 在 `totalChunks` 为 1 时,直接将上传的单个文件重命名为最终文件名,并跳过分片合并逻辑。
3.  **分片文件路径:** 在分片合并逻辑中,需要将分片文件路径修改为新的临时文件名。

通过以上修改,你的代码就能正常处理单个分片上传的情况了



下面是完整代码:
// 定义上传路由
app.post('/upload', upload.single('file'), (req, res) => {
    console.log('接收到文件:', req.file);
    console.log('分片编号:', req.body.chunkNumber);
    console.log('总分片数:', req.body.totalChunks);

    if (!req.file) {
        return res.status(400).json({ message: '未选择文件' });
    }

    const totalChunks = parseInt(req.body.totalChunks);
    const originalName = req.file.originalname;

    // 使用临时文件名保存分片
    const chunkFilePath = path.join('uploads', `${Date.now()}-${req.file.originalname}.part${req.body.chunkNumber}`);
    fs.renameSync(req.file.path, chunkFilePath);

    if (parseInt(req.body.chunkNumber) === totalChunks) {
        // 当只有一个分片时,直接使用上传的文件
        if (totalChunks === 1) {
            const finalFilePath = path.join('uploads', originalName);
            fs.renameSync(chunkFilePath, finalFilePath);
            return res.json({ message: '文件上传成功', filename: originalName });
        }

        const mergedFilePath = path.join('uploads', originalName);
        const writeStream = fs.createWriteStream(mergedFilePath);

        for (let i = 1; i <= totalChunks; i++) {
            const partFilePath = path.join('uploads', `${Date.now()}-${originalName}.part${i}`);
            console.log(`检查分片文件: ${partFilePath}`);

            // 检查分片文件是否存在
            if (fs.existsSync(partFilePath)) {
                const readStream = fs.createReadStream(partFilePath);
                readStream.pipe(writeStream, { end: false });

                readStream.on('end', () => {
                    fs.unlinkSync(partFilePath);
                });
            } else {
                console.error(`分片文件不存在: ${partFilePath}`);
                return res.status(400).json({ message: `分片文件不存在: ${partFilePath}` });
            }
        }

        writeStream.on('finish', () => {
            res.json({ message: '文件上传成功', filename: originalName });
        });

        writeStream.on('error', (err) => {
            console.error(err);
            res.status(500).json({ message: '合并分片失败' });
        });
    } else {
        res.json({ message: '分片上传成功', chunkNumber: req.body.chunkNumber });
    }
});
相关推荐
易辰君22 分钟前
python爬虫 - 初识爬虫
开发语言·爬虫·python
长天一色30 分钟前
【C语言系统编程】【第三部分:网络编程】3.2 数据传输和协议
c语言·开发语言·网络
liangshanbo121531 分钟前
将 Intersection Observer 与自定义 React Hook 结合使用
前端·react.js·前端框架
Python私教32 分钟前
Vue3封装通用确认删除按钮实战案例
前端·javascript·vue.js
结衣结衣.37 分钟前
Python基础语法1
开发语言·笔记·python·学习·编程·编程语法
风雨「83」42 分钟前
centos已安装python3.7环境,还行单独安装python3.10环境,如何安装,具体步骤
linux·开发语言·python
林中白虎1 小时前
使用CSS实现酷炫加载
前端·css
资深前端之路1 小时前
vue2 将页面生成pdf下载
前端·vue.js·pdf
碳苯1 小时前
【rCore OS 开源操作系统】Rust HashMap应用 知识点及练习题
开发语言·rust·操作系统
hakesashou1 小时前
php与python建站的区别有哪些
开发语言·php