'enumerate', 'opts' => $opts, 'archive' => $name, ) ); } public static function step( Marchnote_Job $job, $deadline ) { $d = &$job->data; switch ( $d['stage'] ) { case 'enumerate': self::enumerate( $job ); $d['stage'] = $d['opts']['no_database'] ? 'archive' : 'database'; if ( 'database' === $d['stage'] ) { $d['db'] = Marchnote_Database::dump_init(); } return self::progress( $job, 5, sprintf( 'Found %s files (%s).', number_format_i18n( $d['files'] ), size_format( $d['bytes'] ) ) ); case 'database': $done = Marchnote_Database::dump_step( $d['db'], $job->path( 'database.sql' ), $d['opts'], $deadline ); $n = count( $d['db']['tables'] ); $pct = 5 + (int) ( 25 * ( $n ? $d['db']['ti'] / $n : 1 ) ); if ( $done ) { $d['stage'] = 'archive'; } return self::progress( $job, $pct, sprintf( 'Exporting database: %d of %d tables, %s rows.', min( $d['db']['ti'] + 1, $n ), $n, number_format_i18n( $d['db']['rows'] ) ) ); case 'archive': if ( empty( $d['arc'] ) ) { self::write_package( $job ); $d['arc'] = array( 'offset' => 0, 'specials' => array_values( array_filter( array( 'package.json', $d['opts']['no_database'] ? null : 'database.sql' ) ) ), 'list_pos' => 0, 'cur' => null, 'written' => 0, 'count' => 0, ); } $done = self::archive_step( $job, $deadline ); $pct = 30 + (int) ( 68 * ( $d['bytes'] ? min( 1, $d['arc']['written'] / max( 1, $d['bytes'] ) ) : 1 ) ); if ( $done ) { $d['stage'] = 'finalize'; } return self::progress( $job, $pct, sprintf( 'Archiving files: %s of %s.', number_format_i18n( $d['arc']['count'] ), number_format_i18n( $d['files'] ) ) ); case 'finalize': $part = Marchnote_Storage::backups_dir() . '/' . $d['archive'] . '.part'; $final = Marchnote_Storage::backups_dir() . '/' . $d['archive']; if ( ! rename( $part, $final ) ) { throw new RuntimeException( 'Could not finalise the archive.' ); } $size = filesize( $final ); $job->destroy(); return array( 'status' => 'done', 'progress' => 100, 'message' => sprintf( 'Export complete: %s (%s).', $d['archive'], size_format( $size ) ), 'data' => array( 'file' => $d['archive'], 'size' => $size, 'download' => Marchnote_Admin::download_url( $d['archive'] ), ), ); } throw new RuntimeException( 'Unknown export stage.' ); } private static function progress( Marchnote_Job $job, $pct, $message ) { $job->save(); return array( 'status' => 'continue', 'progress' => $pct, 'message' => $message, ); } /** * Paths (relative to wp-content) left out of this export. */ public static function exclusions( array $opts ) { $ex = self::ALWAYS_SKIP; $ex[] = Marchnote_Storage::rel_to_content( Marchnote_Storage::backups_dir() ); $plug = Marchnote_Storage::rel_to_content( WP_PLUGIN_DIR ); $them = Marchnote_Storage::rel_to_content( get_theme_root() ); $up = wp_upload_dir( null, false ); $upl = Marchnote_Storage::rel_to_content( $up['basedir'] ); $mu = Marchnote_Storage::rel_to_content( WPMU_PLUGIN_DIR ); if ( null !== $plug ) { // The destination keeps its own copy of this plugin. $ex[] = $plug . '/' . dirname( MARCHNOTE_BASENAME ); if ( $opts['no_plugins'] ) { $ex[] = $plug; } elseif ( $opts['no_inactive_plugins'] ) { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } foreach ( array_keys( get_plugins() ) as $file ) { if ( ! is_plugin_active( $file ) ) { $ex[] = $plug . '/' . ( false === strpos( $file, '/' ) ? $file : dirname( $file ) ); } } } } if ( null !== $them ) { if ( $opts['no_themes'] ) { $ex[] = $them; } elseif ( $opts['no_inactive_themes'] ) { foreach ( array_keys( wp_get_themes() ) as $slug ) { if ( get_template() !== $slug && get_stylesheet() !== $slug ) { $ex[] = $them . '/' . $slug; } } } } if ( null !== $upl && $opts['no_media'] ) { $ex[] = $upl; } if ( null !== $mu && $opts['no_muplugins'] ) { $ex[] = $mu; } return array_values( array_filter( array_unique( array_merge( $ex, $opts['exclude_paths'] ) ), 'strlen' ) ); } private static function enumerate( Marchnote_Job $job ) { $root = Marchnote_Storage::content_dir(); $ex = self::exclusions( $job->data['opts'] ); $list = fopen( $job->path( 'files.list' ), 'wb' ); $n = 0; $size = 0; $dir = new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS ); $filter = new RecursiveCallbackFilterIterator( $dir, function ( $f ) use ( $root, $ex ) { $rel = substr( wp_normalize_path( $f->getPathname() ), strlen( $root ) + 1 ); return '.DS_Store' !== $f->getFilename() && ! Marchnote_Storage::matches( $rel, $ex ); } ); $it = new RecursiveIteratorIterator( $filter, RecursiveIteratorIterator::LEAVES_ONLY, RecursiveIteratorIterator::CATCH_GET_CHILD ); foreach ( $it as $f ) { if ( ! $f->isFile() || ! $f->isReadable() ) { continue; } $rel = substr( wp_normalize_path( $f->getPathname() ), strlen( $root ) + 1 ); if ( strlen( $rel ) > 4000 || preg_match( '/[\r\n]/', $rel ) ) { $job->log( "Skipped unusual filename: $rel" ); continue; } fwrite( $list, $rel . "\n" ); $n++; $size += $f->getSize(); } fclose( $list ); $job->data['files'] = $n; $job->data['bytes'] = $size; } private static function write_package( Marchnote_Job $job ) { global $wpdb; $up = wp_upload_dir( null, false ); if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $pkg = array( 'format' => 1, 'plugin_version' => MARCHNOTE_VERSION, 'created' => time(), 'siteurl' => site_url(), 'home' => home_url(), 'uploads_url' => $up['baseurl'], 'abspath' => untrailingslashit( wp_normalize_path( ABSPATH ) ), 'content_dir' => Marchnote_Storage::content_dir(), 'uploads_dir' => untrailingslashit( wp_normalize_path( $up['basedir'] ) ), 'prefix' => $wpdb->prefix, 'wp_version' => get_bloginfo( 'version' ), 'php_version' => PHP_VERSION, 'db_version' => $wpdb->db_version(), 'blogname' => get_option( 'blogname' ), 'template' => get_template(), 'stylesheet' => get_stylesheet(), 'active_plugins' => (array) get_option( 'active_plugins', array() ), 'has_database' => ! $job->data['opts']['no_database'], 'files' => $job->data['files'], 'bytes' => $job->data['bytes'], 'options' => $job->data['opts'], ); file_put_contents( $job->path( 'package.json' ), wp_json_encode( $pkg, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } /** * Append entries to the archive until done or out of time. */ private static function archive_step( Marchnote_Job $job, $deadline ) { $a = &$job->data['arc']; $path = Marchnote_Storage::backups_dir() . '/' . $job->data['archive'] . '.part'; $out = fopen( $path, 'c+b' ); if ( ! $out ) { throw new RuntimeException( 'Could not write the archive in ' . esc_html( Marchnote_Storage::backups_dir() ) ); } // Drop anything a crashed request wrote after the last checkpoint. ftruncate( $out, $a['offset'] ); fseek( $out, $a['offset'] ); if ( 0 === $a['offset'] ) { fwrite( $out, Marchnote_Archive::MAGIC ); } $list = fopen( $job->path( 'files.list' ), 'rb' ); $root = Marchnote_Storage::content_dir(); while ( microtime( true ) < $deadline ) { if ( ! $a['cur'] ) { if ( $a['specials'] ) { $special = array_shift( $a['specials'] ); $src = $job->path( $special ); $name = 'marchnote/' . $special; } else { fseek( $list, $a['list_pos'] ); $line = fgets( $list ); if ( false === $line ) { fwrite( $out, Marchnote_Archive::end_marker() ); fclose( $list ); fclose( $out ); return true; } $a['list_pos'] = ftell( $list ); $rel = rtrim( $line, "\n" ); $src = $root . '/' . $rel; $name = Marchnote_Archive::CONTENT . $rel; $a['count']++; } clearstatcache( true, $src ); if ( ! is_file( $src ) || ! is_readable( $src ) ) { $job->log( "Skipped unreadable file: $src" ); continue; } $size = filesize( $src ); fwrite( $out, Marchnote_Archive::entry_header( $name, $size, (int) filemtime( $src ) ) ); $a['cur'] = array( 'src' => $src, 'size' => $size, 'done' => 0, 'file' => 0 === strpos( $name, Marchnote_Archive::CONTENT ), ); } $cur = &$a['cur']; $eof = false; $in = @fopen( $cur['src'], 'rb' ); if ( $in ) { fseek( $in, $cur['done'] ); while ( $cur['done'] < $cur['size'] && microtime( true ) < $deadline ) { $n = Marchnote_Archive::copy( $in, $out, min( 8 * Marchnote_Archive::CHUNK, $cur['size'] - $cur['done'] ) ); if ( 0 === $n ) { $eof = true; // file shrank while we were reading it break; } $cur['done'] += $n; if ( $cur['file'] ) { $a['written'] += $n; } } fclose( $in ); } else { $eof = true; } if ( $eof && $cur['done'] < $cur['size'] ) { // Keep the header's declared size honest if the source vanished or shrank. $job->log( 'File changed during export: ' . $cur['src'] ); fwrite( $out, str_repeat( "\0", $cur['size'] - $cur['done'] ) ); $cur['done'] = $cur['size']; } if ( $cur['done'] >= $cur['size'] ) { $a['cur'] = null; } unset( $cur ); fflush( $out ); $a['offset'] = ftell( $out ); } fflush( $out ); $a['offset'] = ftell( $out ); fclose( $list ); fclose( $out ); return false; } }