parrot-blackbox 1.0.7 → 1.0.9
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/bin/parrot-blackbox.js +0 -0
- package/package.json +1 -1
- package/src/backup/snapshot.js +69 -11
- package/src/commands/wizard.js +1 -1
package/bin/parrot-blackbox.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parrot-blackbox",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "parrot-blackbox — crash-proof, multi-cloud backup & recovery automation for Parrot OS. Daily/weekly off-disk backups with automatic catch-up, smart storage across many MEGA + Google Drive accounts, Timeshift snapshot backups and one-command restore.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
package/src/backup/snapshot.js
CHANGED
|
@@ -167,9 +167,15 @@ export async function deleteSnapshot(name, { privileged = 'noninteractive' } = {
|
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
169
|
* Resolve the on-disk directory of a snapshot.
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
170
|
+
*
|
|
171
|
+
* BTRFS mode stores snapshots as subvolumes on the BTRFS partition. Timeshift mounts
|
|
172
|
+
* the root subvolume (subvolid=5) temporarily to /run/timeshift/NNNN/backup when needed.
|
|
173
|
+
*
|
|
174
|
+
* Strategy:
|
|
175
|
+
* 1. Check if already have a valid dir
|
|
176
|
+
* 2. Check static paths (rsync mode)
|
|
177
|
+
* 3. Mount the BTRFS root subvolume ourselves to access snapshots persistently
|
|
178
|
+
* 4. Fall back to triggering timeshift --list
|
|
173
179
|
*/
|
|
174
180
|
export function snapshotDirFor(snapshot, { privileged = 'noninteractive' } = {}) {
|
|
175
181
|
if (snapshot.dir && fs.existsSync(snapshot.dir)) return snapshot.dir;
|
|
@@ -184,22 +190,70 @@ export function snapshotDirFor(snapshot, { privileged = 'noninteractive' } = {})
|
|
|
184
190
|
const direct = candidates.find((c) => fs.existsSync(c));
|
|
185
191
|
if (direct) return direct;
|
|
186
192
|
|
|
187
|
-
//
|
|
193
|
+
// BTRFS mode: Mount the root subvolume to access snapshots
|
|
194
|
+
// Find the BTRFS device (usually the root filesystem)
|
|
188
195
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
const mountPoint = `/run/parrot-blackbox-btrfs-${Date.now()}`;
|
|
197
|
+
|
|
198
|
+
// Get the device that contains the root filesystem
|
|
199
|
+
const deviceResult = execaSync('findmnt', ['-n', '-o', 'SOURCE', '/'], { reject: false });
|
|
200
|
+
const device = deviceResult.stdout.trim();
|
|
201
|
+
|
|
202
|
+
if (device && deviceResult.exitCode === 0) {
|
|
203
|
+
// Create temporary mount point
|
|
204
|
+
const mkdirCmd = privileged === 'interactive' ? ['sudo', 'mkdir', '-p', mountPoint] : ['sudo', '-n', 'mkdir', '-p', mountPoint];
|
|
205
|
+
execaSync(mkdirCmd[0], mkdirCmd.slice(1), { reject: false });
|
|
206
|
+
|
|
207
|
+
// Mount BTRFS root subvolume (subvolid=5 contains all subvolumes including snapshots)
|
|
208
|
+
const mountCmd = privileged === 'interactive'
|
|
209
|
+
? ['sudo', 'mount', '-o', 'subvolid=5', device, mountPoint]
|
|
210
|
+
: ['sudo', '-n', 'mount', '-o', 'subvolid=5', device, mountPoint];
|
|
211
|
+
|
|
212
|
+
const mountResult = execaSync(mountCmd[0], mountCmd.slice(1), { reject: false, timeout: 5000 });
|
|
213
|
+
|
|
214
|
+
if (mountResult.exitCode === 0) {
|
|
215
|
+
// Search for the snapshot in the mounted root subvolume
|
|
216
|
+
const searchPaths = [
|
|
217
|
+
`${mountPoint}/timeshift-btrfs/snapshots/${snapshot.name}`,
|
|
218
|
+
`${mountPoint}/@/timeshift-btrfs/snapshots/${snapshot.name}`,
|
|
219
|
+
`${mountPoint}/@timeshift/snapshots/${snapshot.name}`,
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const found = searchPaths.find((p) => fs.existsSync(p));
|
|
223
|
+
|
|
224
|
+
if (found) {
|
|
225
|
+
// Store the mount point for cleanup later
|
|
226
|
+
snapshot._tempMount = mountPoint;
|
|
227
|
+
return found;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Unmount if we didn't find anything
|
|
231
|
+
const umountCmd = privileged === 'interactive' ? ['sudo', 'umount', mountPoint] : ['sudo', '-n', 'umount', mountPoint];
|
|
232
|
+
execaSync(umountCmd[0], umountCmd.slice(1), { reject: false });
|
|
233
|
+
execaSync('sudo', ['-n', 'rmdir', mountPoint], { reject: false });
|
|
234
|
+
}
|
|
196
235
|
}
|
|
197
236
|
} catch {
|
|
198
237
|
/* fall through */
|
|
199
238
|
}
|
|
239
|
+
|
|
240
|
+
// Last resort: return the most likely path (will fail with ENOENT if it doesn't exist)
|
|
200
241
|
return candidates[0];
|
|
201
242
|
}
|
|
202
243
|
|
|
244
|
+
/** Cleanup temporary BTRFS mount if one was created */
|
|
245
|
+
export function cleanupSnapshotMount(snapshot) {
|
|
246
|
+
if (snapshot._tempMount) {
|
|
247
|
+
try {
|
|
248
|
+
execaSync('sudo', ['-n', 'umount', snapshot._tempMount], { reject: false });
|
|
249
|
+
execaSync('sudo', ['-n', 'rmdir', snapshot._tempMount], { reject: false });
|
|
250
|
+
delete snapshot._tempMount;
|
|
251
|
+
} catch {
|
|
252
|
+
/* best effort */
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
203
257
|
/**
|
|
204
258
|
* Run one snapshot generation: create → upload to the pool → prune old ones
|
|
205
259
|
* BOTH locally and in the cloud.
|
|
@@ -225,7 +279,11 @@ export async function runSnapshotBackup(cfg, state, { due, privileged = 'noninte
|
|
|
225
279
|
} catch (e) {
|
|
226
280
|
// The local snapshot exists and is safe; the cloud upload failed.
|
|
227
281
|
journal('snapshots', `upload failed for ${created.name}: ${e.message}`, 'error');
|
|
282
|
+
cleanupSnapshotMount(created); // Clean up any temporary BTRFS mount
|
|
228
283
|
throw e;
|
|
284
|
+
} finally {
|
|
285
|
+
// Always cleanup the temporary mount after upload attempt
|
|
286
|
+
cleanupSnapshotMount(created);
|
|
229
287
|
}
|
|
230
288
|
manifest.due = due;
|
|
231
289
|
manifest.snapshot = created.name;
|
package/src/commands/wizard.js
CHANGED
|
@@ -177,7 +177,7 @@ async function listBackupsAction() {
|
|
|
177
177
|
const accs = listAccounts();
|
|
178
178
|
p.log.message(pc.bold('Local snapshots (Timeshift):'));
|
|
179
179
|
try {
|
|
180
|
-
const local = await listLocalSnapshots({ privileged: '
|
|
180
|
+
const local = await listLocalSnapshots({ privileged: 'interactive' });
|
|
181
181
|
if (!local.length) p.log.message(pc.dim(' none'));
|
|
182
182
|
for (const sn of local) p.log.message(` - ${pc.cyan(sn.name)}`);
|
|
183
183
|
} catch (e) {
|