一。背景
在文件相關的數據加工等場景下,經常面臨生成的物理文件應該如何處理的問題,比如:
-
生成的文件放到哪裡,路徑存在不存在?
-
臨時文件何時清理,如何解決命名衝突,防止覆蓋?
-
並發場景下的讀寫順序如何保證?
-
……
對於讀寫物理文件帶來的這些問題,最好的解決辦法就是不寫文件。然而,一些場景下想要不寫文件可不容易,比如文件上傳
二。問題
文件上傳一般通過表單提交來實現,例如:
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);
});
足夠以假亂真
暫無評論,快來發表你的看法吧