160 lines
6.0 KiB
JavaScript
160 lines
6.0 KiB
JavaScript
import fs from "fs";
|
|
import path from "path";
|
|
import { AppData } from "../data/app-data";
|
|
import grabDirNames from "../data/grab-dir-names";
|
|
export const ExportArchiveMembers = {
|
|
SqlFileName: "dump.sql",
|
|
SchemaFileName: AppData.DbSchemaFileName,
|
|
};
|
|
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"];
|
|
export function isArchivePath(filePath) {
|
|
const lower = filePath.toLowerCase();
|
|
return ARCHIVE_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
}
|
|
export function isSqlPath(filePath) {
|
|
return filePath.toLowerCase().endsWith(".sql");
|
|
}
|
|
/**
|
|
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts.
|
|
*/
|
|
export async function writeExportArchive({ contents, outPath, }) {
|
|
const lower = outPath.toLowerCase();
|
|
if (lower.endsWith(".zip")) {
|
|
await writeZipArchive({ contents, outPath });
|
|
return;
|
|
}
|
|
const members = {
|
|
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
|
[ExportArchiveMembers.SchemaFileName]: contents.schemaTs,
|
|
};
|
|
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
|
if (gzip) {
|
|
await Bun.Archive.write(outPath, members, { compress: "gzip" });
|
|
}
|
|
else {
|
|
await Bun.Archive.write(outPath, members);
|
|
}
|
|
}
|
|
/**
|
|
* Read a portable export archive (tar / tar.gz / zip).
|
|
*/
|
|
export async function readExportArchive(archivePath) {
|
|
const lower = archivePath.toLowerCase();
|
|
if (lower.endsWith(".zip")) {
|
|
return readZipArchive(archivePath);
|
|
}
|
|
const bytes = await Bun.file(archivePath).bytes();
|
|
const archive = new Bun.Archive(bytes);
|
|
const files = await archive.files();
|
|
const sql = (await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
|
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
|
const schemaTs = (await readArchiveMember(files, ExportArchiveMembers.SchemaFileName)) ??
|
|
(await readFirstMatching(files, (name) => name.endsWith("schema.ts")));
|
|
if (!sql) {
|
|
throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`);
|
|
}
|
|
if (!schemaTs) {
|
|
throw new Error(`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`);
|
|
}
|
|
return { sql, schemaTs };
|
|
}
|
|
async function readArchiveMember(files, name) {
|
|
// Exact match, or basename match for nested paths
|
|
for (const [entry, file] of files) {
|
|
if (entry === name || path.basename(entry) === name) {
|
|
return await file.text();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async function readFirstMatching(files, predicate) {
|
|
for (const [entry, file] of files) {
|
|
if (predicate(entry) || predicate(path.basename(entry))) {
|
|
return await file.text();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async function writeZipArchive({ contents, outPath, }) {
|
|
const { BUN_MARIADB_TEMP_DIR } = grabDirNames();
|
|
const tempDir = path.join(BUN_MARIADB_TEMP_DIR, `export-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
try {
|
|
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
|
const schemaPath = path.join(tempDir, ExportArchiveMembers.SchemaFileName);
|
|
fs.writeFileSync(sqlPath, contents.sql, "utf-8");
|
|
fs.writeFileSync(schemaPath, contents.schemaTs, "utf-8");
|
|
const absOut = path.resolve(outPath);
|
|
const proc = Bun.spawn([
|
|
"zip",
|
|
"-q",
|
|
"-j",
|
|
absOut,
|
|
ExportArchiveMembers.SqlFileName,
|
|
ExportArchiveMembers.SchemaFileName,
|
|
], {
|
|
cwd: tempDir,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stderr, exitCode] = await Promise.all([
|
|
new Response(proc.stderr).text(),
|
|
proc.exited,
|
|
]);
|
|
if (exitCode !== 0) {
|
|
throw new Error(`zip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`zip\` is installed, or use .tar.gz.`);
|
|
}
|
|
}
|
|
finally {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
async function readZipArchive(archivePath) {
|
|
const { BUN_MARIADB_TEMP_DIR } = grabDirNames();
|
|
const tempDir = path.join(BUN_MARIADB_TEMP_DIR, `import-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
try {
|
|
const absArchive = path.resolve(archivePath);
|
|
const proc = Bun.spawn(["unzip", "-q", "-o", absArchive, "-d", tempDir], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stderr, exitCode] = await Promise.all([
|
|
new Response(proc.stderr).text(),
|
|
proc.exited,
|
|
]);
|
|
if (exitCode !== 0) {
|
|
throw new Error(`unzip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`unzip\` is installed.`);
|
|
}
|
|
const sql = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"));
|
|
const schemaTs = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SchemaFileName ||
|
|
name.endsWith("schema.ts"));
|
|
if (!sql) {
|
|
throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`);
|
|
}
|
|
if (!schemaTs) {
|
|
throw new Error(`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`);
|
|
}
|
|
return { sql, schemaTs };
|
|
}
|
|
finally {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
function findFileContents(dir, predicate) {
|
|
const stack = [dir];
|
|
while (stack.length) {
|
|
const current = stack.pop();
|
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
const full = path.join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(full);
|
|
}
|
|
else if (predicate(entry.name)) {
|
|
return fs.readFileSync(full, "utf-8");
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|