backupdrill 0.1.2 → 0.1.3
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 +5 -5
- package/dist/drill.d.ts +8 -0
- package/dist/drill.js +24 -6
- package/dist/estimate.d.ts +1 -1
- package/dist/estimate.js +11 -9
- package/dist/index.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Backups stream through the CLI on your machine (or your CI) straight to your buc
|
|
|
9
9
|
## Why
|
|
10
10
|
|
|
11
11
|
- Supabase's own database restore only brings back `storage.objects` **metadata**, not your actual Storage files.
|
|
12
|
-
- Pro-plan backups keep 7 days and can't be pulled into your own infrastructure; PITR is $100/
|
|
12
|
+
- Pro-plan backups keep 7 days and can't be pulled into your own infrastructure; PITR is $100/month per project.
|
|
13
13
|
- A `pg_dump` you've never restored is a guess.
|
|
14
14
|
|
|
15
15
|
This CLI gets your database into a bucket **you** control, with a manifest you can check.
|
|
@@ -106,19 +106,19 @@ With those set, `backupdrill backup` also copies every Storage file into your bu
|
|
|
106
106
|
|
|
107
107
|
## Egress & cost — read this before scheduling daily backups
|
|
108
108
|
|
|
109
|
-
A backup pulls data **out** of Supabase, which counts as **egress on your Supabase bill**. Verified rates (2026-07-
|
|
109
|
+
A backup pulls data **out** of Supabase, which counts as **egress on your Supabase bill**. Verified rates (2026-07-10, [egress docs](https://supabase.com/docs/guides/platform/manage-your-usage/egress) + [pricing](https://supabase.com/pricing)):
|
|
110
110
|
|
|
111
111
|
- Backup traffic (pg_dump + first-time file downloads) is **uncached egress: $0.09/GB** above your plan's included allowance.
|
|
112
112
|
- Included uncached egress: **Free 5 GB · Pro 250 GB · Team 250 GB** per month.
|
|
113
113
|
- **Spend Cap is ON by default (Pro/Team).** If a backup pushes you past the quota, Supabase **throttles your whole project's egress** for the rest of the month and the backup fails — turn Spend Cap OFF to be billed the overage instead.
|
|
114
|
-
- **Free
|
|
114
|
+
- **Free never bills overage.** Exceeding the included 5 GB/month starts Supabase's Fair Use process: a notification to your billing email, then a grace period, then egress restrictions if usage stays over (the restriction clears at the next billing cycle or when you upgrade). Sustained large backups are not viable on Free.
|
|
115
115
|
|
|
116
116
|
Rough cost (Pro, Spend Cap OFF, backups = your only egress):
|
|
117
117
|
|
|
118
118
|
| DB + Storage | Weekly | Daily |
|
|
119
119
|
|---|---|---|
|
|
120
|
-
| 10 GB | $0/
|
|
121
|
-
| 50 GB | ~$0/
|
|
120
|
+
| 10 GB | $0/month (within 250 GB) | ~$4.50/month |
|
|
121
|
+
| 50 GB | ~$0/month | **~$112/month** ⚠ |
|
|
122
122
|
|
|
123
123
|
Cost scales with **size × frequency**. Run `backupdrill estimate` to project it for *your* project, and prefer weekly (or a smaller scope) once your data is large. The compressed dump is smaller than raw DB size, so real DB egress is below the `estimate` upper bound; Storage egress is exact.
|
|
124
124
|
|
package/dist/drill.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { BackupConfig } from "./config.js";
|
|
2
2
|
import type { Manifest } from "./manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* 无放回随机抽样:部分 Fisher-Yates,只洗出前 n 位(O(n) 次交换,不必全 shuffle)。
|
|
5
|
+
* 此前恒取 manifest 顺序的前 CAP 个 → 超过 CAP 的项目里排序靠后的文件永远不被校验;
|
|
6
|
+
* 随机抽样让每次演练覆盖不同子集,损坏的文件长期必然被抽中。
|
|
7
|
+
* 随机源用 Math.random 足够:演练是一次性进程,无可复现性要求——校验失败的文件名
|
|
8
|
+
* 会原样列进报告,定位问题不依赖重放同一批样本。
|
|
9
|
+
*/
|
|
10
|
+
export declare function sampleStorageFiles<T>(files: T[], n: number): T[];
|
|
3
11
|
export interface DrillCheck {
|
|
4
12
|
name: string;
|
|
5
13
|
pass: boolean;
|
package/dist/drill.js
CHANGED
|
@@ -10,6 +10,23 @@ import { log } from "./log.js";
|
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
// Storage 校验默认抽样上限(避免每次演练重下全部文件产生大量 egress);超限如实记录
|
|
12
12
|
const STORAGE_SAMPLE_CAP = 100;
|
|
13
|
+
/**
|
|
14
|
+
* 无放回随机抽样:部分 Fisher-Yates,只洗出前 n 位(O(n) 次交换,不必全 shuffle)。
|
|
15
|
+
* 此前恒取 manifest 顺序的前 CAP 个 → 超过 CAP 的项目里排序靠后的文件永远不被校验;
|
|
16
|
+
* 随机抽样让每次演练覆盖不同子集,损坏的文件长期必然被抽中。
|
|
17
|
+
* 随机源用 Math.random 足够:演练是一次性进程,无可复现性要求——校验失败的文件名
|
|
18
|
+
* 会原样列进报告,定位问题不依赖重放同一批样本。
|
|
19
|
+
*/
|
|
20
|
+
export function sampleStorageFiles(files, n) {
|
|
21
|
+
if (files.length <= n)
|
|
22
|
+
return files;
|
|
23
|
+
const pool = [...files];
|
|
24
|
+
for (let i = 0; i < n; i++) {
|
|
25
|
+
const j = i + Math.floor(Math.random() * (pool.length - i));
|
|
26
|
+
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
27
|
+
}
|
|
28
|
+
return pool.slice(0, n);
|
|
29
|
+
}
|
|
13
30
|
/**
|
|
14
31
|
* 沙箱镜像选择:alpine 镜像只带 contrib 扩展,装不了 pgvector——含 vector 列的
|
|
15
32
|
* 备份在裸镜像里恢复必然硬崩("type vector does not exist")。manifest 记录了
|
|
@@ -264,7 +281,7 @@ async function verifyRestored(connString, manifest) {
|
|
|
264
281
|
*/
|
|
265
282
|
async function verifyStorage(s3, config, manifest, verifyAll) {
|
|
266
283
|
const files = manifest.storage.files;
|
|
267
|
-
const sample = verifyAll ? files : files
|
|
284
|
+
const sample = verifyAll ? files : sampleStorageFiles(files, STORAGE_SAMPLE_CAP);
|
|
268
285
|
// 备份写入时的键:<snapshot base>/storage/<bucket>/<key>,base 从 dump.key 反推
|
|
269
286
|
const base = manifest.dump.key.replace(/\/dump\.pgcustom$/, "");
|
|
270
287
|
const mismatches = [];
|
|
@@ -279,15 +296,16 @@ async function verifyStorage(s3, config, manifest, verifyAll) {
|
|
|
279
296
|
mismatches.push(`${f.bucket}/${f.key} (unreadable)`);
|
|
280
297
|
}
|
|
281
298
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
299
|
+
// 抽样时如实说明这是随机样本(N of M);全量(--verify-all-files 或文件数 ≤ 上限)说 all
|
|
300
|
+
const scope = sample.length === files.length
|
|
301
|
+
? `all ${files.length} files`
|
|
302
|
+
: `random sample of ${sample.length} of ${files.length} files`;
|
|
285
303
|
return {
|
|
286
304
|
name: "storage file integrity",
|
|
287
305
|
pass: mismatches.length === 0,
|
|
288
306
|
detail: mismatches.length === 0
|
|
289
|
-
? `${scope}
|
|
290
|
-
: `${mismatches.length}
|
|
307
|
+
? `${scope} match their manifest checksums`
|
|
308
|
+
: `${mismatches.length} failed in ${scope}: ${mismatches.slice(0, 5).join(", ")}`,
|
|
291
309
|
};
|
|
292
310
|
}
|
|
293
311
|
// ── 主流程 ──────────────────────────────────────────────────────────
|
package/dist/estimate.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BackupConfig } from "./config.js";
|
|
2
2
|
/**
|
|
3
|
-
* Supabase egress 定价(2026-07-
|
|
3
|
+
* Supabase egress 定价(2026-07-10 经核实,来源见 README「Egress & cost」)。
|
|
4
4
|
* 单价 = 超出计划包含额度后的每 GB 价;included = 各计划每月包含的 egress。
|
|
5
5
|
* 备份 = 把数据拉出 Supabase = egress,计入用户账单。这是 PRD §4.3 的上线否决项:
|
|
6
6
|
* 让用户"我们帮你算过账"而不是收到意外账单。
|
package/dist/estimate.js
CHANGED
|
@@ -59,10 +59,10 @@ export async function runEstimate(config, pricing) {
|
|
|
59
59
|
console.error(` Plan: ${pricing.planLabel} — ${pricing.includedGb} GB egress included, ` +
|
|
60
60
|
`then $${pricing.pricePerGb}/GB`);
|
|
61
61
|
console.error("");
|
|
62
|
-
console.error(" Frequency Runs/
|
|
63
|
-
console.error(" ─────────
|
|
62
|
+
console.error(" Frequency Runs/month Monthly egress Overage Est. cost/month");
|
|
63
|
+
console.error(" ───────── ────────── ────────────── ─────── ───────────────");
|
|
64
64
|
for (const r of projection.rows) {
|
|
65
|
-
console.error(` ${r.frequency.padEnd(9)} ${String(r.runsPerMonth).padStart(
|
|
65
|
+
console.error(` ${r.frequency.padEnd(9)} ${String(r.runsPerMonth).padStart(10)} ` +
|
|
66
66
|
`${(r.monthlyEgressGb + " GB").padStart(14)} ` +
|
|
67
67
|
`${(r.overageGb + " GB").padStart(7)} ` +
|
|
68
68
|
`$${r.monthlyCost.toFixed(2)}`);
|
|
@@ -76,11 +76,13 @@ export async function runEstimate(config, pricing) {
|
|
|
76
76
|
const weekly = projection.rows.find((r) => r.frequency === "weekly");
|
|
77
77
|
console.error("");
|
|
78
78
|
if (pricing.hardCap) {
|
|
79
|
-
// Free
|
|
79
|
+
// Free:超额不计费,走 Fair Use 通知 → 宽限期 → 限制;持续超额的大备份在 Free 跑不通
|
|
80
80
|
if (daily && daily.overageGb > 0) {
|
|
81
|
-
log.warn(`${pricing.planLabel} egress is
|
|
82
|
-
|
|
83
|
-
`
|
|
81
|
+
log.warn(`${pricing.planLabel} plan egress is never billed, but exceeding the included ` +
|
|
82
|
+
`${pricing.includedGb} GB/month starts Supabase's Fair Use process: a ` +
|
|
83
|
+
`notification, then a grace period, then egress restrictions. Daily backups ` +
|
|
84
|
+
`(~${daily.monthlyEgressGb} GB/month) would keep you permanently over the ` +
|
|
85
|
+
`line — sustained large backups are not viable on Free. ` +
|
|
84
86
|
`Use weekly at most, or upgrade to Pro.`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
@@ -93,9 +95,9 @@ export async function runEstimate(config, pricing) {
|
|
|
93
95
|
"numbers above are what you'd pay.");
|
|
94
96
|
const worst = daily?.monthlyCost ?? 0;
|
|
95
97
|
if (worst >= SUBSCRIPTION_FLOOR_USD) {
|
|
96
|
-
log.warn(`At daily frequency the egress overage alone (~$${worst.toFixed(0)}/
|
|
98
|
+
log.warn(`At daily frequency the egress overage alone (~$${worst.toFixed(0)}/month) is ` +
|
|
97
99
|
`in the range of a paid backup subscription. Consider weekly ` +
|
|
98
|
-
`(~$${(weekly?.monthlyCost ?? 0).toFixed(0)}/
|
|
100
|
+
`(~$${(weekly?.monthlyCost ?? 0).toFixed(0)}/month overage) or a smaller scope.`);
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
return projection;
|
package/dist/index.js
CHANGED
|
@@ -7,9 +7,10 @@ import { runDrill } from "./drill.js";
|
|
|
7
7
|
import { runRestore } from "./restore.js";
|
|
8
8
|
import { log } from "./log.js";
|
|
9
9
|
import { TOOL_VERSION } from "./version.js";
|
|
10
|
-
// Supabase egress 定价表(2026-07-
|
|
10
|
+
// Supabase egress 定价表(2026-07-10 核实,来源见 README「Egress & cost」)。
|
|
11
11
|
// 备份流量 100% 未缓存,故用 uncached 单价 $0.09/GB;included 用 uncached 额度。
|
|
12
|
-
// Free
|
|
12
|
+
// Free 永不计费:超额走 Fair Use 流程(通知 → 宽限期 → 限制),持续大备份不可行;
|
|
13
|
+
// Pro/Team 由 Spend Cap 决定超额限流还是计费。
|
|
13
14
|
const EGRESS_PRICING = {
|
|
14
15
|
free: { planLabel: "Free", includedGb: 5, pricePerGb: 0.09, hardCap: true },
|
|
15
16
|
pro: { planLabel: "Pro", includedGb: 250, pricePerGb: 0.09, hardCap: false },
|
package/package.json
CHANGED