backupdrill 1.0.0 → 2.0.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/README.md +34 -7
- package/dist/backup.js +47 -0
- package/dist/drill.d.ts +8 -5
- package/dist/drill.js +27 -96
- package/dist/index.js +100 -20
- package/dist/lib.d.ts +1 -1
- package/dist/manifest.d.ts +32 -1
- package/dist/manifest.js +157 -3
- package/dist/recovery-doc.d.ts +11 -0
- package/dist/recovery-doc.js +123 -0
- package/dist/restore-engine.d.ts +80 -0
- package/dist/restore-engine.js +267 -0
- package/dist/restore.d.ts +65 -2
- package/dist/restore.js +515 -23
- package/dist/storage-catalog.d.ts +19 -0
- package/dist/storage-catalog.js +50 -0
- package/dist/storage-restore.d.ts +79 -0
- package/dist/storage-restore.js +634 -0
- package/dist/storage.d.ts +16 -1
- package/dist/storage.js +33 -2
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BucketAttrs } from "./manifest.js";
|
|
2
|
+
interface BucketRow {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
public: boolean | null;
|
|
6
|
+
file_size_limit: string | number | null;
|
|
7
|
+
allowed_mime_types: string[] | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 行 → manifest 形状的纯映射,含完整性关口:查询结果必须覆盖每一个请求的 bucket。
|
|
11
|
+
* storage.buckets 上的 RLS 或并发删除会**静默**少返回行——把不完整清单写进 manifest,
|
|
12
|
+
* 恢复端就会漏建 bucket 还以为自己全知。缺任何一个 → 抛错,由调用方整体降级为"未捕获"。
|
|
13
|
+
* 请求标识来自 S3 层(ListBuckets),优先按 buckets.id 匹配(id 与 name 允许不同),
|
|
14
|
+
* 无中再退 name。null 属性原样保留(= 源端未设置,不是猜测值)。
|
|
15
|
+
*/
|
|
16
|
+
export declare function bucketAttrsFromRows(requestedBuckets: string[], bucketRows: BucketRow[]): BucketAttrs[];
|
|
17
|
+
/** 查询源库的 bucket 属性。bucketNames 含空 bucket(恢复端要能重建它们)。 */
|
|
18
|
+
export declare function inspectBucketAttrs(databaseUrl: string, bucketNames: string[]): Promise<BucketAttrs[]>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Bucket 属性采集(Manifest v2,恢复闭环 PRD §5.1.3):public / 大小限制 / MIME 限制。
|
|
2
|
+
// 通道 = 源库 storage.buckets —— S3 兼容端点看不到这些属性,而备份本来就持有只读库连接,
|
|
3
|
+
// 零新增凭据(R0 spike 实证)。per-file 元数据不在这里:它随 syncStorage 的 GetObject
|
|
4
|
+
// 响应原子捕获(见 storage.ts fileMetadataFromS3)。
|
|
5
|
+
// 任何失败都由调用方降级为"未捕获"(manifest 缺 buckets 字段),绝不弄失败一次备份。
|
|
6
|
+
import { Client } from "pg";
|
|
7
|
+
import { pgConnectOptions } from "./supabase-ca.js";
|
|
8
|
+
/**
|
|
9
|
+
* 行 → manifest 形状的纯映射,含完整性关口:查询结果必须覆盖每一个请求的 bucket。
|
|
10
|
+
* storage.buckets 上的 RLS 或并发删除会**静默**少返回行——把不完整清单写进 manifest,
|
|
11
|
+
* 恢复端就会漏建 bucket 还以为自己全知。缺任何一个 → 抛错,由调用方整体降级为"未捕获"。
|
|
12
|
+
* 请求标识来自 S3 层(ListBuckets),优先按 buckets.id 匹配(id 与 name 允许不同),
|
|
13
|
+
* 无中再退 name。null 属性原样保留(= 源端未设置,不是猜测值)。
|
|
14
|
+
*/
|
|
15
|
+
export function bucketAttrsFromRows(requestedBuckets, bucketRows) {
|
|
16
|
+
const attrs = [];
|
|
17
|
+
const missing = [];
|
|
18
|
+
for (const req of requestedBuckets) {
|
|
19
|
+
const row = bucketRows.find((r) => r.id === req) ?? bucketRows.find((r) => r.name === req);
|
|
20
|
+
if (!row) {
|
|
21
|
+
missing.push(req);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
attrs.push({
|
|
25
|
+
name: req, // 快照对象布局用的标识(S3 层名),恢复端按它重建
|
|
26
|
+
public: row.public,
|
|
27
|
+
fileSizeLimit: row.file_size_limit === null ? null : Number(row.file_size_limit),
|
|
28
|
+
allowedMimeTypes: row.allowed_mime_types,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (missing.length) {
|
|
32
|
+
throw new Error(`storage.buckets returned no row for: ${missing.join(", ")} ` +
|
|
33
|
+
`(RLS filtering or concurrent deletion) — refusing a partial bucket capture`);
|
|
34
|
+
}
|
|
35
|
+
return attrs;
|
|
36
|
+
}
|
|
37
|
+
/** 查询源库的 bucket 属性。bucketNames 含空 bucket(恢复端要能重建它们)。 */
|
|
38
|
+
export async function inspectBucketAttrs(databaseUrl, bucketNames) {
|
|
39
|
+
const client = new Client(pgConnectOptions(databaseUrl));
|
|
40
|
+
await client.connect();
|
|
41
|
+
try {
|
|
42
|
+
const bucketRows = await client.query(`select id, name, public, file_size_limit, allowed_mime_types
|
|
43
|
+
from storage.buckets where id = any($1::text[]) or name = any($1::text[])
|
|
44
|
+
order by name`, [bucketNames]);
|
|
45
|
+
return bucketAttrsFromRows(bucketNames, bucketRows.rows);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await client.end();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import type { Manifest, StorageFile } from "./manifest.js";
|
|
3
|
+
export interface StorageRestoreTarget {
|
|
4
|
+
supabaseUrl: string;
|
|
5
|
+
serviceRoleKey: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ReconcileReport {
|
|
8
|
+
/** manifest 文件在目标上逐键找到且尺寸一致的数量 */
|
|
9
|
+
verified: number;
|
|
10
|
+
missing: string[];
|
|
11
|
+
sizeMismatched: string[];
|
|
12
|
+
/** 目标上存在但 manifest 没有的对象数——非快照内容混在恢复目标里 = 不是干净恢复,计入失败裁决 */
|
|
13
|
+
extras: number;
|
|
14
|
+
matches: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface StorageRestoreSummary {
|
|
17
|
+
bucketsCreated: string[];
|
|
18
|
+
bucketsExisting: string[];
|
|
19
|
+
/** manifest 未捕获属性的 bucket(以默认设置创建,如实上报,不猜测) */
|
|
20
|
+
bucketsWithoutAttrs: string[];
|
|
21
|
+
/** 既有 bucket 与 manifest 属性不一致的描述(warn 级:可能是用户有意配置) */
|
|
22
|
+
bucketAttrDrift: string[];
|
|
23
|
+
filesUploaded: number;
|
|
24
|
+
bytesUploaded: number;
|
|
25
|
+
/** 目标已有同 key 同尺寸对象而跳过的数量(重试残留;字节级抽样校验覆盖) */
|
|
26
|
+
filesSkippedIdentical: number;
|
|
27
|
+
filesFailed: {
|
|
28
|
+
file: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
}[];
|
|
31
|
+
/** 429/5xx/超时后的成功重试次数(可观测性:PRD §10.3) */
|
|
32
|
+
retries: number;
|
|
33
|
+
reconcile: ReconcileReport;
|
|
34
|
+
checksumSample: {
|
|
35
|
+
checked: number;
|
|
36
|
+
mismatched: string[];
|
|
37
|
+
};
|
|
38
|
+
/** 元数据降级说明(如 TUS 路径不保真的字段)——如实上报,不静默丢 */
|
|
39
|
+
metadataNotes: string[];
|
|
40
|
+
/** PRD §5.4.3:owner 语义边界,报告必须显示 */
|
|
41
|
+
ownerNote: string;
|
|
42
|
+
}
|
|
43
|
+
/** bucket 名单 = manifest.buckets(含空桶)∪ files 里出现过的桶(v1 快照没有 buckets 字段) */
|
|
44
|
+
export declare function bucketsToRestore(manifest: Manifest): {
|
|
45
|
+
name: string;
|
|
46
|
+
attrs: boolean;
|
|
47
|
+
}[];
|
|
48
|
+
export declare const STANDARD_UPLOAD_LIMIT_BYTES: number;
|
|
49
|
+
/**
|
|
50
|
+
* TUS 启用阈值:默认 = 6MB(Supabase 官方对 resumable 的建议线)——大文件从零重传
|
|
51
|
+
* 的标准上传既脆弱又贵;env 可覆盖(验证时压低,特殊网络环境可调高)。
|
|
52
|
+
*/
|
|
53
|
+
export declare function tusThresholdBytes(): number;
|
|
54
|
+
/**
|
|
55
|
+
* 递归遍历目标 bucket 的全部对象(list API 是目录级的:id === null 的条目是"目录")。
|
|
56
|
+
* 返回 key → size。不依赖目标 DB 凭据 → storage-only 恢复也有完整对账。
|
|
57
|
+
*/
|
|
58
|
+
export declare function walkTargetObjects(target: StorageRestoreTarget, bucket: string, counters?: {
|
|
59
|
+
retries: number;
|
|
60
|
+
}): Promise<Map<string, number>>;
|
|
61
|
+
/**
|
|
62
|
+
* 写前净度检查(纯函数,可测):既有桶里只允许"本快照的同尺寸残留"。
|
|
63
|
+
* 任何外来对象或尺寸冲突都在**写第一个字节之前**整体拒绝——部分写入的脏目标
|
|
64
|
+
* 比干净失败难收拾得多(评审第 14 轮)。
|
|
65
|
+
*/
|
|
66
|
+
export declare function targetResidueViolations(files: StorageFile[], preexisting: Map<string, Map<string, number>>): string[];
|
|
67
|
+
/** 逐键对账(纯函数,可测):manifest 每个文件都必须在目标出现且尺寸一致;多余对象另计。 */
|
|
68
|
+
export declare function reconcileFiles(files: StorageFile[], actualByBucket: Map<string, Map<string, number>>): ReconcileReport;
|
|
69
|
+
/** dry-run / 执行前预检的公开形态:一次性 scratch 摘要,只返回计划概要与警告。 */
|
|
70
|
+
export declare function planStorageRestore(target: StorageRestoreTarget, manifest: Manifest): Promise<{
|
|
71
|
+
bucketsToCreate: string[];
|
|
72
|
+
verifiedResidue: number;
|
|
73
|
+
warnings: string[];
|
|
74
|
+
}>;
|
|
75
|
+
export declare function restoreStorage(source: {
|
|
76
|
+
s3: S3Client;
|
|
77
|
+
bucket: string;
|
|
78
|
+
base: string;
|
|
79
|
+
}, target: StorageRestoreTarget, manifest: Manifest): Promise<StorageRestoreSummary>;
|