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 });
    }
});
相关推荐
GIS开发特训营几秒前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Narutolxy1 分钟前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader9 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默19 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Cachel wood26 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端27 分钟前
0基础学前端-----CSS DAY9
前端·css
Code哈哈笑29 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
joan_8532 分钟前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
程序猿进阶32 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_4336184434 分钟前
shell 编程(二)
开发语言·bash·shell