文件上传 #

一、文件上传概述 #

1.1 为什么需要文件上传? #

  • 用户头像上传
  • 图片/视频分享
  • 文档管理
  • 数据导入

1.2 常用中间件 #

中间件 说明
multer 最流行的文件上传中间件
formidable 功能强大的表单处理库
busboy 流式解析器

二、multer基础 #

2.1 安装 #

bash
npm install multer

2.2 基本用法 #

javascript
const express = require('express');
const multer = require('multer');
const app = express();

const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('file'), (req, res) => {
    res.json({
        file: req.file,
        body: req.body
    });
});

app.listen(3000);

2.3 req.file属性 #

javascript
{
    fieldname: 'file',
    originalname: 'document.pdf',
    encoding: '7bit',
    mimetype: 'application/pdf',
    destination: 'uploads/',
    filename: 'abc123',
    path: 'uploads/abc123',
    size: 1024
}

三、上传模式 #

3.1 单文件上传 #

javascript
app.post('/avatar', upload.single('avatar'), (req, res) => {
    if (!req.file) {
        return res.status(400).json({ error: '请选择文件' });
    }
    
    res.json({
        message: '上传成功',
        file: {
            originalname: req.file.originalname,
            size: req.file.size,
            mimetype: req.file.mimetype
        }
    });
});

3.2 多文件上传(同名字段) #

javascript
app.post('/photos', upload.array('photos', 5), (req, res) => {
    res.json({
        message: '上传成功',
        count: req.files.length,
        files: req.files.map(f => ({
            originalname: f.originalname,
            size: f.size
        }))
    });
});

3.3 多文件上传(不同字段) #

javascript
app.post('/profile', upload.fields([
    { name: 'avatar', maxCount: 1 },
    { name: 'gallery', maxCount: 8 }
]), (req, res) => {
    res.json({
        avatar: req.files['avatar']?.[0],
        gallery: req.files['gallery']
    });
});

3.4 无文件上传 #

javascript
app.post('/data', upload.none(), (req, res) => {
    res.json(req.body);
});

3.5 任意字段 #

javascript
app.post('/any', upload.any(), (req, res) => {
    res.json({
        files: req.files,
        body: req.body
    });
});

四、存储配置 #

4.1 磁盘存储 #

javascript
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'uploads/');
    },
    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 upload = multer({ storage: storage });

4.2 动态目录 #

javascript
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        const userId = req.user.id;
        const dir = `uploads/${userId}/`;
        
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        
        cb(null, dir);
    },
    filename: (req, file, cb) => {
        cb(null, Date.now() + path.extname(file.originalname));
    }
});

4.3 内存存储 #

javascript
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

app.post('/upload', upload.single('file'), (req, res) => {
    const buffer = req.file.buffer;
    res.json({
        size: buffer.length,
        mimetype: req.file.mimetype
    });
});

五、文件过滤 #

5.1 按类型过滤 #

javascript
const fileFilter = (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    
    if (allowedTypes.includes(file.mimetype)) {
        cb(null, true);
    } else {
        cb(new Error('不支持的文件类型'), false);
    }
};

const upload = multer({
    storage: storage,
    fileFilter: fileFilter
});

5.2 按扩展名过滤 #

javascript
const fileFilter = (req, file, cb) => {
    const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
    const ext = path.extname(file.originalname).toLowerCase();
    
    if (allowedExts.includes(ext)) {
        cb(null, true);
    } else {
        cb(new Error('不支持的文件扩展名'), false);
    }
};

5.3 动态过滤 #

javascript
const createFileFilter = (allowedTypes) => {
    return (req, file, cb) => {
        if (allowedTypes.includes(file.mimetype)) {
            cb(null, true);
        } else {
            cb(new Error(`只支持 ${allowedTypes.join(', ')} 格式`), false);
        }
    };
};

const imageUpload = multer({
    fileFilter: createFileFilter(['image/jpeg', 'image/png'])
});

const documentUpload = multer({
    fileFilter: createFileFilter(['application/pdf', 'application/msword'])
});

