专栏首页黯羽轻扬Node.js 里,如何凭空创建一个以假乱真的 FileStream?

Node.js 里,如何凭空创建一个以假乱真的 FileStream?

感谢支持ayqy个人订阅号,每周义务推送1篇(only unique one)原创精品博文,话题包括但不限于前端、Node、Android、数学(WebGL)、语文(课外书读后感)、英语(文档翻译) 如果觉得弱水三千,一瓢太少,可以去 http://blog.ayqy.net 看个痛快

一.背景

在文件相关的数据加工等场景下,经常面临生成的物理文件应该如何处理的问题,比如:

  • 生成的文件放到哪里,路径存在不存在?
  • 临时文件何时清理,如何解决命名冲突,防止覆盖?
  • 并发场景下的读写顺序如何保证?
  • ……

对于读写物理文件带来的这些问题,最好的解决办法就是不写文件。然而,一些场景下想要不写文件可不那么容易,比如文件上传

二.问题

文件上传一般通过表单提交来实现,例如:

var FormData = require('form-data');
var fs = require('fs');

var form = new FormData();
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));
form.submit('example.org/upload', function(err, res) {
  console.log(res.statusCode);
});

(摘自Form-Data)

不想写物理文件的话,可以这样做:

const FormData = require('form-data');

const filename = 'my-file.txt';
const content = 'balalalalala...变身';

const formData = new FormData();
// 1.先将字符串转换成Buffer
const fileContent = Buffer.from(content);
// 2.补上文件meta信息
formData.append('file', fileContent, {
  filename,
  contentType: 'text/plain',
  knownLength: fileContent.byteLength
});

也就是说,文件流除了能够提供数据外,还具有一些 meta 信息,如文件名、文件路径等,而这些信息是普通 Stream 所不具备的。那么,有没有办法凭空创建一个“真正的”文件流?

三.思路

要想创建出“真正的”文件流,至少有正反 2 种思路:

  • 给普通流添上文件相关的 meta 信息
  • 先拿到一个真正的文件流,再改掉其数据和 meta 信息

显然,前者更灵活一些,并且实现上能够做到完全不依赖文件

文件流的生产过程

沿着凭空创造的思路,探究fs.createReadStream API 的内部实现之后发现,生产文件流的关键过程如下:

function ReadStream(path, options) {
  // 1.打开path指定的文件
  if (typeof this.fd !== 'number')
    this.open();
}

ReadStream.prototype.open = function() {
  fs.open(this.path, this.flags, this.mode, (er, fd) => {
    // 2.拿到文件描述符并持有
    this.fd = fd;
    this.emit('open', fd);
    this.emit('ready');
    // 3.开始流式读取数据
    // read来自父类Readable,主要调用内部方法_read
    // ref: https://github.com/nodejs/node/blob/v10.16.3/lib/_stream_readable.js#L390
    this.read();
  });
};

ReadStream.prototype._read = function(n) {
  // 4.从文件中读取一个chunk
  fs.read(this.fd, pool, pool.used, toRead, this.pos, (er, bytesRead) => {
    let b = null;
    if (bytesRead > 0) {
      this.bytesRead += bytesRead;
      b = thisPool.slice(start, start + bytesRead);
    }
    // 5.(通过触发data事件)吐出一个chunk,如果还有数据,process.nextTick再次this.read,直至this.push(null)触发'end'事件
    // ref: https://github.com/nodejs/node/blob/v10.16.3/lib/_stream_readable.js#L207
    this.push(b);
  });
};

P.S.其中第 5 步相对复杂,this.push(buffer)既能触发下一个 chunk 的读取(this.read()),也能在数据读完之后(通过this.push(null))触发'end'事件,具体见node/lib/_stream_readable.js

重新实现文件流

既然已经摸清了文件流的生产过程,下一步自然是替换掉所有文件操作,直至文件流的实现完全不依赖文件,例如:

// 从文件中读取一个chunk
fs.read(this.fd, pool, pool.used, toRead, this.pos, (er, bytesRead) => {
  /* ... */
});

// 换成
this._fakeReadFile(this.fd, pool, pool.used, toRead, this.pos, (bytesRead) => {
  /* ... */
});

// 从输入字符串对应的Buffer中copy出一个chunk
ReadStream.prototype._fakeReadFile = function(_, buffer, offset, length, position, cb) {
  position = position || this.input._position;
  // fake read file async
  setTimeout(() => {
    let bytesRead = 0;
    if (position < this.input.byteLength) {
      bytesRead = this.input.copy(buffer, offset, position, position + length);
      this.input._position += bytesRead;
    }
    cb(bytesRead);
  }, 0);
}

即从中剔除文件操作,用基于字符串的操作去替代它们

四.解决方案

如此这般,就有了ayqy/string-to-file-stream,用来凭空创建文件流

string2fileStream('string-content') === fs.createReadStream(/* path to a text file with content 'string-content' */)`

例如:

const string2fileStream = require('string-to-file-stream');

const input = 'Oh, my great data!';
const s = string2fileStream(input);
s.on('data', (chunk) => {
  assert.equal(chunk.toString(), input);
});

生成的流同样能够具有文件 meta 信息:

const string2fileStream = require('string-to-file-stream');
const FormData = require('form-data');

const formData = new FormData();
formData.append('filetoupload', string2fileStream('my-string-data', { path: 'no-this-file.txt' }));
form.submit('http://127.0.0.1:8123/fileupload', function(err, res) {
  console.log(res.statusCode);
});

足够以假乱真

参考资料

  • fs.createReadStream(path[, options])
  • fs/streams.js
  • _stream_readable.js

本文分享自微信公众号 - 前端向后(backward-fe),作者:黯羽轻扬

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • React Async Rendering

    React放出Fiber(2017/09/26发布的v16.0.0带上去的)到现在已经快1年了,到目前(2018/06/13发布的v16.4.1)为止,最核心的...

    ayqy贾杰
  • this类型_TypeScript笔记11

    返回类型是this,表示所属类或接口的子类型(称之为有界多态性(F-bounded polymorphism)),例如:

    ayqy贾杰
  • 类_TypeScript笔记4

    TypeScript里的类的定义与ES6 Class规范一致,静态属性,实例属性,访问器等都支持:

    ayqy贾杰
  • 9.1 汽包锅炉水位自动控制

    1,水池Pool,底面积为1m2,初始液位为1m,水的初始容积为1m3,目标水位(targetLevel)控制在1.2m,实际液位(actualLevel)受入...

    周星星9527
  • 8.1 水位自动控制

    1,水池Pool,底面积为1m2,初始液位为1m,水的初始容积为1m3,目标水位(targetLevel)控制在1.2m,实际液位(actualLevel)受入...

    周星星9527
  • 聊聊rocketmq的FlushConsumeQueueService

    本文主要研究一下rocketmq的FlushConsumeQueueService

    codecraft
  • Button按钮--inject与provide

    inject 和 provider 是vue中的组合选项,需要一起使用。目的是允许一个祖先组件向其所有子孙后代注入依赖(简单地说就是祖先组件向子孙后代传值的一种...

    用户1148399
  • 聊聊rocketmq的FlushConsumeQueueService

    本文主要研究一下rocketmq的FlushConsumeQueueService

    codecraft
  • 基于 Vue 的商品主图放大镜方案

    在做电商类应用时,难免会遇到商品主图实现放大镜效果的场景,现有的基于Vue的第三方包不多并且无法直接复用,今天,我来分享一种高稳定性的基于 Vue 的图片放大镜...

    政采云前端团队
  • inputSuggest邮箱提示自动补全js插件

    deepcc

扫码关注云+社区

领取腾讯云代金券