structured-binary-file
Version:
Reads and writes data records stored in fixed width binary. CircularFile adds 'fixed record count' files to the mix
269 lines (228 loc) • 9.4 kB
JavaScript
const Parser = require("binary-parser-encoder").Parser;
const fs = require("fs");
/**
* FixedRecordFile is a binary file that stores data in a "fixed width" format.
* Records are read and written using readRecord() and writeRecord() and specifying
* a "record number". Records are numbered "zero" thru recordCount() - 1.
* @author Joel Kozikowski
*/
module.exports = class FixedRecordFile {
/**
* Constructs a FixedRecordFile object for reading and writing
* binary records. record_parser defines the binary format of
* the record. header_extend_function is an optional parameter
* that adds specific data to the header record.
* @param {Parser} record_parser A parser from the 'binary-parser-encoder' module that
* defines the data record stored in this file. While it may contain variations in
* the record using the choice() method, it is CRITICAL that the record size be
* constant.
* @param {function (header_parser)} header_extend_function An optional function that
* can be used to add additional fields to the header record stored as the first
* record of the file. By default, the header record contains only two fields:
* recordSize and recordCount.
*/
constructor(record_parser, header_extend_function) {
this.recordParser = record_parser;
this.recordSize = this.recordParser.sizeOf();
let header_parser = new Parser()
.uint16("recordSize")
.uint32("recordCount");
if (header_extend_function) {
this.headerParser = header_extend_function(header_parser);
}
else {
this.headerParser = header_parser;
}
this.headerSize = this.headerParser.sizeOf();
this.fd = null;
}
/**
* Opens the data file for reading/writing. If the file does not exist, it is created.
* Be sure to call close() when you are finished.
* @param {string} fileName The name of the file to open.
*/
open(fileName) {
this.fileName = fileName;
if (!fs.existsSync(fileName)) {
this._createFile(fileName);
}
else {
this.fd = fs.openSync(fileName, "r+");
this.header = this.readHeader();
if (this.header.recordSize != this.recordSize) {
throw new Error(`Record size of file (${this.header.recordSize}) does not match defined record size (${this.recordSize})`);
}
}
}
/**
* Closes the data file and releases the file resources.
*/
close() {
if (this.fd) {
fs.closeSync(this.fd);
}
this.fd = null;
}
/**
* Private method used by this class, it is called by open() when the file does not exist.
* @param {*} fileName
*/
_createFile(fileName) {
this.binHeader = Buffer.alloc(this.headerSize);
this.fd = fs.openSync(fileName, "a+");
let written = fs.writeSync(this.fd, this.binHeader, 0, this.headerSize, 0);
if (written != this.headerSize) {
throw new Error(`Could not write new header (tried ${this.headerSize} bytes, wrote ${written})`);
}
// Close and re-open the file in read mode. We want to be in random access mode
// for the duration of using this file.
fs.closeSync(this.fd);
this.fd = fs.openSync(fileName, "r+");
this.header = this.createHeader();
this.header.recordSize = this.recordSize;
this.writeHeader();
}
/**
* Creates the header record when the file is first created. The default parses a
* binary version of the header that is zero filled. That means numbers will be zero,
* and strings will be of zero length.
*/
createHeader() {
return this.headerParser.parse(this.binHeader);
}
/**
* Reads the header record from the file and returns the result. This is used
* by open() when opening a pre-existing file.
*/
readHeader() {
this.binHeader = Buffer.alloc(this.headerSize);
let numRead = fs.readSync(this.fd, this.binHeader, 0, this.headerSize, 0);
if (numRead != this.headerSize) {
throw new Error(`Could not read header (wanted ${this.headerSize} bytes, read ${numRead})`);
}
return this.headerParser.parse(this.binHeader);
}
/**
* Writes the contents of this.header to the file. Should be called whenever code outside
* of this class modifies the header record.
*/
writeHeader() {
this.binHeader = this.headerParser.encode(this.header);
let written = fs.writeSync(this.fd, this.binHeader, 0, this.headerSize, 0);
if (written != this.headerSize) {
throw new Error(`Could not write header (tried ${this.headerSize} bytes, wrote ${written})`);
}
}
/**
* Reads the specified record number from the binary file, decodes it, and returns the Javascript object that
* represents it.
* @param {integer} recordNum the number to read - zero thru recordCount()-1
*/
readRecord(recordNum) {
let pos = this._recordPos(recordNum);
this.binRecord = Buffer.alloc(this.recordSize);
let numRead = fs.readSync(this.fd, this.binRecord, 0, this.recordSize, pos);
if (numRead != this.recordSize) {
throw new Error(`Could not read record #${recordNum} (wanted ${this.recordSize} bytes, read ${numRead})`);
}
let rec = this.recordParser.parse(this.binRecord);
return rec;
}
/**
* Writes the specified record to the binary file by encoding rec in its binary equivelant and writing
* that data out to the data file at the specified record number.
* @param {integer} recordNum The record to write - zero thru recordCount(). Note that existing records
* are numbered starting at zero, so specifying a value that is the same are recordCount() will in
* fact add a new record to the file. It is the same as calling appendRecord()
* @param {object} rec An object that contains the record data to encode. It must match the record_parser
* specified when this object was constructed.
*/
writeRecord(recordNum, rec) {
let pos = this._recordPos(recordNum);
this.binRecord = this.recordParser.encode(rec);
let written = fs.writeSync(this.fd, this.binRecord, 0, this.recordSize, pos);
if (written != this.recordSize) {
throw new Error(`Could not write record #${recordNum} (tried ${this.recordSize} bytes, wrote ${written})`);
}
}
/**
* Adds the specified record to the end of the file.
* @param {object} rec An object that contains the record data to encode. It must match the record_parser
* specified when this object was constructed.
*/
appendRecord(rec) {
let newRecNum = this.recordCount();
this.writeRecord(newRecNum, rec);
this.header.recordCount++;
this.writeHeader();
this.filePointer = newRecNum;
}
/**
* Used internally by this class, it calculations the actual byte position within the file
* where the specified record number starts.
* @param {integer} recordNum
*/
_recordPos(recordNum) {
return recordNum * this.recordSize + this.headerSize;
}
/**
* Returns the number of records that exist in the file.
*/
recordCount() {
if (this.header) {
return this.header.recordCount;
}
else {
throw new Error("Can not get record count - BinaryFile not opened yet.");
}
}
/**
* Retrieves and returns the first record in the file. To
* retrieve the record after that, call getNext(). null is
* returned if there are no records in the file.
*/
getFirst() {
this.filePointer = 0;
if (this.filePointer < this.header.recordCount) {
return this.readRecord(this.filePointer);
}
else {
return null;
}
}
/**
* Retrieves and returns the NEXT record in the data file following
* a call to getFirst(). null is returned if there are no more records
* remaining.
*/
getNext() {
this.filePointer++;
if (this.filePointer < this.header.recordCount) {
return this.readRecord(this.filePointer);
}
else {
return null;
}
}
/**
* Returns the physical record number of the last record retrieved with
* getFirst() or getNext(), or the physical record number of
* a record added with appendRecord() (depending on which method was
* called last)
*/
getCurrentRecordNumber() {
return this.filePointer;
}
/**
* A convenience function that iterates over every record in the file, calling
* fn(rec, recNum) with each iteration.
* @param {function(record, recordNumber)} fn A function that does something with the record
*/
forEach(fn) {
var rec = this.getFirst();
while (rec != null) {
fn(rec, this.filePointer);
rec = this.getNext();
} // while
}
}