175 lines
3.3 KiB
TypeScript
175 lines
3.3 KiB
TypeScript
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<boolean> {
|
|
|
|
try {
|
|
await Node.access(path)
|
|
} catch {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
export async function isDir(path: string): Promise<boolean> {
|
|
const stats = await Node.stat(path)
|
|
|
|
return stats.isDirectory()
|
|
}
|
|
|
|
|
|
export async function loadFile(path: string, create?: boolean): Promise<FileHandle> {
|
|
|
|
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<string[]> {
|
|
|
|
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})
|
|
|
|
}
|