feat: add backup and restore mechanism to ConfigManager
This commit is contained in:
@@ -145,6 +145,7 @@ function validateEditableFields(settings: Partial<SettingsData>): {
|
||||
export class ConfigManager {
|
||||
private static instance: ConfigManager | null = null
|
||||
private envPath: string
|
||||
private backupPath: string
|
||||
private configCache: Map<string, string> = new Map()
|
||||
private initialized: boolean = false
|
||||
|
||||
@@ -153,6 +154,7 @@ export class ConfigManager {
|
||||
return
|
||||
}
|
||||
this.envPath = path.resolve(__dirname, '../../.env')
|
||||
this.backupPath = path.resolve(__dirname, '../../.env.backup')
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
@@ -599,4 +601,39 @@ export class ConfigManager {
|
||||
public getDefaultSettings(): SettingsData {
|
||||
return DEFAULT_SETTINGS
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup current .env file
|
||||
*/
|
||||
private async backupEnvFile(): Promise<boolean> {
|
||||
try {
|
||||
if (fs.existsSync(this.envPath)) {
|
||||
fs.copyFileSync(this.envPath, this.backupPath)
|
||||
console.log('[ConfigManager] Backup created', this.backupPath)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('[ConfigManager] Failed to backup .env file:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore .env file from backup
|
||||
*/
|
||||
private async restoreBackup(): Promise<boolean> {
|
||||
try {
|
||||
if (fs.existsSync(this.backupPath)) {
|
||||
fs.copyFileSync(this.backupPath, this.envPath)
|
||||
await this.loadEnvFile()
|
||||
console.log('[ConfigManager] Restored from backup')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('[ConfigManager] Failed to restore backup:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,38 @@ describe('ConfigManager - deep merge utilities', () => {
|
||||
|
||||
// Setup initial state
|
||||
const initial: SettingsData = {
|
||||
erp: { url: 'http://old.com', username: 'user1', password: 'pass1', headless: true, ignoreHttpsErrors: true, autoCloseBrowser: true },
|
||||
database: { dbType: 'mysql', server: '', mysqlHost: 'localhost', mysqlPort: 3306, database: 'db', username: 'user', password: '' },
|
||||
erp: {
|
||||
url: 'http://old.com',
|
||||
username: 'user1',
|
||||
password: 'pass1',
|
||||
headless: true,
|
||||
ignoreHttpsErrors: true,
|
||||
autoCloseBrowser: true
|
||||
},
|
||||
database: {
|
||||
dbType: 'mysql',
|
||||
server: '',
|
||||
mysqlHost: 'localhost',
|
||||
mysqlPort: 3306,
|
||||
database: 'db',
|
||||
username: 'user',
|
||||
password: ''
|
||||
},
|
||||
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'validation.xlsx' },
|
||||
extraction: { batchSize: 100, verbose: true, autoConvert: true, mergeBatches: true, enableDbPersistence: true },
|
||||
validation: { dataSource: 'database_full', batchSize: 2000, matchMode: 'substring', enableCrud: false, defaultManager: '' },
|
||||
extraction: {
|
||||
batchSize: 100,
|
||||
verbose: true,
|
||||
autoConvert: true,
|
||||
mergeBatches: true,
|
||||
enableDbPersistence: true
|
||||
},
|
||||
validation: {
|
||||
dataSource: 'database_full',
|
||||
batchSize: 2000,
|
||||
matchMode: 'substring',
|
||||
enableCrud: false,
|
||||
defaultManager: ''
|
||||
},
|
||||
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
|
||||
execution: { dryRun: false }
|
||||
}
|
||||
@@ -41,3 +68,48 @@ describe('ConfigManager - deep merge utilities', () => {
|
||||
expect(current.paths.dataDir).toBe('/data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ConfigManager - backup and restore', () => {
|
||||
it('should create backup before saving', async () => {
|
||||
const manager = ConfigManager.getInstance()
|
||||
await manager.initialize()
|
||||
|
||||
const backupSuccess = await manager['backupEnvFile']()
|
||||
|
||||
expect(backupSuccess).toBe(true)
|
||||
|
||||
// Check backup file exists (in same location as .env file)
|
||||
const fs = await import('fs')
|
||||
const path = await import('path')
|
||||
const backupPath = path.resolve('src/main/.env.backup')
|
||||
|
||||
expect(fs.existsSync(backupPath)).toBe(true)
|
||||
})
|
||||
|
||||
it('should restore from backup when save fails', async () => {
|
||||
const manager = ConfigManager.getInstance()
|
||||
await manager.initialize()
|
||||
|
||||
// Create initial state
|
||||
const initial = manager.getAllSettings()
|
||||
const originalUrl = initial.erp.url
|
||||
|
||||
// Mock fs.writeFileSync to fail
|
||||
const fs = await import('fs')
|
||||
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {
|
||||
throw new Error('Disk full')
|
||||
})
|
||||
|
||||
const result = await manager.savePartialSettings({
|
||||
erp: { url: 'http://should-not-save.com' }
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
|
||||
// Restore should have happened
|
||||
const current = manager.getAllSettings()
|
||||
expect(current.erp.url).toBe(originalUrl)
|
||||
|
||||
writeFileSyncSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user