-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrestore.js
More file actions
138 lines (116 loc) · 4.84 KB
/
Copy pathrestore.js
File metadata and controls
138 lines (116 loc) · 4.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
const fs = require('node:fs/promises');
const { constants } = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { loadEnvFile } = require('node:process');
const readline = require('node:readline/promises'); // Using modern Promise API
const MAX_DISPLAY_BACKUPS = 10;
// 1. Safely load the configuration
try {
loadEnvFile(path.join(__dirname, 'config.env'));
} catch (err) {
console.error(`[Error] Failed to load config.env: ${err.message}`);
process.exit(1);
}
// 2. Helper function to resolve paths (handles '~' expansion)
const resolvePath = (p) => {
if (!p) return '';
if (p.startsWith('~/') || p === '~') {
return path.join(os.homedir(), p.slice(1));
}
return path.resolve(p);
};
// 3. Environment validation
const requiredVars = ['BACKUP_DIR', 'MAX_BACKUPS', 'SOURCE_PATH'];
const missingVars = requiredVars.filter(key => !process.env[key]);
if (missingVars.length > 0) {
console.error(`[Error] Missing required environment variables: ${missingVars.join(', ')}`);
console.error(`[Error] Please check your config.env file.`);
process.exit(1);
}
const config = {
backupDir: resolvePath(process.env.BACKUP_DIR),
maxBackups: parseInt(process.env.MAX_BACKUPS, 10),
sourcePath: resolvePath(process.env.SOURCE_PATH),
saveBasename: path.basename(process.env.SOURCE_PATH, path.extname(process.env.SOURCE_PATH)),
saveExtension: path.extname(process.env.SOURCE_PATH)
};
if (isNaN(config.maxBackups) || config.maxBackups <= 0) {
console.error(`[Error] MAX_BACKUPS must be a positive integer.`);
process.exit(1);
}
// 4. Main asynchronous logic
async function main() {
// Check if the backup directory exists and is readable
try {
await fs.access(config.backupDir, constants.R_OK);
} catch {
console.error(`[Error] Backup directory does not exist or is not readable: ${config.backupDir}`);
process.exit(1);
}
// Load and sort files
const allFiles = await fs.readdir(config.backupDir);
const validBackups = allFiles
.filter(file => file.startsWith(`${config.saveBasename}_`) && file.endsWith(config.saveExtension))
// Lexicographical sorting by filename (accurate due to YYYY-MM-DD_HH-MM-SS format) - saves I/O!
.sort()
.reverse();
if (validBackups.length === 0) {
console.error(`[Error] No backups found to restore in directory: ${config.backupDir}`);
process.exit(1);
}
const recentFileNames = validBackups.slice(0, Math.min(config.maxBackups, MAX_DISPLAY_BACKUPS));
// Retrieve precise time metadata (stat) only for the items selected for display
const recentFiles = await Promise.all(recentFileNames.map(async (file) => {
const filePath = path.join(config.backupDir, file);
const stats = await fs.stat(filePath);
return {
name: file,
path: filePath,
time: stats.mtime.getTime()
};
}));
console.log('\n--- AVAILABLE BACKUPS TO RESTORE ---');
recentFiles.forEach((file, index) => {
// You can change 'en-US' to 'cs-CZ' if you prefer European date formatting
const dateStr = new Date(file.time).toLocaleString('en-US');
console.log(`[${index + 1}] ${file.name}`);
console.log(` Backup time: ${dateStr}`);
});
// 5. Interactive interface using asynchronous readline
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
try {
const answer = await rl.question(`\nEnter the backup number to restore (1-${recentFiles.length}) or '0' to cancel: `);
const choice = parseInt(answer, 10);
if (choice === 0) {
console.log('Restore cancelled by user.');
process.exit(0);
}
if (isNaN(choice) || choice < 1 || choice > recentFiles.length) {
console.error(`[Error] Invalid choice. Please enter a number between 1 and ${recentFiles.length}.`);
process.exit(1);
}
const selectedBackup = recentFiles[choice - 1];
const targetDir = path.dirname(config.sourcePath);
// Ensure the target directory exists
await fs.mkdir(targetDir, { recursive: true });
// Restore the file
await fs.copyFile(selectedBackup.path, config.sourcePath);
console.log(`\n[Success] Backup #${choice} successfully restored!`);
console.log(`[Success] Source: ${selectedBackup.name}`);
console.log(`[Success] Target: ${config.sourcePath}`);
} catch (err) {
console.error(`[Error] Failed during restore process: ${err.message}`);
process.exit(1);
} finally {
rl.close();
}
}
// Run the script with top-level error handling
main().catch(err => {
console.error(`[Fatal] Unexpected system error: ${err.message}`);
process.exit(1);
});