六、文件大小限制 #

6.1 全局限制 #

javascript
const upload = multer({
    limits: {
        fileSize: 5 * 1024 * 1024
    }
});

6.2 完整限制配置 #

javascript
const upload = multer({
    limits: {
        fileSize: 5 * 1024 * 1024,
        files: 10,
        fields: 20,
        fieldSize: 1024 * 1024,
        parts: 100
    }
});
限制 说明
fileSize 单文件最大大小
files 最大文件数量
fields 最大字段数量
fieldSize 字段最大大小
parts 最大part数量

七、错误处理 #

7.1 基本错误处理 #

javascript
app.post('/upload', (req, res) => {
    upload.single('file')(req, res, (err) => {
        if (err instanceof multer.MulterError) {
            if (err.code === 'LIMIT_FILE_SIZE') {
                return res.status(400).json({ error: '文件太大' });
            }
            if (err.code === 'LIMIT_FILE_COUNT') {
                return res.status(400).json({ error: '文件数量超限' });
            }
            return res.status(400).json({ error: err.message });
        } else if (err) {
            return res.status(400).json({ error: err.message });
        }
        
        res.json({ file: req.file });
    });
});

7.2 错误类型 #

javascript
const multerErrors = {
    LIMIT_PART_COUNT: 'part数量超限',
    LIMIT_FILE_SIZE: '文件大小超限',
    LIMIT_FILE_COUNT: '文件数量超限',
    LIMIT_FIELD_KEY: '字段名太长',
    LIMIT_FIELD_VALUE: '字段值太长',
    LIMIT_FIELD_COUNT: '字段数量超限',
    LIMIT_UNEXPECTED_FILE: '意外的文件字段'
};

7.3 完整错误处理 #

javascript
const handleUpload = (upload) => {
    return (req, res, next) => {
        upload(req, res, (err) => {
            if (err) {
                if (err instanceof multer.MulterError) {
                    return res.status(400).json({
                        error: multerErrors[err.code] || err.message
                    });
                }
                return res.status(400).json({ error: err.message });
            }
            next();
        });
    };
};

app.post('/upload', 
    handleUpload(upload.single('file')),
    (req, res) => {
        res.json({ file: req.file });
    }
);

八、图片处理 #

8.1 安装sharp #

bash
npm install sharp

8.2 图片压缩 #

javascript
const sharp = require('sharp');

app.post('/avatar', upload.single('avatar'), async (req, res) => {
    try {
        const { buffer, originalname } = req.file;
        const filename = Date.now() + path.extname(originalname);
        const outputPath = `uploads/${filename}`;
        
        await sharp(buffer)
            .resize(300, 300, { fit: 'cover' })
            .jpeg({ quality: 80 })
            .toFile(outputPath);
        
        res.json({
            message: '上传成功',
            path: outputPath
        });
    } catch (error) {
        res.status(500).json({ error: '处理失败' });
    }
});

8.3 生成缩略图 #

javascript
app.post('/photo', upload.single('photo'), async (req, res) => {
    try {
        const { buffer, originalname } = req.file;
        const ext = path.extname(originalname);
        const baseName = path.basename(originalname, ext);
        
        const originalPath = `uploads/${Date.now()}_${originalname}`;
        const thumbnailPath = `uploads/thumbnails/${Date.now()}_${baseName}_thumb${ext}`;
        
        await sharp(buffer).toFile(originalPath);
        
        await sharp(buffer)
            .resize(200, 200, { fit: 'cover' })
            .toFile(thumbnailPath);
        
        res.json({
            original: originalPath,
            thumbnail: thumbnailPath
        });
    } catch (error) {
        res.status(500).json({ error: '处理失败' });
    }
});

九、完整示例 #

9.1 文件上传服务 #

javascript
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const sharp = require('sharp');

const app = express();

const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        const uploadDir = 'uploads/';
        if (!fs.existsSync(uploadDir)) {
            fs.mkdirSync(uploadDir, { recursive: true });
        }
        cb(null, uploadDir);
    },
    filename: (req, file, cb) => {
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
        const ext = path.extname(file.originalname);
        cb(null, uniqueSuffix + ext);
    }
});

