id, migration_id: (int) ( $row->migration_id ?? 0 ), file_path: (string) $row->file_path, file_size: (int) $row->file_size, tables_json: isset( $row->tables_json ) ? (string) $row->tables_json : (string) ( $row->tables ?? '[]' ), created_at: (string) $row->created_at, label: isset( $row->label ) ? (string) $row->label : '', status: isset( $row->status ) ? (string) $row->status : 'complete', connection_id: isset( $row->connection_id ) ? (int) $row->connection_id : null, expires_at: isset( $row->expires_at ) ? (string) $row->expires_at : null ); } /** * The backed-up tables. * * @return string[] */ public function tables(): array { $decoded = json_decode( $this->tables_json, true ); return is_array( $decoded ) ? $decoded : []; } /** Human-readable file size. */ public function formatted_size(): string { $bytes = $this->file_size; if ( $bytes < 1024 ) { return $bytes . ' B'; } if ( $bytes < 1_048_576 ) { return round( $bytes / 1024, 1 ) . ' KB'; } return round( $bytes / 1_048_576, 1 ) . ' MB'; } /** Whether the backup file still exists on disk. */ public function file_exists(): bool { return file_exists( $this->file_path ); } /** * Whether this dump could be restored from right now. * * Status only records that the dump finished writing. Retention, a manual * sweep or a restored backups table can leave a "complete" row with no file, * and a replaced or truncated file is refused by the restorer on the same * size check, so restore controls should call this instead of reading status. */ public function is_restorable(): bool { if ( 'complete' !== $this->status || ! is_file( $this->file_path ) ) { return false; } $size = @filesize( $this->file_path ); return false !== $size && (int) $size === $this->file_size; } /** Whether this snapshot has expired. */ public function is_expired(): bool { if ( null === $this->expires_at ) { return false; } return strtotime( $this->expires_at ) !== false && strtotime( $this->expires_at ) < time(); } }