diff --git a/.changeset/strong-kids-wink.md b/.changeset/strong-kids-wink.md new file mode 100644 index 00000000..2f4bb3c6 --- /dev/null +++ b/.changeset/strong-kids-wink.md @@ -0,0 +1,5 @@ +--- +"@atomicsmash/cli": minor +--- + +[pull-database] Add ability to resume post-download if the process fails when importing. diff --git a/package-lock.json b/package-lock.json index 8d0034e2..0e459ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5204,6 +5204,149 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz", + "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.5.tgz", + "integrity": "sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz", + "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.3.tgz", + "integrity": "sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/external-editor": "^3.0.5", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor/node_modules/@inquirer/external-editor": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.5.tgz", + "integrity": "sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.5.tgz", + "integrity": "sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/external-editor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", @@ -5226,6 +5369,191 @@ } } }, + "node_modules/@inquirer/figures": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.6.tgz", + "integrity": "sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.3.tgz", + "integrity": "sha512-6Yuwh1NGSbu1Lo4N1EWjXs1jKRntLg/ZCwhmeorEHde90v1XxAozdbd4Iu30eOQLW+6h1hp2O9ujNfLSbTPJnA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.2.tgz", + "integrity": "sha512-W9zYdyzogK+6110mqwaSJWCBu2yA5Q/OfnGSjjZB1bNpHlmUozXxTl0+QOZBNeVd6Qo81/qT75gW05gLAtITxw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.2.tgz", + "integrity": "sha512-QoRB4wFIjgH5iOhSjoIKMkTvSHDuV+O3OITlIqAYO0oK5x364GJILXiMBvlPiE+klg7Xx9tq5XVqQHcGUDYYPA==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.5", + "@inquirer/confirm": "^6.3.2", + "@inquirer/editor": "^5.3.3", + "@inquirer/expand": "^5.1.5", + "@inquirer/input": "^5.1.6", + "@inquirer/number": "^4.2.3", + "@inquirer/password": "^5.2.2", + "@inquirer/rawlist": "^5.3.5", + "@inquirer/search": "^4.3.3", + "@inquirer/select": "^5.2.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.5.tgz", + "integrity": "sha512-1oHky1ONfCOwNrnkQGDE1oaSij/3fI6HFMSf2H/WsGO2lEyDX9My82iggITSy9ddSZ8yk8j9v41OI0fVoSIoaA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.3.tgz", + "integrity": "sha512-fyuIU1Nbpvwlikjg3gXwJFDI11+EFjqQ7P+iByfmivIKQ1vmaykNrD/vy5unHuUqUpsOsnvJ25//tPF7E/RBRA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.5.tgz", + "integrity": "sha512-9kc15hr8r/kI+3DO/xLog5nOzTz1jqsHXa6JBFzmQKhkoJ8Slda1I1L/uD8ZSZ9tF1yp79wwXe7mclvX1rqR2Q==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz", + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -12420,7 +12748,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", - "dev": true, "license": "MIT" }, "node_modules/chokidar": { @@ -12500,6 +12827,15 @@ "license": "MIT", "peer": true }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -15025,6 +15361,21 @@ "license": "MIT", "peer": true }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", @@ -15041,6 +15392,15 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -15844,7 +16204,6 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -17609,6 +17968,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/nanoid": { "version": "3.3.19", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", @@ -24614,6 +24982,7 @@ "license": "GPL-3.0-or-later", "dependencies": { "@atomicsmash/smash-config": "^2.0.2", + "@inquirer/prompts": "^8.7.2", "dotenv": "^17.4.2", "yargs": "^18.1.0" }, diff --git a/packages/cli/package.json b/packages/cli/package.json index d969841d..bd881a18 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,6 +41,7 @@ }, "dependencies": { "@atomicsmash/smash-config": "^2.0.2", + "@inquirer/prompts": "^8.7.2", "dotenv": "^17.4.2", "yargs": "^18.1.0" }, diff --git a/packages/cli/src/commands/pull-database.ts b/packages/cli/src/commands/pull-database.ts index 2198a563..d5925b2c 100644 --- a/packages/cli/src/commands/pull-database.ts +++ b/packages/cli/src/commands/pull-database.ts @@ -1,18 +1,48 @@ -import { exec } from "node:child_process"; -import { unlink as deleteFile } from "node:fs/promises"; +import type { YargsInstance } from "../cli.js"; +import type { ArgumentsCamelCase } from "yargs"; +import { execFile, spawn } from "node:child_process"; +import { + unlink as deleteFile, + access, + constants, + stat, + open, + readFile, +} from "node:fs/promises"; import { performance } from "node:perf_hooks"; import { promisify } from "node:util"; import { getSmashConfig } from "@atomicsmash/smash-config"; import { convertMeasureToPrettyString, startRunningMessage } from "../utils.js"; +import { resolve } from "node:path"; +import { select } from "@inquirer/prompts"; +const execute = promisify(execFile); export const command = "pull-database"; export const describe = "Pull the database down from staging and replace local database."; +export const builder = function (yargs: YargsInstance) { + return yargs + .options({ + resume: { + demandOption: false, + boolean: true, + description: "Resume the process from an existing database.", + conflicts: "fresh", + }, + fresh: { + demandOption: false, + boolean: true, + description: "Clean up any previous runs before starting this run.", + conflicts: "resume", + }, + }) + .example("$0 svg --in icons --out public/assets", ""); +}; -export async function handler() { - const execute = promisify(exec); +export async function handler( + args: ArgumentsCamelCase["argv"]>>, +) { const smashConfig = await getSmashConfig(2); - const { projectName, staging: { @@ -29,15 +59,60 @@ export async function handler() { pullMedia: { monthsToPull }, }, } = smashConfig; - - const stopRunningMessage = startRunningMessage( - "Pulling database from staging", + const stagingDatabaseDownloadLocation = resolve( + "/tmp", + `${projectName}-staging-database.sql`, ); + let mode = args.fresh ? "fresh" : args.resume ? "resume" : "unknown"; + + let hasLocalDatabaseFile = await access( + stagingDatabaseDownloadLocation, + constants.R_OK | constants.W_OK, + ) + .then(() => true) + .catch(() => false); + + if (mode === "resume" && !hasLocalDatabaseFile) { + console.log( + "Failed to find an existing database to resume this process from. Please try running this command without the --resume flag.", + ); + return; + } + + if (mode === "unknown") { + if (hasLocalDatabaseFile) { + console.log("We found an existing database download file."); + const fileStats = await stat(stagingDatabaseDownloadLocation); + const timestamp = new Date(fileStats.mtime).toLocaleString(); + const choice = await select({ + message: + "Do you want to resume from the existing file or start a new download?", + choices: [ + { name: `Resume from existing file (${timestamp})`, value: "resume" }, + { name: "Start new download", value: "fresh" }, + ], + }); + + mode = choice; + } else { + mode = "fresh"; + } + } + performance.mark("Start"); - await (async () => { - const tmpFile = "/tmp/staging-database.sql"; - const dbPrefixFile = "/tmp/db-prefix.txt"; + + if (mode === "fresh" && hasLocalDatabaseFile) { + await deleteFile(stagingDatabaseDownloadLocation).then(() => { + hasLocalDatabaseFile = false; + console.log("Cleaned up existing database file."); + }); + } + + if (mode === "fresh") { + const stopRunningMessage = startRunningMessage( + "Pulling database from staging", + ); const tablesToExclude = [ // WordFence "wfauditevents", @@ -74,83 +149,150 @@ export async function handler() { "pmxi_posts", "pmxi_templates", ].map((tableName) => stagingDBPrefix + tableName); - - const port = stagingSSHPort ? `-p ${stagingSSHPort.toString()}` : ``; - await execute( - `ssh -o "StrictHostKeyChecking no" ${stagingSSHUsername}@${stagingSSHHost} ${port} "${stagingWebRoot !== "" ? `cd ${stagingWebRoot} && ` : ""} wp db export - --add-drop-table --exclude_tables=${tablesToExclude.join(",")}" > ${tmpFile}`, + await exportDatabaseOverSshToFile( + stagingSSHUsername, + stagingSSHHost, + stagingSSHPort, + `${stagingWebRoot !== "" ? `cd ${stagingWebRoot} && ` : ""}wp db export - --add-drop-table --exclude_tables=${tablesToExclude.join(",")}`, + stagingDatabaseDownloadLocation, ) .then(async () => { await stopRunningMessage(); console.log("Database downloaded."); - await execute(`wp db check`).catch(async () => { - await execute(`wp db create`); - console.log("Local database created."); - }); - const stopRunningMessage2 = startRunningMessage("Importing database"); - await execute(`wp db query < ${tmpFile}`) - .then(async () => { - await stopRunningMessage2(); - console.log("Database imported."); - }) - .catch(async (error: unknown) => { - await stopRunningMessage2(); - throw error; - }); - }) - .then(async () => { - const stopRunningMessage3 = startRunningMessage( - "Running search and replace", - ); - - await execute( - `wp search-replace --url=${projectName}.test //${stagingURL} '//${projectName}.test'`, - ) - .then(async () => { - await stopRunningMessage3(); - console.log("Search and replace completed."); - }) - .catch(async (error: unknown) => { - await stopRunningMessage3(); - throw error; - }); - }) - .then(async () => { - const stopRunningMessage4 = startRunningMessage("Cleaning up"); - await Promise.allSettled([ - deleteFile(tmpFile), - deleteFile(dbPrefixFile), - ]).then(async () => { - await stopRunningMessage4(); - }); - - console.log( - `Database import complete! ${convertMeasureToPrettyString( - performance.measure("everything", "Start"), - )}`, - ); - console.log( - `If you're using Herd, you can now run the proxy-media command to avoid having to download images. -Otherwise, you can use pull:media for a slow download of ${monthsToPull === -1 ? "all the images" : `${monthsToPull.toString()} months worth of images`} from staging.`, - ); }) .catch(async (error: unknown) => { await stopRunningMessage(); console.error("Error during database pull:", error); - const cleanupResults = await Promise.allSettled([ - deleteFile(tmpFile), - deleteFile(dbPrefixFile), - ]); - - const failedCleanups = cleanupResults.filter( - (result) => result.status === "rejected", - ); - if (failedCleanups.length > 0) { + await deleteFile(stagingDatabaseDownloadLocation).catch(() => { console.warn( - `Warning: Failed to delete ${failedCleanups.length.toString()} temporary file(s). You may need to clean them up manually.`, + `Warning: Failed to delete the local database file. This file is likely unfinished/corrupted. You may need to clean it up manually.`, ); - } + }); + process.exitCode = 1; }); - })(); + } + + // Check if a DB exists, and if it doesn't, create one using the details in wp-config. + await execute("wp", ["db", "check"]).catch(async () => { + await execute("wp", ["db", "create"]); + console.log("Local database created."); + }); + + // Import downloaded database into the local DB + const stopRunningMessage2 = startRunningMessage("Importing database"); + const stagingDatabaseContents = await readFile( + stagingDatabaseDownloadLocation, + "utf8", + ); + await new Promise((resolve, reject) => { + const childProcess = execFile( + "wp", + ["db", "query"], + { + encoding: "utf-8", + }, + (error) => { + if (error) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(error); + } + }, + ); + let output = ""; + childProcess.stdout?.on("data", (data: string) => { + output += data; + }); + let errorOutput = ""; + childProcess.stderr?.on("data", (data: string) => { + errorOutput += data; + }); + childProcess.stdin?.end(stagingDatabaseContents); + childProcess.on("close", () => { + resolve({ stdout: output, stderr: errorOutput }); + }); + }) + .then(async () => { + await stopRunningMessage2(); + console.log("Database imported."); + }) + .catch(async (error: unknown) => { + await stopRunningMessage2(); + throw error; + }); + + // Run search and replace against the local DB (this is required to correctly update serialised values) + const stopRunningMessage3 = startRunningMessage("Running search and replace"); + await execute("wp", [ + "search-replace", + `--url=${projectName}.test`, + `//${stagingURL}`, + `//${projectName}.test`, + ]) + .then(async () => { + await stopRunningMessage3(); + console.log("Search and replace completed."); + }) + .catch(async (error: unknown) => { + await stopRunningMessage3(); + throw error; + }); + + // Cleanup DB file on success + const stopRunningMessage4 = startRunningMessage("Cleaning up"); + await deleteFile(stagingDatabaseDownloadLocation).then(async () => { + await stopRunningMessage4(); + }); + + console.log( + `Database import complete! ${convertMeasureToPrettyString( + performance.measure("everything", "Start"), + )}`, + ); + console.log( + `If you're using Herd, you can now run the proxy-media command to avoid having to download images. +Otherwise, you can use pull:media for a slow download of ${monthsToPull === -1 ? "all the images" : `${monthsToPull.toString()} months worth of images`} from staging.`, + ); +} + +async function exportDatabaseOverSshToFile( + sshUsername: string, + sshHost: string, + sshPort: number | undefined, + remoteCommand: string, + outputFilePath: string, +) { + const fileHandle = await open(outputFilePath, "w"); + try { + await new Promise((resolvePromise, rejectPromise) => { + const sshArgs = ["-o", "StrictHostKeyChecking no"]; + if (sshPort) { + sshArgs.push("-p", sshPort.toString()); + } + sshArgs.push(`${sshUsername}@${sshHost}`, remoteCommand); + const child = spawn("ssh", sshArgs, { + stdio: ["ignore", "pipe", "pipe"], + }); + child.stdout.pipe(fileHandle.createWriteStream()); + let stderr = ""; + child.stderr.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + }); + child.on("error", rejectPromise); + child.on("close", (code) => { + if (code === 0) { + resolvePromise(); + } else { + rejectPromise( + new Error( + `ssh exited with code ${code ? code.toString() : "unknown"}${stderr ? `: ${stderr}` : ""}`, + ), + ); + } + }); + }); + } finally { + await fileHandle.close(); + } }