diff --git a/src/accesscontrol/main.ts b/src/accesscontrol/main.ts index 1a1acea..82f1640 100644 --- a/src/accesscontrol/main.ts +++ b/src/accesscontrol/main.ts @@ -13,6 +13,7 @@ import { FileStream, Logger, LogLevel, + MB, processor, textRenderer, } from "@/lib/ccStructLog"; @@ -29,7 +30,15 @@ const logger = new Logger({ renderer: textRenderer, streams: [ new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream), - new FileStream("accesscontrol.log", DAY), + new FileStream({ + filePath: "accesscontrol.log", + rotationInterval: DAY, + autoCleanup: { + enabled: true, + maxFiles: 7, + maxSizeBytes: MB, + }, + }), ], }); diff --git a/src/lib/ccStructLog/streams.ts b/src/lib/ccStructLog/streams.ts index b79569f..d224de8 100644 --- a/src/lib/ccStructLog/streams.ts +++ b/src/lib/ccStructLog/streams.ts @@ -8,6 +8,29 @@ import { LogLevel, Stream, LogEvent } from "./types"; +/** + * Configuration interface for FileStream with auto-cleanup options. + */ +interface FileStreamConfig { + /** Path to the log file */ + filePath: string; + /** Time in seconds between file rotations (0 = no rotation) */ + rotationInterval?: number; + /** Auto-cleanup configuration */ + autoCleanup?: { + /** Whether to enable auto-cleanup */ + enabled: boolean; + /** Maximum number of log files to keep */ + maxFiles?: number; + /** Maximum total size in bytes for all log files */ + maxSizeBytes?: number; + /** Directory to search for log files (defaults to log file directory) */ + logDir?: string; + /** File pattern to match (defaults to base filename pattern) */ + pattern?: string; + }; +} + /** * Console stream that outputs to the CC:Tweaked terminal. * @@ -59,20 +82,18 @@ export class FileStream implements Stream { private filePath: string; private rotationInterval: number; private lastRotationTime: number; - private baseFilename: string; + private autoCleanupConfig?: FileStreamConfig["autoCleanup"]; /** - * Create a new file stream. + * Create a new file stream with configuration object. * - * @param filePath - Path to the log file - * @param rotationInterval - Time in seconds between file rotations (0 = no rotation) + * @param config - FileStream configuration object */ - constructor(filePath: string, rotationInterval: number = 0) { - this.filePath = filePath; - this.rotationInterval = rotationInterval; + constructor(config: FileStreamConfig) { + this.filePath = config.filePath; + this.rotationInterval = config.rotationInterval || 0; + this.autoCleanupConfig = config.autoCleanup; this.lastRotationTime = os.time(); - this.baseFilename = filePath; - this.openFile(); } @@ -94,6 +115,9 @@ export class FileStream implements Stream { return; } this.fileHandle = handle; + + // Perform auto-cleanup when opening file + this.performAutoCleanup(); } /** @@ -109,9 +133,9 @@ export class FileStream implements Stream { const timestamp = `${date.year}-${string.format("%02d", date.month)}-${string.format("%02d", date.day)}_${string.format("%02d", date.hour)}-${string.format("%02d", date.min)}`; // Split filename and extension - const splitStrs = this.baseFilename.split("."); + const splitStrs = this.filePath.split("."); if (splitStrs.length === 1) { - return `${this.baseFilename}_${timestamp}.log`; + return `${this.filePath}_${timestamp}.log`; } const name = splitStrs[0]; @@ -136,9 +160,49 @@ export class FileStream implements Stream { this.close(); this.lastRotationTime = currentTime; this.openFile(); + // Auto-cleanup is performed in openFile() } } + /** + * Perform auto-cleanup based on configuration. + * This method is called automatically when opening files or rotating. + */ + private performAutoCleanup(): void { + if (!this.autoCleanupConfig || !this.autoCleanupConfig.enabled) { + return; + } + + const config = this.autoCleanupConfig; + + // Cleanup by file count if configured + if (config.maxFiles !== undefined && config.maxFiles > 0) { + this.cleanupOldLogFiles( + config.maxFiles, + config.logDir, + config.pattern, + ); + } + + // Cleanup by total size if configured + if (config.maxSizeBytes !== undefined && config.maxSizeBytes > 0) { + this.cleanupLogFilesBySize( + config.maxSizeBytes, + config.logDir, + config.pattern, + ); + } + } + + /** + * Enable or update auto-cleanup configuration at runtime. + * + * @param config - Auto-cleanup configuration + */ + public setAutoCleanup(config: FileStreamConfig["autoCleanup"]): void { + this.autoCleanupConfig = config; + } + /** * Write a formatted log message to the file. * @@ -163,6 +227,127 @@ export class FileStream implements Stream { this.fileHandle = undefined; } } + + /** + * Search for log files matching the specified pattern in a directory. + * + * @param logDir - Directory containing log files (defaults to directory of current log file) + * @param fileName - Base File Name + * @returns Array of log file information including path, size, and modification time + */ + private searchLogFiles( + logDir?: string, + fileName?: string, + ): Array<{ path: string; size: number; modified: number }> { + const directory = logDir || fs.getDir(this.filePath); + const baseFileName = + fileName || fs.getName(this.filePath).split(".")[0]; + + if (!fs.exists(directory) || !fs.isDir(directory)) { + return []; + } + + const logFiles: Array<{ + path: string; + size: number; + modified: number; + }> = []; + const files = fs.list(directory); + + for (const file of files) { + const fullPath = fs.combine(directory, file); + if (fs.isDir(fullPath) || !file.startsWith(baseFileName)) continue; + + const attributes = fs.attributes(fullPath); + if (attributes !== undefined) { + logFiles.push({ + path: fullPath, + size: attributes.size, + modified: attributes.modified, + }); + } + } + + return logFiles; + } + + /** + * Clean up old log files by keeping only the specified number of most recent files. + * + * @param maxFiles - Maximum number of log files to keep + * @param logDir - Directory containing log files (defaults to directory of current log file) + * @param fileName - Base File Name + */ + public cleanupOldLogFiles( + maxFiles: number, + logDir?: string, + fileName?: string, + ): void { + if (maxFiles <= 0) return; + + const logFiles = this.searchLogFiles(logDir, fileName); + if (logFiles.length <= maxFiles) return; + + // Sort by modification time (newest first) + logFiles.sort((a, b) => b.modified - a.modified); + + // Delete files beyond the limit + for (let i = maxFiles; i < logFiles.length; i++) { + try { + fs.delete(logFiles[i].path); + } catch (err) { + printError( + `Failed to delete old log file ${logFiles[i].path}: ${err}`, + ); + } + } + } + + /** + * Clean up log files by total size, deleting oldest files until total size is under limit. + * + * @param maxSizeBytes - Maximum total size in bytes for all log files + * @param logDir - Directory containing log files (defaults to directory of current log file) + * @param fileName - Base File Name + */ + public cleanupLogFilesBySize( + maxSizeBytes: number, + logDir?: string, + fileName?: string, + ): void { + if (maxSizeBytes <= 0) return; + + const logFiles = this.searchLogFiles(logDir, fileName); + if (logFiles.length === 0) return; + + // Calculate total size + let totalSize = 0; + for (const logFile of logFiles) { + totalSize += logFile.size; + } + + // If total size is within limit, no cleanup needed + if (totalSize <= maxSizeBytes) { + return; + } + + // Sort by modification time (oldest first for deletion) + logFiles.sort((a, b) => a.modified - b.modified); + + // Delete oldest files until we're under the size limit + for (const logFile of logFiles) { + if (totalSize <= maxSizeBytes) { + break; + } + + try { + fs.delete(logFile.path); + totalSize -= logFile.size; + } catch (err) { + printError(`Failed to delete log file ${logFile.path}: ${err}`); + } + } + } } /** @@ -307,3 +492,7 @@ export const MINUTE = 60 * SECOND; export const HOUR = 60 * MINUTE; export const DAY = 24 * HOUR; export const WEEK = 7 * DAY; + +// Byte constants for file rotation +export const MB = 1024 * 1024; +export const KB = 1024; diff --git a/src/logExample/main.ts b/src/logExample/main.ts index 512dd20..f10a76a 100644 --- a/src/logExample/main.ts +++ b/src/logExample/main.ts @@ -36,7 +36,13 @@ const customLogger = new Logger({ }), ], renderer: jsonRenderer, - streams: [new ConsoleStream(), new FileStream("custom.log", HOUR)], + streams: [ + new ConsoleStream(), + new FileStream({ + filePath: "custom.log", + rotationInterval: HOUR, + }), + ], }); customLogger.info("Custom logger example", { @@ -136,7 +142,10 @@ const multiFormatLogger = new Logger({ { write: (_, event) => { const formatted = jsonRenderer(event); - new FileStream("structured.log").write(formatted, event); + new FileStream({ filePath: "structured.log" }).write( + formatted, + event, + ); }, }, ], @@ -193,7 +202,7 @@ print("\n=== Cleanup Examples ==="); const fileLogger = new Logger({ processors: [processor.addTimestamp()], renderer: jsonRenderer, - streams: [new FileStream("temp.log")], + streams: [new FileStream({ filePath: "structured.log" })], }); fileLogger.info("Temporary log entry");