@claude-vector/core
Version:
Core vector search engine for code intelligence
1,117 lines (966 loc) • 33 kB
JavaScript
/**
* IndexIntegrityManager - インデックス整合性チェックと自動復旧システム
*
* 機能:
* - インデックスファイルの整合性チェック
* - 破損検出と診断
* - 自動バックアップ管理
* - 段階的復旧プロセス
* - 復旧進捗とログ
* - インデックス品質評価
*/
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';
import crypto from 'crypto';
import { EventEmitter } from 'events';
export class IndexIntegrityManager extends EventEmitter {
constructor(options = {}) {
super();
this.options = {
// バックアップ設定
enableAutoBackup: options.enableAutoBackup ?? true,
maxBackups: options.maxBackups ?? 10,
backupInterval: options.backupInterval ?? 24 * 60 * 60 * 1000, // 24時間
backupCompressionLevel: options.backupCompressionLevel ?? 6,
// 整合性チェック設定
enablePeriodicCheck: options.enablePeriodicCheck ?? true,
checkInterval: options.checkInterval ?? 60 * 60 * 1000, // 1時間
checksumAlgorithm: options.checksumAlgorithm ?? 'sha256',
// 復旧設定
autoRecoveryEnabled: options.autoRecoveryEnabled ?? true,
maxRecoveryAttempts: options.maxRecoveryAttempts ?? 3,
recoveryTimeout: options.recoveryTimeout ?? 300000, // 5分
// ログ設定
verboseLogging: options.verboseLogging ?? false,
logFile: options.logFile ?? './.claude-integrity.log',
...options
};
// 状態管理
this.state = {
isChecking: false,
isRecovering: false,
lastCheckTime: null,
lastBackupTime: null,
corruptionDetected: false,
recoveryAttempts: 0
};
// 統計情報
this.stats = {
totalChecks: 0,
corruptionsDetected: 0,
successfulRecoveries: 0,
failedRecoveries: 0,
backupsCreated: 0,
averageCheckTime: 0
};
// タイマー
this.checkTimer = null;
this.backupTimer = null;
// 初期化
this.initialize();
}
/**
* 初期化
*/
async initialize() {
if (this.options.enablePeriodicCheck) {
this.startPeriodicChecks();
}
if (this.options.enableAutoBackup) {
this.startAutoBackup();
}
this.emit('initialized', this.options);
}
/**
* インデックス整合性チェック
*/
async checkIndexIntegrity(indexPaths, metadata = {}) {
if (this.state.isChecking) {
throw new Error('Integrity check already in progress');
}
this.state.isChecking = true;
this.state.lastCheckTime = Date.now();
this.stats.totalChecks++;
const startTime = Date.now();
const checkResult = {
timestamp: new Date().toISOString(),
indexPaths,
metadata,
isValid: true,
issues: [],
corruptionLevel: 'none', // none, minor, major, critical
checksums: {},
recommendations: []
};
try {
this.emit('check-started', { indexPaths, metadata });
// 各インデックスファイルのチェック
for (const indexPath of indexPaths) {
await this.checkSingleIndex(indexPath, checkResult);
}
// 相互整合性チェック
if (indexPaths.length > 1) {
await this.checkCrossIndexConsistency(indexPaths, checkResult);
}
// メタデータ整合性チェック
await this.checkMetadataConsistency(indexPaths, checkResult);
// 総合評価
this.evaluateOverallIntegrity(checkResult);
// 統計更新
const checkTime = Date.now() - startTime;
this.updateCheckStats(checkTime);
this.emit('check-completed', checkResult);
if (!checkResult.isValid) {
this.state.corruptionDetected = true;
this.stats.corruptionsDetected++;
// 自動復旧の開始
if (this.options.autoRecoveryEnabled) {
setTimeout(() => {
this.recoverIndex(indexPaths, checkResult);
}, 1000);
}
}
return checkResult;
} catch (error) {
checkResult.isValid = false;
checkResult.issues.push({
type: 'CHECK_ERROR',
severity: 'critical',
message: `Integrity check failed: ${error.message}`,
timestamp: Date.now()
});
this.emit('check-error', { error, checkResult });
throw error;
} finally {
this.state.isChecking = false;
}
}
/**
* 単一インデックスファイルのチェック
*/
async checkSingleIndex(indexPath, checkResult) {
try {
// ファイル存在チェック
if (!fsSync.existsSync(indexPath)) {
checkResult.issues.push({
type: 'FILE_MISSING',
severity: 'critical',
file: indexPath,
message: `Index file does not exist: ${indexPath}`,
timestamp: Date.now()
});
checkResult.isValid = false;
return;
}
// ファイルサイズチェック
const stats = await fs.stat(indexPath);
if (stats.size === 0) {
checkResult.issues.push({
type: 'EMPTY_FILE',
severity: 'critical',
file: indexPath,
message: `Index file is empty: ${indexPath}`,
timestamp: Date.now()
});
checkResult.isValid = false;
return;
}
// チェックサム計算
const checksum = await this.calculateChecksum(indexPath);
checkResult.checksums[indexPath] = checksum;
// JSON構造チェック
await this.validateJsonStructure(indexPath, checkResult);
// データ品質チェック
await this.validateDataQuality(indexPath, checkResult);
} catch (error) {
checkResult.issues.push({
type: 'FILE_ACCESS_ERROR',
severity: 'major',
file: indexPath,
message: `Cannot access file: ${error.message}`,
timestamp: Date.now()
});
checkResult.isValid = false;
}
}
/**
* JSON構造検証
*/
async validateJsonStructure(indexPath, checkResult) {
try {
const content = await fs.readFile(indexPath, 'utf8');
const data = JSON.parse(content);
// ファイルタイプ別検証
if (indexPath.includes('embeddings')) {
this.validateEmbeddingsStructure(data, indexPath, checkResult);
} else if (indexPath.includes('chunks')) {
this.validateChunksStructure(data, indexPath, checkResult);
} else if (indexPath.includes('metadata')) {
this.validateMetadataStructure(data, indexPath, checkResult);
}
} catch (error) {
if (error instanceof SyntaxError) {
checkResult.issues.push({
type: 'INVALID_JSON',
severity: 'critical',
file: indexPath,
message: `Invalid JSON format: ${error.message}`,
timestamp: Date.now()
});
checkResult.isValid = false;
} else {
throw error;
}
}
}
/**
* エンベディング構造検証
*/
validateEmbeddingsStructure(data, filePath, checkResult) {
if (!Array.isArray(data)) {
checkResult.issues.push({
type: 'INVALID_STRUCTURE',
severity: 'critical',
file: filePath,
message: 'Embeddings file should contain an array',
timestamp: Date.now()
});
checkResult.isValid = false;
return;
}
// サンプル検証(最初の10個)
const samples = data.slice(0, Math.min(10, data.length));
for (let i = 0; i < samples.length; i++) {
const embedding = samples[i];
if (!Array.isArray(embedding)) {
checkResult.issues.push({
type: 'INVALID_EMBEDDING',
severity: 'major',
file: filePath,
message: `Embedding at index ${i} is not an array`,
timestamp: Date.now()
});
checkResult.isValid = false;
} else if (embedding.length === 0) {
checkResult.issues.push({
type: 'EMPTY_EMBEDDING',
severity: 'major',
file: filePath,
message: `Embedding at index ${i} is empty`,
timestamp: Date.now()
});
checkResult.isValid = false;
} else if (embedding.some(val => typeof val !== 'number' || isNaN(val))) {
checkResult.issues.push({
type: 'INVALID_EMBEDDING_VALUES',
severity: 'major',
file: filePath,
message: `Embedding at index ${i} contains invalid values`,
timestamp: Date.now()
});
checkResult.isValid = false;
}
}
}
/**
* チャンク構造検証
*/
validateChunksStructure(data, filePath, checkResult) {
if (!Array.isArray(data)) {
checkResult.issues.push({
type: 'INVALID_STRUCTURE',
severity: 'critical',
file: filePath,
message: 'Chunks file should contain an array',
timestamp: Date.now()
});
checkResult.isValid = false;
return;
}
// サンプル検証
const samples = data.slice(0, Math.min(10, data.length));
for (let i = 0; i < samples.length; i++) {
const chunk = samples[i];
if (!chunk || typeof chunk !== 'object') {
checkResult.issues.push({
type: 'INVALID_CHUNK',
severity: 'major',
file: filePath,
message: `Chunk at index ${i} is not an object`,
timestamp: Date.now()
});
checkResult.isValid = false;
continue;
}
// 必須フィールドチェック
const requiredFields = ['content', 'metadata'];
for (const field of requiredFields) {
if (!(field in chunk)) {
checkResult.issues.push({
type: 'MISSING_FIELD',
severity: 'major',
file: filePath,
message: `Chunk at index ${i} missing required field: ${field}`,
timestamp: Date.now()
});
checkResult.isValid = false;
}
}
// コンテンツの有効性チェック
if (chunk.content && typeof chunk.content !== 'string') {
checkResult.issues.push({
type: 'INVALID_CONTENT',
severity: 'minor',
file: filePath,
message: `Chunk at index ${i} content is not a string`,
timestamp: Date.now()
});
}
}
}
/**
* メタデータ構造検証
*/
validateMetadataStructure(data, filePath, checkResult) {
if (!data || typeof data !== 'object') {
checkResult.issues.push({
type: 'INVALID_STRUCTURE',
severity: 'critical',
file: filePath,
message: 'Metadata file should contain an object',
timestamp: Date.now()
});
checkResult.isValid = false;
return;
}
// 基本フィールドのチェック
const expectedFields = ['version', 'timestamp', 'totalChunks', 'indexConfig'];
for (const field of expectedFields) {
if (!(field in data)) {
checkResult.issues.push({
type: 'MISSING_METADATA_FIELD',
severity: 'minor',
file: filePath,
message: `Missing metadata field: ${field}`,
timestamp: Date.now()
});
}
}
}
/**
* データ品質チェック
*/
async validateDataQuality(indexPath, checkResult) {
try {
const content = await fs.readFile(indexPath, 'utf8');
const data = JSON.parse(content);
if (Array.isArray(data)) {
// 配列の場合(embeddings, chunks)
if (data.length === 0) {
checkResult.issues.push({
type: 'EMPTY_DATA',
severity: 'major',
file: indexPath,
message: 'Index file contains no data',
timestamp: Date.now()
});
checkResult.isValid = false;
}
// 重複チェック(サンプル)
if (data.length > 100) {
const sample = data.slice(0, 100);
const unique = new Set(sample.map(item => JSON.stringify(item)));
if (unique.size < sample.length * 0.9) { // 10%以上重複
checkResult.issues.push({
type: 'HIGH_DUPLICATION',
severity: 'minor',
file: indexPath,
message: 'High level of data duplication detected',
timestamp: Date.now()
});
}
}
}
} catch (error) {
// JSON parsing errorは別でキャッチされるのでここではスキップ
if (!(error instanceof SyntaxError)) {
throw error;
}
}
}
/**
* 相互整合性チェック
*/
async checkCrossIndexConsistency(indexPaths, checkResult) {
try {
const loadedData = {};
// 全ファイルを読み込み
for (const indexPath of indexPaths) {
if (fsSync.existsSync(indexPath)) {
const content = await fs.readFile(indexPath, 'utf8');
const data = JSON.parse(content);
loadedData[indexPath] = data;
}
}
// エンベディングとチャンクの数の一致チェック
const embeddingsFile = indexPaths.find(p => p.includes('embeddings'));
const chunksFile = indexPaths.find(p => p.includes('chunks'));
if (embeddingsFile && chunksFile &&
loadedData[embeddingsFile] && loadedData[chunksFile]) {
const embeddingsCount = loadedData[embeddingsFile].length;
const chunksCount = loadedData[chunksFile].length;
if (embeddingsCount !== chunksCount) {
checkResult.issues.push({
type: 'COUNT_MISMATCH',
severity: 'critical',
message: `Embeddings count (${embeddingsCount}) does not match chunks count (${chunksCount})`,
files: [embeddingsFile, chunksFile],
timestamp: Date.now()
});
checkResult.isValid = false;
}
}
} catch (error) {
checkResult.issues.push({
type: 'CONSISTENCY_CHECK_ERROR',
severity: 'major',
message: `Cross-index consistency check failed: ${error.message}`,
timestamp: Date.now()
});
}
}
/**
* メタデータ整合性チェック
*/
async checkMetadataConsistency(indexPaths, checkResult) {
const metadataFile = indexPaths.find(p => p.includes('metadata'));
if (!metadataFile || !fsSync.existsSync(metadataFile)) {
return; // メタデータファイルがない場合はスキップ
}
try {
const content = await fs.readFile(metadataFile, 'utf8');
const metadata = JSON.parse(content);
// 実際のデータとメタデータの整合性チェック
const chunksFile = indexPaths.find(p => p.includes('chunks'));
if (chunksFile && fsSync.existsSync(chunksFile)) {
const chunksContent = await fs.readFile(chunksFile, 'utf8');
const chunks = JSON.parse(chunksContent);
if (metadata.totalChunks && metadata.totalChunks !== chunks.length) {
checkResult.issues.push({
type: 'METADATA_MISMATCH',
severity: 'minor',
message: `Metadata totalChunks (${metadata.totalChunks}) does not match actual chunks (${chunks.length})`,
files: [metadataFile, chunksFile],
timestamp: Date.now()
});
}
}
} catch (error) {
checkResult.issues.push({
type: 'METADATA_CHECK_ERROR',
severity: 'minor',
message: `Metadata consistency check failed: ${error.message}`,
timestamp: Date.now()
});
}
}
/**
* 総合整合性評価
*/
evaluateOverallIntegrity(checkResult) {
const issues = checkResult.issues;
const criticalIssues = issues.filter(i => i.severity === 'critical').length;
const majorIssues = issues.filter(i => i.severity === 'major').length;
const minorIssues = issues.filter(i => i.severity === 'minor').length;
if (criticalIssues > 0) {
checkResult.corruptionLevel = 'critical';
checkResult.recommendations.push('Immediate index rebuild required');
checkResult.recommendations.push('Consider restoring from backup');
} else if (majorIssues > 2) {
checkResult.corruptionLevel = 'major';
checkResult.recommendations.push('Index rebuild recommended');
checkResult.recommendations.push('Monitor for further corruption');
} else if (majorIssues > 0 || minorIssues > 5) {
checkResult.corruptionLevel = 'minor';
checkResult.recommendations.push('Consider index refresh');
checkResult.recommendations.push('Schedule maintenance rebuild');
} else {
checkResult.corruptionLevel = 'none';
checkResult.recommendations.push('Index appears healthy');
}
}
/**
* チェックサム計算
*/
async calculateChecksum(filePath) {
const content = await fs.readFile(filePath);
return crypto.createHash(this.options.checksumAlgorithm)
.update(content)
.digest('hex');
}
/**
* インデックス復旧
*/
async recoverIndex(indexPaths, integrityResult) {
if (this.state.isRecovering) {
throw new Error('Recovery already in progress');
}
if (this.state.recoveryAttempts >= this.options.maxRecoveryAttempts) {
throw new Error(`Maximum recovery attempts (${this.options.maxRecoveryAttempts}) exceeded`);
}
this.state.isRecovering = true;
this.state.recoveryAttempts++;
const recoveryResult = {
timestamp: new Date().toISOString(),
indexPaths,
integrityResult,
success: false,
strategy: null,
steps: [],
restoredFiles: [],
errors: []
};
try {
this.emit('recovery-started', { indexPaths, integrityResult });
// 復旧戦略の決定
const strategy = this.determineRecoveryStrategy(integrityResult);
recoveryResult.strategy = strategy;
this.emit('recovery-strategy-determined', { strategy, integrityResult });
switch (strategy) {
case 'backup_restore':
await this.executeBackupRestore(indexPaths, recoveryResult);
break;
case 'partial_repair':
await this.executePartialRepair(indexPaths, recoveryResult);
break;
case 'full_rebuild':
await this.executeFullRebuild(indexPaths, recoveryResult);
break;
default:
throw new Error(`Unknown recovery strategy: ${strategy}`);
}
// 復旧後の整合性チェック
const postRecoveryCheck = await this.checkIndexIntegrity(indexPaths, {
type: 'post_recovery_verification'
});
if (postRecoveryCheck.isValid) {
recoveryResult.success = true;
this.stats.successfulRecoveries++;
this.state.corruptionDetected = false;
this.state.recoveryAttempts = 0;
} else {
recoveryResult.success = false;
recoveryResult.errors.push('Post-recovery integrity check failed');
this.stats.failedRecoveries++;
}
this.emit('recovery-completed', recoveryResult);
return recoveryResult;
} catch (error) {
recoveryResult.success = false;
recoveryResult.errors.push(error.message);
this.stats.failedRecoveries++;
this.emit('recovery-error', { error, recoveryResult });
throw error;
} finally {
this.state.isRecovering = false;
}
}
/**
* 復旧戦略決定
*/
determineRecoveryStrategy(integrityResult) {
const { corruptionLevel, issues } = integrityResult;
// クリティカルな破損の場合
if (corruptionLevel === 'critical') {
const hasMissingFiles = issues.some(i => i.type === 'FILE_MISSING');
const hasInvalidJson = issues.some(i => i.type === 'INVALID_JSON');
if (hasMissingFiles || hasInvalidJson) {
return 'backup_restore';
} else {
return 'full_rebuild';
}
}
// メジャーな破損の場合
if (corruptionLevel === 'major') {
const hasStructuralIssues = issues.some(i =>
i.type === 'INVALID_STRUCTURE' || i.type === 'COUNT_MISMATCH'
);
if (hasStructuralIssues) {
return 'backup_restore';
} else {
return 'partial_repair';
}
}
// マイナーな破損の場合
return 'partial_repair';
}
/**
* バックアップからの復元
*/
async executeBackupRestore(indexPaths, recoveryResult) {
recoveryResult.steps.push({
step: 'backup_restore_start',
timestamp: Date.now(),
message: 'Starting backup restore process'
});
// バックアップディレクトリの検索
const backupDir = path.join(path.dirname(indexPaths[0]), '.backups');
if (!fsSync.existsSync(backupDir)) {
throw new Error('No backup directory found');
}
// 最新のバックアップを検索
const backups = await this.findAvailableBackups(backupDir);
if (backups.length === 0) {
throw new Error('No backups available');
}
const latestBackup = backups[0]; // 最新のバックアップ
recoveryResult.steps.push({
step: 'backup_selected',
timestamp: Date.now(),
message: `Selected backup: ${latestBackup.timestamp}`,
backup: latestBackup
});
// バックアップからファイルを復元
for (const indexPath of indexPaths) {
const fileName = path.basename(indexPath);
const backupFilePath = path.join(latestBackup.path, fileName);
if (fsSync.existsSync(backupFilePath)) {
await fs.copyFile(backupFilePath, indexPath);
recoveryResult.restoredFiles.push(indexPath);
recoveryResult.steps.push({
step: 'file_restored',
timestamp: Date.now(),
message: `Restored file: ${fileName}`,
file: indexPath
});
}
}
recoveryResult.steps.push({
step: 'backup_restore_complete',
timestamp: Date.now(),
message: 'Backup restore completed'
});
}
/**
* 部分修復
*/
async executePartialRepair(indexPaths, recoveryResult) {
recoveryResult.steps.push({
step: 'partial_repair_start',
timestamp: Date.now(),
message: 'Starting partial repair process'
});
// 修復可能な問題の特定と修正
const issues = recoveryResult.integrityResult.issues;
for (const issue of issues) {
try {
switch (issue.type) {
case 'METADATA_MISMATCH':
await this.repairMetadataMismatch(issue, recoveryResult);
break;
case 'HIGH_DUPLICATION':
await this.repairDuplication(issue, recoveryResult);
break;
case 'EMPTY_DATA':
recoveryResult.steps.push({
step: 'repair_skip',
timestamp: Date.now(),
message: `Cannot repair empty data issue: ${issue.message}`,
issue: issue.type
});
break;
default:
recoveryResult.steps.push({
step: 'repair_skip',
timestamp: Date.now(),
message: `No repair strategy for issue: ${issue.type}`,
issue: issue.type
});
}
} catch (error) {
recoveryResult.errors.push(`Failed to repair ${issue.type}: ${error.message}`);
}
}
recoveryResult.steps.push({
step: 'partial_repair_complete',
timestamp: Date.now(),
message: 'Partial repair completed'
});
}
/**
* フル再構築
*/
async executeFullRebuild(indexPaths, recoveryResult) {
recoveryResult.steps.push({
step: 'full_rebuild_start',
timestamp: Date.now(),
message: 'Starting full index rebuild'
});
// この実装では、実際の再構築プロセスは呼び出し元に委ねる
// イベントを発火して外部システムに再構築を依頼
this.emit('rebuild-requested', {
indexPaths,
reason: 'corruption_recovery',
recoveryResult
});
recoveryResult.steps.push({
step: 'rebuild_requested',
timestamp: Date.now(),
message: 'Index rebuild has been requested from external system'
});
// 実際の再構築は外部で行われるため、ここでは完了とする
recoveryResult.steps.push({
step: 'full_rebuild_delegated',
timestamp: Date.now(),
message: 'Full rebuild delegated to external system'
});
}
/**
* メタデータ不整合修復
*/
async repairMetadataMismatch(issue, recoveryResult) {
// 実際のチャンクファイルからメタデータを再生成
if (issue.files && issue.files.length >= 2) {
const metadataFile = issue.files.find(f => f.includes('metadata'));
const chunksFile = issue.files.find(f => f.includes('chunks'));
if (metadataFile && chunksFile && fsSync.existsSync(chunksFile)) {
const chunksContent = await fs.readFile(chunksFile, 'utf8');
const chunks = JSON.parse(chunksContent);
let metadata = {};
if (fsSync.existsSync(metadataFile)) {
const metadataContent = await fs.readFile(metadataFile, 'utf8');
metadata = JSON.parse(metadataContent);
}
// メタデータを実際のデータに合わせて更新
metadata.totalChunks = chunks.length;
metadata.lastUpdated = new Date().toISOString();
metadata.repairedAt = Date.now();
await fs.writeFile(metadataFile, JSON.stringify(metadata, null, 2));
recoveryResult.steps.push({
step: 'metadata_repaired',
timestamp: Date.now(),
message: `Repaired metadata mismatch: updated totalChunks to ${chunks.length}`,
file: metadataFile
});
}
}
}
/**
* 重複データ修復
*/
async repairDuplication(issue, recoveryResult) {
if (issue.file) {
const content = await fs.readFile(issue.file, 'utf8');
const data = JSON.parse(content);
if (Array.isArray(data)) {
const originalLength = data.length;
const uniqueData = [];
const seen = new Set();
for (const item of data) {
const key = JSON.stringify(item);
if (!seen.has(key)) {
seen.add(key);
uniqueData.push(item);
}
}
if (uniqueData.length < originalLength) {
await fs.writeFile(issue.file, JSON.stringify(uniqueData, null, 2));
recoveryResult.steps.push({
step: 'duplication_repaired',
timestamp: Date.now(),
message: `Removed ${originalLength - uniqueData.length} duplicate entries`,
file: issue.file,
originalLength,
newLength: uniqueData.length
});
}
}
}
}
/**
* 利用可能なバックアップの検索
*/
async findAvailableBackups(backupDir) {
try {
const entries = await fs.readdir(backupDir);
const backups = [];
for (const entry of entries) {
const entryPath = path.join(backupDir, entry);
const stat = await fs.stat(entryPath);
if (stat.isDirectory()) {
// タイムスタンプからバックアップ情報を抽出
const timestamp = entry.replace('backup_', '').replace(/[^0-9]/g, '');
if (timestamp.length >= 8) { // YYYYMMDD形式以上
backups.push({
timestamp: entry,
path: entryPath,
created: stat.ctime,
size: await this.calculateDirectorySize(entryPath)
});
}
}
}
// 新しい順にソート
return backups.sort((a, b) => b.created.getTime() - a.created.getTime());
} catch (error) {
return [];
}
}
/**
* ディレクトリサイズ計算
*/
async calculateDirectorySize(dirPath) {
let totalSize = 0;
try {
const entries = await fs.readdir(dirPath);
for (const entry of entries) {
const entryPath = path.join(dirPath, entry);
const stat = await fs.stat(entryPath);
if (stat.isFile()) {
totalSize += stat.size;
} else if (stat.isDirectory()) {
totalSize += await this.calculateDirectorySize(entryPath);
}
}
} catch (error) {
// エラーの場合は0を返す
}
return totalSize;
}
/**
* 自動バックアップ作成
*/
async createBackup(indexPaths, metadata = {}) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = path.join(path.dirname(indexPaths[0]), '.backups', `backup_${timestamp}`);
// バックアップディレクトリ作成
await fs.mkdir(backupDir, { recursive: true });
const backupResult = {
timestamp,
backupDir,
files: [],
metadata,
size: 0
};
try {
for (const indexPath of indexPaths) {
if (fsSync.existsSync(indexPath)) {
const fileName = path.basename(indexPath);
const backupFilePath = path.join(backupDir, fileName);
await fs.copyFile(indexPath, backupFilePath);
const stat = await fs.stat(backupFilePath);
backupResult.files.push({
original: indexPath,
backup: backupFilePath,
size: stat.size
});
backupResult.size += stat.size;
}
}
// バックアップメタデータ保存
const backupMetadata = {
timestamp,
created: new Date().toISOString(),
files: backupResult.files,
totalSize: backupResult.size,
metadata
};
await fs.writeFile(
path.join(backupDir, 'backup_metadata.json'),
JSON.stringify(backupMetadata, null, 2)
);
this.stats.backupsCreated++;
this.state.lastBackupTime = Date.now();
// 古いバックアップの削除
await this.cleanupOldBackups(path.dirname(backupDir));
this.emit('backup-created', backupResult);
return backupResult;
} catch (error) {
// 失敗した場合はバックアップディレクトリを削除
try {
await fs.rmdir(backupDir, { recursive: true });
} catch (cleanupError) {
// クリーンアップエラーは無視
}
throw error;
}
}
/**
* 古いバックアップのクリーンアップ
*/
async cleanupOldBackups(backupsDir) {
try {
const backups = await this.findAvailableBackups(backupsDir);
if (backups.length > this.options.maxBackups) {
const toDelete = backups.slice(this.options.maxBackups);
for (const backup of toDelete) {
await fs.rmdir(backup.path, { recursive: true });
}
}
} catch (error) {
// クリーンアップエラーはログのみ
if (this.options.verboseLogging) {
console.warn('Failed to cleanup old backups:', error.message);
}
}
}
/**
* 定期的整合性チェック開始
*/
startPeriodicChecks() {
if (this.checkTimer) {
clearInterval(this.checkTimer);
}
this.checkTimer = setInterval(() => {
this.emit('periodic-check-due');
}, this.options.checkInterval);
}
/**
* 自動バックアップ開始
*/
startAutoBackup() {
if (this.backupTimer) {
clearInterval(this.backupTimer);
}
this.backupTimer = setInterval(() => {
this.emit('backup-due');
}, this.options.backupInterval);
}
/**
* チェック統計更新
*/
updateCheckStats(checkTime) {
const alpha = 0.1;
this.stats.averageCheckTime = this.stats.averageCheckTime * (1 - alpha) + checkTime * alpha;
}
/**
* 統計情報取得
*/
getStats() {
return {
...this.stats,
state: this.state,
timers: {
periodicCheckEnabled: !!this.checkTimer,
autoBackupEnabled: !!this.backupTimer
}
};
}
/**
* クリーンアップ
*/
cleanup() {
if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = null;
}
if (this.backupTimer) {
clearInterval(this.backupTimer);
this.backupTimer = null;
}
this.emit('cleanup');
}
}
// デフォルトインスタンス
export const defaultIndexIntegrityManager = new IndexIntegrityManager();