backupdrill 0.1.0 → 0.1.1

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # backupdrill
2
2
 
3
- Open-source CLI to back up a **Supabase** project to **your own** S3/R2/B2 bucket — the Postgres database now, Storage files next — with a checksummed `manifest.json` so you can prove what was captured.
3
+ Open-source CLI to back up a **Supabase** project to **your own** S3/R2/B2 bucket — the Postgres database **and** Storage files, together — with a checksummed `manifest.json` so you can prove what was captured.
4
4
 
5
5
  Backups stream through the CLI on your machine (or your CI) straight to your bucket. **Your data never touches our servers, because there are none — this is just a CLI.**
6
6
 
7
- > Want this on a schedule, with **restore drills that prove your backup actually restores**, plus Storage-file backup and alerts? That's the hosted service → **[backupdrill.com](https://backupdrill.com)**. This CLI is the DIY layer; the cloud adds scheduling, drills, alerting, and reports on top.
7
+ > Want this on a schedule, with **restore drills that prove your backup actually restores**, plus alerts and reports? That's the hosted service → **[backupdrill.com](https://backupdrill.com)**. This CLI is the DIY layer; the cloud adds scheduling, drills, alerting, and reports on top.
8
8
 
9
9
  ## Why
10
10
 
package/dist/backup.js CHANGED
@@ -7,6 +7,7 @@ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
7
7
  import { Upload } from "@aws-sdk/lib-storage";
8
8
  import { syncStorage } from "./storage.js";
9
9
  import { log } from "./log.js";
10
+ import { TOOL_VERSION } from "./version.js";
10
11
  const execFileAsync = promisify(execFile);
11
12
  function majorOf(serverVersionNum) {
12
13
  // server_version_num: 150004 → 15, 170002 → 17
@@ -109,16 +110,19 @@ async function dumpToS3(config, s3, key, pgDumpBin) {
109
110
  },
110
111
  });
111
112
  const uploadDone = upload.done();
112
- // 正确性以 pg_dump 退出码为准:dump 失败则中止上传,绝不把半截转储当成功
113
+ // 失败双向联动:dump 失败要中止上传(绝不把半截转储当成功);上传失败也必须
114
+ // 杀掉 pg_dump —— 否则上传侧停止消费后,背压会让 pg_dump 永远阻塞在 stdout
115
+ // 写入上,调用方(worker 的串行队列)随之永久挂起。Promise.all 会订阅两侧,
116
+ // 先失败的一方抛出后,另一方稍后的 rejection 也已被视为 handled。
113
117
  try {
114
- await dumpDone;
118
+ await Promise.all([dumpDone, uploadDone]);
115
119
  }
116
120
  catch (error) {
117
- meter.destroy(error);
121
+ dump.kill("SIGKILL");
122
+ meter.destroy();
118
123
  await upload.abort().catch(() => { });
119
124
  throw error;
120
125
  }
121
- await uploadDone;
122
126
  return { bytes, sha256: hash.digest("hex") };
123
127
  }
