Express 处理单文件、多文件、切片上传

169 阅读4分钟

Express 处理单文件、多文件、切片上传

我正在参加「掘金·启航计划」。

作为一个小菜鸡除了CV 以外我想还应该学会记录,不然以后CV 都找不到地方CV ,这是我的第一篇文章,我想以后若是可以的话 在各个平台学习的知识都总结到这里来做个记录分享给大家,大家相互监督学习。写这篇文章起因是看到了某站 前端三十的课,所以想着记录一下,主要是脑子笨记不住怕以后找不到了。

下面开始正文简绍:

1、项目目录

下面是目录结构
server
---config
       bd.js  //这是连接 mongooseDB 数据库
---controller
        fileupload // 这是文件上传的所有接口
        todolist  // 这个是 我增删改查练习的 几个接口
---FileStorage
        这里用来存储文件/图片等
---models
        Post.js //这个是 todolist 接口定义的数据类型
---routes
        main.js //这是路由表
.env
app.js
package.json

2、./app.js

require('dotenv').config();

const express = require('express');
const cors =require('cors');
// const expressLayout = require('express-ejs-layouts')
const connectDB = require('./server/config/db');

const app = express();
const POST = process.env.POST || 5000;

// 配置跨域内容处理
app.use(cors({
    origin:'http://localhost:3000', // 允许访问来源
    methods: 'GET,POST,OPTIONS,PUT,DELETE', // 允许的 http 方法
    allowedHeaders: 'Content-Type,Authorization', // 允许的HTTP头部
    credentials: true, // 允许传递身份验证凭据 (例如,cookies)
}))

app.use(express.json()); // 解析 JSON 请求体
app.use(express.urlencoded({ extended: true })); // 解析 URL 编码的请求体

// connect to DB
connectDB();

// app.use(express.static('public'));

app.use('/',require('./server/routes/main'))

app.listen(POST,()=>{
    console.log(`App listening om port ${POST}`);
})

3、server/config/db.js

const mongoose = require('mongoose');
const connectDB = async () => {
    try {
        mongoose.set('strictQuery', false);
        console.log(process.env.MONGODB_URL, 88888)
        const conn = await mongoose.connect(process.env.MONGODB_URL,{
            useNewUrlParser: true,
            useUnifiedTopology: true,
            serverSelectionTimeoutMS: 10000, // 超时时间为 60 秒
        });
        console.log(`Database Connected:   ${conn.connection.host}`);
    } catch (error) {
        console.log(error);
    }
}

module.exports = connectDB;

4、controller/fileupload/index.js

// MulterError: Unexpected field  在这里upload.single()中的名字要与前端formdata的字段名一致!
const path = require('path');
const fs = require('fs');
const multer = require('multer');

// const uploadDirectory = 'uploads'; // 这是你存储上传文件的目录
// const uploadsDirectory = path.join(__dirname, 'uploads');
const uploadsDirectory = path.join(process.cwd()+'/server/FileStorage', 'BigFile');
// 检查目录是否存在,如果不存在则创建它
if (!fs.existsSync(uploadsDirectory)) {
  fs.mkdirSync(uploadsDirectory);
}
/**
 * 
 * @param {*} req
 * @param {*} res
 * 切片上传
 */
 const uploadFile = (req, res) => {
    const { filename, chunkNumber } = req.params;
    const filePath = path.join(uploadsDirectory, filename);
    const chunkFilePath = `${filePath}.part${chunkNumber}`;
    const chunkData = req.file.path; // 假设客户端发送了chunkData字段
    console.log(req.body);
    // 检查chunkData是否为undefined
    if (typeof chunkData !== 'undefined') {
        fs.readFile(chunkData,(err,data)=>{
            // console.log(data, 88888)
            fs.writeFile(chunkFilePath, data, (err) => {
                if (err) {
                    console.error('文件块写入失败', err);
                    res.status(500).send('文件块写入失败');
                } else {
                    res.send('文件块上传成功');
                }
            });
        })
        // 保存上传的文件块
    } else {
        console.error('文件块数据为undefined');
        res.status(400).send('文件块数据为undefined');
    }
};

