i18ntk 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -2
- package/README.md +157 -15
- package/SECURITY.md +14 -8
- package/main/i18ntk-backup.js +305 -62
- package/main/i18ntk-scanner.js +188 -49
- package/main/i18ntk-sizing.js +223 -29
- package/main/i18ntk-usage.js +203 -3
- package/main/i18ntk-validate.js +107 -3
- package/main/manage/commands/FixerCommand.js +23 -21
- package/main/manage/index.js +13 -7
- package/main/manage/services/FileManagementService.js +12 -6
- package/package.json +2 -2
- package/runtime/i18ntk.d.ts +22 -16
- package/runtime/index.d.ts +9 -7
- package/runtime/index.js +240 -50
- package/ui-locales/en.json +1 -1
- package/utils/translate/protection.js +147 -6
- package/utils/watch-locales.js +183 -36
package/main/i18ntk-backup.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const fsp = fs.promises;
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const fsp = fs.promises;
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const SecurityUtils = require('../utils/security');
|
|
8
10
|
|
|
9
11
|
// Simple CLI argument parser
|
|
10
12
|
function parseArgs(args) {
|
|
@@ -37,6 +39,93 @@ function parseArgs(args) {
|
|
|
37
39
|
|
|
38
40
|
return result;
|
|
39
41
|
}
|
|
42
|
+
function handleError(error) {
|
|
43
|
+
logger.error(error.message || 'Unknown error');
|
|
44
|
+
logger.debug(error.stack || error.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function computeFileHash(filePath) {
|
|
49
|
+
const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath));
|
|
50
|
+
if (content === null) return null;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(content);
|
|
53
|
+
const normalized = JSON.stringify(parsed);
|
|
54
|
+
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
55
|
+
} catch {
|
|
56
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function computeContentHash(content) {
|
|
61
|
+
if (typeof content === 'object' && content !== null) {
|
|
62
|
+
const normalized = JSON.stringify(content);
|
|
63
|
+
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
64
|
+
}
|
|
65
|
+
const str = String(content);
|
|
66
|
+
return crypto.createHash('sha256').update(str).digest('hex');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function findMostRecentBackup(backupDirPath) {
|
|
70
|
+
try {
|
|
71
|
+
const files = await fsp.readdir(backupDirPath);
|
|
72
|
+
const backups = [];
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
if (file.startsWith('backup-') && file.endsWith('.json')) {
|
|
75
|
+
const filePath = path.join(backupDirPath, file);
|
|
76
|
+
const stats = await fsp.stat(filePath);
|
|
77
|
+
backups.push({ name: file, path: filePath, mtime: stats.mtime });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
backups.sort((a, b) => b.mtime - a.mtime);
|
|
81
|
+
return backups[0] || null;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err.code === 'ENOENT') return null;
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getParentHashes(parentData) {
|
|
89
|
+
if (parentData._meta && parentData._meta.hashes) {
|
|
90
|
+
return parentData._meta.hashes;
|
|
91
|
+
}
|
|
92
|
+
const hashes = {};
|
|
93
|
+
for (const [file, content] of Object.entries(parentData)) {
|
|
94
|
+
if (typeof content === 'object' && content !== null) {
|
|
95
|
+
hashes[file] = computeContentHash(content);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return hashes;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function buildRestoreChain(startPath, startData) {
|
|
102
|
+
const chain = [{ path: startPath, data: startData }];
|
|
103
|
+
let current = startData;
|
|
104
|
+
let currentPath = startPath;
|
|
105
|
+
|
|
106
|
+
while (current._meta && current._meta.parent) {
|
|
107
|
+
if (chain.length >= 11) {
|
|
108
|
+
throw new Error('Chain broken: incremental backup chain exceeds the maximum depth of 10');
|
|
109
|
+
}
|
|
110
|
+
const parentName = current._meta.parent;
|
|
111
|
+
const parentDir = path.dirname(currentPath);
|
|
112
|
+
const parentPath = path.join(parentDir, parentName);
|
|
113
|
+
if (!SecurityUtils.safeExistsSync(parentPath, parentDir)) {
|
|
114
|
+
throw new Error(`Chain broken: parent backup '${parentName}' not found in ${parentDir}`);
|
|
115
|
+
}
|
|
116
|
+
const parentRaw = SecurityUtils.safeReadFileSync(parentPath, parentDir, 'utf8');
|
|
117
|
+
if (parentRaw === null) {
|
|
118
|
+
throw new Error(`Chain broken: cannot read parent backup '${parentName}'`);
|
|
119
|
+
}
|
|
120
|
+
current = JSON.parse(parentRaw);
|
|
121
|
+
currentPath = parentPath;
|
|
122
|
+
chain.push({ path: currentPath, data: current });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
chain.reverse();
|
|
126
|
+
return chain;
|
|
127
|
+
}
|
|
128
|
+
|
|
40
129
|
const configManager = require('../utils/config-manager');
|
|
41
130
|
const { logger } = require('../utils/logger');
|
|
42
131
|
const { colors } = require('../utils/logger');
|
|
@@ -103,25 +192,51 @@ Options:
|
|
|
103
192
|
--output <path> Output directory for backup/restore
|
|
104
193
|
--force Overwrite existing files without prompting
|
|
105
194
|
--keep <number> Number of backups to keep (default: 10)
|
|
195
|
+
--incremental Create an incremental backup (only changed files)
|
|
106
196
|
`);
|
|
107
197
|
}
|
|
108
198
|
|
|
109
199
|
// Command handlers
|
|
200
|
+
async function cleanupOldBackups(backupDirPath) {
|
|
201
|
+
try {
|
|
202
|
+
const files = await fsp.readdir(backupDirPath);
|
|
203
|
+
const backupFiles = files
|
|
204
|
+
.filter(file => file.startsWith('backup-') && file.endsWith('.json'))
|
|
205
|
+
.map(file => ({
|
|
206
|
+
name: file,
|
|
207
|
+
path: path.join(backupDirPath, file),
|
|
208
|
+
time: fs.statSync(path.join(backupDirPath, file)).mtime.getTime()
|
|
209
|
+
}))
|
|
210
|
+
.sort((a, b) => b.time - a.time);
|
|
211
|
+
|
|
212
|
+
const toDelete = backupFiles.slice(maxBackups);
|
|
213
|
+
for (const file of toDelete) {
|
|
214
|
+
try {
|
|
215
|
+
await fsp.unlink(file.path);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
logger.warn(`Could not remove old backup ${file.name}: ${err.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err.code !== 'ENOENT') {
|
|
222
|
+
logger.warn(`Error during cleanup: ${err.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
110
227
|
async function handleCreate(args) {
|
|
111
|
-
// Use absolute path for the locales directory
|
|
112
228
|
const dir = args._[1] || path.join(__dirname, '..', 'locales');
|
|
113
229
|
const outputDir = args.output || backupDir;
|
|
114
230
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
115
231
|
const backupName = `backup-${timestamp}.json`;
|
|
116
232
|
const backupPath = path.join(outputDir, backupName);
|
|
233
|
+
const isIncremental = !!args.incremental;
|
|
117
234
|
|
|
118
|
-
// Log the paths for debugging
|
|
119
235
|
logger.debug(`Source directory: ${dir}`);
|
|
120
236
|
logger.debug(`Backup will be saved to: ${backupPath}`);
|
|
121
237
|
|
|
122
|
-
// Create backup directory if it doesn't exist
|
|
123
238
|
try {
|
|
124
|
-
await fsp.mkdir(outputDir, { recursive: true });
|
|
239
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
125
240
|
logger.debug(`Created backup directory: ${outputDir}`);
|
|
126
241
|
} catch (err) {
|
|
127
242
|
if (err.code !== 'EEXIST') {
|
|
@@ -131,10 +246,9 @@ async function handleCreate(args) {
|
|
|
131
246
|
logger.debug(`Using existing backup directory: ${outputDir}`);
|
|
132
247
|
}
|
|
133
248
|
|
|
134
|
-
// Validate directory
|
|
135
249
|
const sourceDir = path.resolve(dir);
|
|
136
250
|
try {
|
|
137
|
-
const stats = await fsp.stat(sourceDir);
|
|
251
|
+
const stats = await fsp.stat(sourceDir);
|
|
138
252
|
if (!stats.isDirectory()) {
|
|
139
253
|
throw new Error(`Path exists but is not a directory: ${sourceDir}`);
|
|
140
254
|
}
|
|
@@ -147,41 +261,91 @@ async function handleCreate(args) {
|
|
|
147
261
|
}
|
|
148
262
|
|
|
149
263
|
logger.info('\nCreating backup...');
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const files = (await fsp.readdir(sourceDir, { withFileTypes: true }))
|
|
264
|
+
|
|
265
|
+
const files = (await fsp.readdir(sourceDir, { withFileTypes: true }))
|
|
153
266
|
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
|
|
154
267
|
.map(dirent => dirent.name);
|
|
155
|
-
|
|
268
|
+
|
|
156
269
|
if (files.length === 0) {
|
|
157
270
|
logger.warn('No JSON files found in the specified directory');
|
|
158
271
|
process.exit(0);
|
|
159
272
|
}
|
|
160
|
-
|
|
161
|
-
// Read all translation files
|
|
273
|
+
|
|
162
274
|
const translations = {};
|
|
275
|
+
const hashes = {};
|
|
163
276
|
for (const file of files) {
|
|
164
277
|
const filePath = path.join(sourceDir, file);
|
|
165
278
|
try {
|
|
166
|
-
const
|
|
167
|
-
|
|
279
|
+
const rawContent = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
|
|
280
|
+
if (rawContent === null) {
|
|
281
|
+
logger.error(`Could not read file ${file}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
translations[file] = JSON.parse(rawContent);
|
|
285
|
+
hashes[file] = computeFileHash(filePath);
|
|
168
286
|
} catch (error) {
|
|
169
287
|
logger.error(`Could not read file ${file}: ${error.message}`);
|
|
170
288
|
}
|
|
171
289
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
290
|
+
|
|
291
|
+
let meta = {
|
|
292
|
+
type: 'full',
|
|
293
|
+
parent: null,
|
|
294
|
+
chainDepth: 0,
|
|
295
|
+
hashes: hashes
|
|
296
|
+
};
|
|
297
|
+
let filesToInclude = translations;
|
|
298
|
+
|
|
299
|
+
if (isIncremental) {
|
|
300
|
+
const parentBackup = await findMostRecentBackup(outputDir);
|
|
301
|
+
if (parentBackup) {
|
|
302
|
+
const parentRaw = SecurityUtils.safeReadFileSync(parentBackup.path, path.dirname(parentBackup.path), 'utf8');
|
|
303
|
+
const parentData = JSON.parse(parentRaw);
|
|
304
|
+
const parentDepth = (parentData._meta && parentData._meta.chainDepth) || 0;
|
|
305
|
+
if (parentDepth >= 10) {
|
|
306
|
+
logger.info(' Incremental chain depth limit reached; creating a new full backup');
|
|
307
|
+
} else {
|
|
308
|
+
const parentHashes = getParentHashes(parentData);
|
|
309
|
+
|
|
310
|
+
const changedFiles = {};
|
|
311
|
+
for (const [file, content] of Object.entries(translations)) {
|
|
312
|
+
if (!parentHashes[file] || hashes[file] !== parentHashes[file]) {
|
|
313
|
+
changedFiles[file] = content;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
filesToInclude = changedFiles;
|
|
317
|
+
|
|
318
|
+
meta = {
|
|
319
|
+
type: 'incremental',
|
|
320
|
+
parent: parentBackup.name,
|
|
321
|
+
chainDepth: parentDepth + 1,
|
|
322
|
+
hashes: hashes
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
logger.info(` Incremental mode: found ${Object.keys(filesToInclude).length} changed files (parent: ${parentBackup.name}, depth: ${meta.chainDepth})`);
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
logger.info(' No previous backup found; creating full backup');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const backupData = { _meta: meta };
|
|
333
|
+
for (const [file, content] of Object.entries(filesToInclude)) {
|
|
334
|
+
backupData[file] = content;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await fsp.writeFile(backupPath, JSON.stringify(backupData, null, 2));
|
|
338
|
+
const stats = await fsp.stat(backupPath);
|
|
339
|
+
|
|
177
340
|
logger.success('Backup created successfully');
|
|
178
341
|
logger.info(` Location: ${backupPath}`);
|
|
179
342
|
logger.info(` Size: ${(stats.size / 1024).toFixed(2)} KB`);
|
|
180
343
|
logger.info(` Timestamp: ${new Date().toLocaleString()}`);
|
|
181
|
-
|
|
182
|
-
|
|
344
|
+
logger.info(` Files included: ${Object.keys(filesToInclude).length}`);
|
|
345
|
+
logger.info(` Type: ${meta.type}`);
|
|
346
|
+
|
|
183
347
|
await cleanupOldBackups(outputDir);
|
|
184
|
-
}
|
|
348
|
+
}
|
|
185
349
|
|
|
186
350
|
async function handleRestore(args) {
|
|
187
351
|
const backupFile = args._[1];
|
|
@@ -190,41 +354,54 @@ async function handleRestore(args) {
|
|
|
190
354
|
}
|
|
191
355
|
|
|
192
356
|
const backupPath = path.resolve(process.cwd(), backupFile);
|
|
193
|
-
const outputDir = args.output
|
|
194
|
-
? path.resolve(process.cwd(), args.output)
|
|
357
|
+
const outputDir = args.output
|
|
358
|
+
? path.resolve(process.cwd(), args.output)
|
|
195
359
|
: path.join(process.cwd(), 'restored');
|
|
196
|
-
|
|
197
|
-
// Validate backup file
|
|
360
|
+
|
|
198
361
|
if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
|
|
199
362
|
throw new Error(`Backup file not found: ${backupPath}`);
|
|
200
363
|
}
|
|
201
|
-
|
|
364
|
+
|
|
202
365
|
logger.info('\nRestoring backup...');
|
|
203
|
-
|
|
366
|
+
|
|
204
367
|
try {
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
368
|
+
const backupData = JSON.parse(await fsp.readFile(backupPath, 'utf8'));
|
|
369
|
+
const isIncremental = backupData._meta && backupData._meta.type === 'incremental';
|
|
370
|
+
|
|
371
|
+
if (isIncremental) {
|
|
372
|
+
const chain = await buildRestoreChain(backupPath, backupData);
|
|
373
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
374
|
+
|
|
375
|
+
const restoredFiles = new Set();
|
|
376
|
+
for (const entry of chain) {
|
|
377
|
+
for (const [file, content] of Object.entries(entry.data)) {
|
|
378
|
+
if (file === '_meta') continue;
|
|
379
|
+
const filePath = path.join(outputDir, file);
|
|
380
|
+
SecurityUtils.safeWriteFileSync(filePath, JSON.stringify(content, null, 2), path.dirname(filePath), 'utf8');
|
|
381
|
+
restoredFiles.add(file);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
logger.success('Incremental backup restored successfully');
|
|
386
|
+
logger.info(` ${restoredFiles.size} files restored across ${chain.length} backup(s) to: ${outputDir}`);
|
|
387
|
+
} else {
|
|
388
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
389
|
+
|
|
390
|
+
let count = 0;
|
|
391
|
+
for (const [file, content] of Object.entries(backupData)) {
|
|
392
|
+
if (file === '_meta') continue;
|
|
393
|
+
const filePath = path.join(outputDir, file);
|
|
394
|
+
SecurityUtils.safeWriteFileSync(filePath, JSON.stringify(content, null, 2), path.dirname(filePath), 'utf8');
|
|
395
|
+
count++;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
logger.success('Backup restored successfully');
|
|
399
|
+
logger.info(` Restored ${count} files to: ${outputDir}`);
|
|
220
400
|
}
|
|
221
|
-
|
|
222
|
-
logger.success('Backup restored successfully');
|
|
223
|
-
logger.info(` Restored ${Object.keys(translations).length} files to: ${outputDir}`);
|
|
224
401
|
} catch (error) {
|
|
225
402
|
handleError(error);
|
|
226
403
|
}
|
|
227
|
-
}
|
|
404
|
+
}
|
|
228
405
|
|
|
229
406
|
async function handleList() {
|
|
230
407
|
try {
|
|
@@ -298,32 +475,98 @@ async function handleVerify(args) {
|
|
|
298
475
|
}
|
|
299
476
|
|
|
300
477
|
const backupPath = path.resolve(process.cwd(), backupFile);
|
|
301
|
-
|
|
302
|
-
// Validate backup file
|
|
478
|
+
|
|
303
479
|
if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
|
|
304
480
|
throw new Error(`Backup file not found: ${backupPath}`);
|
|
305
481
|
}
|
|
306
|
-
|
|
482
|
+
|
|
307
483
|
logger.info('\nVerifying backup...');
|
|
308
|
-
|
|
484
|
+
|
|
309
485
|
try {
|
|
310
|
-
const data = await fsp.readFile(backupPath, 'utf8');
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
486
|
+
const data = JSON.parse(await fsp.readFile(backupPath, 'utf8'));
|
|
487
|
+
|
|
488
|
+
if (data._meta && data._meta.hashes) {
|
|
489
|
+
logger.info(' Performing hash chain verification...');
|
|
490
|
+
|
|
491
|
+
const chain = [{ path: backupPath, data: data }];
|
|
492
|
+
let current = data;
|
|
493
|
+
let currentPath = backupPath;
|
|
494
|
+
while (current._meta && current._meta.parent) {
|
|
495
|
+
if (chain.length >= 11) {
|
|
496
|
+
logger.warn(' Chain broken: maximum incremental depth of 10 exceeded');
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
const parentName = current._meta.parent;
|
|
500
|
+
const parentDir = path.dirname(currentPath);
|
|
501
|
+
const parentPath = path.join(parentDir, parentName);
|
|
502
|
+
if (!SecurityUtils.safeExistsSync(parentPath, parentDir)) {
|
|
503
|
+
logger.warn(` Chain broken: parent '${parentName}' not found`);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
const parentRaw = SecurityUtils.safeReadFileSync(parentPath, parentDir, 'utf8');
|
|
507
|
+
if (parentRaw === null) {
|
|
508
|
+
logger.warn(` Chain broken: cannot read parent '${parentName}'`);
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
current = JSON.parse(parentRaw);
|
|
512
|
+
currentPath = parentPath;
|
|
513
|
+
chain.push({ path: currentPath, data: current });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let allValid = true;
|
|
517
|
+
const reconstructed = {};
|
|
518
|
+
for (const entry of chain) {
|
|
519
|
+
const entryMeta = entry.data._meta;
|
|
520
|
+
const entryHashes = entryMeta ? entryMeta.hashes : null;
|
|
521
|
+
const entryName = path.basename(entry.path);
|
|
522
|
+
|
|
523
|
+
for (const [file, content] of Object.entries(entry.data)) {
|
|
524
|
+
if (file !== '_meta') reconstructed[file] = content;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!entryHashes) {
|
|
528
|
+
logger.warn(` ${entryName}: no manifest hashes (legacy backup)`);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let entryValid = true;
|
|
533
|
+
for (const [file, expectedHash] of Object.entries(entryHashes)) {
|
|
534
|
+
if (!Object.prototype.hasOwnProperty.call(reconstructed, file)) {
|
|
535
|
+
logger.warn(` Missing file in manifest: ${file}`);
|
|
536
|
+
entryValid = false;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const computedHash = computeContentHash(reconstructed[file]);
|
|
540
|
+
if (computedHash !== expectedHash) {
|
|
541
|
+
logger.error(` Hash mismatch: ${file} (expected ${expectedHash.slice(0, 12)}..., got ${computedHash.slice(0, 12)}...)`);
|
|
542
|
+
entryValid = false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (entryValid) {
|
|
547
|
+
logger.success(` ${entryName}: ${Object.keys(entryHashes).length} file(s) verified`);
|
|
548
|
+
} else {
|
|
549
|
+
allValid = false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (allValid) {
|
|
554
|
+
logger.success('\nBackup chain verification passed');
|
|
555
|
+
} else {
|
|
556
|
+
logger.error('\nBackup chain verification FAILED');
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
const fileCount = Object.keys(data).filter(k => k !== '_meta').length;
|
|
315
560
|
logger.success('Backup is valid');
|
|
316
561
|
logger.info(` Contains ${fileCount} translation files`);
|
|
317
|
-
logger.info(` Last modified: ${(await fsp.stat(backupPath)).mtime.toLocaleString()}`);
|
|
318
|
-
} else {
|
|
319
|
-
throw new Error('Invalid backup format');
|
|
562
|
+
logger.info(` Last modified: ${(await fsp.stat(backupPath)).mtime.toLocaleString()}`);
|
|
320
563
|
}
|
|
321
564
|
} catch (error) {
|
|
322
565
|
logger.error('Backup verification failed!');
|
|
323
566
|
logger.error(` Error: ${error.message}`);
|
|
324
567
|
process.exit(1);
|
|
325
568
|
}
|
|
326
|
-
}
|
|
569
|
+
}
|
|
327
570
|
|
|
328
571
|
async function handleCleanup(args) {
|
|
329
572
|
const keep = args.keep ? parseInt(args.keep, 10) : maxBackups;
|