Skip to content
84 changes: 84 additions & 0 deletions apps/cli/commands/tests/import.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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' ),
'<?php\n/**\n * Plugin Name: Query Monitor Database Class (Drop-in)\n */\nclass QM_DB extends wpdb {}\n'
);

const file = path.join( workDir, 'jetpack-backup-db-php-replaced.tar.gz' );
await tar.c( { file, cwd, gzip: true }, fs.readdirSync( cwd ) );
const sitePath = await createStoppedSite(
env,
'Foreign DB Import E2E Site',
'import-db-php'
);

const result = await runCli( [ 'import', file, '--path', sitePath ], env );
expect( result.code, result.stderr ).toBe( 0 );

const db = fs.readFileSync( path.join( sitePath, 'wp-content', 'db.php' ), 'utf8' );
expect( db ).toContain( 'SQLITE_DB_DROPIN_VERSION' );
expect( db ).not.toContain( 'QM_DB' );

assertImportedSiteOnDisk( sitePath );
expect( await getBlogname( env, sitePath ) ).toBe( FIXTURE_BLOGNAME );
}
);

it(
'rejects a database backup before mutating a site configured for MySQL',
{ tags: [ 'e2e' ], timeout: 300_000 },
async () => {
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
);
}
);
} );
19 changes: 19 additions & 0 deletions apps/cli/lib/import-export/import/importers/importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
}
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/lib/sqlite-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Expand Down
Loading