@clusterio/lib
Version:
Shared library for Clusterio
170 lines (153 loc) • 4.66 kB
text/typescript
import assert from "assert";
import fs from "fs-extra";
import stream from "stream";
/**
* Transform splitting a stream into lines
*
* Splits up a stream of chunks by newlines and passes on the lines one by
* one removing the line feed and optional carriage return from the lines.
*
* @example
* ```ts
* let lineStream = new lib.LineSplitter({ readableObjectMode: true });
* lineStream.on("data", line => { console.log(line.toString("utf8")); });
* let fileStream = fs.createReadStream(path);
* fileStream.pipe(lineStream);
* ```
*/
export class LineSplitter extends stream.Transform {
_partial: Buffer | null = null;
_transform(chunk: Buffer, _encoding: string, callback: stream.TransformCallback) {
if (this._partial) {
chunk = Buffer.concat([this._partial, chunk]);
this._partial = null;
}
while (chunk.length) {
let end = chunk.indexOf("\n");
if (end === -1) {
this._partial = chunk;
break;
}
let next = end + 1;
// Eat carriage return as well if present
if (end >= 1 && chunk[end-1] === "\r".charCodeAt(0)) {
end -= 1;
}
let line = chunk.subarray(0, end);
chunk = chunk.subarray(next);
this.push(line);
}
callback();
}
_flush(callback: stream.TransformCallback) {
if (this._partial) {
this.push(this._partial);
this._partial = null;
}
callback();
}
}
/**
* Transform splitting a reverse stream into lines
*
* Splits up a reverse stream of chunks created by {@link
* createReverseReadStream} by newlines and passes on the
* lines one by one in reverse order removing the line feed and optional
* carriage return from the lines.
*
* @example
* ```ts
* let revLineStream = new lib.ReverseLineSplitter({ readableObjectMode: true });
* revLineStream.on("data", line => { console.log(line.toString("utf8")); });
* let revFileStream = await lib.createReverseReadStream(path);
* revFileStream.pipe(revLineStream);
* ```
*/
export class ReverseLineSplitter extends stream.Transform {
_partial: Buffer | null = null;
_transform(chunk: Buffer, _encoding: string, callback: stream.TransformCallback) {
if (this._partial) {
chunk = Buffer.concat([chunk, this._partial]);
this._partial = null;
}
while (chunk.length) {
let next = chunk.lastIndexOf("\n", -2);
if (next === -1) {
this._partial = chunk;
break;
}
let end = chunk.length - 1;
if (chunk[end] === "\n".charCodeAt(0)) {
end -= 1;
// Eat carriage return as well if present
if (end - next > 0 && chunk[end] === "\r".charCodeAt(0)) {
end -= 1;
}
}
let line = chunk.subarray(next + 1, end + 1);
chunk = chunk.subarray(0, next + 1);
this.push(line);
}
callback();
}
_flush(callback: stream.TransformCallback) {
if (this._partial) {
let end = this._partial.length - 1;
if (this._partial[end] === "\n".charCodeAt(0)) {
end -= 1;
// Eat carriage return as well if present
if (end > 0 && this._partial[end] === "\r".charCodeAt(0)) {
end -= 1;
}
}
this.push(this._partial.subarray(0, end + 1));
this._partial = null;
}
callback();
}
}
type ReadStreamOptions = Parameters<typeof fs.createReadStream>[1];
/**
* Create a file read stream running in reverse
*
* Like fs.createReadStream but provides data chunks starting from the end
* of the file and progressing towards the start of the file. The chunks
* are not reversed, thus the file content can be recreated by concatenating
* the chunks together in the reverse order they are read.
*
* @param path -
* Path to file to open. May be anything accepted by fs.open.
* @param options -
* Options to pass to fs.createReadStream. Values passed for `fd` and
* `fs` will be ignored.
*/
export async function createReverseReadStream(path: string, options?: ReadStreamOptions) {
if (typeof options === "string") {
options = { encoding: options };
}
const fileFd = await fs.open(path, "r");
const fileSize = (await fs.fstat(fileFd)).size;
let filePosition = fileSize;
const reverseFs = {
read(
fd: number,
buffer: Buffer,
offset: number,
length: number,
position: number | null | undefined,
callback: () => void,
) {
assert(fd === fileFd);
assert(position === undefined);
length = Math.min(length, filePosition);
filePosition -= length;
return fs.read(fd, buffer, offset, length, filePosition, callback);
},
open() { assert(false); },
close(fd: number, callback: () => void) { return fs.close(fd, callback); },
};
return fs.createReadStream(
"",
{ ...options, fd: fileFd, fs: reverseFs } as ReadStreamOptions, // type specification is missing fs prop.
);
}