一.배경
파일 관련 데이터 가공 등의 장면에서, 생성된 물리 파일을 어떻게 처리해야 하는지에 대한 문제에 자주 직면합니다. 예를 들어:
-
생성된 파일은 어디에 배치하는가, 경로는 존재하는가?
-
임시 파일은 언제 정리하는가, 명명 충돌을 어떻게 해결하고 덮어쓰기를 방지하는가?
-
병행 장면에서의 읽기 쓰기 순서를 어떻게 보장하는가?
-
……
물리 파일의 읽기 쓰기로 인한 이러한 문제들에 대해, 최선의 해결책은 파일을 쓰지 않는 것입니다. 그러나, 일부 장면에서는 파일을 쓰지 않는 것이 그렇게 쉽지 않습니다. 예를 들어 파일 업로드
二.문제
파일 업로드는 일반적으로 폼 제출을 통해 구현합니다. 예를 들어:
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);
});
충분히 가짜와 구별할 수 없을 정도입니다
아직 댓글이 없습니다