后端修炼篇⑤:文件上传与云存储 - 多媒体处理实战
掌握现代Web应用的文件处理能力,实现高效安全的文件管理📁
学习目标
- ✅ 掌握文件上传的基本原理和安全性
- ✅ 学会使用Multer处理多类型文件上传
- ✅ 掌握图片压缩、水印等处理技术
- ✅ 学会集成云存储服务(阿里云OSS、腾讯云COS)
- ✅ 实现文件管理API和CDN加速
📖📖 教程概览
文件上传是Web应用的核心功能之一。本教程将带你从零构建完整的文件上传系统,涵盖本地存储、云存储、图片处理、安全防护等全方位知识。
💻💻 第一步:文件上传基础
1.1 文件上传原理
前端表单上传:使用FormData对象和multipart/form-data编码
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*">
<button type="submit">上传</button>
</form>AJAX异步上传:提供更好的用户体验和进度显示
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});1.2 安全风险与防护
- 文件类型验证:防止恶意文件上传
- 文件大小限制:防止DDoS攻击
- 病毒扫描:集成杀毒软件检测
- 重命名存储:防止路径遍历攻击
🛠🛠 第二步:Multer中间件实战
2.1 安装和基础配置
npm install multer
npm install -D @types/multer2.2 Multer配置文件
// config/multer.js
const multer = require('multer');
const path = require('path');
const fs = require('fs');
// 确保上传目录存在
const uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 文件存储配置
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
// 生成唯一文件名:时间戳+随机数+扩展名
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
// 文件过滤器
const fileFilter = (req, file, cb) => {
const allowedTypes = {
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
document: ['application/pdf', 'text/plain', 'application/msword'],
video: ['video/mp4', 'video/mpeg', 'video/quicktime']
};
const allAllowedTypes = [
...allowedTypes.image,
...allowedTypes.document,
...allowedTypes.video
];
if (allAllowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('不支持的文件类型'), false);
}
};
// 创建Multer实例
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB限制
files: 5 // 最多5个文件
}
});
module.exports = upload;2.3 多类型上传配置
// config/uploadConfig.js
const upload = require('./multer');
// 单文件上传(头像)
const singleAvatar = upload.single('avatar');
// 多文件上传(相册)
const multipleImages = upload.array('images', 10);
// 混合字段上传(文章+图片)
const mixedUpload = upload.fields([
{ name: 'cover', maxCount: 1 },
{ name: 'images', maxCount: 10 },
{ name: 'attachments', maxCount: 5 }
]);
// 动态配置
const dynamicUpload = (fieldName, maxCount = 1) => {
return upload.array(fieldName, maxCount);
};
module.exports = {
singleAvatar,
multipleImages,
mixedUpload,
dynamicUpload
};🌟🌟 第三步:图片处理与优化
3.1 安装图片处理库
npm install sharp jimp
npm install -D @types/sharp3.2 图片处理工具类
// utils/imageProcessor.js
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
class ImageProcessor {
// 生成不同尺寸的图片
static async generateThumbnails(filePath, sizes = []) {
const defaultSizes = [
{ width: 150, height: 150, suffix: 'thumb' },
{ width: 300, height: 300, suffix: 'small' },
{ width: 600, height: 600, suffix: 'medium' },
{ width: 1200, height: 1200, suffix: 'large' }
];
const targetSizes = sizes.length > 0 ? sizes : defaultSizes;
const results = [];
const fileInfo = path.parse(filePath);
for (const size of targetSizes) {
const outputPath = path.join(
fileInfo.dir,
`${fileInfo.name}-${size.suffix}${fileInfo.ext}`
);
await sharp(filePath)
.resize(size.width, size.height, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: 80 })
.png({ compressionLevel: 8 })
.toFile(outputPath);
results.push({
path: outputPath,
width: size.width,
height: size.height,
suffix: size.suffix
});
}
return results;
}
// 添加水印
static async addWatermark(inputPath, outputPath, watermarkText) {
const image = sharp(inputPath);
const metadata = await image.metadata();
const svgText = `
`;
const svgBuffer = Buffer.from(svgText);
return await image
.composite([{ input: svgBuffer, blend: 'over' }])
.toFile(outputPath);
}
// 压缩图片
static async compressImage(inputPath, outputPath, options = {}) {
const { quality = 80, maxWidth = 1920, maxHeight = 1080 } = options;
return await sharp(inputPath)
.resize(maxWidth, maxHeight, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality })
.png({ compressionLevel: 9 })
.webp({ quality })
.toFile(outputPath);
}
// 获取图片信息
static async getImageInfo(filePath) {
try {
const metadata = await sharp(filePath).metadata();
const stats = fs.statSync(filePath);
return {
format: metadata.format,
width: metadata.width,
height: metadata.height,
size: stats.size,
space: metadata.space,
channels: metadata.channels
};
} catch (error) {
throw new Error('无法读取图片信息: ' + error.message);
}
}
}
module.exports = ImageProcessor;🎮🎮 第四步:云存储集成
4.1 阿里云OSS配置
// config/aliyunOSS.js
const OSS = require('ali-oss');
const path = require('path');
class AliyunOSS {
constructor() {
this.client = new OSS({
region: process.env.ALIYUN_OSS_REGION,
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
bucket: process.env.ALIYUN_OSS_BUCKET
});
}
// 上传文件到OSS
async uploadFile(filePath, options = {}) {
const fileName = path.basename(filePath);
const objectName = options.folder ?
`${options.folder}/${fileName}` : fileName;
try {
const result = await this.client.put(objectName, filePath);
return {
success: true,
url: result.url,
name: result.name,
etag: result.etag
};
} catch (error) {
throw new Error('OSS上传失败: ' + error.message);
}
}
// 生成预签名URL(临时访问)
async generatePresignedUrl(objectName, expires = 3600) {
try {
const url = this.client.signatureUrl(objectName, {
expires,
method: 'GET'
});
return url;
} catch (error) {
throw new Error('生成预签名URL失败: ' + error.message);
}
}
// 删除OSS文件
async deleteFile(objectName) {
try {
await this.client.delete(objectName);
return { success: true };
} catch (error) {
throw new Error('OSS删除失败: ' + error.message);
}
}
}
module.exports = new AliyunOSS();4.2 腾讯云COS配置
// config/tencentCOS.js
const COS = require('cos-nodejs-sdk-v5');
const path = require('path');
class TencentCOS {
constructor() {
this.cos = new COS({
SecretId: process.env.TENCENT_COS_SECRET_ID,
SecretKey: process.env.TENCENT_COS_SECRET_KEY
});
this.bucket = process.env.TENCENT_COS_BUCKET;
this.region = process.env.TENCENT_COS_REGION;
}
// 上传文件到COS
async uploadFile(filePath, options = {}) {
return new Promise((resolve, reject) => {
const fileName = path.basename(filePath);
const key = options.folder ? `${options.folder}/${fileName}` : fileName;
this.cos.putObject({
Bucket: this.bucket,
Region: this.region,
Key: key,
Body: require('fs').createReadStream(filePath),
ContentLength: require('fs').statSync(filePath).size
}, (err, data) => {
if (err) {
reject(new Error('COS上传失败: ' + err.message));
} else {
resolve({
success: true,
url: `https://${data.Location}`,
etag: data.ETag
});
}
});
});
}
}
module.exports = new TencentCOS();🚀🚀 第五步:完整文件上传API
5.1 文件上传控制器
// controllers/uploadController.js
const ImageProcessor = require('../utils/imageProcessor');
const AliyunOSS = require('../config/aliyunOSS');
const fs = require('fs').promises;
const path = require('path');
class UploadController {
// 单文件上传
async uploadSingle(req, res) {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: '请选择要上传的文件'
});
}
const fileInfo = await this.processUploadedFile(req.file);
res.json({
success: true,
message: '文件上传成功',
data: fileInfo
});
} catch (error) {
// 清理上传的文件
if (req.file) {
await this.cleanupFile(req.file.path);
}
res.status(500).json({
success: false,
error: '文件上传失败: ' + error.message
});
}
}
// 多文件上传
async uploadMultiple(req, res) {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
error: '请选择要上传的文件'
});
}
const results = [];
for (const file of req.files) {
try {
const fileInfo = await this.processUploadedFile(file);
results.push(fileInfo);
} catch (error) {
results.push({
originalname: file.originalname,
success: false,
error: error.message
});
}
}
res.json({
success: true,
message: '文件上传完成',
data: results
});
} catch (error) {
res.status(500).json({
success: false,
error: '文件上传失败: ' + error.message
});
}
}
// 处理上传的文件
async processUploadedFile(file) {
const fileInfo = {
originalname: file.originalname,
filename: file.filename,
size: file.size,
mimetype: file.mimetype,
path: file.path,
uploadTime: new Date()
};
// 如果是图片,生成缩略图和处理信息
if (file.mimetype.startsWith('image/')) {
const imageInfo = await ImageProcessor.getImageInfo(file.path);
fileInfo.imageInfo = imageInfo;
// 生成缩略图
const thumbnails = await ImageProcessor.generateThumbnails(file.path);
fileInfo.thumbnails = thumbnails;
// 上传到云存储
const ossResult = await AliyunOSS.uploadFile(file.path, {
folder: 'images'
});
fileInfo.ossUrl = ossResult.url;
}
// 清理本地临时文件
await this.cleanupFile(file.path);
return fileInfo;
}
// 清理文件
async cleanupFile(filePath) {
try {
await fs.unlink(filePath);
} catch (error) {
console.warn('清理文件失败:', error.message);
}
}
// 获取文件列表
async getFileList(req, res) {
try {
const { page = 1, limit = 20, type } = req.query;
// 这里可以从数据库查询文件记录
const files = []; // 模拟数据
const total = files.length;
res.json({
success: true,
data: {
files,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
}
});
} catch (error) {
res.status(500).json({
success: false,
error: '获取文件列表失败: ' + error.message
});
}
}
// 删除文件
async deleteFile(req, res) {
try {
const { fileId } = req.params;
// 从数据库删除记录
// 从云存储删除文件
res.json({
success: true,
message: '文件删除成功'
});
} catch (error) {
res.status(500).json({
success: false,
error: '文件删除失败: ' + error.message
});
}
}
}
module.exports = new UploadController();5.2 文件上传路由
// routes/upload.js
const express = require('express');
const router = express.Router();
const uploadController = require('../controllers/uploadController');
const { authenticate, authorize } = require('../middleware/auth');
const { singleAvatar, multipleImages } = require('../config/uploadConfig');
// 需要认证的文件上传
router.post('/avatar', authenticate, singleAvatar, uploadController.uploadSingle);
router.post('/images', authenticate, multipleImages, uploadController.uploadMultiple);
// 文件管理
router.get('/files', authenticate, uploadController.getFileList);
router.delete('/files/:fileId', authenticate, authorize('admin'), uploadController.deleteFile);
// 公开上传(如用户反馈附件)
router.post('/public', multipleImages, uploadController.uploadMultiple);
module.exports = router;5.3 错误处理中间件
// middleware/uploadErrorHandler.js
const uploadErrorHandler = (error, req, res, next) => {
if (error instanceof multer.MulterError) {
let message = '文件上传错误';
switch (error.code) {
case 'LIMIT_FILE_SIZE':
message = '文件大小超过限制';
break;
case 'LIMIT_FILE_COUNT':
message = '文件数量超过限制';
break;
case 'LIMIT_UNEXPECTED_FILE':
message = '不支持的文件字段';
break;
}
return res.status(400).json({
success: false,
error: message
});
}
if (error.message.includes('不支持的文件类型')) {
return res.status(400).json({
success: false,
error: '不支持的文件类型'
});
}
next(error);
};
module.exports = uploadErrorHandler;📚📚 小结与练习
本章重点回顾:
- Multer中间件处理multipart/form-data格式文件上传
- Sharp库提供高性能的图片处理能力
- 云存储服务实现文件的可扩展存储和CDN加速
- 完整的上传流程包含验证、处理、存储、清理等环节
练习任务:
- 实现用户头像上传和裁剪功能
- 创建图片相册的多文件上传系统
- 集成阿里云OSS或腾讯云COS云存储
- 实现文件管理后台(列表、删除、统计)
- 添加文件水印和版权保护功能
💡💡 常见问题
Q:如何限制用户上传文件的大小和类型?
A:通过Multer的limits和fileFilter配置,前端也可以使用accept属性进行初步过滤
Q:云存储和本地存储如何选择?
A:小项目用本地存储,大项目用云存储。云存储提供更好的扩展性和CDN加速
Q:如何实现图片的实时处理?
A:可以使用Sharp库进行实时处理,或通过云服务的图片处理API
Q:大文件上传如何优化?
A:实现分片上传、断点续传、进度显示等功能
👉👉 下一篇预告
在下一篇中,我们将学习项目部署与性能优化,掌握Docker容器化、Nginx配置、性能监控等生产环境必备技能!
版权申明
本文系作者 @sgyyds 原创发布在孙哥博客站点。未经许可,禁止转载。
暂无评论数据