阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 文件上传与下载处理

文件上传与下载处理

作者:陈川 阅读数:43403人阅读 分类: Node.js

文件上传处理

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