Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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