// fileHeaderManager.js import fs from 'fs/promises'; import path from 'path'; import '../libs_drpy/_dist/json5.js'; class FileHeaderManager { static COMMENT_CONFIG = { '.js': { start: '/*', end: '*/', // 修改正则表达式,确保只匹配文件开头的注释块 regex: /^(\s*\/\*[\s\S]*?\*\/)/, headerRegex: /@header\(([\s\S]*?)\)/, createComment: (content) => `/*\n${content}\n*/`, topCommentsRegex: /^(\s*(\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)\s*)+/ }, '.py': { start: '"""', end: '"""', // 修改正则表达式,确保只匹配文件开头的注释块 regex: /^(\s*"""[\s\S]*?""")/, headerRegex: /@header\(([\s\S]*?)\)/, createComment: (content) => `"""\n${content}\n"""`, topCommentsRegex: /^(\s*(#[^\n]*\n|'''[\s\S]*?'''|"""[\s\S]*?""")\s*)+/ }, '.php': { start: '/*', end: '*/', // PHP 文件通常以 `/*\n${content}\n*/`, topCommentsRegex: /(<\?php\s*)?(\s*(\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)\s*)+/ } }; /** * Find the @header(...) block in the comment text * @param {string} text Comment text * @param {string} ext File extension (.js or .py) * @returns {Object|null} { start, end, content } */ static findHeaderBlock(text, ext) { const startMarker = '@header('; const startIndex = text.indexOf(startMarker); if (startIndex === -1) return null; let index = startIndex + startMarker.length; let balance = 1; let inString = false; let stringChar = ''; let escape = false; let inLineComment = false; let inBlockComment = false; for (; index < text.length; index++) { const char = text[index]; if (inLineComment) { if (char === '\n') inLineComment = false; continue; } if (inBlockComment) { if (char === '*' && text[index + 1] === '/') { inBlockComment = false; index++; } continue; } if (inString) { if (escape) { escape = false; } else if (char === '\\') { escape = true; } else if (char === stringChar) { inString = false; } continue; } // Start of comment if (char === '/' && text[index + 1] === '/') { inLineComment = true; index++; } else if (char === '/' && text[index + 1] === '*') { inBlockComment = true; index++; } else if (ext === '.py' && char === '#') { inLineComment = true; } else if (char === '"' || char === "'") { inString = true; stringChar = char; } else if (char === '(') { balance++; } else if (char === ')') { balance--; if (balance === 0) { return { start: startIndex, end: index + 1, content: text.substring(startIndex + startMarker.length, index) }; } } } return null; } /** * 解析对象字符串 * @param {string} str 对象字符串 * @returns {Object} 解析后的对象 */ static parseObjectLiteral(str) { try { return JSON5.parse(str); } catch (e) { // console.warn('JSON5 parse failed, falling back to eval:', e.message); try { // 尝试处理一些常见的非标准JSON格式 // 1. 给未加引号的键加上引号 (简单的正则替换,不够完美但能处理大部分情况) // 注意:这里不再做简单的正则替换,因为 JSON5 已经能处理无引号键。 // 如果 JSON5 失败,很可能是因为包含了函数或其他非数据类型,或者格式严重错误。 // 为了兼容旧的非标准写法,保留 eval 方式作为最后的手段 return (new Function(`return ${str}`))(); } catch (evalError) { throw new Error(`Invalid header object: ${str}. Error: ${evalError.message}`); } } } /** * 读取文件头信息 * @param {string} filePath 文件路径 * @returns {Object|null} 头信息对象 */ static async readHeader(filePath) { // 对于大多数脚本文件(通常 < 1MB),直接全量读取比 open+read+close 更快 const content = await fs.readFile(filePath, 'utf8'); const ext = path.extname(filePath); const config = this.COMMENT_CONFIG[ext]; if (!config) throw new Error(`Unsupported file type: ${ext}`); const match = content.match(config.regex); if (!match) return null; const headerBlock = this.findHeaderBlock(match[0], ext); if (!headerBlock) return null; try { return this.parseObjectLiteral(headerBlock.content.trim()); } catch { return null; } } /** * 创建文件备份 * @param {string} filePath 原文件路径 * @returns {string} 备份文件路径 */ static async createBackup(filePath) { const backupPath = `${filePath}.backup.${Date.now()}`; try { const content = await fs.readFile(filePath, 'utf8'); await fs.writeFile(backupPath, content); return backupPath; } catch (error) { throw new Error(`Failed to create backup: ${error.message}`); } } /** * 从备份恢复文件 * @param {string} filePath 目标文件路径 * @param {string} backupPath 备份文件路径 */ static async restoreFromBackup(filePath, backupPath) { try { const backupContent = await fs.readFile(backupPath, 'utf8'); await fs.writeFile(filePath, backupContent); // 删除备份文件 await fs.unlink(backupPath); } catch (error) { throw new Error(`Failed to restore from backup: ${error.message}`); } } /** * 写入/更新文件头信息 * @param {string} filePath 文件路径 * @param {Object} headerObj 头信息对象 * @param {Object} [options] 配置选项 * @param {boolean} [options.createBackup=true] 是否创建备份 */ static async writeHeader(filePath, headerObj, options = {}) { const {createBackup = false} = options; // 添加参数验证 if (!filePath || typeof filePath !== 'string') { throw new Error('Invalid file path'); } if (!headerObj || typeof headerObj !== 'object') { throw new Error('Invalid header object'); } let content; try { content = await fs.readFile(filePath, 'utf8'); } catch (error) { throw new Error(`Failed to read file: ${error.message}`); } const ext = path.extname(filePath); const config = this.COMMENT_CONFIG[ext]; if (!config) throw new Error(`Unsupported file type: ${ext}`); const headerStr = `@header(${JSON5.stringify(headerObj, null, 2)})`; // 优化:先尝试只读取头部来匹配正则,避免全量匹配 // 但由于 writeHeader 需要重写整个文件,全量内容是必须的 // 所以这里的优化点主要在于减少不必要的全量正则匹配 // 使用优化后的 regex 只匹配头部注释 const match = content.match(config.regex); let newContent; if (match) { const [fullComment] = match; const commentStartIndex = content.indexOf(fullComment); const commentEndIndex = commentStartIndex + fullComment.length; // 确保匹配的注释块确实在文件开头(允许前面有空白字符) const beforeComment = content.substring(0, commentStartIndex); // 针对 PHP 特殊处理:忽略开头的 100 && diffRatio > 0.5 && newContent.length < content.length) { throw new Error('Content integrity check failed: significant size reduction detected, operation aborted'); } // 创建备份(如果启用) let backupPath = null; if (createBackup) { try { backupPath = await this.createBackup(filePath); } catch (error) { console.warn(`Warning: Failed to create backup for ${filePath}: ${error.message}`); } } try { await fs.writeFile(filePath, newContent); // 写入成功后,删除备份文件 if (backupPath) { try { await fs.unlink(backupPath); } catch (error) { console.warn(`Warning: Failed to delete backup file ${backupPath}: ${error.message}`); } } } catch (error) { // 写入失败,尝试从备份恢复 if (backupPath) { try { await this.restoreFromBackup(filePath, backupPath); console.log(`File restored from backup: ${filePath}`); } catch (restoreError) { console.error(`Failed to restore from backup: ${restoreError.message}`); } } throw new Error(`Failed to write file: ${error.message}`); } } /** * 移除头信息区域 * @param {string} input 文件路径或文件内容 * @param {Object} [options] 配置选项 * @param {string} [options.mode='header-only'] 移除模式: * - 'header-only': 只移除@header行(默认) * - 'top-comments': 移除文件顶部所有连续注释块 * @param {string} [options.fileType] 文件类型(当input为内容时必需) * @returns {Promise|string} 移除头信息后的内容 */ static async removeHeader(input, options = {}) { const {mode = 'header-only', fileType} = options; // 判断输入类型:文件路径 or 文件内容 const isFilePath = !input.includes('\n') && input.length < 256 && (input.endsWith('.js') || input.endsWith('.py')); let content, ext; if (isFilePath) { content = await fs.readFile(input, 'utf8'); ext = path.extname(input); } else { content = input; ext = fileType ? `.${fileType.replace(/^\./, '')}` : null; if (!ext) { throw new Error('fileType option is required when input is content'); } } const config = this.COMMENT_CONFIG[ext]; if (!config) throw new Error(`Unsupported file type: ${ext}`); // 模式1: 移除顶部所有连续注释块 if (mode === 'top-comments') { const match = content.match(config.topCommentsRegex); if (match) { content = content.substring(match[0].length); } return content.trim(); } // 模式2: 只移除@header行(默认模式) const match = content.match(config.regex); if (!match) return content.trim(); let [fullComment] = match; const headerBlock = this.findHeaderBlock(fullComment, ext); if (headerBlock) { // 获取 header 之前和之后的内容 let beforeHeader = fullComment.substring(0, headerBlock.start); let afterHeader = fullComment.substring(headerBlock.end); // 移除注释开始符和结束符 beforeHeader = beforeHeader.replace(config.start, ''); afterHeader = afterHeader.replace(config.end, ''); // PHP: 如果是 line.trim()) .filter(line => line.length > 0) .join('\n'); if (!cleanedInner) { // 如果只剩下 header,整个注释块移除 // 但如果是 PHP 文件, newComment); } } return content.trim(); } /** * 获取文件大小 * @param {string} filePath 文件路径 * @param {Object} [options] 配置选项 * @param {boolean} [options.humanReadable=false] 是否返回人类可读格式 * @returns {Promise} 文件大小(字节或人类可读字符串) */ static async getFileSize(filePath, options = {}) { try { const stats = await fs.stat(filePath); const sizeInBytes = stats.size; if (options.humanReadable) { return this.formatFileSize(sizeInBytes); } return sizeInBytes; } catch (error) { throw new Error(`获取文件大小失败: ${error.message}`); } } /** * 格式化文件大小为人类可读格式 * @param {number} bytes 文件大小(字节) * @returns {string} 格式化后的文件大小 */ static formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } } export default FileHeaderManager;