メインコンテンツへ移動

Node.js で文字列からファイルストリームを生成

無料2019-08-17#Node#nodejs create read stream from string#nodejs implements file stream#nodejs create filestream from string#Nodejs从字符串创建文件流

Node.js で、どのようにして本物と見分けがつかない FileStream を無から作成するか?

一.背景

ファイル関連のデータ加工などのシーンでは、生成された物理ファイルをどのように処理すべきかという問題に頻繁に直面します。例えば:

  • 生成されたファイルはどこに配置するか、パスは存在するか?

  • 一時ファイルはいつ清理するか、命名衝突をどのように解決し、上書きを防止するか?

  • 並行シーンでの読み書き順序をどのように保証するか?

  • ……

物理ファイルの読み書きに伴うこれらの問題に対して、最良の解決策はファイルを書かないことです。しかし、一部のシーンではファイルを書かないことはそれほど簡単ではありません。例えばファイルアップロードです

二.問題

ファイルアップロードは一般的にフォーム送信を通じて実現します。例えば:

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);
});

十分に本物と見分けがつかないほどです

参考資料

コメント

コメントはまだありません

コメントを書く