Push model: upload local site to remote WordPress#97
Open
adamziel wants to merge 7 commits into
Open
Conversation
…over The pull model downloads from remote to local. This adds the reverse: push local database and files to a remote WordPress site with blue-green deployment semantics. Database tables are pushed with a staging prefix (_push_wp_*), then atomically swapped via a single RENAME TABLE statement. Files are pushed to a staging directory, then swapped via symlink or directory rename. A unified commit endpoint orchestrates both swaps with rollback on partial failure. New classes: MultipartBodyStream (streaming multipart resource backed by php://temp for bounded-memory uploads) and ChunkWriter (shared filesystem write operations for the receiver). New CLI commands: push-sql, push-files, push-commit — all resumable via receiver-authoritative cursors, matching the pull model's reentrancy design.
Initialize $existing_tables before the conditional block so PHPStan can see it's always defined at usage. Fix FileTreeProducer namespace (it's in global namespace, not WordPress\DataLiberation). Remove redundant null coalescing on $staging_id. Baseline the is_finished() false-positive warnings that stem from the same MySQLDumpProducer state machine analysis already baselined for "unreachable statement."
… ChunkWriter security Five test files covering the critical production-safety gaps in the push implementation: - TableNameRewritingTest: verifies SQL rewriting handles substring table names, FK references, multi-statement blocks, and doesn't corrupt string VALUES data - CommitEndpointTest: tests RENAME TABLE generation, documents the silent table deletion bug (pushing a subset of tables permanently deletes remote-only tables like WooCommerce orders), validates missing staging table detection, tests file swap and rollback mechanics, and wp-config.php credential preservation - MultipartBodyStreamTest: round-trip tests for all chunk types (SQL, file, directory, symlink, completion) through write → parse_multipart_body, binary data integrity, finalization idempotency, HMAC consistency - ChunkWriterTest: filesystem write correctness, multi-chunk streaming, path traversal rejection, NUL byte injection, symlink escape prevention, symlink in directory path replacement, binary data integrity, interrupted write handling - SqlReceiveIntegrationTest: full pipeline from MySQLDumpProducer export through multipart streaming, table name rewriting, staging table creation, RENAME TABLE commit, and data integrity verification including special characters and the empty-database-wipes-target scenario Tests use a standalone push-test-bootstrap.php to avoid the utils.php double-load fatal caused by Composer vendor copy vs. local package path collision.
assert_valid_path() throws InvalidArgumentException, not RuntimeException. PDO::FETCH_COLUMN returns int on MySQL 8+, use COUNT(*) for simpler assertions.
… FK breakage Five new integration tests that simulate what happens when the production site has data the source doesn't know about: - testPushDestroysProductionRowsNotInSource: production grew from 3 to 6 posts while dev was working locally. Push replaces the table entirely, posts 4-6 are gone. Documents that this is wholesale replacement, not a merge. - testConflictingAutoIncrementIdsAreOverwrittenSilently: both dev and production independently created a post with ID=4 but different content. Push silently replaces the CEO's announcement with a dev draft. No conflict detection. - testAutoIncrementCounterResetAfterPush: production's counter was at 11, source's at 3. After push, new inserts get ID=3, reusing an ID that existed in the old production data. Any cached references to old ID=3 now point to wrong content. - testForeignKeyIntegrityAcrossPushedTables: when both wp_posts and wp_postmeta are pushed together, FK references remain internally consistent (positive test). - testPartialTablePushBreaksForeignKeyIntegrity: pushing only wp_posts without wp_postmeta causes wp_postmeta to be silently deleted by the "dropped table" logic, since it wasn't in the pushed set. Documents the cascade from the silent table deletion bug.
…HEMA INFORMATION_SCHEMA.TABLES.AUTO_INCREMENT can return stale values after RENAME TABLE due to MySQL's internal caching. The observable behavior (what ID does the next INSERT get) is what matters, so test that directly.
…rge file truncation push-files crashed on every invocation because FileTreeProducer requires a `paths` option that was never provided. push-commit failed on repeat runs when a prior commit's best-effort cleanup was interrupted, leaving _old_* tables that block MySQL's RENAME TABLE. And files larger than the per-request body budget were silently truncated because ChunkWriter had no resume recovery for continuation chunks arriving in a fresh instance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds the reverse of the current pull model. Instead of downloading from a remote site, the local machine uploads its database and files to a remote WordPress site with atomic cutover semantics.
The push uses the same multipart protocol, producers, cursors, and HMAC auth as the pull model, just with roles swapped. The receiver is the authority on progress — the client tracks "how much the server consumed," not "how much I sent."
Database tables land with a staging prefix (
_push_wp_*), then a singleRENAME TABLEstatement atomically swaps all tables. Files land in a staging directory, then swap via symlink or directory rename. A unified commit endpoint orchestrates both swaps — files first (rollbackable), then DB (atomic) — with rollback on partial failure.Nothing goes live until the explicit commit step. Push-sql and push-files can run in any order, be interrupted, and resume from the receiver's last cursor.
What's new
MultipartBodyStream — streaming multipart/mixed resource backed by
php://tempfor bounded-memory curl uploads with HMAC signing.ChunkWriter — shared filesystem write operations (file chunks, directories, symlinks with security validation) used by the file_receive endpoint.
Receiver endpoints —
sql_receive(rewrites table names to staging prefix, executes SQL),file_receive(writes to staging directory),commit(validates staging state, swaps files then DB, rolls back on failure).CLI commands —
push-sql,push-files,push-commit, all resumable with state management matching the pull model's reentrancy design.Test plan