import * as fs from 'fs/promises'; import * as path from 'path'; const MAX_LOG_SIZE_BYTES = 100 * 1024 * 1024; // 100MB const LOG_DIRECTORY = './data/temp_suspended_ssh_logs/'; /** * TemporaryLogStorageService负责管理临时日志文件的原子化读、写、删除及轮替操作。 */ export class TemporaryLogStorageService { constructor() { this.ensureLogDirectoryExists(); } /** * 确保日志目录存在,如果不存在则创建它。 */ async ensureLogDirectoryExists(): Promise { try { await fs.mkdir(LOG_DIRECTORY, { recursive: true }); // console.log(`日志目录 '${LOG_DIRECTORY}' 已确保存在。`); } catch (error) { console.error(`创建日志目录 '${LOG_DIRECTORY}' 失败:`, error); // 在实际应用中,这里可能需要更健壮的错误处理 } } private getLogFilePath(suspendSessionId: string): string { return path.join(LOG_DIRECTORY, `${suspendSessionId}.log`); } /** * 将数据写入指定挂起会话的日志文件。 * 如果文件大小超过MAX_LOG_SIZE_BYTES,将采取轮替策略(清空并从头开始写)。 * @param suspendSessionId - 挂起会话的ID。 * @param data - 要写入的数据。 */ async writeToLog(suspendSessionId: string, data: string): Promise { const filePath = this.getLogFilePath(suspendSessionId); try { await this.ensureLogDirectoryExists(); // 确保目录存在 let stat; try { stat = await fs.stat(filePath); } catch (e: any) { if (e.code !== 'ENOENT') { throw e; } // 文件不存在,是正常情况,后续会创建 } if (stat && stat.size >= MAX_LOG_SIZE_BYTES) { // 文件过大,执行轮替策略:清空文件 console.log(`日志文件 '${filePath}' 大小达到 ${MAX_LOG_SIZE_BYTES / (1024 * 1024)}MB,执行轮替(清空)。`); await fs.writeFile(filePath, data, 'utf8'); // 清空并写入新数据 } else { await fs.appendFile(filePath, data, 'utf8'); } } catch (error) { console.error(`写入日志文件 '${filePath}' 失败:`, error); throw error; // 重新抛出错误,让调用者处理 } } /** * 读取指定挂起会话的日志文件内容。 * @param suspendSessionId - 挂起会话的ID。 * @returns 返回日志文件的内容。如果文件不存在,则返回空字符串。 */ async readLog(suspendSessionId: string): Promise { const filePath = this.getLogFilePath(suspendSessionId); try { const data = await fs.readFile(filePath, 'utf8'); return data; } catch (error: any) { if (error.code === 'ENOENT') { // console.log(`日志文件 '${filePath}' 不存在,返回空内容。`); return ''; // 文件不存在,通常意味着没有日志 } console.error(`读取日志文件 '${filePath}' 失败:`, error); throw error; } } /** * 删除指定挂起会话的日志文件。 * @param suspendSessionId - 挂起会话的ID。 */ async deleteLog(suspendSessionId: string): Promise { const filePath = this.getLogFilePath(suspendSessionId); try { await fs.unlink(filePath); // console.log(`日志文件 '${filePath}' 已成功删除。`); } catch (error: any) { if (error.code === 'ENOENT') { // console.warn(`尝试删除日志文件 '${filePath}' 时发现文件已不存在,操作忽略。`); return; // 文件不存在,无需操作 } console.error(`删除日志文件 '${filePath}' 失败:`, error); throw error; } } /** * 列出日志目录中的所有日志文件名(不含扩展名,即suspendSessionId)。 * 这可以用于 `SshSuspendService` 初始化时加载已断开的会话。 * @returns 返回包含所有 suspendSessionId 的数组。 */ async listLogFiles(): Promise { try { await this.ensureLogDirectoryExists(); const files = await fs.readdir(LOG_DIRECTORY); return files .filter(file => file.endsWith('.log')) .map(file => file.replace(/\.log$/, '')); } catch (error) { console.error(`列出日志目录 '${LOG_DIRECTORY}' 中的文件失败:`, error); return []; // 发生错误时返回空数组 } } } // 单例模式导出 export const temporaryLogStorageService = new TemporaryLogStorageService();