diff --git a/apps/cli/commands/tests/import.e2e.test.ts b/apps/cli/commands/tests/import.e2e.test.ts index c808546820..a1b6d8c184 100644 --- a/apps/cli/commands/tests/import.e2e.test.ts +++ b/apps/cli/commands/tests/import.e2e.test.ts @@ -17,6 +17,7 @@ */ import fs from 'fs'; import path from 'path'; +import * as tar from 'tar'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { cleanupCliEnv, @@ -187,4 +188,87 @@ describe.skipIf( ! cliE2ePrerequisitesMet() )( 'CLI e2e: studio import', () => { expect( await getBlogname( env, sitePath ) ).toBe( FIXTURE_BLOGNAME ); } ); + + // Regression test for https://github.com/Automattic/studio/issues/3518 + // where db.php overwrites SQLite drop-in during restore causing database import error + // "Could not determine the version of the SQLite integration plugin". + it( + 'imports a backup containing db.php drop-in', + { tags: [ 'e2e' ], timeout: 300_000 }, + async () => { + if ( ! env ) { + throw new Error( 'CLI e2e env was not initialised' ); + } + + const workDir = path.join( env.root, 'db-php' ); + const cwd = path.join( workDir, 'contents' ); + fs.mkdirSync( cwd, { recursive: true } ); + await tar.x( { file: path.join( FIXTURES_DIR, 'jetpack-backup.tar.gz' ), cwd } ); + + fs.writeFileSync( + path.join( cwd, 'wp-content', 'db.php' ), + ' { + if ( ! env ) { + throw new Error( 'CLI e2e env was not initialised' ); + } + + const sitePath = await createStoppedSite( env, 'MySQL Import E2E Site', 'import-mysql' ); + const sqlitePaths = [ + path.join( sitePath, 'wp-content', 'db.php' ), + path.join( sitePath, 'wp-content', 'database', '.ht.sqlite' ), + path.join( sitePath, 'wp-content', 'mu-plugins', 'sqlite-database-integration' ), + ]; + fs.copyFileSync( + path.join( sitePath, 'wp-config-sample.php' ), + path.join( sitePath, 'wp-config.php' ) + ); + + for ( const sqlitePath of sqlitePaths ) { + fs.rmSync( sqlitePath, { recursive: true, force: true } ); + } + expect( fs.existsSync( path.join( sitePath, 'wp-config.php' ) ) ).toBe( true ); + + const result = await runCli( + [ 'import', path.join( FIXTURES_DIR, 'jetpack-backup.tar.gz' ), '--path', sitePath ], + env + ); + expect( result.code ).not.toBe( 0 ); + expect( result.stderr ).toContain( + 'Database import requires SQLite, but this site is configured to use an external database.' + ); + + for ( const sqlitePath of sqlitePaths ) { + expect( fs.existsSync( sqlitePath ) ).toBe( false ); + } + expect( fs.existsSync( path.join( sitePath, 'wp-content', 'themes', 'mypet-theme' ) ) ).toBe( + false + ); + } + ); } ); diff --git a/apps/cli/lib/import-export/import/importers/importer.ts b/apps/cli/lib/import-export/import/importers/importer.ts index 16b8e2d94b..47f3758857 100644 --- a/apps/cli/lib/import-export/import/importers/importer.ts +++ b/apps/cli/lib/import-export/import/importers/importer.ts @@ -17,6 +17,7 @@ import semver from 'semver'; import trash from 'trash'; import { SiteData } from 'cli/lib/cli-config/core'; import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; +import { installSqliteIntegration, needsSqliteSetup } from 'cli/lib/sqlite-integration'; import { ImportExportEventEmitter } from '../../events'; import { BackupContents, MetaFileData } from '../types'; import { updateSiteUrl } from '../update-site-url'; @@ -66,17 +67,31 @@ abstract class BaseImporter extends ImportExportEventEmitter implements Importer abstract import( site: SiteData ): Promise< ImporterResult >; + protected async assertSqliteDatabaseImportSupported( site: SiteData ): Promise< void > { + if ( ! ( await needsSqliteSetup( site.path ) ) ) { + throw new Error( + __( + 'Database import requires SQLite, but this site is configured to use an external database.' + ) + ); + } + } + protected async importDatabase( site: SiteData, sqlFiles: string[] ): Promise< void > { if ( ! sqlFiles.length ) { return; } + await this.assertSqliteDatabaseImportSupported( site ); this.emit( ImportEvents.IMPORT_DATABASE_START ); const sortedSqlFiles = sqlFiles.sort( ( a, b ) => a.localeCompare( b ) ); let processedFiles = 0; const totalFiles = sortedSqlFiles.length; + // db.php may be non-SQLite drop-in from backup + await installSqliteIntegration( site.path ); + for ( const sqlFile of sortedSqlFiles ) { const sqlTempFile = `${ generateBackupFilename( 'sql' ) }.sql`; const tmpPath = path.join( site.path, sqlTempFile ); @@ -145,6 +160,10 @@ abstract class BaseBackupImporter extends BaseImporter { async import( site: SiteData ): Promise< ImporterResult > { try { + if ( this.backup.sqlFiles.length ) { + // Classify the destination before restored files can make MySQL look like SQLite. + await this.assertSqliteDatabaseImportSupported( site ); + } if ( this.shouldCleanUpBeforeImport ) { await this.moveExistingWpContentToTrash( site.path ); } diff --git a/apps/cli/lib/sqlite-integration.ts b/apps/cli/lib/sqlite-integration.ts index a5a5ee3151..a5e57a732d 100644 --- a/apps/cli/lib/sqlite-integration.ts +++ b/apps/cli/lib/sqlite-integration.ts @@ -24,6 +24,10 @@ export async function installSqliteIntegration( sitePath: string ) { return provider.installSqliteIntegration( sitePath ); } +export async function needsSqliteSetup( sitePath: string ) { + return provider.needsSqliteSetup( sitePath ); +} + export async function keepSqliteIntegrationUpdated( sitePath: string ) { return provider.keepSqliteIntegrationUpdated( sitePath ); }