diff --git a/src/instance/manager.ts b/src/instance/manager.ts index 25624a8..abb17a9 100644 --- a/src/instance/manager.ts +++ b/src/instance/manager.ts @@ -1,6 +1,6 @@ import { spawn, exec } from 'child_process'; import { promisify } from 'util'; -import { access, mkdir, writeFile, readFile } from 'fs/promises'; +import { access, mkdir, writeFile, readFile, readdir, rmdir, unlink, stat } from 'fs/promises'; import { join, dirname } from 'path'; import { ConfigManager } from '../config/manager.js'; import type { PostgreSQLInstanceConfig } from '../config/types.js'; @@ -252,14 +252,24 @@ export class InstanceManager { await access(dataDirectory); // Directory exists, check if it's empty - const { readdir } = await import('fs/promises'); const files = await readdir(dataDirectory); if (files.length > 0) { + // Check if this looks like a partial pgforge installation + const isPartialInstallation = await this.detectPartialInstallation(dataDirectory, files); + + if (isPartialInstallation) { + // Automatically clean up partial installation + console.log(`Detected partial installation in '${dataDirectory}', cleaning up...`); + await this.cleanupDirectory(dataDirectory); + return; + } + + // Directory contains unrecognized files - require manual intervention throw new Error( - `Data directory '${dataDirectory}' exists but is not empty. ` + - `This may be from a previous failed installation. ` + - `Please remove the directory and try again: rm -rf "${dataDirectory}"` + `Data directory '${dataDirectory}' exists and contains files that don't appear to be from a failed pgforge installation. ` + + `Please manually review and remove the directory if safe: rm -rf "${dataDirectory}"\n` + + `Files found: ${files.slice(0, 5).join(', ')}${files.length > 5 ? ` (and ${files.length - 5} more)` : ''}` ); } } catch (error: any) { @@ -271,6 +281,95 @@ export class InstanceManager { } } + private async detectPartialInstallation(dataDirectory: string, files: string[]): Promise { + // Files that indicate a PostgreSQL/pgforge installation attempt + const pgforgeIndicators = [ + 'postgresql.conf', + 'pg_hba.conf', + 'PG_VERSION', + 'postmaster.pid', + 'postmaster.opts', + 'sockets', + 'pg_stat', + 'pg_xact', + 'pg_wal', + 'base', + 'global', + 'pg_tblspc' + ]; + + // Check if the directory contains only recognized PostgreSQL/pgforge files + const recognizedFiles = files.filter(file => + pgforgeIndicators.some(indicator => file.startsWith(indicator)) + ); + + // If most files are recognized as PostgreSQL-related, consider it a partial installation + const recognitionRatio = recognizedFiles.length / files.length; + + // Also check for specific pgforge markers + const hasPgforgeMarkers = files.some(file => + file === 'postgresql.conf' || file === 'pg_hba.conf' + ); + + // If there's a postgresql.conf file, check if it contains pgforge signature + if (hasPgforgeMarkers) { + try { + const configPath = join(dataDirectory, 'postgresql.conf'); + await access(configPath); + const configContent = await readFile(configPath, 'utf-8'); + + // Check for pgforge signature in the config file + if (configContent.includes('PostgreSQL configuration generated by PgForge')) { + return true; + } + } catch { + // Ignore errors reading config file + } + } + + // Consider it a partial installation if: + // 1. Most files are PostgreSQL-related (80% or more), OR + // 2. There are clear PostgreSQL indicators and few other files + return recognitionRatio >= 0.8 || (recognizedFiles.length >= 3 && files.length <= 10); + } + + private async cleanupDirectory(dataDirectory: string): Promise { + try { + // Recursively remove all contents of the directory + const files = await readdir(dataDirectory); + + for (const file of files) { + const filePath = join(dataDirectory, file); + const fileStat = await stat(filePath); + + if (fileStat.isDirectory()) { + await this.removeDirectoryRecursive(filePath); + } else { + await unlink(filePath); + } + } + } catch (error) { + throw new Error(`Failed to cleanup directory '${dataDirectory}': ${error}`); + } + } + + private async removeDirectoryRecursive(dirPath: string): Promise { + const files = await readdir(dirPath); + + for (const file of files) { + const filePath = join(dirPath, file); + const fileStat = await stat(filePath); + + if (fileStat.isDirectory()) { + await this.removeDirectoryRecursive(filePath); + } else { + await unlink(filePath); + } + } + + await rmdir(dirPath); + } + private async generateConfigFiles(config: PostgreSQLInstanceConfig): Promise { const configPath = join(config.spec.storage.dataDirectory, 'postgresql.conf'); const hbaPath = join(config.spec.storage.dataDirectory, 'pg_hba.conf');