From 4b5c9a887a6a009ba82aeb153bafe756fafb7abc Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Mon, 2 Sep 2024 07:30:44 -0700 Subject: [PATCH 01/24] Add support for importing `.wpress` files --- src/components/content-tab-import-export.tsx | 2 +- src/components/site-form.tsx | 2 +- src/hooks/use-import-export.tsx | 2 +- .../import/handlers/backup-handler-factory.ts | 8 +- .../import/handlers/backup-handler-wpress.ts | 219 ++++++++++++++++++ .../import-export/import/import-manager.ts | 10 +- .../import/importers/importer.ts | 44 +++- .../import-export/import/validators/index.ts | 1 + .../import/validators/wpress-validator.ts | 55 +++++ 9 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 src/lib/import-export/import/handlers/backup-handler-wpress.ts create mode 100644 src/lib/import-export/import/validators/wpress-validator.ts diff --git a/src/components/content-tab-import-export.tsx b/src/components/content-tab-import-export.tsx index 00db7450e..ec1494c3f 100644 --- a/src/components/content-tab-import-export.tsx +++ b/src/components/content-tab-import-export.tsx @@ -233,7 +233,7 @@ const ImportSite = ( props: { selectedSite: SiteDetails } ) => { className="hidden" type="file" data-testid="backup-file" - accept=".zip,.sql,.tar,.gz" + accept=".zip,.sql,.tar,.gz,.wpress" onChange={ onFileSelected } /> diff --git a/src/components/site-form.tsx b/src/components/site-form.tsx index 8d042c370..c4b2d8352 100644 --- a/src/components/site-form.tsx +++ b/src/components/site-form.tsx @@ -192,7 +192,7 @@ function FormImportComponent( { className="hidden" type="file" data-testid="backup-file" - accept=".zip,.tar,.gz" + accept=".zip,.tar,.gz,.wpress" onChange={ handleFileChange } /> diff --git a/src/hooks/use-import-export.tsx b/src/hooks/use-import-export.tsx index 604e6d2b9..d22cb1c52 100644 --- a/src/hooks/use-import-export.tsx +++ b/src/hooks/use-import-export.tsx @@ -95,7 +95,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode type: 'error', message: __( 'Failed importing site' ), detail: __( - 'An error occurred while importing the site. Verify the file is a valid Jetpack backup, Local, Playground or .sql database file and try again. If this problem persists, please contact support.' + 'An error occurred while importing the site. Verify the file is a valid Jetpack backup, Local, Playground, .wpress, or .sql database file and try again. If this problem persists, please contact support.' ), buttons: [ __( 'OK' ) ], } ); diff --git a/src/lib/import-export/import/handlers/backup-handler-factory.ts b/src/lib/import-export/import/handlers/backup-handler-factory.ts index 420cf3868..af3902a6f 100644 --- a/src/lib/import-export/import/handlers/backup-handler-factory.ts +++ b/src/lib/import-export/import/handlers/backup-handler-factory.ts @@ -2,8 +2,8 @@ import { EventEmitter } from 'events'; import { BackupArchiveInfo } from '../types'; import { BackupHandlerSql } from './backup-handler-sql'; import { BackupHandlerTarGz } from './backup-handler-tar-gz'; +import { BackupHandlerWpress } from './backup-handler-wpress'; import { BackupHandlerZip } from './backup-handler-zip'; - export interface BackupHandler extends Partial< EventEmitter > { listFiles( file: BackupArchiveInfo ): Promise< string[] >; extractFiles( file: BackupArchiveInfo, extractionDirectory: string ): Promise< void >; @@ -54,6 +54,8 @@ export class BackupHandlerFactory { return new BackupHandlerTarGz(); } else if ( this.isSql( file ) ) { return new BackupHandlerSql(); + } else if ( this.isWpress( file ) ) { + return new BackupHandlerWpress(); } } @@ -77,4 +79,8 @@ export class BackupHandlerFactory { this.sqlExtensions.some( ( ext ) => file.path.endsWith( ext ) ) ); } + + private static isWpress( file: BackupArchiveInfo ): boolean { + return file.path.endsWith( '.wpress' ); + } } diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts new file mode 100644 index 000000000..3acfc7bba --- /dev/null +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -0,0 +1,219 @@ +import { EventEmitter } from 'events'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as fse from 'fs-extra'; +import { BackupArchiveInfo } from '../types'; +import { BackupHandler } from './backup-handler-factory'; + +/** + * The .wpress format is a custom archive format used by All-In-One WP Migration. + * It is designed to encapsulate all necessary components of a WordPress site, including the database, + * plugins, themes, uploads, and other wp-content files, into a single file for easy transport and restoration. + * + * The .wpress file is structured as follows: + * 1. Header: Contains metadata about the file, such as the name, size, modification time, and prefix. + * The header is a fixed size of 4377 bytes. + * 2. Data Blocks: The actual content of the files, stored in 512-byte blocks. Each file's data is stored + * sequentially, following its corresponding header. + * 3. End of File Marker: A special marker indicating the end of the archive. This is represented by a + * block of 4377 bytes filled with zeroes. + * + * The .wpress format ensures that all necessary components of a WordPress site are included in the backup, + * making it easy to restore the site to its original state. The format is designed to be efficient and + * easy to parse, allowing for quick extraction and restoration of the site's contents. + */ + +const HEADER_SIZE = 4377; +const HEADER_CHUNK_EOF = Buffer.alloc( HEADER_SIZE ); + +interface Header { + name: string; + size: number; + mTime: string; + prefix: string; +} + +/** + * Reads a string from a buffer at a given start and end position. + * + * @param {Buffer} buffer - The buffer to read from. + * @param {number} start - The start position of the string in the buffer. + * @param {number} end - The end position of the string in the buffer. + * @returns + */ +function readFromBuffer( buffer: Buffer, start: number, end: number ): string { + const _buffer = buffer.subarray( start, end ); + return _buffer.subarray( 0, _buffer.indexOf( 0x00 ) ).toString(); +} + +/** + * Reads the header of a .wpress file. + * + * @param {fs.promises.FileHandle} fd - The file handle to read from. + * @returns {Promise
} - A promise that resolves to the header or null if the end of the file is reached. + */ +async function readHeader( fd: fs.promises.FileHandle ): Promise< Header | null > { + const headerChunk = Buffer.alloc( HEADER_SIZE ); + await fd.read( headerChunk, 0, HEADER_SIZE, null ); + + if ( Buffer.compare( headerChunk, HEADER_CHUNK_EOF ) === 0 ) { + return null; + } + + const name = readFromBuffer( headerChunk, 0, 255 ); + const size = parseInt( readFromBuffer( headerChunk, 255, 269 ), 10 ); + const mTime = readFromBuffer( headerChunk, 269, 281 ); + const prefix = readFromBuffer( headerChunk, 281, HEADER_SIZE ); + + return { + name, + size, + mTime, + prefix, + }; +} + +/** + * Reads a block of data from a .wpress file and writes it to a file. + * + * @param {fs.promises.FileHandle} fd - The file handle to read from. + * @param {Header} header - The header of the file to read. + * @param {string} outputPath - The path to write the file to. + */ +async function readBlockToFile( fd: fs.promises.FileHandle, header: Header, outputPath: string ) { + const outputFilePath = path.join( outputPath, header.prefix, header.name ); + fse.ensureDirSync( path.dirname( outputFilePath ) ); + const outputStream = fs.createWriteStream( outputFilePath ); + + let totalBytesToRead = header.size; + while ( totalBytesToRead > 0 ) { + let bytesToRead = 512; + if ( bytesToRead > totalBytesToRead ) { + bytesToRead = totalBytesToRead; + } + + if ( bytesToRead === 0 ) { + break; + } + + const buffer = Buffer.alloc( bytesToRead ); + const data = await fd.read( buffer, 0, bytesToRead, null ); + outputStream.write( buffer ); + + totalBytesToRead -= data.bytesRead; + } + + outputStream.close(); +} + +export class BackupHandlerWpress extends EventEmitter implements BackupHandler { + private bytesRead: number; + private eof: Buffer; + + constructor() { + super(); + this.bytesRead = 0; + this.eof = Buffer.alloc( HEADER_SIZE, '\0' ); + } + + /** + * Lists all files in a .wpress backup file by reading the headers sequentially. + * + * It opens the .wpress file, reads each header to get the file names, and stores them in an array. + * The function continues reading headers until it reaches the end of the file. + * + * @param {BackupArchiveInfo} file - The backup archive information, including the file path. + * @returns {Promise} - A promise that resolves to an array of file names. + */ + async listFiles( file: BackupArchiveInfo ): Promise< string[] > { + const fileNames: string[] = []; + + if ( ! fs.existsSync( file.path ) ) { + throw new Error( `Input file at location "${ file.path }" could not be found.` ); + } + + const inputFile = await fs.promises.open( file.path, 'r' ); + + // Read all of the headers and file data into memory. + try { + let header; + do { + header = await readHeader( inputFile ); + if ( header ) { + fileNames.push( path.join( header.prefix, header.name ) ); + await inputFile.read( Buffer.alloc( header.size ), 0, header.size, null ); + } + } while ( header ); + } finally { + await inputFile.close(); + } + + return fileNames; + } + + /** + * Extracts files from a .wpress backup file into a specified extraction directory. + * + * @param {BackupArchiveInfo} file - The backup archive information, including the file path. + * @param {string} extractionDirectory - The directory where the files will be extracted. + * @returns {Promise} - A promise that resolves when the extraction is complete. + */ + async extractFiles( file: BackupArchiveInfo, extractionDirectory: string ): Promise< void > { + return new Promise( ( resolve, reject ) => { + ( async () => { + try { + if ( ! fs.existsSync( file.path ) ) { + throw new Error( `Input file at location "${ file.path }" could not be found.` ); + } + + fse.emptyDirSync( extractionDirectory ); + + const inputFile = await fs.promises.open( file.path, 'r' ); + + let offset = 0; + + let header; + while ( ( header = await readHeader( inputFile ) ) !== null ) { + if ( ! header ) { + break; + } + + await readBlockToFile( inputFile, header, extractionDirectory ); + offset = offset + HEADER_SIZE + header.size; + } + + await inputFile.close(); + resolve(); + } catch ( err ) { + reject( err ); + } + } )(); + } ); + } + + /** + * Checks if the provided file is a valid backup file. + * + * A valid backup file should have a header size of 4377 bytes and the last 4377 bytes should be 0. + * + * @param {BackupArchiveInfo} file - The backup archive information, including the file path. + * @returns {boolean} - True if the file is valid, otherwise false. + */ + isValid( file: BackupArchiveInfo ): boolean { + const fd = fs.openSync( file.path, 'r' ); + const fileSize = fs.fstatSync( fd ).size; + + if ( fs.readSync( fd, this.eof, 0, HEADER_SIZE, fileSize - HEADER_SIZE ) !== HEADER_SIZE ) { + fs.closeSync( fd ); + return false; + } + + if ( this.eof.toString() !== Buffer.alloc( HEADER_SIZE, '\0' ).toString() ) { + fs.closeSync( fd ); + return false; + } + + fs.closeSync( fd ); + return true; + } +} diff --git a/src/lib/import-export/import/import-manager.ts b/src/lib/import-export/import/import-manager.ts index 92c1c7f77..f17c4f125 100644 --- a/src/lib/import-export/import/import-manager.ts +++ b/src/lib/import-export/import/import-manager.ts @@ -11,9 +11,16 @@ import { LocalImporter, PlaygroundImporter, SQLImporter, + WpressImporter, } from './importers/importer'; import { BackupArchiveInfo, NewImporter } from './types'; -import { JetpackValidator, SqlValidator, LocalValidator, PlaygroundValidator } from './validators'; +import { + JetpackValidator, + SqlValidator, + LocalValidator, + PlaygroundValidator, + WpressValidator, +} from './validators'; import { Validator } from './validators/validator'; export interface ImporterOption { @@ -75,4 +82,5 @@ export const defaultImporterOptions: ImporterOption[] = [ { validator: new LocalValidator(), importer: LocalImporter }, { validator: new SqlValidator(), importer: SQLImporter }, { validator: new PlaygroundValidator(), importer: PlaygroundImporter }, + { validator: new WpressValidator(), importer: WpressImporter }, ]; diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index fffb1db0e..b10d4769f 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -1,10 +1,12 @@ -import { shell } from 'electron'; import { EventEmitter } from 'events'; -import fs from 'fs'; +import fs, { createReadStream, createWriteStream } from 'fs'; import fsPromises from 'fs/promises'; import path from 'path'; +import { createInterface } from 'readline'; import { lstat, move } from 'fs-extra'; import semver from 'semver'; +import { shell } from 'electron'; + import { DEFAULT_PHP_VERSION } from '../../../../../vendor/wp-now/src/constants'; import { SiteServer } from '../../../../site-server'; import { generateBackupFilename } from '../../export/generate-backup-filename'; @@ -49,6 +51,7 @@ abstract class BaseImporter extends EventEmitter implements Importer { try { await move( sqlFile, tmpPath ); + await this.prepareSqlFile( tmpPath ); const { stderr, exitCode } = await server.executeWpCliCommand( `sqlite import ${ sqlTempFile } --require=/tmp/sqlite-command/command.php` ); @@ -69,6 +72,10 @@ abstract class BaseImporter extends EventEmitter implements Importer { this.emit( ImportEvents.IMPORT_DATABASE_COMPLETE ); } + protected async prepareSqlFile( _tmpPath: string ): Promise< void > { + // This method can be overridden by subclasses to prepare the SQL file before import. + } + protected async replaceSiteUrl( siteId: string ) { const server = SiteServer.get( siteId ); if ( ! server ) { @@ -284,3 +291,36 @@ export class SQLImporter extends BaseImporter { } } } + +export class WpressImporter extends BaseBackupImporter { + protected async parseMetaFile(): Promise< MetaFileData | undefined > { + return undefined; + } + + protected async prepareSqlFile( tmpPath: string ): Promise< void > { + const tempOutputPath = `${ tmpPath }.tmp`; + const readStream = createReadStream( tmpPath, 'utf8' ); + const writeStream = createWriteStream( tempOutputPath, 'utf8' ); + + const rl = createInterface( { + input: readStream, + crlfDelay: Infinity, + } ); + + rl.on( 'line', ( line: string ) => { + writeStream.write( line.replace( /SERVMASK_PREFIX/g, 'wp' ) + '\n' ); + } ); + + await new Promise( ( resolve, reject ) => { + rl.on( 'close', resolve ); + rl.on( 'error', reject ); + } ); + + await new Promise( ( resolve, reject ) => { + writeStream.end( resolve ); + writeStream.on( 'error', reject ); + } ); + + await fsPromises.rename( tempOutputPath, tmpPath ); + } +} diff --git a/src/lib/import-export/import/validators/index.ts b/src/lib/import-export/import/validators/index.ts index 1fe38fde9..d1f3a3d83 100644 --- a/src/lib/import-export/import/validators/index.ts +++ b/src/lib/import-export/import/validators/index.ts @@ -3,3 +3,4 @@ export * from './sql-validator'; export * from './jetpack-validator'; export * from './local-validator'; export * from './playground-validator'; +export * from './wpress-validator'; diff --git a/src/lib/import-export/import/validators/wpress-validator.ts b/src/lib/import-export/import/validators/wpress-validator.ts new file mode 100644 index 000000000..10d691159 --- /dev/null +++ b/src/lib/import-export/import/validators/wpress-validator.ts @@ -0,0 +1,55 @@ +import { EventEmitter } from 'events'; +import path from 'path'; +import { ImportEvents } from '../events'; +import { BackupContents } from '../types'; +import { Validator } from './validator'; + +export class WpressValidator extends EventEmitter implements Validator { + canHandle( fileList: string[] ): boolean { + const requiredFiles = [ 'database.sql' ]; + const optionalDirs = [ 'uploads', 'plugins', 'themes' ]; + return ( + requiredFiles.every( ( file ) => fileList.includes( file ) ) && + fileList.some( ( file ) => optionalDirs.some( ( dir ) => file.startsWith( dir + '/' ) ) ) + ); + } + + parseBackupContents( fileList: string[], extractionDirectory: string ): BackupContents { + this.emit( ImportEvents.IMPORT_VALIDATION_START ); + const extractedBackup: BackupContents = { + extractionDirectory: extractionDirectory, + sqlFiles: [], + wpConfig: '', + wpContent: { + uploads: [], + plugins: [], + themes: [], + }, + wpContentDirectory: '', + }; + /* File rules: + * - Accept .wpress + * - Must include database.sql in the root + * - Support optional directories: uploads, plugins, themes, mu-plugins + * */ + + for ( const file of fileList ) { + const fullPath = path.join( extractionDirectory, file ); + if ( file === 'database.sql' ) { + extractedBackup.sqlFiles.push( fullPath ); + } else if ( file.startsWith( 'uploads/' ) ) { + extractedBackup.wpContent.uploads.push( fullPath ); + } else if ( file.startsWith( 'plugins/' ) ) { + extractedBackup.wpContent.plugins.push( fullPath ); + } else if ( file.startsWith( 'themes/' ) ) { + extractedBackup.wpContent.themes.push( fullPath ); + } + } + extractedBackup.sqlFiles.sort( ( a: string, b: string ) => + path.basename( a ).localeCompare( path.basename( b ) ) + ); + + this.emit( ImportEvents.IMPORT_VALIDATION_COMPLETE ); + return extractedBackup; + } +} From 0f5be5cfbab136b4a6148385e61a7a25524d5459 Mon Sep 17 00:00:00 2001 From: Daniel Bachhuber Date: Mon, 2 Sep 2024 07:35:05 -0700 Subject: [PATCH 02/24] Fix linting errors --- src/lib/import-export/import/importers/importer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index b10d4769f..3fde7cdeb 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -1,3 +1,4 @@ +import { shell } from 'electron'; import { EventEmitter } from 'events'; import fs, { createReadStream, createWriteStream } from 'fs'; import fsPromises from 'fs/promises'; @@ -5,8 +6,6 @@ import path from 'path'; import { createInterface } from 'readline'; import { lstat, move } from 'fs-extra'; import semver from 'semver'; -import { shell } from 'electron'; - import { DEFAULT_PHP_VERSION } from '../../../../../vendor/wp-now/src/constants'; import { SiteServer } from '../../../../site-server'; import { generateBackupFilename } from '../../export/generate-backup-filename'; From a6182b6a01e48885680f4c6500fb25fdb9e2a1c8 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Tue, 24 Sep 2024 23:18:13 +0100 Subject: [PATCH 03/24] Update theme and childtheme from wpress package json --- .../import/importers/importer.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index 41a489041..425528e8e 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -346,4 +346,50 @@ export class WpressImporter extends BaseBackupImporter { await fsPromises.rename( tempOutputPath, tmpPath ); } + + protected async parseWpressPackage(): Promise< { + template: string; + stylesheet: string; + } > { + const packageJsonPath = path.join( this.backup.extractionDirectory, 'package.json' ); + try { + const packageContent = await fsPromises.readFile( packageJsonPath, 'utf8' ); + const { Template: template = '', Stylesheet: stylesheet = '' } = JSON.parse( packageContent ); + return { template, stylesheet }; + } catch ( error ) { + console.error( 'Error reading package.json:', error ); + return { template: '', stylesheet: '' }; + } + } + + protected async addSqlToSetTheme( sqlFiles: string[] ): Promise< void > { + const { template, stylesheet } = await this.parseWpressPackage(); + if ( ! template || ! stylesheet ) { + return; + } + + const themeUpdateSql = ` + UPDATE wp_options SET option_value = '${ template }' WHERE option_name = 'template'; + UPDATE wp_options SET option_value = '${ stylesheet }' WHERE option_name = 'stylesheet'; + `; + const sqliteSetThemePath = path.join( + this.backup.extractionDirectory, + 'studio-wpress-theme.sql' + ); + await fsPromises.writeFile( sqliteSetThemePath, themeUpdateSql ); + sqlFiles.push( sqliteSetThemePath ); + } + + protected async importDatabase( + rootPath: string, + siteId: string, + sqlFiles: string[] + ): Promise< void > { + const server = SiteServer.get( siteId ); + if ( ! server ) { + throw new Error( 'Site not found.' ); + } + await this.addSqlToSetTheme( sqlFiles ); + await super.importDatabase( rootPath, siteId, sqlFiles ); + } } From 8e7300d5a24558dcaf85b110efc03b3653c5a472 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Mon, 30 Sep 2024 20:32:33 +0100 Subject: [PATCH 04/24] Remove optional parameter on fd.read --- .../import-export/import/handlers/backup-handler-wpress.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index 3acfc7bba..8347e5bf9 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -54,7 +54,7 @@ function readFromBuffer( buffer: Buffer, start: number, end: number ): string { */ async function readHeader( fd: fs.promises.FileHandle ): Promise< Header | null > { const headerChunk = Buffer.alloc( HEADER_SIZE ); - await fd.read( headerChunk, 0, HEADER_SIZE, null ); + await fd.read( headerChunk, 0, HEADER_SIZE ); if ( Buffer.compare( headerChunk, HEADER_CHUNK_EOF ) === 0 ) { return null; @@ -97,7 +97,7 @@ async function readBlockToFile( fd: fs.promises.FileHandle, header: Header, outp } const buffer = Buffer.alloc( bytesToRead ); - const data = await fd.read( buffer, 0, bytesToRead, null ); + const data = await fd.read( buffer, 0, bytesToRead ); outputStream.write( buffer ); totalBytesToRead -= data.bytesRead; From 66dceeb4ccb29fd420a149042a06f9b6bc4c6393 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Mon, 30 Sep 2024 20:47:34 +0100 Subject: [PATCH 05/24] add returns comment --- src/lib/import-export/import/handlers/backup-handler-wpress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index 8347e5bf9..94f31f0f5 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -39,7 +39,7 @@ interface Header { * @param {Buffer} buffer - The buffer to read from. * @param {number} start - The start position of the string in the buffer. * @param {number} end - The end position of the string in the buffer. - * @returns + * @returns {string} - The substring buffer, stopping at a null-terminator if present. */ function readFromBuffer( buffer: Buffer, start: number, end: number ): string { const _buffer = buffer.subarray( start, end ); From 8bc06a88d8eb8ca8d725b3d99fbb6ceb146cdb8f Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Mon, 30 Sep 2024 20:56:56 +0100 Subject: [PATCH 06/24] Move chunk size to read as a constant --- src/lib/import-export/import/handlers/backup-handler-wpress.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index 94f31f0f5..356e49fd9 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -25,6 +25,7 @@ import { BackupHandler } from './backup-handler-factory'; const HEADER_SIZE = 4377; const HEADER_CHUNK_EOF = Buffer.alloc( HEADER_SIZE ); +const CHUNK_SIZE_TO_READ = 1024; interface Header { name: string; @@ -87,7 +88,7 @@ async function readBlockToFile( fd: fs.promises.FileHandle, header: Header, outp let totalBytesToRead = header.size; while ( totalBytesToRead > 0 ) { - let bytesToRead = 512; + let bytesToRead = CHUNK_SIZE_TO_READ; if ( bytesToRead > totalBytesToRead ) { bytesToRead = totalBytesToRead; } From 2dda7dfa873471169e55e6d06c147144d7cccdea Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Mon, 30 Sep 2024 20:59:03 +0100 Subject: [PATCH 07/24] use async ensureDir --- src/lib/import-export/import/handlers/backup-handler-wpress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index 356e49fd9..1ad019664 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -83,7 +83,7 @@ async function readHeader( fd: fs.promises.FileHandle ): Promise< Header | null */ async function readBlockToFile( fd: fs.promises.FileHandle, header: Header, outputPath: string ) { const outputFilePath = path.join( outputPath, header.prefix, header.name ); - fse.ensureDirSync( path.dirname( outputFilePath ) ); + await fse.ensureDir( path.dirname( outputFilePath ) ); const outputStream = fs.createWriteStream( outputFilePath ); let totalBytesToRead = header.size; From 34d773c6e3d17b21f9ff7d36d0a607751e6bbceb Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Mon, 30 Sep 2024 21:13:00 +0100 Subject: [PATCH 08/24] Use buffer compare --- src/lib/import-export/import/handlers/backup-handler-wpress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index 1ad019664..b8c335376 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -209,7 +209,7 @@ export class BackupHandlerWpress extends EventEmitter implements BackupHandler { return false; } - if ( this.eof.toString() !== Buffer.alloc( HEADER_SIZE, '\0' ).toString() ) { + if ( Buffer.compare( this.eof, Buffer.alloc( HEADER_SIZE, '\0' ) ) !== 0 ) { fs.closeSync( fd ); return false; } From 4a08615e713a1d442bfa2e11c977f4f673d9f1a0 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Mon, 30 Sep 2024 22:16:09 +0100 Subject: [PATCH 09/24] use parse meta file instead of custom method --- .../import/importers/importer.ts | 40 ++++++++----------- src/lib/import-export/import/types.ts | 6 ++- .../import/validators/wpress-validator.ts | 2 + 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index 231dc1646..d398f1067 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -21,6 +21,8 @@ export interface Importer extends Partial< EventEmitter > { } abstract class BaseImporter extends EventEmitter implements Importer { + protected meta?: MetaFileData; + constructor( protected backup: BackupContents ) { super(); } @@ -125,17 +127,16 @@ abstract class BaseBackupImporter extends BaseImporter { try { const databaseDir = path.join( rootPath, 'wp-content', 'database' ); const dbPath = path.join( databaseDir, '.ht.sqlite' ); - await this.moveExistingDatabaseToTrash( dbPath ); await this.moveExistingWpContentToTrash( rootPath ); await this.createEmptyDatabase( dbPath ); await this.importWpConfig( rootPath ); await this.importWpContent( rootPath ); - await this.importDatabase( rootPath, siteId, this.backup.sqlFiles ); - let meta: MetaFileData | undefined; if ( this.backup.metaFile ) { - meta = await this.parseMetaFile(); + this.meta = await this.parseMetaFile(); + console.log( '-----_>', { meta: this.meta } ); } + await this.importDatabase( rootPath, siteId, this.backup.sqlFiles ); this.emit( ImportEvents.IMPORT_COMPLETE ); return { @@ -144,7 +145,7 @@ abstract class BaseBackupImporter extends BaseImporter { wpContent: this.backup.wpContent, wpContentDirectory: this.backup.wpContentDirectory, wpConfig: this.backup.wpConfig, - meta, + meta: this.meta, }; } catch ( error ) { this.emit( ImportEvents.IMPORT_ERROR, error ); @@ -318,8 +319,16 @@ export class SQLImporter extends BaseImporter { } export class WpressImporter extends BaseBackupImporter { - protected async parseMetaFile(): Promise< MetaFileData | undefined > { - return undefined; + protected async parseMetaFile(): Promise< Pick< MetaFileData, 'template' | 'stylesheet' > > { + const packageJsonPath = path.join( this.backup.extractionDirectory, 'package.json' ); + try { + const packageContent = await fsPromises.readFile( packageJsonPath, 'utf8' ); + const { Template: template = '', Stylesheet: stylesheet = '' } = JSON.parse( packageContent ); + return { template, stylesheet }; + } catch ( error ) { + console.error( 'Error reading package.json:', error ); + return { template: '', stylesheet: '' }; + } } protected async prepareSqlFile( tmpPath: string ): Promise< void > { @@ -349,23 +358,8 @@ export class WpressImporter extends BaseBackupImporter { await fsPromises.rename( tempOutputPath, tmpPath ); } - protected async parseWpressPackage(): Promise< { - template: string; - stylesheet: string; - } > { - const packageJsonPath = path.join( this.backup.extractionDirectory, 'package.json' ); - try { - const packageContent = await fsPromises.readFile( packageJsonPath, 'utf8' ); - const { Template: template = '', Stylesheet: stylesheet = '' } = JSON.parse( packageContent ); - return { template, stylesheet }; - } catch ( error ) { - console.error( 'Error reading package.json:', error ); - return { template: '', stylesheet: '' }; - } - } - protected async addSqlToSetTheme( sqlFiles: string[] ): Promise< void > { - const { template, stylesheet } = await this.parseWpressPackage(); + const { template, stylesheet } = this.meta || {}; if ( ! template || ! stylesheet ) { return; } diff --git a/src/lib/import-export/import/types.ts b/src/lib/import-export/import/types.ts index 676dd535c..aa0383a55 100644 --- a/src/lib/import-export/import/types.ts +++ b/src/lib/import-export/import/types.ts @@ -1,8 +1,10 @@ import { Importer } from './importers'; export interface MetaFileData { - phpVersion: string; - wordpressVersion: string; + phpVersion?: string; + wordpressVersion?: string; + template?: string; + stylesheet?: string; } export interface WpContent { diff --git a/src/lib/import-export/import/validators/wpress-validator.ts b/src/lib/import-export/import/validators/wpress-validator.ts index 10d691159..67c930720 100644 --- a/src/lib/import-export/import/validators/wpress-validator.ts +++ b/src/lib/import-export/import/validators/wpress-validator.ts @@ -43,6 +43,8 @@ export class WpressValidator extends EventEmitter implements Validator { extractedBackup.wpContent.plugins.push( fullPath ); } else if ( file.startsWith( 'themes/' ) ) { extractedBackup.wpContent.themes.push( fullPath ); + } else if ( file === 'package.json' ) { + extractedBackup.metaFile = fullPath; } } extractedBackup.sqlFiles.sort( ( a: string, b: string ) => From e16f25069f0aaa8f37778da22012bc0a3ccc78aa Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Mon, 30 Sep 2024 22:24:05 +0100 Subject: [PATCH 10/24] remove unused offset --- src/lib/import-export/import/handlers/backup-handler-wpress.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index b8c335376..39f14a931 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -171,8 +171,6 @@ export class BackupHandlerWpress extends EventEmitter implements BackupHandler { const inputFile = await fs.promises.open( file.path, 'r' ); - let offset = 0; - let header; while ( ( header = await readHeader( inputFile ) ) !== null ) { if ( ! header ) { @@ -180,7 +178,6 @@ export class BackupHandlerWpress extends EventEmitter implements BackupHandler { } await readBlockToFile( inputFile, header, extractionDirectory ); - offset = offset + HEADER_SIZE + header.size; } await inputFile.close(); From a2aad3cf837b90c33c480486b7c559ecc61c796c Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Mon, 30 Sep 2024 22:26:13 +0100 Subject: [PATCH 11/24] remove console log --- src/lib/import-export/import/importers/importer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index d398f1067..f536e26e2 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -134,7 +134,6 @@ abstract class BaseBackupImporter extends BaseImporter { await this.importWpContent( rootPath ); if ( this.backup.metaFile ) { this.meta = await this.parseMetaFile(); - console.log( '-----_>', { meta: this.meta } ); } await this.importDatabase( rootPath, siteId, this.backup.sqlFiles ); From 876bcf235b6143bdea94b42de320bd8bdc632308 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Tue, 1 Oct 2024 19:28:39 +0100 Subject: [PATCH 12/24] Refactor file types to use them globally --- src/components/content-tab-import-export.tsx | 4 ++-- src/components/site-form.tsx | 4 ++-- src/constants.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/content-tab-import-export.tsx b/src/components/content-tab-import-export.tsx index aaf03b3b9..665bed2ea 100644 --- a/src/components/content-tab-import-export.tsx +++ b/src/components/content-tab-import-export.tsx @@ -4,7 +4,7 @@ import { sprintf, __ } from '@wordpress/i18n'; import { Icon, download } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; import { useRef } from 'react'; -import { STUDIO_DOCS_URL_IMPORT_EXPORT } from '../constants'; +import { ACCEPTED_IMPORT_FILE_TYPES, STUDIO_DOCS_URL_IMPORT_EXPORT } from '../constants'; import { useConfirmationDialog } from '../hooks/use-confirmation-dialog'; import { useDragAndDropFile } from '../hooks/use-drag-and-drop-file'; import { useImportExport } from '../hooks/use-import-export'; @@ -233,7 +233,7 @@ const ImportSite = ( props: { selectedSite: SiteDetails } ) => { className="hidden" type="file" data-testid="backup-file" - accept=".zip,.sql,.tar,.gz,.wpress" + accept={ `${ ACCEPTED_IMPORT_FILE_TYPES.join( ',' ) },.sql` } onChange={ onFileSelected } /> diff --git a/src/components/site-form.tsx b/src/components/site-form.tsx index 6bb5972ac..aad06e0ef 100644 --- a/src/components/site-form.tsx +++ b/src/components/site-form.tsx @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import { tip, warning, trash, chevronRight, chevronDown, chevronLeft } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; import { FormEvent, useRef, useState } from 'react'; -import { STUDIO_DOCS_URL_IMPORT_EXPORT } from '../constants'; +import { ACCEPTED_IMPORT_FILE_TYPES, STUDIO_DOCS_URL_IMPORT_EXPORT } from '../constants'; import { cx } from '../lib/cx'; import { getIpcApi } from '../lib/get-ipc-api'; import Button from './button'; @@ -192,7 +192,7 @@ function FormImportComponent( { className="hidden" type="file" data-testid="backup-file" - accept=".zip,.tar,.gz,.wpress" + accept={ ACCEPTED_IMPORT_FILE_TYPES.join( ',' ) } onChange={ handleFileChange } /> diff --git a/src/constants.ts b/src/constants.ts index bca455a6b..a286af4dc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,7 +25,7 @@ export const CHAT_MESSAGES_STORE_KEY = 'ai_chat_messages'; //Import file constants -export const ACCEPTED_IMPORT_FILE_TYPES = [ '.zip', '.gz', '.gzip', '.tar', '.tar.gz' ]; +export const ACCEPTED_IMPORT_FILE_TYPES = [ '.zip', '.gz', '.gzip', '.tar', '.tar.gz', '.wpress' ]; // OAuth constants export const CLIENT_ID = '95109'; From 952f8d6f71467659ec125fb0be5bad4d1125876e Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Tue, 1 Oct 2024 20:18:10 +0100 Subject: [PATCH 13/24] create SQL to auto activate plugins that were installed --- .../import/importers/importer.ts | 39 +++++++++++++++++-- src/lib/import-export/import/types.ts | 1 + 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index f536e26e2..1c5c80204 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -318,15 +318,19 @@ export class SQLImporter extends BaseImporter { } export class WpressImporter extends BaseBackupImporter { - protected async parseMetaFile(): Promise< Pick< MetaFileData, 'template' | 'stylesheet' > > { + protected async parseMetaFile(): Promise< MetaFileData > { const packageJsonPath = path.join( this.backup.extractionDirectory, 'package.json' ); try { const packageContent = await fsPromises.readFile( packageJsonPath, 'utf8' ); - const { Template: template = '', Stylesheet: stylesheet = '' } = JSON.parse( packageContent ); - return { template, stylesheet }; + const { + Template: template = '', + Stylesheet: stylesheet = '', + Plugins: plugins = [], + } = JSON.parse( packageContent ); + return { template, stylesheet, plugins }; } catch ( error ) { console.error( 'Error reading package.json:', error ); - return { template: '', stylesheet: '' }; + return { template: '', stylesheet: '', plugins: [] }; } } @@ -375,6 +379,32 @@ export class WpressImporter extends BaseBackupImporter { sqlFiles.push( sqliteSetThemePath ); } + protected async addSqlToActivatePlugins( sqlFiles: string[] ): Promise< void > { + const { plugins = [] } = this.meta || {}; + if ( plugins.length === 0 ) { + return; + } + + const serializedPlugins = this.serializePlugins( plugins ); + const activatePluginsSql = ` + INSERT INTO wp_options (option_name, option_value, autoload) VALUES ('active_plugins', '${ serializedPlugins }', 'yes'); + `; + + const sqliteActivatePluginsPath = path.join( + this.backup.extractionDirectory, + 'studio-wpress-activate-plugins.sql' + ); + await fsPromises.writeFile( sqliteActivatePluginsPath, activatePluginsSql ); + sqlFiles.push( sqliteActivatePluginsPath ); + } + + private serializePlugins( plugins: string[] ): string { + const serializedArray = plugins + .map( ( plugin, index ) => `i:${ index };s:${ plugin.length }:"${ plugin }";` ) + .join( '' ); + return `a:${ plugins.length }:{${ serializedArray }}`; + } + protected async importDatabase( rootPath: string, siteId: string, @@ -385,6 +415,7 @@ export class WpressImporter extends BaseBackupImporter { throw new Error( 'Site not found.' ); } await this.addSqlToSetTheme( sqlFiles ); + await this.addSqlToActivatePlugins( sqlFiles ); await super.importDatabase( rootPath, siteId, sqlFiles ); } } diff --git a/src/lib/import-export/import/types.ts b/src/lib/import-export/import/types.ts index aa0383a55..1314f74c6 100644 --- a/src/lib/import-export/import/types.ts +++ b/src/lib/import-export/import/types.ts @@ -5,6 +5,7 @@ export interface MetaFileData { wordpressVersion?: string; template?: string; stylesheet?: string; + plugins?: string[]; } export interface WpContent { From 19aeaf8c44089a88b04b499a37810195631dd107 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Tue, 1 Oct 2024 20:24:19 +0100 Subject: [PATCH 14/24] move serializePlugins to its own file --- src/lib/import-export/import/importers/importer.ts | 10 ++-------- src/lib/serialize-plugins.ts | 6 ++++++ 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 src/lib/serialize-plugins.ts diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index 1c5c80204..6461f55fa 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -8,6 +8,7 @@ import { lstat, move } from 'fs-extra'; import semver from 'semver'; import { DEFAULT_PHP_VERSION } from '../../../../../vendor/wp-now/src/constants'; import { SiteServer } from '../../../../site-server'; +import { serializePlugins } from '../../../serialize-plugins'; import { generateBackupFilename } from '../../export/generate-backup-filename'; import { ImportEvents } from '../events'; import { BackupContents, MetaFileData } from '../types'; @@ -385,7 +386,7 @@ export class WpressImporter extends BaseBackupImporter { return; } - const serializedPlugins = this.serializePlugins( plugins ); + const serializedPlugins = serializePlugins( plugins ); const activatePluginsSql = ` INSERT INTO wp_options (option_name, option_value, autoload) VALUES ('active_plugins', '${ serializedPlugins }', 'yes'); `; @@ -398,13 +399,6 @@ export class WpressImporter extends BaseBackupImporter { sqlFiles.push( sqliteActivatePluginsPath ); } - private serializePlugins( plugins: string[] ): string { - const serializedArray = plugins - .map( ( plugin, index ) => `i:${ index };s:${ plugin.length }:"${ plugin }";` ) - .join( '' ); - return `a:${ plugins.length }:{${ serializedArray }}`; - } - protected async importDatabase( rootPath: string, siteId: string, diff --git a/src/lib/serialize-plugins.ts b/src/lib/serialize-plugins.ts new file mode 100644 index 000000000..bdc266880 --- /dev/null +++ b/src/lib/serialize-plugins.ts @@ -0,0 +1,6 @@ +export function serializePlugins( plugins: string[] ): string { + const serializedArray = plugins + .map( ( plugin, index ) => `i:${ index };s:${ plugin.length }:"${ plugin }";` ) + .join( '' ); + return `a:${ plugins.length }:{${ serializedArray }}`; +} From ce7cbd34b210587ea7159818a561e77dc0befb6a Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Wed, 2 Oct 2024 15:51:58 +0100 Subject: [PATCH 15/24] Fix conflict on option already existing to active the plugins --- src/lib/import-export/import/importers/importer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index 6461f55fa..ba41b953f 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -388,7 +388,8 @@ export class WpressImporter extends BaseBackupImporter { const serializedPlugins = serializePlugins( plugins ); const activatePluginsSql = ` - INSERT INTO wp_options (option_name, option_value, autoload) VALUES ('active_plugins', '${ serializedPlugins }', 'yes'); + INSERT INTO wp_options (option_name, option_value, autoload) VALUES ('active_plugins', '${ serializedPlugins }', 'yes') + ON CONFLICT(option_name) DO UPDATE SET option_value = excluded.option_value, autoload = excluded.autoload; `; const sqliteActivatePluginsPath = path.join( @@ -396,6 +397,7 @@ export class WpressImporter extends BaseBackupImporter { 'studio-wpress-activate-plugins.sql' ); await fsPromises.writeFile( sqliteActivatePluginsPath, activatePluginsSql ); + await fsPromises.writeFile( '/tmp/activate-plugins.sql', activatePluginsSql ); sqlFiles.push( sqliteActivatePluginsPath ); } From 11dd3e2c78f60538feffce7378c91825a3cb9078 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Wed, 2 Oct 2024 15:53:30 +0100 Subject: [PATCH 16/24] remove unnecessary debug line to duplicate sql file --- src/lib/import-export/import/importers/importer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/import-export/import/importers/importer.ts b/src/lib/import-export/import/importers/importer.ts index ba41b953f..84e0a71af 100644 --- a/src/lib/import-export/import/importers/importer.ts +++ b/src/lib/import-export/import/importers/importer.ts @@ -397,7 +397,6 @@ export class WpressImporter extends BaseBackupImporter { 'studio-wpress-activate-plugins.sql' ); await fsPromises.writeFile( sqliteActivatePluginsPath, activatePluginsSql ); - await fsPromises.writeFile( '/tmp/activate-plugins.sql', activatePluginsSql ); sqlFiles.push( sqliteActivatePluginsPath ); } From 2514225c45a9c5ff63ef4338f882a9e1c917af54 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Thu, 3 Oct 2024 15:40:00 +0100 Subject: [PATCH 17/24] check if file exists --- .../import/handlers/backup-handler-wpress.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index 39f14a931..dbd37a3cb 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; import * as fs from 'fs'; +import { constants } from 'fs'; import * as path from 'path'; import * as fse from 'fs-extra'; import { BackupArchiveInfo } from '../types'; @@ -129,7 +130,9 @@ export class BackupHandlerWpress extends EventEmitter implements BackupHandler { async listFiles( file: BackupArchiveInfo ): Promise< string[] > { const fileNames: string[] = []; - if ( ! fs.existsSync( file.path ) ) { + try { + await fs.promises.access( file.path, constants.F_OK ); + } catch ( error ) { throw new Error( `Input file at location "${ file.path }" could not be found.` ); } @@ -163,7 +166,9 @@ export class BackupHandlerWpress extends EventEmitter implements BackupHandler { return new Promise( ( resolve, reject ) => { ( async () => { try { - if ( ! fs.existsSync( file.path ) ) { + try { + await fs.promises.access( file.path, constants.F_OK ); + } catch ( error ) { throw new Error( `Input file at location "${ file.path }" could not be found.` ); } From a4eda36efa38dbcd20bb9d80b16d929c9f609942 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Thu, 3 Oct 2024 15:41:08 +0100 Subject: [PATCH 18/24] replace empty dir sync for async version --- src/lib/import-export/import/handlers/backup-handler-wpress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index dbd37a3cb..6f91b9d4a 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -172,7 +172,7 @@ export class BackupHandlerWpress extends EventEmitter implements BackupHandler { throw new Error( `Input file at location "${ file.path }" could not be found.` ); } - fse.emptyDirSync( extractionDirectory ); + await fse.emptyDir( extractionDirectory ); const inputFile = await fs.promises.open( file.path, 'r' ); From 350bfd3b28f5744734caed5fc0f8ee8dc3888e22 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Thu, 3 Oct 2024 15:43:14 +0100 Subject: [PATCH 19/24] simplify construction of extractionDirectory --- src/lib/import-export/import/validators/wpress-validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/import-export/import/validators/wpress-validator.ts b/src/lib/import-export/import/validators/wpress-validator.ts index 67c930720..da1c0febe 100644 --- a/src/lib/import-export/import/validators/wpress-validator.ts +++ b/src/lib/import-export/import/validators/wpress-validator.ts @@ -17,7 +17,7 @@ export class WpressValidator extends EventEmitter implements Validator { parseBackupContents( fileList: string[], extractionDirectory: string ): BackupContents { this.emit( ImportEvents.IMPORT_VALIDATION_START ); const extractedBackup: BackupContents = { - extractionDirectory: extractionDirectory, + extractionDirectory, sqlFiles: [], wpConfig: '', wpContent: { From de7aec87f52e2f8374b777c7b202a3820d18fa1d Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Sun, 6 Oct 2024 23:24:44 +0100 Subject: [PATCH 20/24] update error message --- src/hooks/tests/use-import-export.test.tsx | 2 +- src/hooks/use-import-export.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/tests/use-import-export.test.tsx b/src/hooks/tests/use-import-export.test.tsx index 1fddb3c36..780441695 100644 --- a/src/hooks/tests/use-import-export.test.tsx +++ b/src/hooks/tests/use-import-export.test.tsx @@ -239,7 +239,7 @@ describe( 'useImportExport hook', () => { expect( getIpcApi().showErrorMessageBox ).toHaveBeenCalledWith( { title: 'Failed importing site', message: - 'An error occurred while importing the site. Verify the file is a valid Jetpack backup, Local, Playground or .sql database file and try again. If this problem persists, please contact support.', + 'An error occurred while importing the site. Verify the file is a valid Jetpack backup, Local, Playground, .wpress or .sql database file and try again. If this problem persists, please contact support.', error: 'error', } ); } ); diff --git a/src/hooks/use-import-export.tsx b/src/hooks/use-import-export.tsx index 78acc990d..131faf586 100644 --- a/src/hooks/use-import-export.tsx +++ b/src/hooks/use-import-export.tsx @@ -94,7 +94,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode await getIpcApi().showErrorMessageBox( { title: __( 'Failed importing site' ), message: __( - 'An error occurred while importing the site. Verify the file is a valid Jetpack backup, Local, Playground or .sql database file and try again. If this problem persists, please contact support.' + 'An error occurred while importing the site. Verify the file is a valid Jetpack backup, Local, Playground, .wpress or .sql database file and try again. If this problem persists, please contact support.' ), error, } ); From 4b0637094f52e1914f90bddeb07b9a9d35f68310 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Sun, 6 Oct 2024 23:29:49 +0100 Subject: [PATCH 21/24] remove unnecessary function --- .../import/handlers/backup-handler-wpress.ts | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/src/lib/import-export/import/handlers/backup-handler-wpress.ts b/src/lib/import-export/import/handlers/backup-handler-wpress.ts index 6f91b9d4a..1287b08ba 100644 --- a/src/lib/import-export/import/handlers/backup-handler-wpress.ts +++ b/src/lib/import-export/import/handlers/backup-handler-wpress.ts @@ -193,30 +193,4 @@ export class BackupHandlerWpress extends EventEmitter implements BackupHandler { } )(); } ); } - - /** - * Checks if the provided file is a valid backup file. - * - * A valid backup file should have a header size of 4377 bytes and the last 4377 bytes should be 0. - * - * @param {BackupArchiveInfo} file - The backup archive information, including the file path. - * @returns {boolean} - True if the file is valid, otherwise false. - */ - isValid( file: BackupArchiveInfo ): boolean { - const fd = fs.openSync( file.path, 'r' ); - const fileSize = fs.fstatSync( fd ).size; - - if ( fs.readSync( fd, this.eof, 0, HEADER_SIZE, fileSize - HEADER_SIZE ) !== HEADER_SIZE ) { - fs.closeSync( fd ); - return false; - } - - if ( Buffer.compare( this.eof, Buffer.alloc( HEADER_SIZE, '\0' ) ) !== 0 ) { - fs.closeSync( fd ); - return false; - } - - fs.closeSync( fd ); - return true; - } } From 2707dddcd426864320d2f4548b8ca9184922ae53 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Sun, 6 Oct 2024 23:39:02 +0100 Subject: [PATCH 22/24] add test for serialize plugins function --- src/lib/tests/serialize-plugins.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/lib/tests/serialize-plugins.test.ts diff --git a/src/lib/tests/serialize-plugins.test.ts b/src/lib/tests/serialize-plugins.test.ts new file mode 100644 index 000000000..653639099 --- /dev/null +++ b/src/lib/tests/serialize-plugins.test.ts @@ -0,0 +1,21 @@ +import { serializePlugins } from '../serialize-plugins'; + +describe( 'serializePlugins', () => { + it( 'should correctly serialize an empty array', () => { + const result = serializePlugins( [] ); + expect( result ).toBe( 'a:0:{}' ); + } ); + + it( 'should correctly serialize an array with one plugin', () => { + const result = serializePlugins( [ 'hello-dolly' ] ); + expect( result ).toBe( 'a:1:{i:0;s:11:"hello-dolly";}' ); + } ); + + it( 'should correctly serialize an array with multiple plugins', () => { + const plugins = [ 'akismet', 'jetpack', 'woocommerce', 'classc-editor' ]; + const result = serializePlugins( plugins ); + expect( result ).toBe( + 'a:4:{i:0;s:7:"akismet";i:1;s:7:"jetpack";i:2;s:11:"woocommerce";i:3;s:13:"classc-editor";}' + ); + } ); +} ); From c5ee97fbea9a92f6ef8d5dcc4c8e8219157f91f6 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Sun, 6 Oct 2024 23:47:43 +0100 Subject: [PATCH 23/24] add tests for wpress validator --- .../validators/wpress-validator.test.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/lib/import-export/tests/import/validators/wpress-validator.test.ts diff --git a/src/lib/import-export/tests/import/validators/wpress-validator.test.ts b/src/lib/import-export/tests/import/validators/wpress-validator.test.ts new file mode 100644 index 000000000..d9f570a5c --- /dev/null +++ b/src/lib/import-export/tests/import/validators/wpress-validator.test.ts @@ -0,0 +1,75 @@ +import path from 'path'; +import { ImportEvents } from '../../../import/events'; +import { WpressValidator } from '../../../import/validators/wpress-validator'; + +describe( 'WpressValidator', () => { + let validator: WpressValidator; + + beforeEach( () => { + validator = new WpressValidator(); + } ); + + describe( 'canHandle', () => { + it( 'should return true for valid wpress file structure', () => { + const fileList = [ + 'database.sql', + 'uploads/image.jpg', + 'plugins/some-plugin/plugin.php', + 'themes/some-theme/style.css', + ]; + expect( validator.canHandle( fileList ) ).toBe( true ); + } ); + + it( 'should return false if database.sql is missing', () => { + const fileList = [ + 'uploads/image.jpg', + 'plugins/some-plugin/plugin.php', + 'themes/some-theme/style.css', + ]; + expect( validator.canHandle( fileList ) ).toBe( false ); + } ); + + it( 'should return false if no optional directories are present', () => { + const fileList = [ 'database.sql', 'some-other-file.txt' ]; + expect( validator.canHandle( fileList ) ).toBe( false ); + } ); + } ); + + describe( 'parseBackupContents', () => { + const extractionDirectory = '/path/to/extraction'; + const fileList = [ + 'database.sql', + 'uploads/image.jpg', + 'plugins/some-plugin/plugin.php', + 'themes/some-theme/style.css', + 'package.json', + ]; + + it( 'should correctly parse backup contents', () => { + const result = validator.parseBackupContents( fileList, extractionDirectory ); + + expect( result.extractionDirectory ).toBe( extractionDirectory ); + expect( result.sqlFiles ).toEqual( [ path.join( extractionDirectory, 'database.sql' ) ] ); + expect( result.wpContent.uploads ).toEqual( [ + path.join( extractionDirectory, 'uploads/image.jpg' ), + ] ); + expect( result.wpContent.plugins ).toEqual( [ + path.join( extractionDirectory, 'plugins/some-plugin/plugin.php' ), + ] ); + expect( result.wpContent.themes ).toEqual( [ + path.join( extractionDirectory, 'themes/some-theme/style.css' ), + ] ); + expect( result.metaFile ).toBe( path.join( extractionDirectory, 'package.json' ) ); + } ); + + it( 'should emit validation events', () => { + const startSpy = jest.spyOn( validator, 'emit' ); + const completeSpy = jest.spyOn( validator, 'emit' ); + + validator.parseBackupContents( fileList, extractionDirectory ); + + expect( startSpy ).toHaveBeenCalledWith( ImportEvents.IMPORT_VALIDATION_START ); + expect( completeSpy ).toHaveBeenCalledWith( ImportEvents.IMPORT_VALIDATION_COMPLETE ); + } ); + } ); +} ); From ae6e28cd007bd8ab72de96ce96238cc79f2a5126 Mon Sep 17 00:00:00 2001 From: Antonio Sejas Date: Sun, 6 Oct 2024 23:48:51 +0100 Subject: [PATCH 24/24] add package.json as required file --- .../import/validators/wpress-validator.ts | 2 +- .../import/validators/wpress-validator.test.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/lib/import-export/import/validators/wpress-validator.ts b/src/lib/import-export/import/validators/wpress-validator.ts index da1c0febe..7f99c1131 100644 --- a/src/lib/import-export/import/validators/wpress-validator.ts +++ b/src/lib/import-export/import/validators/wpress-validator.ts @@ -6,7 +6,7 @@ import { Validator } from './validator'; export class WpressValidator extends EventEmitter implements Validator { canHandle( fileList: string[] ): boolean { - const requiredFiles = [ 'database.sql' ]; + const requiredFiles = [ 'database.sql', 'package.json' ]; const optionalDirs = [ 'uploads', 'plugins', 'themes' ]; return ( requiredFiles.every( ( file ) => fileList.includes( file ) ) && diff --git a/src/lib/import-export/tests/import/validators/wpress-validator.test.ts b/src/lib/import-export/tests/import/validators/wpress-validator.test.ts index d9f570a5c..c9381e4d5 100644 --- a/src/lib/import-export/tests/import/validators/wpress-validator.test.ts +++ b/src/lib/import-export/tests/import/validators/wpress-validator.test.ts @@ -13,6 +13,7 @@ describe( 'WpressValidator', () => { it( 'should return true for valid wpress file structure', () => { const fileList = [ 'database.sql', + 'package.json', 'uploads/image.jpg', 'plugins/some-plugin/plugin.php', 'themes/some-theme/style.css', @@ -22,6 +23,17 @@ describe( 'WpressValidator', () => { it( 'should return false if database.sql is missing', () => { const fileList = [ + 'package.json', + 'uploads/image.jpg', + 'plugins/some-plugin/plugin.php', + 'themes/some-theme/style.css', + ]; + expect( validator.canHandle( fileList ) ).toBe( false ); + } ); + + it( 'should return false if package.json is missing', () => { + const fileList = [ + 'database.sql', 'uploads/image.jpg', 'plugins/some-plugin/plugin.php', 'themes/some-theme/style.css', @@ -30,7 +42,7 @@ describe( 'WpressValidator', () => { } ); it( 'should return false if no optional directories are present', () => { - const fileList = [ 'database.sql', 'some-other-file.txt' ]; + const fileList = [ 'database.sql', 'package.json', 'some-other-file.txt' ]; expect( validator.canHandle( fileList ) ).toBe( false ); } ); } );