// 在所有切片上传完成后,合并文件
const mergeFile = (req, res) => {
    const { filename } = req.params;
    const filePath = path.join(uploadsDirectory, filename);
    // 获取所有切片文件
    console.log(fs.readdirSync(uploadsDirectory), 'fs.readdirSync(uploadsDirectory)')
    const regex = new RegExp(`${filename}\\.part\\d+`);
    const chunkFiles = fs.readdirSync(uploadsDirectory)
      .filter((file) => regex.test(file));

    // 合并文件
    const writeStream = fs.createWriteStream(filePath);

    chunkFiles.forEach((chunkFile) => {
      const chunkFilePath = path.join(uploadsDirectory, chunkFile);
      const chunkData = fs.readFileSync(chunkFilePath);
    //   console.log(chunkData,9999999999)
      writeStream.write(chunkData);

        //   fs.unlinkSync(chunkFilePath); // 同步删除 删除已合并的切片文件
        console.log(chunkFilePath,8888888)
        fs.unlink(chunkFilePath,(err)=>{
            if(err){
                console.error('文件删除出错了')
            }
        })
    });
    writeStream.end();
    res.status(200).send('文件合并成功');
};

const download= (req, res) => {
    const filename = req.params.filename;
    const filePath = path.join(uploadsDirectory, filename);

    // 检查文件是否存在
    if (!fs.existsSync(filePath)) {
        res.status(404).send('File not found');
        return;
    }

    // 读取文件并发送响应
    fs.readFile(filePath, (err, datas) => {
        if (err) {
            console.error(`Error reading file: ${err}`);
            res.status(500).send('Internal Server Error');
            return;
        }

        // 推断文件类型
        // path.extname(filename) 获取文件名的扩展名,文件后缀格式
        // 推断文件类型
        const fileType = path.extname(filename).slice(1);
        const contentType = `video/${fileType}`;

        // 设置响应头
        res.setHeader('Content-Type', contentType);
        res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
        res.setHeader('Content-Length', datas.length);
        // 发送文件数据
         res.status(200).send(datas);
    });
}

const ImageFileDirectory = path.join(process.cwd()+'/server/FileStorage', 'ImageFile');
const MoreFileUpload = (req, res) => {
    // req.files 包含上传的文件数组
    // 遍历上传的每个文件
    req.files.forEach(async (file, index) => {
        const filePath = path.join(ImageFileDirectory, file.originalname);

        // 确保 file.buffer 存在且为有效数据
        if (!file.buffer || !(file.buffer instanceof Buffer)) {
            console.error(`Invalid file buffer for file ${index + 1}`);
            return
        }
        // 检查目录是否存在,如果不存在则创建它
        if (!fs.existsSync(ImageFileDirectory)) {
            fs.mkdirSync(ImageFileDirectory, { recursive: true });
        }
        // 将文件写入磁盘
        try {
            fs.writeFile(filePath, file.buffer);
            console.log(`File ${index + 1} saved successfully: ${filePath}`);
        } catch (err) {
            console.error('Error writing file:', err);
            return res.status(500).json({ error: 'Internal Server Error' });
        }
    });

    res.status(200).json({ message: 'Files uploaded and saved successfully' });
}

module.exports = {
    uploadFile,
    mergeFile,
    download,
    MoreFileUpload,
};

5、todo/list/index.js

const Post = require("../../models/Post");

/**
 * 
 * @param {*} req 客户端请求的信息
 * @param {*} res 服务器对客户端请求的响应。它用于将数据发送回客户端的请求。
 * @returns todolist 的列表数据
 */
const todosList = async (req, res) => {
  const locals = {
    title: "NodeJs Blog",
    description: "这是描述,不知道该写啥",
  };
  console.log(55);
  // const data = await Post.find(); // 总的查询
  // 分页数据查询
  let perPage = 10;
  let page = req.query.page || 1;
  const data = await Post.aggregate([{ $sort: { creatdAt: -1 } }])
    .skip(perPage * page - perPage)
    .limit(perPage)
    .exec();

  const count = await Post.count();
  const nextPage = parseInt(page) + 1;
  const hasNextPage = nextPage <= Math.ceil(count / perPage);

  // console.log(data);
  // res.render('index', locals);
  // res.send(`${data},888${}`);
  res.json({
    status: 200,
    data: data,
    per_page: hasNextPage ? hasNextPage : null,
  });
};

/**
 *
 * @param {*} req 客户端请求的信息
 * @param {*} res 服务器对客户端请求的响应。它用于将数据发送回客户端的请求。
 * @returns 数据创建接口
 */
const todoCreate = async (req, res) => {
  const newData = req.body; // 从请求体获取新数据
  console.log(newData, 8888);
  try {
    await Post.create(newData); // 在数据库中创建新数据
    res.json({
      status: 200,
      msg: "创建成功!",
    });
  } catch (error) {
    res.status(500).json({ error: error });
  }
};
/**
 *
 * @param {*} req 客户端请求的信息
 * @param {*} res 服务器对客户端请求的响应。它用于将数据发送回客户端的请求。
 * @returns 数据更新接口
 */
