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