parrot-blackbox 2.0.0 → 2.0.1
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/package.json +1 -1
- package/src/backup/btrfs-send.js +21 -0
- package/src/backup/snapshot.js +16 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parrot-blackbox",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "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/btrfs-send.js
CHANGED
|
@@ -58,6 +58,21 @@ export async function getBtrfsDevice(mountPoint = '/') {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Check if a path is actually a BTRFS subvolume (not just a directory on BTRFS).
|
|
63
|
+
* @param {string} path
|
|
64
|
+
* @returns {Promise<boolean>}
|
|
65
|
+
*/
|
|
66
|
+
export async function isSubvolume(path) {
|
|
67
|
+
try {
|
|
68
|
+
// Need sudo to check subvolume info
|
|
69
|
+
const res = await execa('sudo', ['-n', 'btrfs', 'subvolume', 'show', path], { reject: false });
|
|
70
|
+
return res.exitCode === 0;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
/**
|
|
62
77
|
* Set a BTRFS subvolume to read-only mode (required for send).
|
|
63
78
|
* @param {string} subvolPath - Path to the subvolume
|
|
@@ -66,6 +81,12 @@ export async function getBtrfsDevice(mountPoint = '/') {
|
|
|
66
81
|
* @returns {Promise<void>}
|
|
67
82
|
*/
|
|
68
83
|
export async function setSubvolumeReadOnly(subvolPath, readOnly = true, { privileged = 'noninteractive' } = {}) {
|
|
84
|
+
// First verify it's actually a subvolume
|
|
85
|
+
const isSubvol = await isSubvolume(subvolPath);
|
|
86
|
+
if (!isSubvol) {
|
|
87
|
+
throw new Error(`${subvolPath} is not a BTRFS subvolume`);
|
|
88
|
+
}
|
|
89
|
+
|
|
69
90
|
const roValue = readOnly ? 'true' : 'false';
|
|
70
91
|
const args = ['btrfs', 'property', 'set', '-ts', subvolPath, 'ro', roValue];
|
|
71
92
|
const res = await sudoExec(args, { privileged });
|
package/src/backup/snapshot.js
CHANGED
|
@@ -401,19 +401,24 @@ export async function runSnapshotBackup(cfg, state, { due, privileged = 'noninte
|
|
|
401
401
|
/**
|
|
402
402
|
* Upload a snapshot using BTRFS send/receive streaming.
|
|
403
403
|
* Creates: btrfs send [-p parent] | zstd | [openssl] | rclone rcat (chunked)
|
|
404
|
+
*
|
|
405
|
+
* Note: For BTRFS send to work, we need to use paths that btrfs recognizes as subvolumes.
|
|
406
|
+
* On most Parrot systems, snapshots are at: /timeshift-btrfs/snapshots/<name>
|
|
407
|
+
* We construct the proper subvolume path rather than using potentially-mounted paths.
|
|
404
408
|
*/
|
|
405
409
|
async function uploadViaBtrfsSend({ snapshot, snapshotDir, parentSnapshot, parentDir, accounts, cfg, due, privileged, onProgress }) {
|
|
406
|
-
const {
|
|
410
|
+
const { createSendStream, estimateSendSize } = await import('./btrfs-send.js');
|
|
407
411
|
const { planAndPlaceStream } = await import('../storage/allocator.js');
|
|
408
412
|
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
413
|
+
// Construct the actual subvolume path for BTRFS send
|
|
414
|
+
// Timeshift stores snapshots at /timeshift-btrfs/snapshots/<name> on the root BTRFS volume
|
|
415
|
+
// We need to use this path for btrfs send, not any temporarily mounted paths
|
|
416
|
+
const subvolPath = `/timeshift-btrfs/snapshots/${snapshot.name}`;
|
|
417
|
+
const parentSubvolPath = parentSnapshot ? `/timeshift-btrfs/snapshots/${parentSnapshot.name}` : null;
|
|
418
|
+
|
|
419
|
+
journal('snapshots', `using subvolume path: ${subvolPath}`);
|
|
420
|
+
|
|
421
|
+
// Estimate size for progress reporting (use original snapshotDir for filesystem operations)
|
|
417
422
|
const estimatedSize = await estimateSendSize(snapshotDir, { parent: parentDir });
|
|
418
423
|
const btrfsCfg = cfg.jobs.snapshots.btrfs || {};
|
|
419
424
|
|
|
@@ -422,8 +427,8 @@ async function uploadViaBtrfsSend({ snapshot, snapshotDir, parentSnapshot, paren
|
|
|
422
427
|
if (btrfsCfg.compression) console.log(` Compression: zstd enabled`);
|
|
423
428
|
if (btrfsCfg.encryption && cfg.storage.encryptionPassphrase) console.log(` Encryption: AES-256 enabled`);
|
|
424
429
|
|
|
425
|
-
// Create the BTRFS send stream
|
|
426
|
-
const sendStream = await createSendStream(
|
|
430
|
+
// Create the BTRFS send stream using the actual subvolume paths
|
|
431
|
+
const sendStream = await createSendStream(subvolPath, { parent: parentSubvolPath, privileged });
|
|
427
432
|
|
|
428
433
|
// Build the pipeline: btrfs send -> [zstd] -> [openssl] -> chunked rclone rcat
|
|
429
434
|
const { spawn } = await import('node:child_process');
|