backupdrill 2.2.2 → 2.2.4

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/dist/backup.js CHANGED
@@ -17,7 +17,7 @@ import { dumpPartSizeFor } from "./part-size.js";
17
17
  // 解析函数搬到了 pgbin(演练与恢复都要读 pg_restore 版本,放这里会成环);保留旧出口
18
18
  export { parsePgDumpMajor };
19
19
  import { dumpUrlFor, connectPg, isSupabaseHost } from "./supabase-ca.js";
20
- import { libpqChildEnv, urlSslModeOf } from "./restore-engine.js";
20
+ import { credentialSafeDbArgs, libpqChildEnv, urlSslModeOf } from "./restore-engine.js";
21
21
  const execFileAsync = promisify(execFile);
22
22
  function majorOf(serverVersionNum) {
23
23
  // server_version_num: 150004 → 15, 170002 → 17
@@ -106,7 +106,9 @@ async function dumpToS3(config, s3, key, pgDumpBin, partSize) {
106
106
  });
107
107
  const schemaArgs = config.schemas.map((s) => `--schema=${s}`);
108
108
  // pg_dump(libpq):Supabase 主机才改写成 sslmode=verify-full + sslrootcert=<打包CA临时文件>
109
- const dumpDbUrl = dumpUrlFor(config.databaseUrl);
109
+ // 凭据不进 argv(D6):argv 对同机任何进程的 `ps` 可见。与恢复路径同一个函数——
110
+ // 先改写 SSL 再拆密码,密码经 PGPASSWORD 交给 libpq
111
+ const { url: dumpDbUrl, env: credentialEnv } = credentialSafeDbArgs(dumpUrlFor(config.databaseUrl));
110
112
  const dump = spawn(pgDumpBin, [
111
113
  "--format=custom",
112
114
  "--no-owner",
@@ -118,7 +120,7 @@ async function dumpToS3(config, s3, key, pgDumpBin, partSize) {
118
120
  // 环境剔除改写目标/身份的 PG* 变量:pg_dump 只认 --dbname 里的 URL(TLS 策略等保留)
119
121
  {
120
122
  stdio: ["ignore", "pipe", "pipe"],
121
- env: libpqChildEnv({}, process.env, {
123
+ env: libpqChildEnv(credentialEnv, process.env, {
122
124
  supabaseHost: isSupabaseHost(config.databaseUrl),
123
125
  urlSslMode: urlSslModeOf(dumpDbUrl),
124
126
  }),
@@ -141,7 +141,14 @@ export function libpqChildEnv(extraEnv = {}, base = process.env, opts = { supaba
141
141
  }
142
142
  if (noVerify)
143
143
  env.PGSSLROOTCERT = NO_VERIFY_ROOTCERT_SENTINEL;
144
- return { ...env, ...extraEnv };
144
+ // 消息语言钉成 C:恢复的成败判定按英文前缀 `pg_restore: error:` 切块(classifyBlocks)。带翻译
145
+ // (NLS)的客户端在非英文系统上写的是 "pg_restore: 错误:" / "Fehler:",一个预期冲突都认不出来,
146
+ // 健康的备份被判失败(2026-10-11 在中文 macOS 上用 Homebrew postgresql@17 复现)。macOS 的
147
+ // gettext 在 LANG 未设时还会去读系统首选语言,所以必须显式给 LC_ALL,extraEnv 也不许覆盖;
148
+ // LANGUAGE 在 C locale 下本就被 gettext 忽略,一并去掉免得歧义。只影响消息语言:pg_dump /
149
+ // pg_restore 的客户端编码取自数据库 / 归档本身,与 locale 无关。
150
+ delete env.LANGUAGE;
151
+ return { ...env, ...extraEnv, LC_ALL: "C" };
145
152
  }
146
153
  /** 见 libpqChildEnv:一个绝不存在的根证书路径 = 让 libpq 的 require 既不验证也不去找默认 root.crt。 */
147
154
  export const NO_VERIFY_ROOTCERT_SENTINEL = "/nonexistent/backupdrill-no-verify-root.crt";
@@ -388,6 +395,10 @@ export function credentialSafeDbArgs(connString) {
388
395
  }
389
396
  return { url: connString, env: {} };
390
397
  }
398
+ // 兜底(入口的 assertNoHostOverride 已拒字面 #):片段留在输出里,libpq 会把 `#?host=…` 当查询参数
399
+ if (url.hash) {
400
+ throw new Error("connection string contains a literal '#' — percent-encode it as %23 (for example inside the password).");
401
+ }
391
402
  let password = "";
392
403
  if (url.password) {
393
404
  try {
@@ -420,9 +431,9 @@ export function credentialSafeDbArgs(connString) {
420
431
  if (/^password$/i.test(key)) {
421
432
  const rawValue = eq === -1 ? "" : part.slice(eq + 1);
422
433
  try {
423
- const value = decodeURIComponent(rawValue.replace(/\+/g, "%20"));
424
- if (value)
425
- queryPasswords.push(value);
434
+ // 空值也要记:libpq 取最后一个(空 = 回落 .pgpass),只记非空会把前面的旧值
435
+ // 当成密码——?password=stale&password= 预检走 .pgpass、pg_dump 却用 stale
436
+ queryPasswords.push(decodeURIComponent(rawValue.replace(/\+/g, "%20")));
426
437
  }
427
438
  catch {
428
439
  throw new Error("the connection string ?password= value contains malformed percent-encoding — " +
@@ -462,6 +473,12 @@ export function assertNoHostOverride(connString) {
462
473
  if (containsNul(connString)) {
463
474
  throw new Error("connection string contains a NUL byte — use a plain connection string.");
464
475
  }
476
+ // 字面 # 同样在解析前拒:WHATWG URL 把 # 之后当片段,这里看到的主机干净;libpq 不认片段,
477
+ // 剥掉 ?password= 后剩下的 `#?host=…` 会被它读成查询参数,改写连接目标(交叉审查)。
478
+ // 合法的 # 只会出现在密码等值里,写成 %23 即可
479
+ if (connString.includes("#")) {
480
+ throw new Error("connection string contains a literal '#' — percent-encode it as %23 (for example inside the password).");
481
+ }
465
482
  // 解析不了的连接串直接拒:曾经"放行让 pg 自己报错",但 pg 的解析器比 WHATWG URL 宽
466
483
  // (`postgresql://user@/db?host=…` 这类串 pg 接受、这里解析失败),放行 = 身份判定为空却照样连接
467
484
  // (交叉审查)。校验逻辑放在 try 外面——曾按文案里有没有 "override" 决定重抛,新理由一措辞不同就被吞。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backupdrill",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
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": {