From de2784938f3342affd65895854958cfabeb6a6e2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 06:05:22 +0000 Subject: [PATCH] fix: automatically cleanup partial pgforge installations - Enhanced ensureDataDirectoryIsEmpty() to detect partial installations - Added intelligent detection of PostgreSQL/pgforge files and signatures - Automatic cleanup of detected partial installations without manual intervention - Improved error messages for unrecognized files with detailed file listing - Added safe recursive directory cleanup functionality Resolves issue where failed pgforge create attempts left directories in an unusable state requiring manual rm -rf cleanup. Co-authored-by: Samuel --- src/instance/manager.ts | 109 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 5 deletions(-) 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');