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 {
|
export class ConfigManager {
|
||||||
private static instance: ConfigManager | null = null
|
private static instance: ConfigManager | null = null
|
||||||
private envPath: string
|
private envPath: string
|
||||||
|
private backupPath: string
|
||||||
private configCache: Map<string, string> = new Map()
|
private configCache: Map<string, string> = new Map()
|
||||||
private initialized: boolean = false
|
private initialized: boolean = false
|
||||||
|
|
||||||
@@ -153,6 +154,7 @@ export class ConfigManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.envPath = path.resolve(__dirname, '../../.env')
|
this.envPath = path.resolve(__dirname, '../../.env')
|
||||||
|
this.backupPath = path.resolve(__dirname, '../../.env.backup')
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,4 +601,39 @@ export class ConfigManager {
|
|||||||
public getDefaultSettings(): SettingsData {
|
public getDefaultSettings(): SettingsData {
|
||||||
return DEFAULT_SETTINGS
|
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
|
// Setup initial state
|
||||||
const initial: SettingsData = {
|
const initial: SettingsData = {
|
||||||
erp: { url: 'http://old.com', username: 'user1', password: 'pass1', headless: true, ignoreHttpsErrors: true, autoCloseBrowser: true },
|
erp: {
|
||||||
database: { dbType: 'mysql', server: '', mysqlHost: 'localhost', mysqlPort: 3306, database: 'db', username: 'user', password: '' },
|
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' },
|
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'validation.xlsx' },
|
||||||
extraction: { batchSize: 100, verbose: true, autoConvert: true, mergeBatches: true, enableDbPersistence: true },
|
extraction: {
|
||||||
validation: { dataSource: 'database_full', batchSize: 2000, matchMode: 'substring', enableCrud: false, defaultManager: '' },
|
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 },
|
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
|
||||||
execution: { dryRun: false }
|
execution: { dryRun: false }
|
||||||
}
|
}
|
||||||
@@ -41,3 +68,48 @@ describe('ConfigManager - deep merge utilities', () => {
|
|||||||
expect(current.paths.dataDir).toBe('/data')
|
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