diff --git a/src/lib/import-export/export/exporters/default-exporter.ts b/src/lib/import-export/export/exporters/default-exporter.ts index 4b3cef4151..8bdfc70fe3 100644 --- a/src/lib/import-export/export/exporters/default-exporter.ts +++ b/src/lib/import-export/export/exporters/default-exporter.ts @@ -27,7 +27,6 @@ export class DefaultExporter extends EventEmitter implements Exporter { private archive!: archiver.Archiver; private backup: BackupContents; private readonly options: ExportOptions; - private siteFiles: string[]; private readonly pathsToExclude = [ 'wp-content/mu-plugins/sqlite-database-integration', 'wp-content/mu-plugins/0-allowed-redirect-hosts.php', @@ -43,21 +42,12 @@ export class DefaultExporter extends EventEmitter implements Exporter { constructor( options: ExportOptions ) { super(); this.options = options; - this.siteFiles = []; this.backup = { backupFile: options.backupFile, sqlFiles: [], - wpContent: { - uploads: [], - plugins: [], - themes: [], - muPlugins: [], - fonts: [], - }, }; } async canHandle(): Promise< boolean > { - // Check for supported extension const supportedExtension = [ 'tar.gz', 'tzg', 'zip' ].find( ( ext ) => this.options.backupFile.endsWith( ext ) ); @@ -67,30 +57,32 @@ export class DefaultExporter extends EventEmitter implements Exporter { } const requiredPaths = [ - { path: [ 'wp-content' ], isDir: true }, - { path: [ 'wp-includes' ], isDir: true }, + { path: 'wp-content', isDir: true }, + { path: 'wp-includes', isDir: true }, { path: 'wp-load.php', isDir: false }, { path: 'wp-config.php', isDir: false }, ]; - this.siteFiles = await this.getSiteFiles(); - - return requiredPaths.every( ( requiredPath ) => - this.siteFiles.some( ( file ) => { - const relativePath = path.relative( this.options.site.path, file ); - const relativePathItems = relativePath.split( path.sep ); - return requiredPath.isDir - ? ( requiredPath.path as string[] ).every( - ( path, index ) => path === relativePathItems[ index ] - ) - : relativePath === requiredPath.path; - } ) - ); + try { + for ( const requiredPath of requiredPaths ) { + const stats = await fsPromises.stat( + path.join( this.options.site.path, requiredPath.path ) + ); + if ( requiredPath.isDir && ! stats.isDirectory() ) { + return false; + } + if ( ! requiredPath.isDir && ! stats.isFile() ) { + return false; + } + } + return true; + } catch ( error ) { + return false; + } } async export(): Promise< void > { this.emit( ExportEvents.EXPORT_START ); - this.backup = await this.getBackupContents(); const output = fs.createWriteStream( this.options.backupFile ); this.archive = this.createArchive(); @@ -151,8 +143,11 @@ export class DefaultExporter extends EventEmitter implements Exporter { } private addWpConfig(): void { - if ( this.backup.wpConfigFile ) { - this.archive.file( this.backup.wpConfigFile, { name: 'wp-config.php' } ); + const wpConfigPath = path.join( this.options.site.path, 'wp-config.php' ); + if ( fs.existsSync( wpConfigPath ) ) { + this.archive.file( wpConfigPath, { + name: 'wp-config.php', + } ); } } @@ -181,13 +176,7 @@ export class DefaultExporter extends EventEmitter implements Exporter { } ); this.emit( ExportEvents.WP_CONTENT_EXPORT_PROGRESS, { directory: absolutePath } ); } - this.emit( ExportEvents.WP_CONTENT_EXPORT_COMPLETE, { - uploads: this.backup.wpContent.uploads.length, - plugins: this.backup.wpContent.plugins.length, - themes: this.backup.wpContent.themes.length, - muPlugins: this.backup.wpContent.muPlugins.length, - fonts: this.backup.wpContent.fonts.length, - } ); + this.emit( ExportEvents.WP_CONTENT_EXPORT_COMPLETE ); } private async addDatabase(): Promise< void > { @@ -223,77 +212,6 @@ export class DefaultExporter extends EventEmitter implements Exporter { } } - private async getSiteFiles(): Promise< string[] > { - if ( this.siteFiles.length ) { - return this.siteFiles; - } - - const directoryContents = await fsPromises.readdir( this.options.site.path, { - recursive: true, - withFileTypes: true, - } ); - - return directoryContents.reduce< string[] >( ( files: string[], directoryContent ) => { - const filePath = path.join( directoryContent.path, directoryContent.name ); - const relativePath = path.relative( this.options.site.path, filePath ); - - // Check for exact path exclusions - const isExcluded = this.pathsToExclude.some( ( pathToExclude ) => - relativePath.startsWith( path.normalize( pathToExclude ) ) - ); - - // Check for node_modules and .git directories anywhere - const isNodeModulesDirectory = relativePath.includes( 'node_modules' ); - const isGitDirectory = relativePath.includes( '.git' ); - - if ( isExcluded || isNodeModulesDirectory || isGitDirectory ) { - return files; - } - if ( directoryContent.isFile() ) { - files.push( filePath ); - } - return files; - }, [] ); - } - - private async getBackupContents(): Promise< BackupContents > { - const options = this.options; - const backupContents: BackupContents = { - backupFile: options.backupFile, - sqlFiles: [], - wpContent: { - uploads: [], - plugins: [], - themes: [], - muPlugins: [], - fonts: [], - }, - }; - - const siteFiles = await this.getSiteFiles(); - siteFiles.forEach( ( file ) => { - const relativePath = path.relative( options.site.path, file ); - const relativePathItems = relativePath.split( path.sep ); - const [ wpContent, wpContentDirectory ] = relativePathItems; - if ( path.basename( file ) === 'wp-config.php' ) { - backupContents.wpConfigFile = file; - } else if ( wpContent === 'wp-content' ) { - if ( - wpContentDirectory === 'uploads' || - wpContentDirectory === 'plugins' || - wpContentDirectory === 'themes' || - wpContentDirectory === 'fonts' - ) { - backupContents.wpContent[ wpContentDirectory as BackupContentsCategory ].push( file ); - } else if ( wpContentDirectory === 'mu-plugins' ) { - backupContents.wpContent.muPlugins.push( file ); - } - } - } ); - - return backupContents; - } - private async createStudioJsonFile(): Promise< string > { const wpVersion = await getWordPressVersionFromInstallation( this.options.site.path ); const studioJson: StudioJson = { diff --git a/src/lib/import-export/export/types.ts b/src/lib/import-export/export/types.ts index f70cc12cf9..37f970942b 100644 --- a/src/lib/import-export/export/types.ts +++ b/src/lib/import-export/export/types.ts @@ -14,8 +14,6 @@ export type ExportOptionsIncludes = BackupContentsCategory | 'database'; export interface BackupContents { backupFile: string; sqlFiles: string[]; - wpContent: { [ index in BackupContentsCategory ]: string[] }; - wpConfigFile?: string; } export type BackupContentsCategory = 'uploads' | 'plugins' | 'themes' | 'muPlugins' | 'fonts'; diff --git a/src/lib/import-export/tests/export/exporters/default-exporter.test.ts b/src/lib/import-export/tests/export/exporters/default-exporter.test.ts index b75d382d4a..0f386a017e 100644 --- a/src/lib/import-export/tests/export/exporters/default-exporter.test.ts +++ b/src/lib/import-export/tests/export/exporters/default-exporter.test.ts @@ -134,17 +134,30 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { ( fsPromises.readdir as jest.Mock ).mockResolvedValue( mockFiles ); + // Mock fsPromises.stat for canHandle method + ( fsPromises.stat as jest.Mock ).mockImplementation( async ( filePath: string ) => { + const normalizedPath = normalize( filePath ); + if ( normalizedPath.endsWith( 'wp-content' ) || normalizedPath.endsWith( 'wp-includes' ) ) { + return { isDirectory: () => true, isFile: () => false }; + } + if ( + normalizedPath.endsWith( 'wp-load.php' ) || + normalizedPath.endsWith( 'wp-config.php' ) + ) { + return { isDirectory: () => false, isFile: () => true }; + } + throw new Error( 'File not found' ); + } ); + + // Mock fs.existsSync for addWpConfig method + ( fs.existsSync as jest.Mock ).mockImplementation( ( filePath: string ) => { + const normalizedPath = normalize( filePath ); + return normalizedPath.endsWith( 'wp-config.php' ); + } ); + mockBackup = { backupFile: normalize( '/path/to/backup.tar.gz' ), - wpConfigFile: normalize( '/path/to/wp-config.php' ), sqlFiles: [ normalize( '/tmp/studio_export_123/file.sql' ) ], - wpContent: { - uploads: [ normalize( '/path/to/wp-content/uploads/file1.jpg' ) ], - plugins: [ normalize( '/path/to/wp-content/plugins/plugin1' ) ], - themes: [ normalize( '/path/to/wp-content/themes/theme1' ) ], - muPlugins: [ normalize( '/path/to/wp-content/mu-plugins/custom-mu-plugin.php' ) ], - fonts: [ normalize( '/path/to/wp-content/fonts/custom-font.woff2' ) ], - }, }; mockOptions = { @@ -247,9 +260,17 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { const exporter = new DefaultExporter( options ); await exporter.export(); - expect( mockArchiver.file ).toHaveBeenCalledWith( normalize( '/path/to/site/wp-config.php' ), { - name: 'wp-config.php', - } ); + // wp-config.php should be called first, then meta.json + expect( mockArchiver.file ).toHaveBeenNthCalledWith( + 1, + normalize( '/path/to/site/wp-config.php' ), + { name: 'wp-config.php' } + ); + expect( mockArchiver.file ).toHaveBeenNthCalledWith( + 2, + normalize( '/tmp/studio_export_123/meta.json' ), + { name: 'meta.json' } + ); } ); it( 'should add meta.json to the archive', async () => { @@ -281,12 +302,19 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { const exporter = new DefaultExporter( options ); await exporter.export(); - // Check each expected call individually + + // Check that wp-config.php and meta.json are both added expect( mockArchiver.file ).toHaveBeenNthCalledWith( 1, normalize( '/path/to/site/wp-config.php' ), { name: 'wp-config.php' } ); + expect( mockArchiver.file ).toHaveBeenNthCalledWith( + 2, + normalize( '/tmp/studio_export_123/meta.json' ), + { name: 'meta.json' } + ); + expect( mockArchiver.directory ).toHaveBeenNthCalledWith( 1, normalize( '/path/to/site/wp-content/uploads' ), @@ -334,6 +362,11 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { normalize( '/path/to/site/wp-config.php' ), { name: 'wp-config.php' } ); + expect( mockArchiver.file ).toHaveBeenNthCalledWith( + 2, + normalize( '/tmp/studio_export_123/meta.json' ), + { name: 'meta.json' } + ); expect( mockArchiver.directory ).toHaveBeenNthCalledWith( 1, normalize( '/path/to/site/wp-content/mu-plugins' ), @@ -369,6 +402,11 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { normalize( '/tmp/studio_export_123/studio-backup-db-export-2023-07-31-12-00-00.sql' ), { name: 'sql/studio-backup-db-export-2023-07-31-12-00-00.sql' } ); + expect( mockArchiver.file ).toHaveBeenNthCalledWith( + 3, + normalize( '/tmp/studio_export_123/meta.json' ), + { name: 'meta.json' } + ); } ); it( 'should add a multiple SQL dumps to the archive when `splitDatabaseDumpByTable` is true', async () => { @@ -401,6 +439,12 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { { name: `sql/${ tableName }.sql` } ); } + + expect( mockArchiver.file ).toHaveBeenNthCalledWith( + defaultTableNames.length + 2, + normalize( '/tmp/studio_export_123/meta.json' ), + { name: 'meta.json' } + ); } ); it( 'should finalize the archive', async () => { @@ -486,6 +530,11 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { normalize( '/path/to/site/wp-config.php' ), { name: 'wp-config.php' } ); + expect( mockArchiver.file ).toHaveBeenNthCalledWith( + 2, + normalize( '/tmp/studio_export_123/meta.json' ), + { name: 'meta.json' } + ); expect( mockArchiver.directory ).toHaveBeenNthCalledWith( 1, normalize( '/path/to/site/wp-content/fonts' ), @@ -493,4 +542,50 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { expect.any( Function ) ); } ); + + it( "should not add wp-config if it doesn't exists", async () => { + ( fs.existsSync as jest.Mock ).mockImplementation( ( filePath: string ) => { + const normalizedPath = normalize( filePath ); + return ! normalizedPath.endsWith( 'wp-config.php' ); + } ); + + await exporter.export(); + + expect( mockArchiver.file ).not.toHaveBeenCalledWith( + normalize( '/path/to/site/wp-config.php' ), + { name: 'wp-config.php' } + ); + } ); + + it( 'should initialize backup object with correct structure when creating a new exporter', () => { + const testOptions: ExportOptions = { + site: { + running: false, + id: 'test-site', + name: 'Test Site', + path: normalize( '/path/to/test/site' ), + port: 8080, + phpVersion: '8.3', + }, + backupFile: normalize( '/path/to/test-backup.tar.gz' ), + includes: { + uploads: true, + plugins: true, + themes: true, + database: true, + muPlugins: true, + fonts: true, + }, + phpVersion: '8.4', + }; + + const testExporter = new DefaultExporter( testOptions ); + + const { backup } = testExporter as unknown as { backup: BackupContents }; + + expect( backup ).toEqual( { + backupFile: normalize( '/path/to/test-backup.tar.gz' ), + sqlFiles: [], + } ); + } ); } );