const todoUpdate = async (req, res) => {
  console.log(req.query);
  const id = req.query.id; // 从路由参数获取要更新的数据的ID
  const updatedData = req.body; // 从请求体获取更新后的数据
  try {
    const data = await Post.findByIdAndUpdate(id, updatedData, { new: true }); // 根据ID更新数据库中的数据
    res.json({
      status: 200,
      msg: "修改成功!",
    });
  } catch (error) {
    res.status(500).json({ error: `更新数据时出错,${error}` });
  }
};
/**
 *
 * @param {*} req 客户端请求的信息
 * @param {*} res 服务器对客户端请求的响应。它用于将数据发送回客户端的请求。
 * @returns 数据删除接口
 */
const todoDel = async (req, res) => {
  console.log(req);
  const id = req.query.id; // 从路由参数获取要删除的数据的ID
  try {
    await Post.findByIdAndRemove(id); // 根据ID从数据库中删除数据
    res.json({ status: 200, message: "数据已成功删除" });
  } catch (error) {
    res.status(500).json({ error: "删除数据时出错" });
  }
};

module.exports = {
  todosList,
  todoCreate,
  todoUpdate,
  todoDel,
};

6、models/post.js

const mongoose = require('mongoose');

const Schema = mongoose.Schema;
const PostSchema = new Schema({
    title:{
        type:String,
        required:true
    },
    body:{
        type:String,
        required:true
    },
    creatdAt:{
        type:Date,
        default:Date.now
    },
    undateAt:{
        type:Date,
        default:Date.now
    }
})

module.exports = mongoose.model('Post', PostSchema)

7、../routes/main.js multer 中间间定义了两个,一个处理切片的,一个处理单文件上传和多文件上传的。我不确定能不能合并处理,若是有大佬知道可以给个建议。

const express = require('express');
const router = express.Router();
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const controllerTodos = require('../controller/todoList/index')
const controllerUpload = require('../controller/fileupload/index')
// routers
// 获取数据
router.get('/api/todos', controllerTodos.todosList);  // 获取数据
router.post('/api/create', controllerTodos.todoCreate); // 创建数据
router.put('/api/update', controllerTodos.todoUpdate); // 更新数据
router.post('/api/del', controllerTodos.todoDel); // 删除数据

// 文件上传
// const uploadDirectory = 'uploads'; // 这是你存储上传文件的目录
// const uploadsDirectory = path.join(__dirname, 'BigFile');
const uploadsDirectory = path.join(process.cwd()+'/server/FileStorage', 'BigFile');
// 检查目录是否存在,如果不存在则创建它
if (!fs.existsSync(uploadsDirectory)) {
  fs.mkdirSync(uploadsDirectory);
}
// 配置 multer 中间件
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
      cb(null, uploadsDirectory);
    },
    filename: function (req, file, cb) {
      cb(null, file.originalname);
    },
  });
const upload = multer({
  storage: storage,
 });

// 处理文件上传的路由
router.post('/upload/:filename/:chunkNumber', upload.single('chunkData'), controllerUpload.uploadFile);
// 合并文件块为完整文件
router.post('/merge/:filename',  controllerUpload.mergeFile);
// 处理下载或发送文件给客户端的路由
router.get('/download/:filename', controllerUpload.download);


// 构建动态的 multer 中间件
const ImageFileDirectory = path.join(process.cwd()+'/server/FileStorage', 'ImageFile');
// 配置 multer 中间件
const dynamicMulterMiddleware = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, ImageFileDirectory);
  },
  filename: function (req, file, cb) {
    cb(null, file.originalname);
  },
});
const uploadImage = multer({
    storage: dynamicMulterMiddleware,
    limits: {
      files: 5,
      fileSize: 1024 * 1024 * 10, // 限制文件大小,例如 5MB
    },
});
// 处理文件上传的路由
router.post('/upload/more_file', uploadImage.array('fileImage') , controllerUpload.MoreFileUpload);

router.get('/about',(req,res)=>{
    res.render('about')
});

module.exports = router;

8、vue 代码

<template>
    <div class="container mx-auto bg-silver">
        <!-- <input type="file" @change="fileChange" /> -->
        <input type="file" @change="fileChange" multiple/>
        <img style="width:200px" v-if="imgUrl" :src="imgUrl"/>
        <button @click="onSubmit">文件上传</button>
        <br/>
        <button @click="onDownload">文件下载</button>
        <video controls width="640" height="360">
            <source src="http://localhost:5000/download/video.mp4" type="video/mp4">
            Your browser does not support the video tag.
        </video>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import axios from "axios";
