igir
Version:
🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.
155 lines (154 loc) • 5.3 kB
JavaScript
import fs from 'node:fs';
import Defaults from '../globals/defaults.js';
import FsPoly from './fsPoly.js';
/**
* A wrapper for readable and writable files
*/
export default class IOFile {
pathLike;
fileHandle;
size;
tempBuffer;
readPosition = 0;
fileBuffer;
constructor(pathLike, fileHandle, size) {
this.pathLike = pathLike;
this.fileHandle = fileHandle;
this.size = size;
this.tempBuffer = Buffer.allocUnsafe(Math.min(this.size, Defaults.FILE_READING_CHUNK_SIZE));
}
/**
* Return a new {@link IOFile} from a {@param pathLike}, with the {@param flags} mode.
*/
static async fileFrom(pathLike, flags) {
return new IOFile(pathLike, await fs.promises.open(pathLike,
/**
* "On Linux, positional writes don't work when the file is opened in append mode. The
* kernel ignores the position argument and always appends the data to the end of the
* file."
* @see https://nodejs.org/api/fs.html#file-system-flags
*/
flags.toString().startsWith('a') ? 'r+' : flags), await FsPoly.size(pathLike));
}
/**
* Return a new {@link IOFile} of size {@param size} from a {@param pathLike}, with the
* {@param flags} mode. If the {@param pathLike} already exists, the existing file will be
* deleted.
*/
static async fileOfSize(pathLike, flags, size) {
if (await FsPoly.exists(pathLike)) {
await FsPoly.rm(pathLike, { force: true });
}
const write = await this.fileFrom(pathLike, 'wx');
let written = 0;
while (written < size) {
const buffer = Buffer.alloc(Math.min(size - written, Defaults.FILE_READING_CHUNK_SIZE));
await write.write(buffer);
written += buffer.length;
}
await write.close();
return this.fileFrom(pathLike, flags);
}
getPathLike() {
return this.pathLike;
}
/**
* @returns if the seek position of the file has reached the end
*/
isEOF() {
return this.getPosition() >= this.getSize();
}
getPosition() {
return this.readPosition;
}
getSize() {
return this.size;
}
/**
* Seek to a specific {@param position} in the file
*/
seek(position) {
this.readPosition = position;
}
/**
* Seek to the current position plus {@param size}
*/
skipNext(size) {
this.readPosition += size;
}
/**
* @returns the next {@param size} bytes from the current seek position, without changing the
* seek position
*/
async peekNext(size) {
return this.readAt(this.readPosition, size);
}
/**
* @returns the next {@param size} bytes from the current seek position, also incrementing the
* seek position the same amount
*/
async readNext(size) {
const result = await this.readAt(this.readPosition, size);
this.readPosition += size;
return result;
}
/**
* @returns bytes of size {@param size} at the seek position {@param offset}
*/
async readAt(position, size) {
if (size > this.tempBuffer.length) {
this.tempBuffer = Buffer.allocUnsafe(size);
}
// If the file is small, read the entire file to memory and "read" from there
if (this.size <= Defaults.MAX_MEMORY_FILE_SIZE) {
if (!this.fileBuffer) {
this.tempBuffer = Buffer.alloc(0);
this.fileBuffer = await FsPoly.readFile(this.fileHandle.fd);
}
return Buffer.from(this.fileBuffer.subarray(position, position + size));
}
// If the file is large, read from the open file handle
let bytesRead = 0;
try {
bytesRead = (await this.fileHandle.read(this.tempBuffer, 0, size, position)).bytesRead;
}
catch {
// NOTE(cemmer): Windows will give "EINVAL: invalid argument, read" when reading out of
// bounds, but other OSes don't. Swallow the error.
return Buffer.alloc(0);
}
return Buffer.from(this.tempBuffer.subarray(0, bytesRead));
}
/**
* Write {@param buffer} to the current seek position
*/
async write(buffer) {
const bytesWritten = await this.writeAt(buffer, this.readPosition);
this.readPosition += buffer.length;
return bytesWritten;
}
/**
* Write {@param buffer} at the seek position {@param offset}
*/
async writeAt(buffer, position) {
const { bytesWritten } = await this.fileHandle.write(buffer, 0, buffer.length, position);
if (this.fileBuffer) {
if (position + bytesWritten > this.fileBuffer.length) {
this.fileBuffer = Buffer.concat([
this.fileBuffer,
Buffer.allocUnsafe(position + bytesWritten - this.fileBuffer.length),
]);
}
for (let i = 0; i < bytesWritten; i += 1) {
this.fileBuffer[position + i] = buffer[i];
}
}
return bytesWritten;
}
/**
* Close the underlying file handle
*/
async close() {
return this.fileHandle.close();
}
}