使用nodejs原生API实现文件上传下载

搭建HTTP服务器

  使用原生http模块创建一个服务器提供给前端访问,访问路径为localhost:8000

const http = require("http");
const path = require("path");
const querystring = require("querystring");

const { getPostData, downloadFile } = require("./util");

 const server = http.createServer((req, res) => {

 const { method, url } = req;

 if (method === "POST" && url === "/download") { //post方法下载

    getPostData(req).then((params) => {

      if (params && querystring.parse(params).type_id == 1) {
        const filePath = path.join(__dirname, "1.txt");
        downloadFile(filePath, res);
      }

    })

  } else if (method === "POST" && url === "/upload") { //post方法上传

    getPostData(req).then((params) => {

      console.log(params);

      res.end(JSON.stringify(params));

    })

  } else if (method === "POST" && url === "/post_data") { //普通post请求

    getPostData(req).then((params) => {

      console.log(params);

      res.end(JSON.stringify(params));

    })

  }

})

server.listen(8000);

 

下载功能

/**
 * 下载文件
 */
const downloadFile = (pathUrl, res) => {

  const readStream = fs.createReadStream(pathUrl);

  const stats = fs.statSync(pathUrl);

  const filename = path.basename(pathUrl);

  res.writeHead(200, {
    'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
    'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
    'Content-Length': stats.size
  });

  readStream.pipe(res);

}

使用原生模块fs创建一个读流,读的是提供给用户下载的文件地址pathUrl,res需要设置头信息Content-Type,Content-Disposition,Content-Length告诉浏览器这是一个二进制文件需要下载,最后要将读流通过pipe通过流向res就实现了文件的下载.需要格外注意的是前端不要使用ajax异步请求下载,因为ajax无法触发浏览器的下载机制.

 

文件上传

//Buffer类型的数据有indexOf和slice方法,但是没有split方法所以要手动实现一个
Buffer.prototype.split = Buffer.prototype.split || function (spl) {
  let arr = [];
  let cur = 0;
  let n = 0;
  while ((n = this.indexOf(spl, cur)) != -1) {
    arr.push(this.slice(cur, n));
    cur = n + spl.length
  }
  arr.push(this.slice(cur))
  return arr
}


/**
 * 获取post过来的数据
 */
const getPostData = (req) => {

  return new Promise((resolve) => {

    let chunk = [];

    req.on("data", (buffer) => {
      chunk.push(buffer);
    })

    req.on("end", () => {

      const buffers = Buffer.concat(chunk);

      let boundary = req.headers['content-type'].split('; ')[1]; //获取boundary分隔符,上传文件的时候在req的Content-Type中会带这个参数

      if (boundary && boundary.includes("boundary")) { //触发文件下载
        boundary = fileHandler(boundary, buffers, req); //将文件上传解析完毕的参数赋值给boundary
      } else {
        boundary = null;
      }

      if (boundary) { //文件上传

        resolve(boundary);

      } else { //普通post请求

        let data = buffers.toString();

        resolve(data);

      }


    })

  })

}


const fileHandler = (str, buffers, req) => {

  let boundary = '--' + str.split('=')[1];

  //第一步使用boundary切割,形式类似于------WebKitFormBoundary8QtZlZVe7Tqym8tG

  let result = buffers.split(boundary);

  //第二步丢弃首部的空元素和尾部的--元素

  result.shift();
  result.pop();

  /**
   * 第三步现在只剩下两种形式
   * 
   * \r\n数据描述\r\n\r\n数据值\r\n 和 \r\n数据描述1\r\n数据描述2\r\n\r\n文件内容\r\n
   * 
   */
  result = result.map(buffer => buffer.slice(2, buffer.length - 2))

  /**
   * 第四步 现在只剩下两种数据形式
   * 
   * 数据描述\r\n\r\n数据值 和 数据描述1\r\n数据描述2\r\n\r\n文件内容
   * 
   * 继续使用\r\n\r\n进行切割
   */

  result = result.map((buffer) => {
    return buffer.split("\r\n\r\n");
  })

  /**
   * 第五步要组合数据了
   * 
   */
  let obj = {};

  result.forEach((array) => {

    /**
     *   普通表单数据格式
     *   Content-Disposition: form-data; name="type_id"
     *   1 
     */
    if (array[0].indexOf("\r\n") == -1) { //普通表单数据

      const arr = array[0].toString().split(";");

      let key = arr[arr.length - 1].split("=")[1].toString();

      key = key.replace(/['"]/ig, "");

      obj[key] = array[1].toString();

    } else {//文件数据
      /**
       Content-Disposition: form-data; name="photo"; filename="854138674122619089.jpg"
       Content-Type: image/jpeg

       xxxxxxxxxxxxxx一堆二进制数据
     */
      const fileData = array[1]; //承载二进制的文件数据
      const arr = array[0].split("\r\n")[0].split(";");
      let filename = arr[arr.length - 1].split("=")[1].toString();
      filename = filename.replace(/['"]/ig, "");
      const filePath = `/upload/${filename}`;

      //现在要开始写文件了
      fs.writeFile(path.join(__dirname, filePath), fileData, (err) => {
        console.log(err);
      })

      obj.fileUrl = filePath;


    }

  })

  return obj;

}

文件上传功能相对来说就比较复杂了,主方法是getPostData,通过req监听"data"和"end"方法先将用户上传的数据(其实是Buffer类型的)存储在chunk数组中,利用Buffer.concat这个API将chunk数组合成一个大的Buffer即上面定义的变量buffers.此时我们要通过截取request的Header头部有没有boundary字段来判断用户有没有上传文件,boundary字段的值类似于

------WebKitFormBoundary8QtZlZVe7Tqym8tG

我们需要这个字段值来解析用户上传的文件数据buffers,最终目的是要得到上传的文件名和后缀以及附属传递过来的key-value值和纯文件的二进制数据.而这三类数据需要通过解析buffers才能得到,具体的实现过程在fileHandler方法中.原理是根据浏览器上传的数据报文的格式的规律对数据进行一系列处理最后分解出我们想要获得的三类数据.其中我们需要将纯文件的二进制数据和文件名作为参数使用fs.writeFile来生成文件,并将附属传递过来的key-value值和文件路径作为结果值返回.

 

点击下载完整demo

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值