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 +1 -1
- package/src/storage/accounts.js +4 -2
- package/src/storage/allocator.js +32 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parrot-blackbox",
|
|
3
|
-
"version": "1.0
|
|
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",
|
package/src/storage/accounts.js
CHANGED
|
@@ -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
|
-
*
|
|
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';
|
package/src/storage/allocator.js
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
* (many MEGA + Google Drive logins) from ever running out of space.
|
|
4
4
|
*
|
|
5
5
|
* Strategy:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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 &&
|
|
86
|
+
if (score < bestScore || (score === bestScore && acc.free > (best?.free ?? -1))) {
|
|
64
87
|
bestScore = score;
|
|
65
88
|
best = acc;
|
|
66
89
|
}
|