Major version update. Major bugfixes. Security vulnerabilites fixed
This commit is contained in:
Vendored
+173
@@ -0,0 +1,173 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { AppData } from "../data/app-data";
|
||||
import grabDirNames from "../data/grab-dir-names";
|
||||
import { isSchemaFileName } from "./resolve-and-load-data-file";
|
||||
export const ExportArchiveMembers = {
|
||||
DbFileName: "database.sqlite",
|
||||
SchemaFileName: AppData.DbSchemaFileName,
|
||||
};
|
||||
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"];
|
||||
/** Every SQLite database file starts with this header */
|
||||
const SQLITE_HEADER = "SQLite format 3\0";
|
||||
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");
|
||||
}
|
||||
export function isSQLiteFile(bytes) {
|
||||
return (new TextDecoder().decode(bytes.subarray(0, SQLITE_HEADER.length)) ===
|
||||
SQLITE_HEADER);
|
||||
}
|
||||
/**
|
||||
* Create a portable export archive (tar / tar.gz / zip) with the database
|
||||
* file + schema file.
|
||||
*/
|
||||
export async function writeExportArchive({ contents, outPath, }) {
|
||||
const lower = outPath.toLowerCase();
|
||||
if (lower.endsWith(".zip")) {
|
||||
await writeZipArchive({ contents, outPath });
|
||||
return;
|
||||
}
|
||||
const members = {
|
||||
[ExportArchiveMembers.DbFileName]: contents.db,
|
||||
[contents.schemaFileName]: contents.schema,
|
||||
};
|
||||
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) {
|
||||
if (archivePath.toLowerCase().endsWith(".zip")) {
|
||||
return readZipArchive(archivePath);
|
||||
}
|
||||
const bytes = await Bun.file(archivePath).bytes();
|
||||
const archive = new Bun.Archive(bytes);
|
||||
const files = await archive.files();
|
||||
let db = null;
|
||||
let schemaEntry = null;
|
||||
for (const [entry, file] of files) {
|
||||
const base = path.basename(entry);
|
||||
if (!db && base === ExportArchiveMembers.DbFileName) {
|
||||
db = await file.bytes();
|
||||
}
|
||||
else if (!schemaEntry && isSchemaFileName(base)) {
|
||||
schemaEntry = {
|
||||
content: await file.text(),
|
||||
fileName: schemaMemberName(base),
|
||||
};
|
||||
}
|
||||
}
|
||||
return validateContents({ db, schemaEntry });
|
||||
}
|
||||
function schemaMemberName(base) {
|
||||
return base === AppData.DbSchemaFileName ? "schema.ts" : base;
|
||||
}
|
||||
function validateContents({ db, schemaEntry, }) {
|
||||
if (!db) {
|
||||
throw new Error(`Archive is missing the database file (expected \`${ExportArchiveMembers.DbFileName}\`)`);
|
||||
}
|
||||
if (!isSQLiteFile(db)) {
|
||||
throw new Error(`\`${ExportArchiveMembers.DbFileName}\` in the archive is not a SQLite database`);
|
||||
}
|
||||
if (!schemaEntry) {
|
||||
throw new Error(`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|js|json|yaml|yml\`)`);
|
||||
}
|
||||
return {
|
||||
db,
|
||||
schema: schemaEntry.content,
|
||||
schemaFileName: schemaEntry.fileName,
|
||||
};
|
||||
}
|
||||
async function runCommand(cmd, cwd) {
|
||||
const proc = Bun.spawn(cmd, {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`${cmd[0]} failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`${cmd[0]}\` is installed, or use .tar.gz.`);
|
||||
}
|
||||
}
|
||||
function makeTempDir(prefix) {
|
||||
const { BUN_SQLITE_TEMP_DIR } = grabDirNames();
|
||||
const tempDir = path.join(BUN_SQLITE_TEMP_DIR, `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
return tempDir;
|
||||
}
|
||||
async function writeZipArchive({ contents, outPath, }) {
|
||||
const tempDir = makeTempDir("export");
|
||||
try {
|
||||
fs.writeFileSync(path.join(tempDir, ExportArchiveMembers.DbFileName), contents.db);
|
||||
fs.writeFileSync(path.join(tempDir, contents.schemaFileName), contents.schema, "utf-8");
|
||||
await runCommand([
|
||||
"zip",
|
||||
"-q",
|
||||
"-j",
|
||||
path.resolve(outPath),
|
||||
ExportArchiveMembers.DbFileName,
|
||||
contents.schemaFileName,
|
||||
], tempDir);
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
async function readZipArchive(archivePath) {
|
||||
const tempDir = makeTempDir("import");
|
||||
try {
|
||||
await runCommand([
|
||||
"unzip",
|
||||
"-q",
|
||||
"-o",
|
||||
path.resolve(archivePath),
|
||||
"-d",
|
||||
tempDir,
|
||||
]);
|
||||
let db = null;
|
||||
let schemaEntry = null;
|
||||
const stack = [tempDir];
|
||||
while (stack.length) {
|
||||
const current = stack.pop();
|
||||
for (const entry of fs.readdirSync(current, {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
const full = path.join(current, entry.name);
|
||||
// Never follow symlinks from an extracted (untrusted) archive
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(full);
|
||||
}
|
||||
else if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
else if (!db &&
|
||||
entry.name === ExportArchiveMembers.DbFileName) {
|
||||
db = new Uint8Array(fs.readFileSync(full));
|
||||
}
|
||||
else if (!schemaEntry && isSchemaFileName(entry.name)) {
|
||||
schemaEntry = {
|
||||
content: fs.readFileSync(full, "utf-8"),
|
||||
fileName: schemaMemberName(entry.name),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return validateContents({ db, schemaEntry });
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user