@mieweb/wikigdrive
Version:
Google Drive to MarkDown synchronization
233 lines (232 loc) • 9.01 kB
JavaScript
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import zlib from 'node:zlib';
import Transport from 'winston-transport';
import { FileStreamRotator } from './FileStreamRotator.js';
import { DailyRotateFileProcessor } from './DailyRotateFileProcessor.js';
import { generateMD5Hash } from '../generateMD5Hash.js';
const loggerDefaults = {
json: false,
colorize: false,
eol: os.EOL,
logstash: null,
prettyPrint: false,
label: null,
stringify: false,
depth: null,
showLevel: true,
timestamp: () => {
return new Date().toISOString();
}
};
async function hash(obj) {
return await generateMD5Hash(new TextEncoder().encode(JSON.stringify(obj)));
}
function getMaxSize(size) {
if (size && typeof size === 'string') {
const _s = size.toLowerCase().match(/^((?:0\.)?\d+)([k|mg])$/);
if (_s) {
return size;
}
}
else if (size && typeof size === 'number' && Number.isInteger(size)) {
const sizeK = Math.round(size / 1024);
return sizeK === 0 ? '1k' : sizeK + 'k';
}
return null;
}
function isValidFileName(filename) {
// deno-lint-ignore no-control-regex
return !/["<>|:*?\\/\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]/g.test(filename);
}
function isValidDirName(dirname) {
// deno-lint-ignore no-control-regex
return !/["<>|\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]/g.test(dirname);
}
export class DailyRotateFile extends Transport {
constructor(options = {}) {
super(options);
Object.defineProperty(this, "name", {
enumerable: true,
configurable: true,
writable: true,
value: 'dailyRotateFile'
});
Object.defineProperty(this, "dirname", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "filename", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "logStreamMap", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "options", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.options = Object.assign({}, loggerDefaults, options);
this.filename = options.filename ? path.basename(options.filename) : 'winston.log';
this.dirname = options.dirname || path.dirname(options.filename);
if (!isValidFileName(this.filename) || !isValidDirName(this.dirname)) {
throw new Error('Your path or filename contain an invalid character.');
}
this.logStreamMap = {};
}
async getLogStream(driveId) {
driveId = driveId || '';
if (this.logStreamMap[driveId]) {
return this.logStreamMap[driveId];
}
const options = this.options;
const dirname = this.dirname.replace('%driveId%', driveId).replace('//', '/');
const logStream = new FileStreamRotator({
filename: path.join(dirname, this.filename),
frequency: options.frequency ? options.frequency : 'custom',
verbose: false,
size: getMaxSize(options.maxSize),
max_logs: options.maxFiles,
end_stream: true,
audit_file: options.auditFile ? options.auditFile : path.join(dirname, '.' + await hash(options) + '-audit.json'),
file_options: options.file_options ? options.file_options : { flags: 'a' },
utc: options.utc ? options.utc : false,
extension: options.extension ? options.extension : '',
create_symlink: options.createSymlink ? options.createSymlink : false,
symlink_name: options.symlinkName ? options.symlinkName : 'current.log'
});
logStream.on('new', (newFile) => {
this.emit('new', newFile);
});
logStream.on('rotate', (oldFile, newFile) => {
this.emit('rotate', oldFile, newFile);
});
logStream.on('logRemoved', (params) => {
if (options.zippedArchive) {
const gzName = params.name + '.gz';
if (fs.existsSync(gzName)) {
try {
fs.unlinkSync(gzName);
}
catch (_err) {
// file is there but we got an error when trying to delete,
// so permissions problem or concurrency issue and another
// process already deleted it we could detect the concurrency
// issue by checking err.type === ENOENT or EACCESS for
// permissions ... but then?
}
this.emit('logRemoved', gzName);
return;
}
}
this.emit('logRemoved', params.name);
});
if (options.zippedArchive) {
logStream.on('rotate', (oldFile) => {
const oldFileExist = fs.existsSync(oldFile);
const gzExist = fs.existsSync(oldFile + '.gz');
if (!oldFileExist || gzExist) {
return;
}
const gzip = zlib.createGzip();
const inp = fs.createReadStream(oldFile);
const out = fs.createWriteStream(oldFile + '.gz');
inp.pipe(gzip).pipe(out).on('finish', () => {
if (fs.existsSync(oldFile)) {
fs.unlinkSync(oldFile);
}
this.emit('archive', oldFile + '.gz');
});
});
}
this.logStreamMap[driveId] = logStream;
return logStream;
}
async log(info, callback) {
if (!info?.jobId) {
const logStream = await this.getLogStream(info.driveId);
logStream.write(JSON.stringify(info) + this.options.eol);
this.emit('logged', info);
}
if (callback) {
callback(null, true);
}
}
async close() {
const promises = Object.values(this.logStreamMap)
.map(logStream => new Promise(resolve => logStream.end(resolve)));
this.logStreamMap = {};
await Promise.all(promises);
this.emit('finish');
}
async query(options, callback) {
if (!this.options.json) {
throw new Error('query() may not be used without the json option being set to true');
}
if (!this.filename) {
throw new Error('query() may not be used when initializing with a stream');
}
options = options || {};
if (options.jobId) {
callback(null, []);
return [];
}
// limit
options.rows = options.rows || options.limit || 10;
// starting row offset
options.start = options.start || 0;
// 'asc' or 'desc'
options.order = options.order || 'desc';
const fileRegex = new RegExp(this.filename.replace('%DATE%', '.*'), 'i');
const driveId = options.driveId || '';
const dirname = this.dirname.replace('%driveId%', driveId).replace('//', '/');
if (fs.existsSync(dirname)) {
const logFiles = fs.readdirSync(dirname)
.filter((file) => {
const base = path.basename(file);
if (!base.match(fileRegex))
return false;
if (base.indexOf('-combined.log') === -1)
return false;
const fileDate = new Date(base.substring(0, 10));
if (+fileDate <= 0)
return false;
if (options.from && +fileDate < +options.from - 3 * 24 * 3600 * 1000)
return false;
if (options.until && +fileDate > +options.until + 3 * 24 * 3600 * 1000)
return false;
return true;
})
.map(file => path.join(dirname, file));
if (logFiles.length === 0) {
callback(null, []);
return [];
}
try {
const processor = new DailyRotateFileProcessor(logFiles, options);
const results = await processor.query();
callback(null, results);
return results;
}
catch (err) {
callback(err);
throw err;
}
}
else {
callback(null, []);
return [];
}
}
}