UNPKG

@mieweb/wikigdrive

Version:

Google Drive to MarkDown synchronization

273 lines (223 loc) 7.85 kB
import fs, {ReadStream} from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; import {StreamOptions} from 'node:stream'; import zlib from 'node:zlib'; import Transport, {TransportStreamOptions} from 'winston-transport'; import {FileStreamRotator} from './FileStreamRotator.ts'; import {DailyRotateFileProcessor} from './DailyRotateFileProcessor.ts'; 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(); } }; function hash(obj) { const hash = crypto.createHash('md5'); hash.setEncoding('hex'); hash.write(JSON.stringify(obj)); hash.end(); return hash.read(); } function getMaxSize(size: string | number) { 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) { // eslint-disable-next-line 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) { // eslint-disable-next-line 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); } interface RotateStreamOptions extends TransportStreamOptions { json?: boolean; file_options?: StreamOptions<ReadStream>; datePattern?: string; zippedArchive?: boolean; maxSize?: string; symlinkName?: string; createSymlink?: boolean; extension?: string; utc?: boolean; auditFile?: string; maxFiles?: string; dirname?: string; filename?: string; eol?: string; frequency?: string; } export class DailyRotateFile extends Transport { name = 'dailyRotateFile'; private dirname: string; private filename: string; private logStreamMap: { [driveId: string]: FileStreamRotator; }; private options: RotateStreamOptions; constructor(options: RotateStreamOptions = {}) { super(options); 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 = {}; } getLogStream(driveId: string) { 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, '.' + 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; } log(info, callback) { if (!info?.jobId) { const logStream = 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 []; } } }