124
128
  export async function runBackup(config) {
@@ -170,7 +174,7 @@ export async function runBackup(config) {
170
174
  }
171
175
  const manifest = {
172
176
  tool: "backupdrill-cli",
173
- toolVersion: process.env.npm_package_version || "0.1.0",
177
+ toolVersion: TOOL_VERSION,
174
178
  createdAt: new Date().toISOString(),
175
179
  projectName: config.projectName,
176
180
  database: {
package/dist/config.js CHANGED
@@ -50,6 +50,13 @@ async function fromFile(path) {
50
50
  throw new Error(`Could not read config file ${path}: ${error.message}`);
51
51
  }
52
52
  }
53
+ // spread 里显式的 undefined 会覆盖前一层的值(CLI 未传的 flag 正是 undefined),
54
+ // 合并前必须剔除,否则 env/配置文件里的 storage 配置会被整体清空。
55
+ function definedOnly(obj) {
56
+ if (!obj)
57
+ return {};
58
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
59
+ }
53
60
  /** 合并优先级:显式 flag > 环境变量 > 配置文件。 */
54
61
  export async function loadConfig(opts) {
55
62
  const file = await fromFile(opts.configPath);
@@ -59,14 +66,14 @@ export async function loadConfig(opts) {
59
66
  const projectName = o.projectName ?? env.projectName ?? file.projectName ?? "supabase";
60
67
  const schemas = o.schemas ?? env.schemas ?? file.schemas ?? ["public"];
61
68
  const storage = {
62
- ...file.storage,
63
- ...env.storage,
64
- ...o.storage,
69
+ ...definedOnly(file.storage),
70
+ ...definedOnly(env.storage),
71
+ ...definedOnly(o.storage),
65
72
  };
66
73
  const src = {
67
- ...file.supabaseStorage,
68
- ...env.supabaseStorage,
69
- ...o.supabaseStorage,
74
+ ...definedOnly(file.supabaseStorage),
75
+ ...definedOnly(env.supabaseStorage),
76
+ ...definedOnly(o.supabaseStorage),
70
77
  };
71
78
  const missing = [];
72
79
  if (!databaseUrl)
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { runEstimate } from "./estimate.js";
6
6
  import { runDrill } from "./drill.js";
7
7
  import { runRestore } from "./restore.js";
8
8
  import { log } from "./log.js";
9
+ import { TOOL_VERSION } from "./version.js";
9
10
  // Supabase egress 定价表(2026-07-04 核实,来源见 README「Egress & cost」)。
10
11
  // 备份流量 100% 未缓存,故用 uncached 单价 $0.09/GB;included 用 uncached 额度。
11
12
  // Free 硬上限(超额即限流,不计费);Pro/Team 由 Spend Cap 决定超额限流还是计费。
@@ -17,10 +18,10 @@ const EGRESS_PRICING = {
17
18
  const program = new Command();
18
19
  program
19
20
  .name("backupdrill")
20
- .description("Back up a Supabase project — Postgres database (and, soon, Storage files) — " +
21
+ .description("Back up a Supabase project — Postgres database and Storage files — " +
21
22
  "to your own S3/R2/B2 bucket, with a checksummed manifest.\n\n" +
22
23
  "Want scheduling, restore drills, and alerts? → https://backupdrill.com")
23
- .version(process.env.npm_package_version || "0.1.0");
24
+ .version(TOOL_VERSION);
24
25
  program
25
26
  .command("backup")
26
27
  .description("Run a one-off backup to your own bucket")
package/dist/storage.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Transform } from "node:stream";
3
+ import { pipeline } from "node:stream/promises";
3
4
  import { S3Client, ListBucketsCommand, ListObjectsV2Command, GetObjectCommand, } from "@aws-sdk/client-s3";
4
5
  import { Upload } from "@aws-sdk/lib-storage";
5
6
  import { log } from "./log.js";
@@ -74,15 +75,26 @@ export async function syncStorage(cfg, target) {
74
75
  cb(null, chunk);
75
76
  },
76
77
  });
77
- body.pipe(meter);
78
- await new Upload({
78
+ // pipeline(而非裸 pipe)才会传播源流错误;否则 Supabase 侧读取中断时
79
+ // meter 永不 end,Upload.done() 永久挂起。两侧任一失败都终止另一侧。
80
+ const copy = pipeline(body, meter);
81
+ const upload = new Upload({
79
82
  client: target.client,
80
83
  params: {
81
84
  Bucket: target.bucket,
82
85
  Key: `${target.base}/storage/${bucket}/${obj.key}`,
83
86
  Body: meter,
84
87
  },
85
- }).done();
88
+ });
89
+ try {
90
+ await Promise.all([copy, upload.done()]);
91
+ }
92
+ catch (error) {
93
+ meter.destroy();
94
+ body.destroy();
95
+ await upload.abort().catch(() => { });
96
+ throw error;
97
+ }
86
98
  files.push({ bucket, key: obj.key, bytes, sha256: hash.digest("hex") });
87
99
  totalBytes += bytes;
88
100
  }
@@ -0,0 +1 @@
1
+ export declare const TOOL_VERSION: string;
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "node:module";
2
+ // npm_package_version 只在 npm scripts 环境里存在;全局安装 / npx 场景必须
3
+ // 从包内 package.json 读,否则 --version 与 manifest.toolVersion 永远是旧值。
4
+ export const TOOL_VERSION = createRequire(import.meta.url)("../package.json").version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backupdrill",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Open-source CLI to back up a Supabase project — Postgres database and Storage files — to your own S3/R2/B2 bucket, with a checksummed manifest.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "build": "tsc",
25
+ "prepublishOnly": "npm run build && npm test",
25
26
  "dev": "node src/index.ts",
26
27
  "typecheck": "tsc --noEmit",
27
28
  "pretest": "tsc",