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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parrot-blackbox",
3
- "version": "2.0.6",
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 res = await runDueJobs({ force: true, privileged: 'interactive' });
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 r = await runSnapshotNow();
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) {
@@ -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 r = await runSnapshotNow();
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) {
@@ -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
- if (onProgress) onProgress({ done: totalBytes, total: originalSize, text: `uploading stream: ${(totalBytes / (1024**2)).toFixed(1)} MB` });
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
  }