diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 22a3a940a6..ed1e29a2f9 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,7 +1,8 @@ Unreleased ========== * Miscellaneous security improvements #1411 -* Follow symlinks when creating preview sites #1447 +* Included symlinked directories and files when creating preview sites #1447 +* Included symlinked directories and files when exporting and pushing sites #1448 1.5.2 ===== diff --git a/src/constants.ts b/src/constants.ts index bcb32686d0..2d8871a59a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -37,10 +37,12 @@ export const ACCEPTED_IMPORT_FILE_TYPES = [ '.zip', '.gz', '.gzip', '.tar', '.ta export const ARCHIVER_OPTIONS = { zip: { zlib: { level: 9 }, + followSymlinks: true, }, tar: { gzip: true, gzipOptions: { level: 9 }, + followSymlinks: true, }, }; diff --git a/src/lib/import-export/export/exporters/default-exporter.ts b/src/lib/import-export/export/exporters/default-exporter.ts index c248f1dcc0..4b3cef4151 100644 --- a/src/lib/import-export/export/exporters/default-exporter.ts +++ b/src/lib/import-export/export/exporters/default-exporter.ts @@ -162,11 +162,24 @@ export class DefaultExporter extends EventEmitter implements Exporter { ).filter( ( category ) => this.options.includes[ category ] ); this.emit( ExportEvents.WP_CONTENT_EXPORT_START ); for ( const category of categories ) { - for ( const file of this.backup.wpContent[ category ] ) { - const relativePath = path.relative( this.options.site.path, file ); - this.archive.file( file, { name: relativePath } ); - this.emit( ExportEvents.WP_CONTENT_EXPORT_PROGRESS, { file: relativePath } ); - } + const folderName = category === 'muPlugins' ? 'mu-plugins' : category; + const absolutePath = path.join( this.options.site.path, 'wp-content', folderName ); + const archivePath = path.relative( this.options.site.path, absolutePath ); + this.archive.directory( absolutePath, archivePath, ( entry ) => { + const fullArchivePath = path.join( archivePath, entry.name ); + const isExcluded = this.pathsToExclude.some( ( pathToExclude ) => + fullArchivePath.startsWith( path.normalize( pathToExclude ) ) + ); + if ( + isExcluded || + entry.name.includes( '.git' ) || + entry.name.includes( 'node_modules' ) + ) { + return false; + } + return entry; + } ); + this.emit( ExportEvents.WP_CONTENT_EXPORT_PROGRESS, { directory: absolutePath } ); } this.emit( ExportEvents.WP_CONTENT_EXPORT_COMPLETE, { uploads: this.backup.wpContent.uploads.length, 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 8227061d93..b75d382d4a 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 @@ -217,14 +217,18 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { it( 'should create a tar.gz archive', async () => { await exporter.export(); - expect( archiver ).toHaveBeenCalledWith( 'tar', { gzip: true, gzipOptions: { level: 9 } } ); + expect( archiver ).toHaveBeenCalledWith( 'tar', { + followSymlinks: true, + gzip: true, + gzipOptions: { level: 9 }, + } ); } ); it( 'should create a zip archive when the backup file ends with .zip', async () => { mockOptions.backupFile = '/path/to/backup.zip'; await exporter.export(); - expect( archiver ).toHaveBeenCalledWith( 'zip', { zlib: { level: 9 } } ); + expect( archiver ).toHaveBeenCalledWith( 'zip', { followSymlinks: true, zlib: { level: 9 } } ); } ); it( 'should add wp-config.php to the archive', async () => { @@ -262,7 +266,7 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { ); } ); - it( 'should add wp-content files to the archive', async () => { + it( 'should add wp-content directories to the archive', async () => { const options = { ...mockOptions, includes: { @@ -283,25 +287,29 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { normalize( '/path/to/site/wp-config.php' ), { name: 'wp-config.php' } ); - expect( mockArchiver.file ).toHaveBeenNthCalledWith( + expect( mockArchiver.directory ).toHaveBeenNthCalledWith( + 1, + normalize( '/path/to/site/wp-content/uploads' ), + normalize( 'wp-content/uploads' ), + expect.any( Function ) + ); + expect( mockArchiver.directory ).toHaveBeenNthCalledWith( 2, - normalize( '/path/to/site/wp-content/uploads/file1.jpg' ), - { name: normalize( 'wp-content/uploads/file1.jpg' ) } + normalize( '/path/to/site/wp-content/plugins' ), + normalize( 'wp-content/plugins' ), + expect.any( Function ) ); - expect( mockArchiver.file ).toHaveBeenNthCalledWith( + expect( mockArchiver.directory ).toHaveBeenNthCalledWith( 3, - normalize( '/path/to/site/wp-content/plugins/plugin1/plugin1.php' ), - { name: normalize( 'wp-content/plugins/plugin1/plugin1.php' ) } + normalize( '/path/to/site/wp-content/themes' ), + normalize( 'wp-content/themes' ), + expect.any( Function ) ); - expect( mockArchiver.file ).toHaveBeenNthCalledWith( + expect( mockArchiver.directory ).toHaveBeenNthCalledWith( 4, - normalize( '/path/to/site/wp-content/themes/theme1/index.php' ), - { name: normalize( 'wp-content/themes/theme1/index.php' ) } - ); - expect( mockArchiver.file ).toHaveBeenNthCalledWith( - 5, - normalize( '/path/to/site/wp-content/fonts/custom-font.woff2' ), - { name: normalize( 'wp-content/fonts/custom-font.woff2' ) } + normalize( '/path/to/site/wp-content/fonts' ), + normalize( 'wp-content/fonts' ), + expect.any( Function ) ); } ); @@ -326,19 +334,11 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { normalize( '/path/to/site/wp-config.php' ), { name: 'wp-config.php' } ); - expect( mockArchiver.file ).toHaveBeenNthCalledWith( - 2, - normalize( '/path/to/site/wp-content/mu-plugins/custom-mu-plugin.php' ), - { name: normalize( 'wp-content/mu-plugins/custom-mu-plugin.php' ) } - ); - - expect( mockArchiver.file ).not.toHaveBeenCalledWith( - normalize( '/path/to/site/wp-content/mu-plugins/sqlite-database-integration/load.php' ), - { name: normalize( 'wp-content/mu-plugins/sqlite-database-integration/load.php' ) } - ); - expect( mockArchiver.file ).not.toHaveBeenCalledWith( - normalize( '/path/to/site/wp-content/mu-plugins/0-allowed-redirect-hosts.php' ), - { name: normalize( 'wp-content/mu-plugins/0-allowed-redirect-hosts.php' ) } + expect( mockArchiver.directory ).toHaveBeenNthCalledWith( + 1, + normalize( '/path/to/site/wp-content/mu-plugins' ), + normalize( 'wp-content/mu-plugins' ), + expect.any( Function ) ); } ); @@ -486,10 +486,11 @@ platformTestSuite( 'DefaultExporter', ( { normalize } ) => { normalize( '/path/to/site/wp-config.php' ), { name: 'wp-config.php' } ); - expect( mockArchiver.file ).toHaveBeenNthCalledWith( - 2, - normalize( '/path/to/site/wp-content/fonts/custom-font.woff2' ), - { name: normalize( 'wp-content/fonts/custom-font.woff2' ) } + expect( mockArchiver.directory ).toHaveBeenNthCalledWith( + 1, + normalize( '/path/to/site/wp-content/fonts' ), + normalize( 'wp-content/fonts' ), + expect.any( Function ) ); } ); } );