parrot-blackbox 1.0.18 → 1.1.0

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": "1.0.18",
3
+ "version": "1.1.0",
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",
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Storage accounts — the gitswitch-style account manager for the multi-cloud
3
3
  * pool. Each account maps to ONE rclone remote (one MEGA or Google Drive
4
- * login). 5 MEGA + 5 Drive accounts with ~20GiB / ~15GiB each 175GiB that the
5
- * allocator can spread backups across so we are never out of space.
4
+ * login). You can add any number of MEGA and/or Google Drive accounts e.g.
5
+ * 2, 5, 11, or even 30 of each. MEGA accounts are always filled first; the
6
+ * allocator only spills over to Google Drive once every MEGA account is at
7
+ * capacity.
6
8
  */
7
9
 
8
10
  import crypto from 'node:crypto';
@@ -3,13 +3,16 @@
3
3
  * (many MEGA + Google Drive logins) from ever running out of space.
4
4
  *
5
5
  * Strategy:
6
- * 1. Every file of an artifact is placed WHOLE on a single account when it
7
- * fits. The account is chosen to minimise the resulting used-percentage
8
- * (water-filling), then most free, then oldest spreading wear.
6
+ * 0. MEGA accounts are always filled first. Google Drive accounts are only
7
+ * used once every MEGA account is at capacity for the file being placed.
8
+ * 1. Within each provider tier, every file of an artifact is placed WHOLE on
9
+ * a single account when it fits. The account is chosen to minimise the
10
+ * resulting used-percentage (water-filling), then most free, then oldest —
11
+ * spreading wear evenly across all accounts of the same type.
9
12
  * 2. If a single file is bigger than any one account's free space, it is
10
13
  * split into byte-range chunks distributed across several accounts
11
- * (first-fit over free space). A manifest records the exact ranges so
12
- * restore reassembles the file byte-perfectly.
14
+ * (first-fit over free space, MEGA-first). A manifest records the exact
15
+ * ranges so restore reassembles the file byte-perfectly.
13
16
  * The manifest is written to the cloud (`<root>/<kind>/<id>/__MANIFEST__.json`)
14
17
  * AND mirrored locally, so restore works even from a wiped machine.
15
18
  */
@@ -52,15 +55,35 @@ export function walkFiles(localDir) {
52
55
  return out;
53
56
  }
54
57
 
55
- /** Choose the account that minimises its resulting used-percentage. */
58
+ /**
59
+ * Choose the best account to store `needed` bytes.
60
+ *
61
+ * Priority:
62
+ * 1. MEGA accounts are always preferred over Google Drive.
63
+ * Only when ALL MEGA accounts are full (or have insufficient free space)
64
+ * will Google Drive accounts be used.
65
+ * 2. Within each provider tier the classic water-filling strategy applies:
66
+ * pick the account that results in the lowest used-percentage after
67
+ * placing the file (most headroom wins on ties).
68
+ */
56
69
  export function chooseAccount(needed, accounts) {
70
+ // Tier 0 = mega, Tier 1 = gdrive / everything else.
71
+ const providerTier = (acc) => (acc.provider === 'mega' ? 0 : 1);
72
+
73
+ // Find the lowest tier that has at least one account with enough free space.
74
+ const eligible = accounts.filter((a) => a.free >= needed);
75
+ if (eligible.length === 0) return null;
76
+
77
+ const bestTier = Math.min(...eligible.map(providerTier));
78
+ const candidates = eligible.filter((a) => providerTier(a) === bestTier);
79
+
80
+ // Water-fill within the chosen tier: minimise resulting used-percentage.
57
81
  let best = null;
58
82
  let bestScore = Infinity;
59
- for (const acc of accounts) {
60
- if (acc.free < needed) continue;
83
+ for (const acc of candidates) {
61
84
  const quota = acc.total || 0;
62
85
  const score = quota ? (acc.used + needed) / quota : needed;
63
- if (score < bestScore || (score === bestScore && (acc.free > (best?.free ?? -1)))) {
86
+ if (score < bestScore || (score === bestScore && acc.free > (best?.free ?? -1))) {
64
87
  bestScore = score;
65
88
  best = acc;
66
89
  }