@mahe_pkm/buzl-capi 0.1.2 → 0.1.3
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/Buzl_GoogleAppsScript_Template.gs +948 -948
- package/CHANGELOG.md +109 -83
- package/LICENSE +21 -21
- package/README.md +339 -253
- package/USER_HANDBOOK.html +808 -0
- package/USER_HANDBOOK.md +842 -0
- package/assets/js/buzl-tracking.js +390 -390
- package/bin/cli.js +175 -155
- package/package.json +3 -1
- package/src/cli/terminal.js +388 -388
- package/src/core/injector.js +321 -321
- package/src/core/rollback.js +281 -281
- package/src/core/scanner.js +506 -506
- package/src/core/tester.js +666 -357
- package/src/gui/public/app.js +915 -87
- package/src/gui/public/index.html +620 -230
- package/src/gui/public/style.css +1603 -509
- package/src/gui/server.js +242 -232
- package/src/index.js +47 -47
- package/src/templates/GoogleAppsScript.gs +948 -948
- package/src/templates/buzl-tracking.js +475 -475
- package/src/templates/gtm-meta-snippets.js +149 -149
package/src/core/rollback.js
CHANGED
|
@@ -1,281 +1,281 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================================
|
|
3
|
-
* BUZL BACKUP & SNAPSHOT MANAGER
|
|
4
|
-
* Provides 100% safe file snapshots with SHA content hashing inside `.buzl/snapshots/`
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) 2026 Buzl Digital Solutions
|
|
7
|
-
* Licensed under the MIT License
|
|
8
|
-
* ============================================================================
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const crypto = require('crypto');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Compute a deterministic SHA-1 content hash across all candidate files
|
|
17
|
-
*/
|
|
18
|
-
function computeFilesHash(files, rootDir) {
|
|
19
|
-
const hashSum = crypto.createHash('sha1');
|
|
20
|
-
for (const filePath of files) {
|
|
21
|
-
if (fs.existsSync(filePath)) {
|
|
22
|
-
hashSum.update(path.relative(rootDir, filePath));
|
|
23
|
-
try {
|
|
24
|
-
hashSum.update(fs.readFileSync(filePath));
|
|
25
|
-
} catch (e) {}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return hashSum.digest('hex').substring(0, 8);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Create a point-in-time snapshot backup
|
|
33
|
-
* Stored in `<rootDir>/.buzl/snapshots/<timestamp>_<hash>/`
|
|
34
|
-
*/
|
|
35
|
-
function createBackup(arg1, arg2, arg3 = '') {
|
|
36
|
-
let files;
|
|
37
|
-
let rootDir;
|
|
38
|
-
let backupName = '';
|
|
39
|
-
|
|
40
|
-
if (Array.isArray(arg1)) {
|
|
41
|
-
files = arg1;
|
|
42
|
-
rootDir = arg2;
|
|
43
|
-
backupName = arg3 || '';
|
|
44
|
-
} else if (typeof arg1 === 'string') {
|
|
45
|
-
rootDir = arg1;
|
|
46
|
-
backupName = typeof arg2 === 'string' ? arg2 : '';
|
|
47
|
-
const { findHtmlFiles } = require('./scanner');
|
|
48
|
-
files = findHtmlFiles(rootDir);
|
|
49
|
-
} else {
|
|
50
|
-
files = [];
|
|
51
|
-
rootDir = process.cwd();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
55
|
-
const hash = computeFilesHash(files, rootDir) || '00000000';
|
|
56
|
-
const dirName = `${timestamp}_${hash}`;
|
|
57
|
-
|
|
58
|
-
// Dedicated unified .buzl/snapshots directory
|
|
59
|
-
const snapshotsDir = path.join(rootDir, '.buzl', 'snapshots');
|
|
60
|
-
const backupDir = path.join(snapshotsDir, dirName);
|
|
61
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
62
|
-
|
|
63
|
-
const manifest = {
|
|
64
|
-
id: dirName,
|
|
65
|
-
hash: hash,
|
|
66
|
-
name: (backupName || '').trim() || `Snapshot ${new Date().toLocaleTimeString()} [${hash}]`,
|
|
67
|
-
timestamp: new Date().toISOString(),
|
|
68
|
-
files: []
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
for (const filePath of files) {
|
|
72
|
-
const relPath = path.relative(rootDir, filePath);
|
|
73
|
-
const destPath = path.join(backupDir, relPath);
|
|
74
|
-
const destDir = path.dirname(destPath);
|
|
75
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
76
|
-
fs.copyFileSync(filePath, destPath);
|
|
77
|
-
manifest.files.push(relPath);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
fs.writeFileSync(
|
|
81
|
-
path.join(backupDir, 'backup-manifest.json'),
|
|
82
|
-
JSON.stringify(manifest, null, 2),
|
|
83
|
-
'utf8'
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
success: true,
|
|
88
|
-
backupDir,
|
|
89
|
-
dirName,
|
|
90
|
-
hash,
|
|
91
|
-
timestamp: manifest.timestamp,
|
|
92
|
-
name: manifest.name,
|
|
93
|
-
fileCount: manifest.files.length
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* List all available snapshot backups
|
|
99
|
-
* Checks `.buzl/snapshots/` first, plus legacy `.buzl-backup-*` folders
|
|
100
|
-
*/
|
|
101
|
-
function listBackups(rootDir) {
|
|
102
|
-
const allBackups = [];
|
|
103
|
-
|
|
104
|
-
// 1. Primary: Check .buzl/snapshots/
|
|
105
|
-
const snapshotsDir = path.join(rootDir, '.buzl', 'snapshots');
|
|
106
|
-
if (fs.existsSync(snapshotsDir)) {
|
|
107
|
-
try {
|
|
108
|
-
const entries = fs.readdirSync(snapshotsDir);
|
|
109
|
-
for (const e of entries) {
|
|
110
|
-
const dirPath = path.join(snapshotsDir, e);
|
|
111
|
-
if (fs.statSync(dirPath).isDirectory()) {
|
|
112
|
-
let manifest = null;
|
|
113
|
-
try {
|
|
114
|
-
const mPath = path.join(dirPath, 'backup-manifest.json');
|
|
115
|
-
if (fs.existsSync(mPath)) {
|
|
116
|
-
manifest = JSON.parse(fs.readFileSync(mPath, 'utf8'));
|
|
117
|
-
}
|
|
118
|
-
} catch (err) {}
|
|
119
|
-
|
|
120
|
-
allBackups.push({
|
|
121
|
-
id: (manifest && manifest.id) || e,
|
|
122
|
-
dirName: e,
|
|
123
|
-
hash: (manifest && manifest.hash) || (e.split('_')[1] || ''),
|
|
124
|
-
fullPath: dirPath,
|
|
125
|
-
name: (manifest && manifest.name) || e,
|
|
126
|
-
timestamp: (manifest && manifest.timestamp) || '',
|
|
127
|
-
filesCount: (manifest && manifest.files && manifest.files.length) || 0,
|
|
128
|
-
isLegacy: false,
|
|
129
|
-
manifest
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
} catch (e) {}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 2. Fallback: Check legacy root-level `.buzl-backup-*` directories
|
|
137
|
-
try {
|
|
138
|
-
const rootEntries = fs.readdirSync(rootDir);
|
|
139
|
-
for (const e of rootEntries) {
|
|
140
|
-
if (e.startsWith('.buzl-backup-')) {
|
|
141
|
-
const dirPath = path.join(rootDir, e);
|
|
142
|
-
if (fs.statSync(dirPath).isDirectory()) {
|
|
143
|
-
let manifest = null;
|
|
144
|
-
try {
|
|
145
|
-
const mPath = path.join(dirPath, 'backup-manifest.json');
|
|
146
|
-
if (fs.existsSync(mPath)) {
|
|
147
|
-
manifest = JSON.parse(fs.readFileSync(mPath, 'utf8'));
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {}
|
|
150
|
-
|
|
151
|
-
allBackups.push({
|
|
152
|
-
id: e,
|
|
153
|
-
dirName: e,
|
|
154
|
-
hash: (manifest && manifest.hash) || '',
|
|
155
|
-
fullPath: dirPath,
|
|
156
|
-
name: (manifest && manifest.name) || e.replace('.buzl-backup-', ''),
|
|
157
|
-
timestamp: (manifest && manifest.timestamp) || '',
|
|
158
|
-
filesCount: (manifest && manifest.files && manifest.files.length) || 0,
|
|
159
|
-
isLegacy: true,
|
|
160
|
-
manifest
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
} catch (e) {}
|
|
166
|
-
|
|
167
|
-
// Sort newest first
|
|
168
|
-
allBackups.sort((a, b) => b.dirName.localeCompare(a.dirName));
|
|
169
|
-
return allBackups;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Restore a specific snapshot or 'latest'
|
|
174
|
-
*/
|
|
175
|
-
function restoreBackup(rootDir, identifier) {
|
|
176
|
-
const backups = listBackups(rootDir);
|
|
177
|
-
if (backups.length === 0) {
|
|
178
|
-
return { success: false, message: 'No backups found to restore.' };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let targetBackup = backups[0];
|
|
182
|
-
if (identifier && identifier !== 'latest') {
|
|
183
|
-
targetBackup = backups.find(b =>
|
|
184
|
-
b.dirName === identifier ||
|
|
185
|
-
b.name === identifier ||
|
|
186
|
-
b.hash === identifier ||
|
|
187
|
-
b.id === identifier
|
|
188
|
-
);
|
|
189
|
-
if (!targetBackup) {
|
|
190
|
-
return { success: false, message: `Backup "${identifier}" not found.` };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const manifest = targetBackup.manifest;
|
|
195
|
-
if (!manifest || !manifest.files) {
|
|
196
|
-
return { success: false, message: 'Backup manifest corrupted or missing.' };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
let restoredCount = 0;
|
|
200
|
-
for (const relFile of manifest.files) {
|
|
201
|
-
const src = path.join(targetBackup.fullPath, relFile);
|
|
202
|
-
const dest = path.join(rootDir, relFile);
|
|
203
|
-
if (fs.existsSync(src)) {
|
|
204
|
-
const destDir = path.dirname(dest);
|
|
205
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
206
|
-
fs.copyFileSync(src, dest);
|
|
207
|
-
restoredCount++;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
success: true,
|
|
213
|
-
backupUsed: targetBackup.dirName,
|
|
214
|
-
backupName: targetBackup.name,
|
|
215
|
-
hash: targetBackup.hash,
|
|
216
|
-
restoredCount,
|
|
217
|
-
message: `Successfully restored ${restoredCount} file(s) from "${targetBackup.name}" [${targetBackup.hash || targetBackup.dirName}].`
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Restore the latest snapshot
|
|
223
|
-
*/
|
|
224
|
-
function restoreLatestBackup(rootDir) {
|
|
225
|
-
return restoreBackup(rootDir, 'latest');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Delete a specific backup
|
|
230
|
-
*/
|
|
231
|
-
function deleteBackup(rootDir, identifier) {
|
|
232
|
-
const backups = listBackups(rootDir);
|
|
233
|
-
const target = backups.find(b =>
|
|
234
|
-
b.dirName === identifier ||
|
|
235
|
-
b.name === identifier ||
|
|
236
|
-
b.id === identifier ||
|
|
237
|
-
b.hash === identifier
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
if (!target) {
|
|
241
|
-
return { success: false, message: `Backup "${identifier}" not found.` };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
fs.rmSync(target.fullPath, { recursive: true, force: true });
|
|
246
|
-
return { success: true, message: `Backup "${target.name}" deleted successfully.` };
|
|
247
|
-
} catch (e) {
|
|
248
|
-
return { success: false, message: e.message };
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Create a manual named backup
|
|
254
|
-
*/
|
|
255
|
-
function manualBackup(rootDir, backupName = '') {
|
|
256
|
-
const { findHtmlFiles } = require('./scanner');
|
|
257
|
-
const files = findHtmlFiles(rootDir);
|
|
258
|
-
if (files.length === 0) {
|
|
259
|
-
return { success: false, message: 'No HTML files found to backup.' };
|
|
260
|
-
}
|
|
261
|
-
const result = createBackup(files, rootDir, backupName);
|
|
262
|
-
return {
|
|
263
|
-
success: true,
|
|
264
|
-
backupDir: result.backupDir,
|
|
265
|
-
dirName: result.dirName,
|
|
266
|
-
hash: result.hash,
|
|
267
|
-
name: result.name,
|
|
268
|
-
fileCount: result.fileCount,
|
|
269
|
-
message: `Snapshot "${result.name}" created with ${result.fileCount} file(s) [SHA: ${result.hash}].`
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
module.exports = {
|
|
274
|
-
createBackup,
|
|
275
|
-
computeFilesHash,
|
|
276
|
-
listBackups,
|
|
277
|
-
restoreBackup,
|
|
278
|
-
restoreLatestBackup,
|
|
279
|
-
deleteBackup,
|
|
280
|
-
manualBackup
|
|
281
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* BUZL BACKUP & SNAPSHOT MANAGER
|
|
4
|
+
* Provides 100% safe file snapshots with SHA content hashing inside `.buzl/snapshots/`
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Buzl Digital Solutions
|
|
7
|
+
* Licensed under the MIT License
|
|
8
|
+
* ============================================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute a deterministic SHA-1 content hash across all candidate files
|
|
17
|
+
*/
|
|
18
|
+
function computeFilesHash(files, rootDir) {
|
|
19
|
+
const hashSum = crypto.createHash('sha1');
|
|
20
|
+
for (const filePath of files) {
|
|
21
|
+
if (fs.existsSync(filePath)) {
|
|
22
|
+
hashSum.update(path.relative(rootDir, filePath));
|
|
23
|
+
try {
|
|
24
|
+
hashSum.update(fs.readFileSync(filePath));
|
|
25
|
+
} catch (e) {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return hashSum.digest('hex').substring(0, 8);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a point-in-time snapshot backup
|
|
33
|
+
* Stored in `<rootDir>/.buzl/snapshots/<timestamp>_<hash>/`
|
|
34
|
+
*/
|
|
35
|
+
function createBackup(arg1, arg2, arg3 = '') {
|
|
36
|
+
let files;
|
|
37
|
+
let rootDir;
|
|
38
|
+
let backupName = '';
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(arg1)) {
|
|
41
|
+
files = arg1;
|
|
42
|
+
rootDir = arg2;
|
|
43
|
+
backupName = arg3 || '';
|
|
44
|
+
} else if (typeof arg1 === 'string') {
|
|
45
|
+
rootDir = arg1;
|
|
46
|
+
backupName = typeof arg2 === 'string' ? arg2 : '';
|
|
47
|
+
const { findHtmlFiles } = require('./scanner');
|
|
48
|
+
files = findHtmlFiles(rootDir);
|
|
49
|
+
} else {
|
|
50
|
+
files = [];
|
|
51
|
+
rootDir = process.cwd();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
55
|
+
const hash = computeFilesHash(files, rootDir) || '00000000';
|
|
56
|
+
const dirName = `${timestamp}_${hash}`;
|
|
57
|
+
|
|
58
|
+
// Dedicated unified .buzl/snapshots directory
|
|
59
|
+
const snapshotsDir = path.join(rootDir, '.buzl', 'snapshots');
|
|
60
|
+
const backupDir = path.join(snapshotsDir, dirName);
|
|
61
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const manifest = {
|
|
64
|
+
id: dirName,
|
|
65
|
+
hash: hash,
|
|
66
|
+
name: (backupName || '').trim() || `Snapshot ${new Date().toLocaleTimeString()} [${hash}]`,
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
files: []
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const filePath of files) {
|
|
72
|
+
const relPath = path.relative(rootDir, filePath);
|
|
73
|
+
const destPath = path.join(backupDir, relPath);
|
|
74
|
+
const destDir = path.dirname(destPath);
|
|
75
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
76
|
+
fs.copyFileSync(filePath, destPath);
|
|
77
|
+
manifest.files.push(relPath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(backupDir, 'backup-manifest.json'),
|
|
82
|
+
JSON.stringify(manifest, null, 2),
|
|
83
|
+
'utf8'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
backupDir,
|
|
89
|
+
dirName,
|
|
90
|
+
hash,
|
|
91
|
+
timestamp: manifest.timestamp,
|
|
92
|
+
name: manifest.name,
|
|
93
|
+
fileCount: manifest.files.length
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* List all available snapshot backups
|
|
99
|
+
* Checks `.buzl/snapshots/` first, plus legacy `.buzl-backup-*` folders
|
|
100
|
+
*/
|
|
101
|
+
function listBackups(rootDir) {
|
|
102
|
+
const allBackups = [];
|
|
103
|
+
|
|
104
|
+
// 1. Primary: Check .buzl/snapshots/
|
|
105
|
+
const snapshotsDir = path.join(rootDir, '.buzl', 'snapshots');
|
|
106
|
+
if (fs.existsSync(snapshotsDir)) {
|
|
107
|
+
try {
|
|
108
|
+
const entries = fs.readdirSync(snapshotsDir);
|
|
109
|
+
for (const e of entries) {
|
|
110
|
+
const dirPath = path.join(snapshotsDir, e);
|
|
111
|
+
if (fs.statSync(dirPath).isDirectory()) {
|
|
112
|
+
let manifest = null;
|
|
113
|
+
try {
|
|
114
|
+
const mPath = path.join(dirPath, 'backup-manifest.json');
|
|
115
|
+
if (fs.existsSync(mPath)) {
|
|
116
|
+
manifest = JSON.parse(fs.readFileSync(mPath, 'utf8'));
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {}
|
|
119
|
+
|
|
120
|
+
allBackups.push({
|
|
121
|
+
id: (manifest && manifest.id) || e,
|
|
122
|
+
dirName: e,
|
|
123
|
+
hash: (manifest && manifest.hash) || (e.split('_')[1] || ''),
|
|
124
|
+
fullPath: dirPath,
|
|
125
|
+
name: (manifest && manifest.name) || e,
|
|
126
|
+
timestamp: (manifest && manifest.timestamp) || '',
|
|
127
|
+
filesCount: (manifest && manifest.files && manifest.files.length) || 0,
|
|
128
|
+
isLegacy: false,
|
|
129
|
+
manifest
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. Fallback: Check legacy root-level `.buzl-backup-*` directories
|
|
137
|
+
try {
|
|
138
|
+
const rootEntries = fs.readdirSync(rootDir);
|
|
139
|
+
for (const e of rootEntries) {
|
|
140
|
+
if (e.startsWith('.buzl-backup-')) {
|
|
141
|
+
const dirPath = path.join(rootDir, e);
|
|
142
|
+
if (fs.statSync(dirPath).isDirectory()) {
|
|
143
|
+
let manifest = null;
|
|
144
|
+
try {
|
|
145
|
+
const mPath = path.join(dirPath, 'backup-manifest.json');
|
|
146
|
+
if (fs.existsSync(mPath)) {
|
|
147
|
+
manifest = JSON.parse(fs.readFileSync(mPath, 'utf8'));
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {}
|
|
150
|
+
|
|
151
|
+
allBackups.push({
|
|
152
|
+
id: e,
|
|
153
|
+
dirName: e,
|
|
154
|
+
hash: (manifest && manifest.hash) || '',
|
|
155
|
+
fullPath: dirPath,
|
|
156
|
+
name: (manifest && manifest.name) || e.replace('.buzl-backup-', ''),
|
|
157
|
+
timestamp: (manifest && manifest.timestamp) || '',
|
|
158
|
+
filesCount: (manifest && manifest.files && manifest.files.length) || 0,
|
|
159
|
+
isLegacy: true,
|
|
160
|
+
manifest
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {}
|
|
166
|
+
|
|
167
|
+
// Sort newest first
|
|
168
|
+
allBackups.sort((a, b) => b.dirName.localeCompare(a.dirName));
|
|
169
|
+
return allBackups;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Restore a specific snapshot or 'latest'
|
|
174
|
+
*/
|
|
175
|
+
function restoreBackup(rootDir, identifier) {
|
|
176
|
+
const backups = listBackups(rootDir);
|
|
177
|
+
if (backups.length === 0) {
|
|
178
|
+
return { success: false, message: 'No backups found to restore.' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let targetBackup = backups[0];
|
|
182
|
+
if (identifier && identifier !== 'latest') {
|
|
183
|
+
targetBackup = backups.find(b =>
|
|
184
|
+
b.dirName === identifier ||
|
|
185
|
+
b.name === identifier ||
|
|
186
|
+
b.hash === identifier ||
|
|
187
|
+
b.id === identifier
|
|
188
|
+
);
|
|
189
|
+
if (!targetBackup) {
|
|
190
|
+
return { success: false, message: `Backup "${identifier}" not found.` };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const manifest = targetBackup.manifest;
|
|
195
|
+
if (!manifest || !manifest.files) {
|
|
196
|
+
return { success: false, message: 'Backup manifest corrupted or missing.' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let restoredCount = 0;
|
|
200
|
+
for (const relFile of manifest.files) {
|
|
201
|
+
const src = path.join(targetBackup.fullPath, relFile);
|
|
202
|
+
const dest = path.join(rootDir, relFile);
|
|
203
|
+
if (fs.existsSync(src)) {
|
|
204
|
+
const destDir = path.dirname(dest);
|
|
205
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
206
|
+
fs.copyFileSync(src, dest);
|
|
207
|
+
restoredCount++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
backupUsed: targetBackup.dirName,
|
|
214
|
+
backupName: targetBackup.name,
|
|
215
|
+
hash: targetBackup.hash,
|
|
216
|
+
restoredCount,
|
|
217
|
+
message: `Successfully restored ${restoredCount} file(s) from "${targetBackup.name}" [${targetBackup.hash || targetBackup.dirName}].`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Restore the latest snapshot
|
|
223
|
+
*/
|
|
224
|
+
function restoreLatestBackup(rootDir) {
|
|
225
|
+
return restoreBackup(rootDir, 'latest');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Delete a specific backup
|
|
230
|
+
*/
|
|
231
|
+
function deleteBackup(rootDir, identifier) {
|
|
232
|
+
const backups = listBackups(rootDir);
|
|
233
|
+
const target = backups.find(b =>
|
|
234
|
+
b.dirName === identifier ||
|
|
235
|
+
b.name === identifier ||
|
|
236
|
+
b.id === identifier ||
|
|
237
|
+
b.hash === identifier
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (!target) {
|
|
241
|
+
return { success: false, message: `Backup "${identifier}" not found.` };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
fs.rmSync(target.fullPath, { recursive: true, force: true });
|
|
246
|
+
return { success: true, message: `Backup "${target.name}" deleted successfully.` };
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return { success: false, message: e.message };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create a manual named backup
|
|
254
|
+
*/
|
|
255
|
+
function manualBackup(rootDir, backupName = '') {
|
|
256
|
+
const { findHtmlFiles } = require('./scanner');
|
|
257
|
+
const files = findHtmlFiles(rootDir);
|
|
258
|
+
if (files.length === 0) {
|
|
259
|
+
return { success: false, message: 'No HTML files found to backup.' };
|
|
260
|
+
}
|
|
261
|
+
const result = createBackup(files, rootDir, backupName);
|
|
262
|
+
return {
|
|
263
|
+
success: true,
|
|
264
|
+
backupDir: result.backupDir,
|
|
265
|
+
dirName: result.dirName,
|
|
266
|
+
hash: result.hash,
|
|
267
|
+
name: result.name,
|
|
268
|
+
fileCount: result.fileCount,
|
|
269
|
+
message: `Snapshot "${result.name}" created with ${result.fileCount} file(s) [SHA: ${result.hash}].`
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
createBackup,
|
|
275
|
+
computeFilesHash,
|
|
276
|
+
listBackups,
|
|
277
|
+
restoreBackup,
|
|
278
|
+
restoreLatestBackup,
|
|
279
|
+
deleteBackup,
|
|
280
|
+
manualBackup
|
|
281
|
+
};
|