文件上传与下载处理
文件上传处理
Express中处理文件上传通常依赖multer
中间件。该中间件专门用于处理multipart/form-data
类型的数据,这是文件上传的标准格式。安装方式如下:
npm install multer
基础配置示例:
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// 磁盘存储配置
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/')
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname))
}
})
const upload = multer({ storage: storage })
单文件上传路由处理:
app.post('/upload', upload.single('avatar'), (req, res) => {
console.log(req.file) // 上传文件信息
res.send('文件上传成功')
})
多文件上传处理:
app.post('/upload-multiple', upload.array('photos', 12), (req, res) => {
console.log(req.files) // 文件数组
res.send(`${req.files.length}个文件上传成功`)
})
文件过滤与验证:
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png']
if (!allowedTypes.includes(file.mimetype)) {
const error = new Error('文件类型不支持')
error.code = 'LIMIT_FILE_TYPE'
return cb(error, false)
}
cb(null, true)
}
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: { fileSize: 1024 * 1024 * 5 } // 5MB限制
})
错误处理中间件:
app.use((err, req, res, next) => {
if (err.code === 'LIMIT_FILE_SIZE') {
res.status(413).json({ error: '文件大小超过限制' })
return
}
if (err.code === 'LIMIT_FILE_TYPE') {
res.status(415).json({ error: '不支持的文件类型' })
return
}
next(err)
})
文件下载处理
Express实现文件下载主要通过res.download()
方法,该方法会自动处理文件流和适当的HTTP头信息。
基本下载示例:
app.get('/download/:filename', (req, res) => {
const file = `./downloads/${req.params.filename}`
res.download(file) // 自动设置Content-Disposition头
})
带自定义文件名下载:
app.get('/download-custom-name', (req, res) => {
const file = './reports/monthly.pdf'
res.download(file, '2023-08-report.pdf') // 客户端将看到自定义文件名
})
处理不存在的文件:
const fs = require('fs')
app.get('/safe-download/:filename', (req, res) => {
const filePath = path.join(__dirname, 'downloads', req.params.filename)
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
res.status(404).send('文件不存在')
return
}
res.download(filePath)
})
})
大文件流式下载:
app.get('/stream-download/:filename', (req, res) => {
const filePath = path.join(__dirname, 'large-files', req.params.filename)
const stat = fs.statSync(filePath)
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Length': stat.size,
'Content-Disposition': `attachment; filename="${req.params.filename}"`
})
const readStream = fs.createReadStream(filePath)
readStream.pipe(res)
})
文件管理增强功能
实现文件列表API:
app.get('/api/files', (req, res) => {
fs.readdir('./uploads', (err, files) => {
if (err) {
res.status(500).json({ error: '无法读取目录' })
return
}
res.json(files.filter(file => !file.startsWith('.')))
})
})
带分页的文件列表:
app.get('/api/files/paginated', (req, res) => {
const page = parseInt(req.query.page) || 1
const limit = parseInt(req.query.limit) || 10
const offset = (page - 1) * limit
fs.readdir('./uploads', (err, files) => {
if (err) return res.status(500).send(err)
const filteredFiles = files
.filter(file => !file.startsWith('.'))
.slice(offset, offset + limit)
res.json({
total: files.length,
page,
limit,
files: filteredFiles
})
})
})
文件删除接口:
app.delete('/api/files/:filename', (req, res) => {
const filePath = path.join(__dirname, 'uploads', req.params.filename)
fs.unlink(filePath, (err) => {
if (err) {
if (err.code === 'ENOENT') {
return res.status(404).json({ error: '文件不存在' })
}
return res.status(500).json({ error: '删除失败' })
}
res.json({ success: true })
})
})
安全防护措施
文件类型白名单验证:
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.pdf']
function validateExtension(filename) {
const ext = path.extname(filename).toLowerCase()
return allowedExtensions.includes(ext)
}
app.post('/secure-upload', upload.single('file'), (req, res) => {
if (!req.file) return res.status(400).send('未上传文件')
if (!validateExtension(req.file.originalname)) {
fs.unlinkSync(req.file.path) // 删除已上传的不合法文件
return res.status(415).send('不支持的文件类型')
}
res.send('文件安全上传成功')
})
病毒扫描集成:
const { exec } = require('child_process')
function scanFile(filePath) {
return new Promise((resolve, reject) => {
exec(`clamscan ${filePath}`, (error, stdout, stderr) => {
if (error && error.code === 1) {
resolve({ infected: true, output: stdout })
} else if (error) {
reject(error)
} else {
resolve({ infected: false, output: stdout })
}
})
})
}
app.post('/scan-upload', upload.single('file'), async (req, res) => {
try {
const result = await scanFile(req.file.path)
if (result.infected) {
fs.unlinkSync(req.file.path)
return res.status(422).send('文件包含病毒')
}
res.send('文件扫描通过')
} catch (err) {
res.status(500).send('扫描服务出错')
}
})
性能优化技巧
上传进度反馈:
const progress = require('progress-stream')
app.post('/upload-with-progress', (req, res) => {
const progressStream = progress({
length: req.headers['content-length'],
time: 100 // 毫秒
})
progressStream.on('progress', (state) => {
console.log(`进度: ${Math.round(state.percentage)}%`)
// 可通过WebSocket实时推送给客户端
})
const upload = multer({ storage: storage }).single('file')
req.pipe(progressStream).pipe(req)
upload(req, res, (err) => {
if (err) return res.status(500).send(err.message)
res.send('上传完成')
})
})
分块上传处理:
const chunkUploadDir = './chunks'
app.post('/upload-chunk', upload.single('chunk'), (req, res) => {
const { chunkNumber, totalChunks, fileId } = req.body
const chunkDir = path.join(chunkUploadDir, fileId)
if (!fs.existsSync(chunkDir)) {
fs.mkdirSync(chunkDir, { recursive: true })
}
const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}`)
fs.renameSync(req.file.path, chunkPath)
const chunks = fs.readdirSync(chunkDir)
if (chunks.length === parseInt(totalChunks)) {
// 所有分块已上传,触发合并
res.json({ status: 'complete' })
} else {
res.json({ status: 'partial', received: chunks.length })
}
})
前端配合实现
React文件上传组件示例:
function FileUpload() {
const [progress, setProgress] = useState(0)
const handleUpload = async (e) => {
const file = e.target.files[0]
const formData = new FormData()
formData.append('file', file)
const config = {
onUploadProgress: progressEvent => {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
setProgress(percent)
}
}
try {
await axios.post('/upload', formData, config)
alert('上传成功')
} catch (err) {
console.error(err)
}
}
return (
<div>
<input type="file" onChange={handleUpload} />
{progress > 0 && <progress value={progress} max="100" />}
</div>
)
}
带拖放功能的HTML实现:
<div id="drop-area">
<form class="upload-form">
<input type="file" id="fileElem" multiple accept="image/*" />
<label for="fileElem">选择文件</label>
<p>或拖放文件到此处</p>
</form>
<div id="preview"></div>
</div>
<script>
const dropArea = document.getElementById('drop-area')
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false)
})
function preventDefaults(e) {
e.preventDefault()
e.stopPropagation()
}
dropArea.addEventListener('drop', handleDrop, false)
function handleDrop(e) {
const dt = e.dataTransfer
const files = dt.files
handleFiles(files)
}
function handleFiles(files) {
const formData = new FormData()
Array.from(files).forEach(file => {
formData.append('files', file)
})
fetch('/upload-multiple', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
}
</script>
云存储集成
AWS S3上传示例:
const AWS = require('aws-sdk')
const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY
})
app.post('/upload-to-s3', upload.single('file'), (req, res) => {
const fileContent = fs.readFileSync(req.file.path)
const params = {
Bucket: process.env.S3_BUCKET,
Key: `uploads/${req.file.filename}`,
Body: fileContent
}
s3.upload(params, (err, data) => {
if (err) {
return res.status(500).send(err)
}
// 上传成功后删除本地临时文件
fs.unlinkSync(req.file.path)
res.json({
location: data.Location,
key: data.Key
})
})
})
七牛云存储集成:
const qiniu = require('qiniu')
const mac = new qiniu.auth.digest.Mac(
process.env.QINIU_ACCESS_KEY,
process.env.QINIU_SECRET_KEY
)
const config = new qiniu.conf.Config()
const formUploader = new qiniu.form_up.FormUploader(config)
app.post('/upload-to-qiniu', upload.single('file'), (req, res) => {
const putPolicy = new qiniu.rs.PutPolicy({
scope: process.env.QINIU_BUCKET
})
const uploadToken = putPolicy.uploadToken(mac)
const localFile = req.file.path
const putExtra = new qiniu.form_up.PutExtra()
const key = `uploads/${req.file.filename}`
formUploader.putFile(uploadToken, key, localFile, putExtra, (err, body, info) => {
fs.unlinkSync(localFile) // 清理临时文件
if (err) {
return res.status(500).json(err)
}
if (info.statusCode === 200) {
const url = `http://${process.env.QINIU_DOMAIN}/${body.key}`
res.json({ url })
} else {
res.status(info.statusCode).json(body)
}
})
})
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:Cookie与Session管理
下一篇:RESTful API开发支持