跳到主要內容
黯羽輕揚每天積累一點點

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

足夠以假亂真

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論