parrot-blackbox 2.0.6 → 2.0.7
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/cli.js +7 -3
- package/src/commands/wizard.js +4 -2
- package/src/storage/allocator.js +25 -1
- package/src/util/misc.js +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parrot-blackbox",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
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/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { listAccounts, addAccount, removeAccount, refreshAccounts, poolSummary }
|
|
|
19
19
|
import { listArtifacts } from './storage/archive.js';
|
|
20
20
|
import { loadConfig, loadState, saveConfig } from './core/store.js';
|
|
21
21
|
import { configFile, stateDir } from './core/paths.js';
|
|
22
|
-
import { bytesHuman } from './util/misc.js';
|
|
22
|
+
import { bytesHuman, makeProgressRenderer } from './util/misc.js';
|
|
23
23
|
import { isOnline } from './util/network.js';
|
|
24
24
|
|
|
25
25
|
const require = createRequire(import.meta.url);
|
|
@@ -388,7 +388,9 @@ const main = defineCommand({
|
|
|
388
388
|
case 'force':
|
|
389
389
|
case 'backup': {
|
|
390
390
|
// Runs every ENABLED job right now (default = the weekly snapshot).
|
|
391
|
-
const
|
|
391
|
+
const progress = makeProgressRenderer();
|
|
392
|
+
const res = await runDueJobs({ force: true, privileged: 'interactive', onProgress: progress });
|
|
393
|
+
progress.stop();
|
|
392
394
|
const report = res.report || [];
|
|
393
395
|
if (report.length === 0) console.log(pc.dim('No enabled backup jobs — run `parrot-blackbox` to set up the schedule.'));
|
|
394
396
|
for (const r of report) {
|
|
@@ -410,7 +412,9 @@ const main = defineCommand({
|
|
|
410
412
|
const [sub, ...args] = rest;
|
|
411
413
|
if (sub === 'now' || sub === 'create' || sub === 'force') {
|
|
412
414
|
try {
|
|
413
|
-
const
|
|
415
|
+
const progress = makeProgressRenderer();
|
|
416
|
+
const r = await runSnapshotNow(undefined, undefined, { onProgress: progress });
|
|
417
|
+
progress.stop();
|
|
414
418
|
console.log(`${pc.green('✔')} Snapshot ${r.snapshot} created & uploaded (${bytesHuman(r.manifest.totalSize)}).`);
|
|
415
419
|
if (r.pruned?.length) console.log(pc.dim(`Pruned: ${r.pruned.join(', ')}`));
|
|
416
420
|
} catch (e) {
|
package/src/commands/wizard.js
CHANGED
|
@@ -24,7 +24,7 @@ import { installService, removeService } from './service.js';
|
|
|
24
24
|
import { startDaemon, stopDaemon, daemonRunning } from '../daemon/daemon.js';
|
|
25
25
|
import { runDoctor, runStatus, runUninstallWizard } from './manage.js';
|
|
26
26
|
import { runSetup } from './setup.js';
|
|
27
|
-
import { bytesHuman } from '../util/misc.js';
|
|
27
|
+
import { bytesHuman, makeProgressRenderer } from '../util/misc.js';
|
|
28
28
|
|
|
29
29
|
const require = createRequire(import.meta.url);
|
|
30
30
|
const pkg = require('../../package.json');
|
|
@@ -163,7 +163,9 @@ async function backupNowAction() {
|
|
|
163
163
|
/** Create + upload a snapshot immediately. */
|
|
164
164
|
async function snapshotNowAction() {
|
|
165
165
|
try {
|
|
166
|
-
const
|
|
166
|
+
const progress = makeProgressRenderer();
|
|
167
|
+
const r = await runSnapshotNow(undefined, undefined, { onProgress: progress });
|
|
168
|
+
progress.stop();
|
|
167
169
|
p.log.success(`✔ Snapshot ${r.snapshot} created & uploaded (${bytesHuman(r.manifest?.totalSize ?? 0)}).`);
|
|
168
170
|
if (r.pruned?.length) p.log.message(pc.dim(`Pruned: ${r.pruned.join(', ')}`));
|
|
169
171
|
} catch (e) {
|
package/src/storage/allocator.js
CHANGED
|
@@ -269,6 +269,12 @@ export async function planAndPlaceStream(btrfsStream, { kind, id, accounts, remo
|
|
|
269
269
|
let locs = [];
|
|
270
270
|
let currentStart = 0;
|
|
271
271
|
let target = getNextAccountAndPath();
|
|
272
|
+
|
|
273
|
+
// Speed tracking
|
|
274
|
+
let speedBytesWindow = 0;
|
|
275
|
+
let speedWindowStart = Date.now();
|
|
276
|
+
let currentSpeedMBs = 0;
|
|
277
|
+
const SPEED_WINDOW_MS = 1500; // recalculate speed every 1.5 s
|
|
272
278
|
|
|
273
279
|
currentChild = spawn(process.env.PBB_RCLONE || 'rclone', ['rcat', `${target.remote}:${target.path}`]);
|
|
274
280
|
let childFailed = false;
|
|
@@ -287,6 +293,7 @@ export async function planAndPlaceStream(btrfsStream, { kind, id, accounts, remo
|
|
|
287
293
|
|
|
288
294
|
currentBytesInChunk += toWrite;
|
|
289
295
|
totalBytes += toWrite;
|
|
296
|
+
speedBytesWindow += toWrite;
|
|
290
297
|
offset += toWrite;
|
|
291
298
|
|
|
292
299
|
if (!canContinue) {
|
|
@@ -309,7 +316,24 @@ export async function planAndPlaceStream(btrfsStream, { kind, id, accounts, remo
|
|
|
309
316
|
currentChild.on('exit', (code) => { if (code !== 0) childFailed = true; });
|
|
310
317
|
}
|
|
311
318
|
}
|
|
312
|
-
|
|
319
|
+
|
|
320
|
+
// Recalculate speed on each chunk and emit progress
|
|
321
|
+
if (onProgress) {
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
const elapsed = now - speedWindowStart;
|
|
324
|
+
if (elapsed >= SPEED_WINDOW_MS) {
|
|
325
|
+
currentSpeedMBs = (speedBytesWindow / (1024 * 1024)) / (elapsed / 1000);
|
|
326
|
+
speedBytesWindow = 0;
|
|
327
|
+
speedWindowStart = now;
|
|
328
|
+
}
|
|
329
|
+
onProgress({
|
|
330
|
+
done: totalBytes,
|
|
331
|
+
total: originalSize,
|
|
332
|
+
speedMBs: currentSpeedMBs,
|
|
333
|
+
remote: target.remote,
|
|
334
|
+
text: `uploading stream: ${(totalBytes / (1024 ** 2)).toFixed(1)} MB`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
if (currentChild) {
|
package/src/util/misc.js
CHANGED
|
@@ -111,4 +111,54 @@ export function shellQuote(s) {
|
|
|
111
111
|
|
|
112
112
|
export function pad2(n) {
|
|
113
113
|
return String(n).padStart(2, '0');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Returns an onProgress handler that renders a live, in-place progress bar
|
|
118
|
+
* with percentage, MB transferred, and upload speed.
|
|
119
|
+
*
|
|
120
|
+
* Expected event shape:
|
|
121
|
+
* { done: number (bytes), total: number (bytes or 0), speedMBs?: number, remote?: string }
|
|
122
|
+
*
|
|
123
|
+
* Call renderer.stop() when done to advance to the next line.
|
|
124
|
+
*
|
|
125
|
+
* When `total` is 0 (stream size unknown) we display bytes + speed only.
|
|
126
|
+
*/
|
|
127
|
+
export function makeProgressRenderer() {
|
|
128
|
+
const isTTY = process.stdout.isTTY;
|
|
129
|
+
const BAR_WIDTH = 25;
|
|
130
|
+
let lastLine = '';
|
|
131
|
+
|
|
132
|
+
function render({ done = 0, total = 0, speedMBs = 0, remote = '' } = {}) {
|
|
133
|
+
const doneMB = done / (1024 * 1024);
|
|
134
|
+
const totalMB = total / (1024 * 1024);
|
|
135
|
+
const speedStr = speedMBs > 0 ? ` ${speedMBs.toFixed(1)} MB/s` : '';
|
|
136
|
+
const destStr = remote ? ` → ${remote}` : '';
|
|
137
|
+
|
|
138
|
+
let line;
|
|
139
|
+
if (total > 0) {
|
|
140
|
+
const pct = Math.min(100, Math.round((done / total) * 100));
|
|
141
|
+
const filled = Math.round((pct / 100) * BAR_WIDTH);
|
|
142
|
+
const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
|
|
143
|
+
line = ` [${bar}] ${String(pct).padStart(3)}% ${doneMB.toFixed(1)} MB / ${totalMB.toFixed(1)} MB${speedStr}${destStr}`;
|
|
144
|
+
} else {
|
|
145
|
+
line = ` ⬆ ${doneMB.toFixed(1)} MB streamed${speedStr}${destStr}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isTTY) {
|
|
149
|
+
process.stdout.write(`\r${line}\x1b[K`);
|
|
150
|
+
} else if (line !== lastLine) {
|
|
151
|
+
// Non-TTY (piped / daemon log): only emit when something changes to
|
|
152
|
+
// avoid flooding the journal with thousands of identical lines.
|
|
153
|
+
process.stdout.write(line + '\n');
|
|
154
|
+
}
|
|
155
|
+
lastLine = line;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
render.stop = function stop() {
|
|
159
|
+
if (isTTY && lastLine) process.stdout.write('\n');
|
|
160
|
+
lastLine = '';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return render;
|
|
114
164
|
}
|