parrot-blackbox 1.0.3 → 1.0.5
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/README.md +6 -5
- package/package.json +1 -1
- package/src/backup/snapshot.js +21 -6
- package/src/cli.js +1 -1
- package/src/commands/manage.js +21 -6
- package/src/commands/remote.js +1 -1
- package/src/commands/service.js +3 -0
- package/src/commands/setup.js +1 -1
- package/src/commands/wizard.js +1 -1
- package/src/core/store.js +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Parrot install died" — a single CLI + background daemon that:
|
|
|
16
16
|
missed backups are run **in order, oldest first, the moment WiFi is back**.
|
|
17
17
|
Every job is journalled and retried; a lock prevents collisions; state is
|
|
18
18
|
written atomically. Nothing is silently lost.
|
|
19
|
-
4. **Manages ~
|
|
19
|
+
4. **Manages ~175 GB of free cloud storage for you** — 5 MEGA + 5 Google Drive
|
|
20
20
|
accounts are connected as one pool. Files are placed whole on whichever
|
|
21
21
|
account has the most relative headroom; anything bigger than a single
|
|
22
22
|
account's free space is split into byte-range chunks across accounts and a
|
|
@@ -436,13 +436,14 @@ y/e> y ⬅ y confirms permanent deletion
|
|
|
436
436
|
## How the smart storage pool works
|
|
437
437
|
|
|
438
438
|
```
|
|
439
|
-
5 MEGA (20 GiB each)
|
|
440
|
-
5 Drive (
|
|
439
|
+
5 MEGA (20 GiB each) = 100 GiB
|
|
440
|
+
5 Drive (15 GiB each) = 75 GiB ← Google Drive free tier is 15 GiB
|
|
441
441
|
──────────────────────────────
|
|
442
|
-
≈
|
|
442
|
+
≈ 175 GiB managed automatically
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
(Your live `account list` proves it: 10 accounts → 175 GiB total.)
|
|
446
|
+
Every account is exactly one rclone remote (`mega-1:`, `gdrive-6:` …).
|
|
446
447
|
- Each backup has **one manifest** that records where every file (or chunk) is.
|
|
447
448
|
- Placement picks the account that would end up with the **lowest used
|
|
448
449
|
percentage** (water-filling), then most free.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parrot-blackbox",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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
|
@@ -17,7 +17,7 @@ import { refreshAccounts } from '../storage/accounts.js';
|
|
|
17
17
|
import { planAndPlace } from '../storage/allocator.js';
|
|
18
18
|
import { listArtifacts, removeArtifact } from '../storage/archive.js';
|
|
19
19
|
import { planPrune } from './retention.js';
|
|
20
|
-
import { sudoInteractive, sudoNonInteractive } from '../util/sudo.js';
|
|
20
|
+
import { sudoInteractive, sudoNonInteractive, sudoInteractiveCapture } from '../util/sudo.js';
|
|
21
21
|
|
|
22
22
|
export class SudoDeferredError extends Error {
|
|
23
23
|
constructor() {
|
|
@@ -50,18 +50,33 @@ function findPathInLine(line) {
|
|
|
50
50
|
null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* `timeshift --list` needs admin on most installs — run it with sudo.
|
|
55
|
+
* Interactive: capture stdout while keeping stdin (so the sudo password
|
|
56
|
+
* prompt works). Non-interactive: `sudo -n`, falling back to a direct call
|
|
57
|
+
* for the few builds that allow unprivileged listing.
|
|
58
|
+
*/
|
|
59
|
+
async function runTimeshiftList({ privileged = 'noninteractive' } = {}) {
|
|
60
|
+
if (privileged === 'interactive') {
|
|
61
|
+
try {
|
|
62
|
+
const res = await sudoInteractiveCapture(['timeshift', '--list']);
|
|
63
|
+
return (res.exitCode === 0 ? res.stdout : res.stdout || res.stderr) || '';
|
|
64
|
+
} catch {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
55
68
|
try {
|
|
56
|
-
const res = await
|
|
57
|
-
|
|
69
|
+
const res = await sudoNonInteractive(['timeshift', '--list']);
|
|
70
|
+
if (res.exitCode === 0) return res.stdout || '';
|
|
71
|
+
const direct = await execa('timeshift', ['--list'], { reject: false });
|
|
72
|
+
return (direct.exitCode === 0 ? direct.stdout : direct.stdout || direct.stderr) || '';
|
|
58
73
|
} catch {
|
|
59
74
|
return '';
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
export async function listLocalSnapshots({ privileged = 'noninteractive' } = {}) {
|
|
64
|
-
const text = await runTimeshiftList();
|
|
79
|
+
const text = await runTimeshiftList({ privileged });
|
|
65
80
|
return parseTimeshiftList(text);
|
|
66
81
|
}
|
|
67
82
|
|
package/src/cli.js
CHANGED
|
@@ -293,7 +293,7 @@ async function remoteCommands(rest) {
|
|
|
293
293
|
message: 'Provider?',
|
|
294
294
|
options: [
|
|
295
295
|
{ value: 'mega', label: 'MEGA (20 GB free tier)' },
|
|
296
|
-
{ value: 'gdrive', label: 'Google Drive (
|
|
296
|
+
{ value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
|
|
297
297
|
],
|
|
298
298
|
});
|
|
299
299
|
if (p.isCancel(choice)) return;
|
package/src/commands/manage.js
CHANGED
|
@@ -185,14 +185,29 @@ export async function runRepair({ auto = false } = {}) {
|
|
|
185
185
|
// 3. Service
|
|
186
186
|
try {
|
|
187
187
|
const { serviceFile, daemonLogFile } = await import('../core/paths.js');
|
|
188
|
-
const { serviceBackend } = await import('./service.js');
|
|
188
|
+
const { serviceBackend, installService } = await import('./service.js');
|
|
189
189
|
const fsMod = await import('node:fs');
|
|
190
|
-
if (serviceBackend() === 'systemd'
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
if (serviceBackend() === 'systemd') {
|
|
191
|
+
// The ExecStart line must reference a REAL absolute path; the v1.0.4 unit
|
|
192
|
+
// wrote `node parrot-blackbox daemon foreground` (bare name) which crashed
|
|
193
|
+
// with "Cannot find module '/home/artkins/parrot-blackbox'".
|
|
194
|
+
let unitOk = false;
|
|
195
|
+
try {
|
|
196
|
+
const unit = fsMod.readFileSync(serviceFile(), 'utf8');
|
|
197
|
+
const execLine = unit.split('\n').find((l) => l.startsWith('ExecStart=')) || '';
|
|
198
|
+
unitOk = fsMod.existsSync(serviceFile()) && /ExecStart=\S+/.test(execLine) && /\//.test(execLine);
|
|
199
|
+
} catch {
|
|
200
|
+
unitOk = false;
|
|
201
|
+
}
|
|
202
|
+
if (!unitOk) {
|
|
203
|
+
const backend = await installService();
|
|
204
|
+
p.log.success(`Always-on service re-written via ${backend}.`);
|
|
205
|
+
fixed.push('service');
|
|
206
|
+
} else {
|
|
207
|
+
p.log.success('Service OK.');
|
|
208
|
+
}
|
|
194
209
|
} else {
|
|
195
|
-
p.log.success('Service OK.');
|
|
210
|
+
p.log.success('Service backend OK.');
|
|
196
211
|
}
|
|
197
212
|
} catch (e) {
|
|
198
213
|
p.log.warn(`Service check failed: ${e.message}`);
|
package/src/commands/remote.js
CHANGED
|
@@ -115,7 +115,7 @@ export async function registerRemotesAsAccounts() {
|
|
|
115
115
|
message: `Provider for "${remote}"?`,
|
|
116
116
|
options: [
|
|
117
117
|
{ value: 'mega', label: 'MEGA (20 GB free tier)' },
|
|
118
|
-
{ value: 'gdrive', label: 'Google Drive (
|
|
118
|
+
{ value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
|
|
119
119
|
],
|
|
120
120
|
});
|
|
121
121
|
if (p.isCancel(provider)) continue;
|
package/src/commands/service.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import { execa } from 'execa';
|
|
11
12
|
import { serviceFile, daemonLogFile } from '../core/paths.js';
|
|
12
13
|
import { loadConfig, journal, hasCommandSync } from '../core/store.js';
|
|
@@ -15,6 +16,8 @@ function findCliBin() {
|
|
|
15
16
|
// npm-installed global binary (preferred) or our own bin entry.
|
|
16
17
|
const candidates = [
|
|
17
18
|
process.env.PBB_BIN,
|
|
19
|
+
// This module lives in src/commands/ → ../../bin/parrot-blackbox.js
|
|
20
|
+
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'bin', 'parrot-blackbox.js'),
|
|
18
21
|
path.join(path.dirname(process.argv[1] || ''), 'parrot-blackbox.js'),
|
|
19
22
|
];
|
|
20
23
|
return candidates.find((c) => c && fs.existsSync(c)) || 'parrot-blackbox';
|
package/src/commands/setup.js
CHANGED
|
@@ -99,7 +99,7 @@ async function storagePoolStep() {
|
|
|
99
99
|
message: 'Which provider?',
|
|
100
100
|
options: [
|
|
101
101
|
{ value: 'mega', label: 'MEGA (20 GB free tier)' },
|
|
102
|
-
{ value: 'gdrive', label: 'Google Drive (
|
|
102
|
+
{ value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
|
|
103
103
|
],
|
|
104
104
|
});
|
|
105
105
|
if (p.isCancel(provider)) break;
|
package/src/commands/wizard.js
CHANGED
|
@@ -51,7 +51,7 @@ async function addAccountAction() {
|
|
|
51
51
|
message: 'Which provider?',
|
|
52
52
|
options: [
|
|
53
53
|
{ value: 'mega', label: 'MEGA (20 GB free tier)' },
|
|
54
|
-
{ value: 'gdrive', label: 'Google Drive (
|
|
54
|
+
{ value: 'gdrive', label: 'Google Drive (15 GB free tier)' },
|
|
55
55
|
{ value: 'back', label: '← Back' },
|
|
56
56
|
],
|
|
57
57
|
});
|
package/src/core/store.js
CHANGED