sizemtime" line per file. */ public const LIST_FILE = 'files.list'; /** Folder in the job directory holding table row files while they are added. */ public const DB_DIR = 'db'; /** wp-content, or the folder a test points the filter at. Archive paths are relative to it. */ public static function content_dir(): string { $dir = (string) apply_filters( 'mench_migrate_content_dir', WP_CONTENT_DIR ); $real = realpath( $dir ); return untrailingslashit( false === $real ? $dir : $real ); } /** Final archive path in backups/. */ public static function archive_path( Mench_Migrate_Job $job ): string { $name = (string) ( $job->data['archive'] ?? '' ); if ( ! Mench_Migrate_Storage_Local::valid_name( $name ) ) { throw new Mench_Migrate_Exception( 'bad_archive_name', 'export' ); } return Mench_Migrate_Paths::backups() . '/' . $name; } /** The archive while it is being written; renamed to archive_path() once closed. */ public static function part_path( Mench_Migrate_Job $job ): string { return self::archive_path( $job ) . '.part'; } public static function list_path( Mench_Migrate_Job $job ): string { return $job->dir() . '/' . self::LIST_FILE; } public static function db_dir( Mench_Migrate_Job $job ): string { return $job->dir() . '/' . self::DB_DIR; } /** Reopens the archive from the writer state saved in the job. */ public static function writer( Mench_Migrate_Job $job ): Mench_Migrate_Archive_Writer { return Mench_Migrate_Archive_Writer::resume( self::part_path( $job ), (array) ( $job->data['writer'] ?? [] ), self::password( $job ), self::index_path( $job ) ); } /** The writer's index file: one line per finished entry, so the job state holds only counters. */ public static function index_path( Mench_Migrate_Job $job ): string { return $job->dir() . '/' . Mench_Migrate_Archive_Writer::INDEX_FILE; } // --- password --------------------------------------------------------------------------------------------------- /** * The archive password, or null for an unencrypted export. Job::create stashed it with Mench_Migrate_Secrets. * * @throws Mench_Migrate_Exception password_unavailable when the transient expired, was deleted or the salt changed. */ public static function password( Mench_Migrate_Job $job ): ?string { if ( empty( $job->data['options']['encrypted'] ) ) { return null; } $plain = Mench_Migrate_Secrets::read( $job->id ); if ( null === $plain ) { throw new Mench_Migrate_Exception( 'password_unavailable', 'export' ); } return $plain; } // --- cleanup ---------------------------------------------------------------------------------------------------- /** * A failure keeps the partial archive for Resume, except when the disk is full (write_failed): then the .part and * the working files go at once, so the space is free for the next attempt, and Resume starts the archive again from * the first step (same name). */ public function on_failure( Mench_Migrate_Job $job, Throwable $e ): void { if ( 'write_failed' !== Mench_Migrate_Rest::code_of( $e->getMessage() ) ) { return; } try { $part = self::part_path( $job ); is_file( $part ) && @unlink( $part ); // phpcs:ignore WordPress.PHP.NoSilencedErrors } catch ( Mench_Migrate_Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- no archive yet. } $list = self::list_path( $job ); is_file( $list ) && @unlink( $list ); // phpcs:ignore WordPress.PHP.NoSilencedErrors self::remove_index( $job ); self::remove_db_dir( $job ); unset( $job->data['writer'] ); $job->step_index = 0; $job->cursor = []; $job->log( 'warning', 'The disk is full: the partial archive was deleted to free the space. Resume starts the export again.' ); } /** A cancelled job removes the password, the partial archive and its working files. */ public function on_cancel( Mench_Migrate_Job $job ): void { Mench_Migrate_Secrets::forget( $job->id ); try { $part = self::part_path( $job ); is_file( $part ) && unlink( $part ); } catch ( Mench_Migrate_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- no archive yet. } $list = self::list_path( $job ); is_file( $list ) && unlink( $list ); self::remove_index( $job ); self::remove_db_dir( $job ); } /** Deletes the writer's index and keys files. */ public static function remove_index( Mench_Migrate_Job $job ): void { foreach ( [ self::index_path( $job ), self::index_path( $job ) . '.keys' ] as $f ) { is_file( $f ) && @unlink( $f ); // phpcs:ignore WordPress.PHP.NoSilencedErrors } } /** Deletes the table row folder and anything left in it. */ public static function remove_db_dir( Mench_Migrate_Job $job ): void { $dir = self::db_dir( $job ); if ( ! is_dir( $dir ) ) { return; } foreach ( (array) scandir( $dir ) as $f ) { if ( '.' !== $f && '..' !== $f && is_file( $dir . '/' . $f ) ) { unlink( $dir . '/' . $f ); } } @rmdir( $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- best-effort. } }