import { Stats, type OpenMode } from 'node:fs'; import * as Node from 'node:fs/promises'; import { createHash, type BinaryLike } from 'node:crypto'; export type FileStats = Stats export class FileHandle { private path: string private fd: Node.FileHandle | null private get file() { if (!this.fd) { return this.path } return this.fd } constructor( path: string ) { this.path = path this.fd = null } public async text(encoding?: BufferEncoding) { if (!encoding) { encoding = "utf-8" } const fileContent = await Node.readFile(this.path, { encoding: encoding }) return fileContent } public async write(text: string, append?: boolean) { if (!append) { append = true } const flag: OpenMode = append ? "a+" : "w" await Node.writeFile(this.file, text, { flag: flag }) } public async lastChange() { const stats = await Node.stat(this.path) return stats.mtime } public async getStats() { const stats = await Node.stat(this.path) return stats } public async hash() { return hashUtil( await Node.readFile(this.file) ) } public async lock() { this.fd = await Node.open(this.path, "r+") } public async release() { if (!this.fd) { return } await this.fd.close() this.fd = null } public async delete() { await Node.unlink(this.path) } } /** * Checks if the file exists * * @param path `string` path for the file * @returns if the file is readable */ export async function doesFileExist(path: string): Promise { try { await Node.access(path) } catch { return false } return true } export async function isDir(path: string): Promise { const stats = await Node.stat(path) return stats.isDirectory() } export async function loadFile(path: string, create?: boolean): Promise { const DEFAULT_MODE = 0o600 // Safe to use // worst case: create = false -> create = false if (!create) { create = false } let fd: Node.FileHandle | null = null try { fd = await Node.open(path, "r+", DEFAULT_MODE) } catch { } // We have a FD, return it if (fd) { await fd.close() return new FileHandle(path) } if (!create) { // UGLY: make more specific throw new Error("The required file does not exist") } // We do want this to throw without catching here fd = await Node.open(path, "w", DEFAULT_MODE) await fd.close() return new FileHandle(path) } // UGLY: move this to a new file export function hashUtil(data: BinaryLike) { const hash = createHash("sha256") .update( data ).digest('base64') return hash } export async function listFiles(path: string, recursive?: boolean): Promise { if (!recursive) { recursive = false } if (!await isDir(path)) { // UGLY: be specific throw new Error("This is not a directory") } return await Node.readdir(path, {recursive: recursive}) }