掌握现代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/multer

2.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/sharp

3.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 = `
            
                
                
                    ${watermarkText}
                
            
        `;
        
        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加速
  • 完整的上传流程包含验证、处理、存储、清理等环节

练习任务:

  1. 实现用户头像上传和裁剪功能
  2. 创建图片相册的多文件上传系统
  3. 集成阿里云OSS或腾讯云COS云存储
  4. 实现文件管理后台(列表、删除、统计)
  5. 添加文件水印和版权保护功能

💡💡 常见问题

Q:如何限制用户上传文件的大小和类型?
A:通过Multer的limits和fileFilter配置,前端也可以使用accept属性进行初步过滤

Q:云存储和本地存储如何选择?
A:小项目用本地存储,大项目用云存储。云存储提供更好的扩展性和CDN加速

Q:如何实现图片的实时处理?
A:可以使用Sharp库进行实时处理,或通过云服务的图片处理API

Q:大文件上传如何优化?
A:实现分片上传、断点续传、进度显示等功能

👉👉 下一篇预告

在下一篇中,我们将学习项目部署与性能优化,掌握Docker容器化、Nginx配置、性能监控等生产环境必备技能!

分类: 「从零到上线」Web开发新手村 🔧后端修炼 标签: 文件上传云存储Multer图片处理OSS

评论

暂无评论数据

暂无评论数据

目录