176 lines
5.2 KiB
JavaScript
176 lines
5.2 KiB
JavaScript
|
const zlib = require('zlib');
|
||
|
const {pipeline} = require('stream');
|
||
|
const {promisify} = require('util');
|
||
|
const {createReadStream, createWriteStream} = require('fs');
|
||
|
const fs = require('fs').promises;
|
||
|
const pipe = promisify(pipeline);
|
||
|
const path = require('path');
|
||
|
const {processSize} = require('../util/configProcessors');
|
||
|
|
||
|
const {EventEmitter} = require('events');
|
||
|
const {NewFile} = require('./customEvents');
|
||
|
|
||
|
class FileRotator extends EventEmitter {
|
||
|
constructor(config) {
|
||
|
super();
|
||
|
this._startNewFile = config.startNewFile;
|
||
|
this._totalFiles = config.totalFiles;
|
||
|
this._totalSize = processSize(config.totalSize);
|
||
|
this._gzip = config.gzip;
|
||
|
this._path = config.path;
|
||
|
this._initialised = false;
|
||
|
this._currentHandle = null;
|
||
|
}
|
||
|
|
||
|
async initialise() {
|
||
|
if (this._initialised) {
|
||
|
return;
|
||
|
}
|
||
|
this._initialised = true;
|
||
|
|
||
|
await this._deleteFiles();
|
||
|
if (this._startNewFile) {
|
||
|
await this.rotate();
|
||
|
} else {
|
||
|
// Will open existing rather than immediately rotate
|
||
|
await this._initialiseNewFile();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
getCurrentHandle() {
|
||
|
return this._currentHandle;
|
||
|
}
|
||
|
|
||
|
async rotate() {
|
||
|
if (this._currentHandle) {
|
||
|
await this._closeFileHandle();
|
||
|
this._currentHandle = null;
|
||
|
}
|
||
|
if (this._gzip) {
|
||
|
await this._gzipCurrentFile();
|
||
|
}
|
||
|
await this._deleteFiles();
|
||
|
await this._moveIntermediateFiles();
|
||
|
await this._initialiseNewFile();
|
||
|
// Set by _initialiseNewFile
|
||
|
return this._currentHandle;
|
||
|
}
|
||
|
|
||
|
async shutdown() {
|
||
|
if (this._currentHandle) {
|
||
|
await this._closeFileHandle();
|
||
|
this._currentHandle = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async _initialiseNewFile() {
|
||
|
this._currentHandle = await fs.open(this._getFileName(0, false), 'a');
|
||
|
const fileInfo = await this._currentHandle.stat();
|
||
|
this.emit(NewFile, fileInfo);
|
||
|
return this._currentHandle;
|
||
|
}
|
||
|
|
||
|
async _gzipCurrentFile() {
|
||
|
const inputName = this._getFileName(0, false);
|
||
|
const outputName = this._getFileName(0, true);
|
||
|
|
||
|
try {
|
||
|
fs.stat(inputName);
|
||
|
} catch (err) {
|
||
|
if (err.code === 'ENOENT') {
|
||
|
return;
|
||
|
} else {
|
||
|
throw err;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const gzip = zlib.createGzip();
|
||
|
const source = createReadStream(inputName);
|
||
|
const destination = createWriteStream(outputName);
|
||
|
await pipe(source, gzip, destination);
|
||
|
source.close();
|
||
|
destination.close();
|
||
|
await fs.unlink(inputName);
|
||
|
}
|
||
|
|
||
|
_getFileName(number, gzip) {
|
||
|
const parsedPath = path.parse(this._path);
|
||
|
let base = `${parsedPath.name}.${String(number - 1)}${parsedPath.ext}`;
|
||
|
if (number === 0) {
|
||
|
base = parsedPath.name
|
||
|
.replace('.%N', '')
|
||
|
.replace('_%N', '')
|
||
|
.replace('-%N', '')
|
||
|
.replace('%N', '') + parsedPath.ext;
|
||
|
} else if (parsedPath.name.indexOf('%N') >= 0) {
|
||
|
base = parsedPath.name.replace('%N', String(number - 1)) + parsedPath.ext;
|
||
|
}
|
||
|
|
||
|
const isGzip = this._gzip && gzip;
|
||
|
if (isGzip) {
|
||
|
base += '.gz';
|
||
|
}
|
||
|
|
||
|
return path.join(parsedPath.dir, base);
|
||
|
}
|
||
|
|
||
|
async _deleteFiles() {
|
||
|
const filesToDelete = [];
|
||
|
// Only count backups - start at file 1 to ignore current file
|
||
|
let fileNumber = 1;
|
||
|
let bytesTotal = 0;
|
||
|
for (; ; fileNumber++) {
|
||
|
const name = this._getFileName(fileNumber, true);
|
||
|
try {
|
||
|
const result = await fs.stat(name);
|
||
|
bytesTotal += result.size;
|
||
|
if ((this._totalSize && bytesTotal > this._totalSize) || (this._totalFiles && fileNumber >= this._totalFiles)) {
|
||
|
filesToDelete.push(name);
|
||
|
}
|
||
|
} catch (err) {
|
||
|
if (err.code === 'ENOENT') {
|
||
|
break;
|
||
|
} else {
|
||
|
throw err;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
for (const file of filesToDelete) {
|
||
|
await fs.unlink(file);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async _moveIntermediateFiles() {
|
||
|
const filesToMove = [];
|
||
|
let fileNumber = 0;
|
||
|
for (; ; fileNumber++) {
|
||
|
const name = this._getFileName(fileNumber, true);
|
||
|
try {
|
||
|
const result = await fs.stat(name);
|
||
|
filesToMove.push(result);
|
||
|
} catch (err) {
|
||
|
if (err.code === 'ENOENT') {
|
||
|
break;
|
||
|
} else {
|
||
|
throw err;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// Set to last file in sequence
|
||
|
fileNumber -= 1;
|
||
|
while (fileNumber >= 0) {
|
||
|
await fs.rename(this._getFileName(fileNumber, true), this._getFileName(fileNumber + 1, true));
|
||
|
fileNumber -= 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async _closeFileHandle() {
|
||
|
if (this._currentHandle) {
|
||
|
const closePromise = this._currentHandle.close();
|
||
|
this._currentHandle = null;
|
||
|
await closePromise;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = FileRotator;
|