Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 104 additions & 5 deletions src/instance/manager.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -271,6 +281,95 @@ export class InstanceManager {
}
}

private async detectPartialInstallation(dataDirectory: string, files: string[]): Promise<boolean> {
// 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<void> {
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<void> {
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<void> {
const configPath = join(config.spec.storage.dataDirectory, 'postgresql.conf');
const hbaPath = join(config.spec.storage.dataDirectory, 'pg_hba.conf');
Expand Down