const imgUrl = ref('')

let fileList = ref<any>([])
const fileChange = (e:any) =>{
    // console.log(e.target.files[0]);
    /*
    const file = e.target.files[0]
    fileObj.value = file
    // if(file.size>10*24*24){
    //     console.log('文件不能大于10M')
    // }
    let _sliceBlob = new Blob([file]).slice(0,5000)
    let _sliceFile = new File([_sliceBlob],'test.jpg')
    console.log(_sliceFile)
    let fr = new FileReader();
    // fr.readAsDataURL(_sliceFile);
    fr.readAsText(_sliceFile);
    console.log(fr, 'fr')
    fr.onload = function  () {
        console.log(fr.result)
        imgUrl.value = fr.result
    }
    // console.log(_sliceBlob)
    // console.log(_sliceFile)
    // console.log(fr)
    // 做缩略图,文本预览
    */

    // 多文件上传
    if(e.target.files.length>1){
        fileList.value = fileList.value.concat(...e.target.files)
        console.log( fileList.value)
    } else{
        fileList.value.push(e.target.files[0])
    }

    // 切片上传
    // fileObj.value =  e.target.files[0]
}
 // 多文件上传 /单文件上传
const onSubmit = () => {
    // console.log(_formData, 6666)
    let _formData:any = new FormData();
    fileList.value.forEach((element:any) => {
        _formData.append(`fileImage`, element);
    });
    axios.post('http://localhost:5000/upload/more_file', _formData,{
        headers: {
                    'Content-Type': 'multipart/form-data'
                }
    })
        .then((res) => {
            console.log(res);
            fileList.value = []
            _formData = null
        })
        .catch((error) => {
            console.error('Error uploading files:', error);
        });

    // console.log(_formData, 888);

}

// // 切片上传
// let fileObj= ref<any>()
// const onSubmit = () =>{
//     // 客户端上传文件块的代码
//     let size = 1 * 1024 * 1024; // 块大小
//     let fileSize = fileObj.value.size;
//     console.log(fileSize, size);
//     let current = 0;
//     function uploadChunk(start:any, end:any) {
//         const formData = new FormData();
//         formData.append('chunkData', fileObj.value.slice(start, end));
//         // console.log('上传块数据', formData.get('file')); // 检查文件块数据是否有效
//         console.log('Sending chunk with start:', start, 'end:', end);
//         axios
//             .post(`http://localhost:5000/upload/${fileObj.value.name}/${start / size}`, formData,{
//                 headers: {
//                     'Content-Type': 'multipart/form-data'
//                 }
//             })
//             .then(() => {
//             // 继续上传下一个块
//             current += size;
//             if (current < fileSize) {
//                 uploadChunk(current, Math.min(current + size, fileSize));
//             } else {
//                  // // 所有文件块上传完毕后触发合并操作
//                 axios
//                 .post(`http://localhost:5000/merge/${fileObj.value.name}`)
//                 .then((response) => {
//                     // 文件合并完成的回调
//                     console.log('文件合并完成', response.data);
//                 })
//                 .catch((error) => {
//                     console.error('文件合并失败', error);
//                 });
//             }
//             })
//             .catch((error) => {
//             console.error('上传文件块失败', error);
//             });
//     }
//     // 开始上传文件块
//     uploadChunk(current, Math.min(current + size, fileSize));
// }

// const pathView = ref('')
const onDownload = () => {
    const filename = 'texts.mp4';
    axios
        .get(`http://localhost:5000/download/${filename}`, {
            responseType: 'arraybuffer',
            headers: {
                'Content-Type': 'application/octet-stream',
            },
        })
        .then((res) => {
            const blob = new Blob([res.data], { type: 'video/mp4' });
            // 创建一个虚拟的链接元素
            const a = document.createElement('a');
            // 设置链接元素的属性,指定下载文件的名称
            a.href = URL.createObjectURL(blob);
            a.download = filename;
            // 将链接元素添加到页面中
            document.body.appendChild(a);
            // 模拟点击链接以触发下载
            a.click();
            // 从页面中移除链接元素
            document.body.removeChild(a);
            // 释放 Blob 对象占用的资源
            URL.revokeObjectURL(a.href);
        })
        .catch((error) => {
            console.error('Error downloading file:', error);
        });
};
</script>

各位佬,希望指出不足之处,并指点我。(可以根据项目特性、代码习惯可以进行自由封装、改动) 代码中可能对于目录结构或者代码的封装不是很好,若是有大佬觉得存在问题或者有更好的方法或者思想,欢迎评论打假。