const fileFilter = (req, file, cb) => {
    const allowedTypes = {
        image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
        document: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
        video: ['video/mp4', 'video/mpeg', 'video/quicktime']
    };
    
    const allAllowed = [...allowedTypes.image, ...allowedTypes.document, ...allowedTypes.video];
    
    if (allAllowed.includes(file.mimetype)) {
        cb(null, true);
    } else {
        cb(new Error('不支持的文件类型'), false);
    }
};

const upload = multer({
    storage: storage,
    fileFilter: fileFilter,
    limits: {
        fileSize: 10 * 1024 * 1024
    }
});

const handleUploadError = (err, req, res, next) => {
    if (err instanceof multer.MulterError) {
        const messages = {
            LIMIT_FILE_SIZE: '文件大小不能超过10MB',
            LIMIT_FILE_COUNT: '文件数量超限',
            LIMIT_UNEXPECTED_FILE: '意外的文件字段'
        };
        return res.status(400).json({ error: messages[err.code] || err.message });
    }
    if (err) {
        return res.status(400).json({ error: err.message });
    }
    next();
};

app.post('/upload/single', upload.single('file'), handleUploadError, (req, res) => {
    res.json({
        success: true,
        file: {
            originalname: req.file.originalname,
            filename: req.file.filename,
            size: req.file.size,
            mimetype: req.file.mimetype,
            path: req.file.path
        }
    });
});

app.post('/upload/multiple', upload.array('files', 5), handleUploadError, (req, res) => {
    res.json({
        success: true,
        count: req.files.length,
        files: req.files.map(f => ({
            originalname: f.originalname,
            filename: f.filename,
            size: f.size
        }))
    });
});

app.post('/upload/avatar', upload.single('avatar'), handleUploadError, async (req, res) => {
    try {
        const { path: tempPath, filename } = req.file;
        const avatarPath = `uploads/avatars/${filename}`;
        
        if (!fs.existsSync('uploads/avatars/')) {
            fs.mkdirSync('uploads/avatars/', { recursive: true });
        }
        
        await sharp(tempPath)
            .resize(200, 200, { fit: 'cover' })
            .jpeg({ quality: 80 })
            .toFile(avatarPath);
        
        fs.unlinkSync(tempPath);
        
        res.json({
            success: true,
            avatar: avatarPath
        });
    } catch (error) {
        res.status(500).json({ error: '处理失败' });
    }
});

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).json({ error: '服务器错误' });
});

app.listen(3000);

十、安全考虑 #

10.1 文件类型验证 #

javascript
const fileType = require('file-type');

app.post('/upload', upload.single('file'), async (req, res) => {
    const buffer = fs.readFileSync(req.file.path);
    const type = await fileType.fromBuffer(buffer);
    
    if (!type || !['image/jpeg', 'image/png'].includes(type.mime)) {
        fs.unlinkSync(req.file.path);
        return res.status(400).json({ error: '无效的文件类型' });
    }
    
    res.json({ file: req.file });
});

10.2 文件名清理 #

javascript
const sanitizeFilename = (filename) => {
    return filename
        .replace(/[^a-zA-Z0-9.-]/g, '_')
        .toLowerCase();
};

10.3 病毒扫描 #

javascript
const clamav = require('clamav.js');

app.post('/upload', upload.single('file'), (req, res) => {
    clamav.scan(req.file.path, (err, isInfected) => {
        if (isInfected) {
            fs.unlinkSync(req.file.path);
            return res.status(400).json({ error: '文件包含病毒' });
        }
        res.json({ file: req.file });
    });
});

十一、总结 #

文件上传要点:

概念 说明
multer 文件上传中间件
storage 存储配置
fileFilter 文件过滤
limits 大小限制
错误处理 MulterError处理
图片处理 sharp库

下一步,让我们学习模板引擎!

最后更新:2026-03-28