UNPKG

@pgxsinkit/pgwasm

Version:
69 lines (63 loc) • 2.84 kB
// Began as a copy of `@electric-sql/pglite` (taken under its PostgreSQL License option, © ElectricSQL // — see NOTICE). Owned outright (ADR-0062); compatibility with PGlite is an anti-goal — evolve freely. /** * Store backups: a data directory as a tarball (ADR-0035). pgwasm owns the format so every build * produces the same artefact: ustar, entry names relative to the data directory with a leading `/`, * a directory before its contents, optionally gzipped. Backups written by earlier releases restore. */ import type { DataDirEntry } from "../build/seam"; import { BackupFormatError } from "../errors"; import type { DumpCompression } from "../interface"; import { gunzipIfCompressed, gzip } from "../tar/gzip"; import { readTar, TarFormatError, writeTar, type TarEntry } from "../tar/tar"; /** * Normalize a backup member's name to a data-directory path: a leading `/`, no trailing `/`, no empty, * `.` or `..` segments. A name that would leave the data directory is refused. */ export function normalizeDataDirPath(name: string): string { const segments = name.split("/").filter((segment) => segment !== "" && segment !== "."); if (segments.includes("..")) { throw new BackupFormatError(`the backup member "${name}" points outside the data directory`); } return `/${segments.join("/")}`; } /** Read a Store backup (gzipped or not, whatever its MIME type or name says) into entries. */ export async function readDataDirArchive(archive: Blob): Promise<DataDirEntry[]> { const bytes = await gunzipIfCompressed(new Uint8Array(await archive.arrayBuffer())); let members: TarEntry[]; try { members = readTar(bytes); } catch (error) { if (error instanceof TarFormatError) { throw new BackupFormatError(`the Store backup is not a readable tarball: ${error.message}`, { cause: error }); } throw error; } const entries: DataDirEntry[] = []; for (const entry of members) { const path = normalizeDataDirPath(entry.name); if (path === "/") continue; entries.push({ path, type: entry.type, mode: entry.mode, mtimeSeconds: entry.mtimeSeconds, data: entry.data }); } return entries; } /** Write entries as a Store backup `File`, named `<baseName>.tar` or `<baseName>.tar.gz`. */ export async function writeDataDirArchive( entries: readonly DataDirEntry[], baseName: string, compression: DumpCompression = "auto", ): Promise<File> { const tarball = writeTar( entries.map((entry) => ({ name: entry.path, type: entry.type, mode: entry.mode, mtimeSeconds: entry.mtimeSeconds, data: entry.data, })), ); if (compression === "none") { return new File([tarball.slice()], `${baseName}.tar`, { type: "application/x-tar" }); } return new File([await gzip(tarball)], `${baseName}.tar.gz`, { type: "application/x-gzip" }); }