import { Lock } from "@hibas123/utils"; import * as fs from "fs"; import * as path from "path"; import { Adapter, Message, Formatted } from "@hibas123/logging"; const MAX_FILE_SIZE = 500000000; export class LoggingFiles implements Adapter { file: Files; constructor(private filename: string, private maxFileSize = MAX_FILE_SIZE) {} init() { if (!this.file) { this.file = Files.getFile(this.filename); this.file.init(this.maxFileSize); } } flush(sync: boolean) { this.file.flush(sync); } onMessage(message: Message) { let msg = Buffer.from(Formatted.strip(message.text) + "\n"); this.file.write(msg); } close() { this.file.close(); this.file = undefined; } } //TODO: Optimise write path const Debounce = (callback: () => void, iv = 500, max = 100) => { let to: any; let curr = 0; return { trigger: () => { curr++; if (curr >= max) { if (to) { clearTimeout(to); to = undefined; } curr = 0; callback(); } else if (!to) { to = setTimeout(() => { to = undefined; curr = 0; callback(); }, iv); } }, }; }; export class Files { private open = 0; private static files = new Map(); static getFile(filename: string): Files { filename = path.resolve(filename); let file = this.files.get(filename); if (!file) { file = new Files(filename); this.files.set(filename, file); } file.open++; return file; } private maxFileSize = MAX_FILE_SIZE; private size: number = 0; private stream: fs.WriteStream = undefined; private lock = new Lock(); private debounce = Debounce(this.checkQueue.bind(this)); #initialized = false; public get initlialized() { return this.#initialized; } private constructor(private file: string) {} public async init(maxFileSize: number) { if (this.#initialized) return; let lock = await this.lock.getLock(); this.maxFileSize == maxFileSize; await this.initializeFile(); this.#initialized = true; lock.release(); this.checkQueue(); } private async initializeFile(new_file = false) { try { if (this.stream) { this.stream.close(); } const folder = path.dirname(this.file); if (folder) { if (!(await fsExists(folder))) { await fsMkDir(folder).catch(() => {}); //Could happen, if two seperate instances want to create the same folder so ignoring } } let size = 0; if (await fsExists(this.file)) { let stats = await fsStat(this.file); if (new_file || stats.size >= this.maxFileSize) { if (await fsExists(this.file + ".old")) await fsUnlink(this.file + ".old"); await fsMove(this.file, this.file + ".old"); } else { size = stats.size; } } this.stream = fs.createWriteStream(this.file, { flags: "a" }); this.size = size; } catch (e) { console.log(e); //TODO: is this the right behavior? process.exit(1); } } private queue: Buffer[] = []; async checkQueue() { if (this.lock.locked) return; let lock = await this.lock.getLock(); let msg: Buffer; while ((msg = this.queue.shift())) { await this.write_to_file(msg); } lock.release(); } public async close() { await this.flush(false); this.open--; if (this.open <= 0) { this.stream.close(); Files.files.delete(this.file); } } public flush(sync: boolean) { if (sync) { // if sync flush, the process most likely is in failstate, so checkQueue stopped its work. let msg: Buffer; while ((msg = this.queue.shift())) { this.stream.write(msg); } } else { return Promise.resolve().then(async () => { const lock = await this.lock.getLock(); lock.release(); await this.checkQueue(); }); } } private async write_to_file(data: Buffer) { try { if ( data.byteLength < this.maxFileSize && this.size + data.byteLength > this.maxFileSize ) { await this.initializeFile(true); } this.size += data.byteLength; this.stream.write(data); } catch (err) { // TODO: Better error handling! console.error(err); this.initializeFile(false); this.write_to_file(data); } } public write(data: Buffer) { this.queue.push(data); this.debounce.trigger(); } public dispose() {} } function fsUnlink(path) { return new Promise((resolve, reject) => { fs.unlink(path, (err) => { if (err) reject(err); else resolve(); }); }); } function fsStat(path: string) { return new Promise((resolve, reject) => { fs.stat(path, (err, stats) => { if (err) reject(err); else resolve(stats); }); }); } function fsMove(oldPath: string, newPath: string) { return new Promise((resolve, reject) => { let callback = (err?) => { if (err) reject(err); else resolve(); }; fs.rename(oldPath, newPath, function (err) { if (err) { if (err.code === "EXDEV") { copy(); } else { callback(err); } return; } callback(); }); function copy() { fs.copyFile(oldPath, newPath, (err) => { if (err) callback(err); else fs.unlink(oldPath, callback); }); } }); } function fsExists(path: string) { return new Promise((resolve, reject) => { fs.exists(path, resolve); }); } function fsMkDir(path: string) { return new Promise((resolve, reject) => { fs.mkdir(path, (err) => (err ? reject(err) : resolve())); }); }