Created
March 11, 2026 11:33
-
-
Save obiPlabon/47e008c050077c98016e8e2a8c12dcec to your computer and use it in GitHub Desktop.
Pull migration backup from Hosting A to Hosting B through HTTP pull request. WORKS really fast π
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
| <?php | |
| declare(strict_types=1); | |
| /** | |
| * Chunked, resumable HTTP downloader for large .wpress backups. | |
| * | |
| * Usage: | |
| * 1. Upload this file to the destination hosting. | |
| * 2. Edit $config below. | |
| * 3. Open in browser: | |
| * https://newsite.com/pull-ai1wm.php?token=CHANGE_ME | |
| * | |
| * Notes: | |
| * - Downloads in chunks to avoid request timeout. | |
| * - Resumes automatically from where it stopped. | |
| * - Requires source server to allow direct HTTP/HTTPS download. | |
| * - Works best if source supports Accept-Ranges, but can still work without it | |
| * if the connection is stable enough for each chunk. | |
| */ | |
| @ini_set('max_execution_time', '120'); | |
| @set_time_limit(120); | |
| @ini_set('memory_limit', '256M'); | |
| $config = [ | |
| 'token' => 'CHANGE_ME_TO_A_LONG_RANDOM_SECRET', | |
| 'source_url' => 'https://oldsite.com/wp-content/ai1wm-backups/site-backup.wpress', | |
| 'dest_dir' => __DIR__ . '/wp-content/ai1wm-backups', | |
| 'dest_file' => 'site-backup.wpress', | |
| 'state_file' => __DIR__ . '/pull-ai1wm-state.json', | |
| 'chunk_size' => 10 * 1024 * 1024, // 10 MB per request | |
| 'connect_timeout' => 15, | |
| 'timeout' => 90, | |
| 'user_agent' => 'AI1WM-Chunk-Puller/1.0', | |
| ]; | |
| function fail(string $message, int $code = 400): void { | |
| http_response_code($code); | |
| echo '<pre style="font:14px/1.4 monospace;color:#b00020;">' . htmlspecialchars($message) . '</pre>'; | |
| exit; | |
| } | |
| function out(string $message): void { | |
| echo htmlspecialchars($message) . "\n"; | |
| } | |
| function read_state(string $file): array { | |
| if (!file_exists($file)) { | |
| return [ | |
| 'offset' => 0, | |
| 'size' => null, | |
| 'done' => false, | |
| 'etag' => null, | |
| 'last_modified' => null, | |
| ]; | |
| } | |
| $json = file_get_contents($file); | |
| $data = json_decode((string)$json, true); | |
| if (!is_array($data)) { | |
| return [ | |
| 'offset' => 0, | |
| 'size' => null, | |
| 'done' => false, | |
| 'etag' => null, | |
| 'last_modified' => null, | |
| ]; | |
| } | |
| return array_merge([ | |
| 'offset' => 0, | |
| 'size' => null, | |
| 'done' => false, | |
| 'etag' => null, | |
| 'last_modified' => null, | |
| ], $data); | |
| } | |
| function write_state(string $file, array $state): void { | |
| file_put_contents($file, json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); | |
| } | |
| function format_bytes(?int $bytes): string { | |
| if ($bytes === null) { | |
| return 'unknown'; | |
| } | |
| $units = ['B', 'KB', 'MB', 'GB', 'TB']; | |
| $i = 0; | |
| $value = (float)$bytes; | |
| while ($value >= 1024 && $i < count($units) - 1) { | |
| $value /= 1024; | |
| $i++; | |
| } | |
| return round($value, 2) . ' ' . $units[$i]; | |
| } | |
| function get_remote_meta(array $config): array { | |
| $headers = []; | |
| $ch = curl_init($config['source_url']); | |
| curl_setopt_array($ch, [ | |
| CURLOPT_NOBODY => true, | |
| CURLOPT_RETURNTRANSFER => true, | |
| CURLOPT_HEADER => true, | |
| CURLOPT_FOLLOWLOCATION => true, | |
| CURLOPT_CONNECTTIMEOUT => $config['connect_timeout'], | |
| CURLOPT_TIMEOUT => $config['timeout'], | |
| CURLOPT_USERAGENT => $config['user_agent'], | |
| CURLOPT_SSL_VERIFYPEER => true, | |
| CURLOPT_SSL_VERIFYHOST => 2, | |
| ]); | |
| $response = curl_exec($ch); | |
| if ($response === false) { | |
| $error = curl_error($ch); | |
| curl_close($ch); | |
| fail('HEAD request failed: ' . $error, 500); | |
| } | |
| $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); | |
| curl_close($ch); | |
| foreach (explode("\n", (string)$response) as $line) { | |
| $line = trim($line); | |
| if ($line === '' || strpos($line, ':') === false) { | |
| continue; | |
| } | |
| [$key, $value] = explode(':', $line, 2); | |
| $headers[strtolower(trim($key))] = trim($value); | |
| } | |
| if ($status >= 400) { | |
| fail('Source server returned HTTP ' . $status . ' on HEAD request.', 500); | |
| } | |
| return [ | |
| 'size' => isset($headers['content-length']) ? (int)$headers['content-length'] : null, | |
| 'etag' => $headers['etag'] ?? null, | |
| 'last_modified' => $headers['last-modified'] ?? null, | |
| 'accept_ranges' => $headers['accept-ranges'] ?? null, | |
| ]; | |
| } | |
| function download_chunk(array $config, int $start, int $end, string $destPath): array { | |
| $fp = fopen($destPath, 'c+b'); | |
| if (!$fp) { | |
| fail('Could not open destination file for writing: ' . $destPath, 500); | |
| } | |
| if (fseek($fp, $start) !== 0) { | |
| fclose($fp); | |
| fail('Could not seek to destination offset.', 500); | |
| } | |
| $responseHeaders = []; | |
| $ch = curl_init($config['source_url']); | |
| curl_setopt_array($ch, [ | |
| CURLOPT_FILE => $fp, | |
| CURLOPT_FOLLOWLOCATION => true, | |
| CURLOPT_CONNECTTIMEOUT => $config['connect_timeout'], | |
| CURLOPT_TIMEOUT => $config['timeout'], | |
| CURLOPT_USERAGENT => $config['user_agent'], | |
| CURLOPT_SSL_VERIFYPEER => true, | |
| CURLOPT_SSL_VERIFYHOST => 2, | |
| CURLOPT_RANGE => $start . '-' . $end, | |
| CURLOPT_HEADERFUNCTION => function ($ch, $headerLine) use (&$responseHeaders) { | |
| $len = strlen($headerLine); | |
| $headerLine = trim($headerLine); | |
| if ($headerLine !== '' && strpos($headerLine, ':') !== false) { | |
| [$key, $value] = explode(':', $headerLine, 2); | |
| $responseHeaders[strtolower(trim($key))] = trim($value); | |
| } | |
| return $len; | |
| }, | |
| ]); | |
| $ok = curl_exec($ch); | |
| $error = curl_error($ch); | |
| $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); | |
| curl_close($ch); | |
| fclose($fp); | |
| if ($ok === false) { | |
| fail('Chunk download failed: ' . $error, 500); | |
| } | |
| if (!in_array($status, [200, 206], true)) { | |
| fail('Unexpected HTTP status during chunk download: ' . $status, 500); | |
| } | |
| clearstatcache(true, $destPath); | |
| return [ | |
| 'status' => $status, | |
| 'headers' => $responseHeaders, | |
| 'local_size' => file_exists($destPath) ? filesize($destPath) : 0, | |
| ]; | |
| } | |
| $token = $_GET['token'] ?? ''; | |
| if (!hash_equals($config['token'], (string)$token)) { | |
| fail('Unauthorized.', 403); | |
| } | |
| if (!is_dir($config['dest_dir']) && !mkdir($config['dest_dir'], 0755, true) && !is_dir($config['dest_dir'])) { | |
| fail('Could not create destination directory: ' . $config['dest_dir'], 500); | |
| } | |
| $destPath = rtrim($config['dest_dir'], '/\\') . DIRECTORY_SEPARATOR . $config['dest_file']; | |
| $state = read_state($config['state_file']); | |
| if (isset($_GET['reset']) && $_GET['reset'] === '1') { | |
| @unlink($destPath); | |
| @unlink($config['state_file']); | |
| $state = [ | |
| 'offset' => 0, | |
| 'size' => null, | |
| 'done' => false, | |
| 'etag' => null, | |
| 'last_modified' => null, | |
| ]; | |
| } | |
| $meta = get_remote_meta($config); | |
| if ($state['etag'] && $meta['etag'] && $state['etag'] !== $meta['etag']) { | |
| fail('Remote file ETag changed. Use ?reset=1 to restart safely.', 409); | |
| } | |
| if ($state['last_modified'] && $meta['last_modified'] && $state['last_modified'] !== $meta['last_modified']) { | |
| fail('Remote file Last-Modified changed. Use ?reset=1 to restart safely.', 409); | |
| } | |
| $state['size'] = $meta['size']; | |
| $state['etag'] = $meta['etag']; | |
| $state['last_modified'] = $meta['last_modified']; | |
| if (file_exists($destPath)) { | |
| $actualLocalSize = filesize($destPath); | |
| if ($actualLocalSize > (int)$state['offset']) { | |
| $state['offset'] = $actualLocalSize; | |
| } | |
| } | |
| if ($state['done'] === true) { | |
| header('Content-Type: text/html; charset=utf-8'); | |
| echo '<pre>'; | |
| out('Download already completed.'); | |
| out('File: ' . $destPath); | |
| out('Size: ' . format_bytes($state['size'])); | |
| out(''); | |
| out('Now go to WordPress admin β All-in-One WP Migration β Backups'); | |
| out('Then restore the backup.'); | |
| echo '</pre>'; | |
| exit; | |
| } | |
| $start = (int)$state['offset']; | |
| if ($state['size'] !== null && $start >= (int)$state['size']) { | |
| $state['done'] = true; | |
| write_state($config['state_file'], $state); | |
| header('Content-Type: text/html; charset=utf-8'); | |
| echo '<pre>'; | |
| out('Download completed.'); | |
| out('File: ' . $destPath); | |
| out('Size: ' . format_bytes($state['size'])); | |
| echo '</pre>'; | |
| exit; | |
| } | |
| $end = $start + $config['chunk_size'] - 1; | |
| if ($state['size'] !== null) { | |
| $end = min($end, (int)$state['size'] - 1); | |
| } | |
| $result = download_chunk($config, $start, $end, $destPath); | |
| $downloaded = ($end - $start) + 1; | |
| $state['offset'] = $end + 1; | |
| if ($state['size'] !== null && $state['offset'] >= (int)$state['size']) { | |
| $state['offset'] = (int)$state['size']; | |
| $state['done'] = true; | |
| } | |
| write_state($config['state_file'], $state); | |
| $percent = $state['size'] | |
| ? round(($state['offset'] / $state['size']) * 100, 2) | |
| : null; | |
| header('Content-Type: text/html; charset=utf-8'); | |
| echo '<!doctype html><html><head><meta charset="utf-8">'; | |
| if (!$state['done']) { | |
| echo '<meta http-equiv="refresh" content="1">'; | |
| } | |
| echo '<title>Chunked Pull</title></head><body><pre>'; | |
| out('Chunk downloaded successfully.'); | |
| out('Source: ' . $config['source_url']); | |
| out('Destination: ' . $destPath); | |
| out('Accept-Ranges: ' . ($meta['accept_ranges'] ?? 'unknown')); | |
| out(''); | |
| out('Downloaded this request: ' . format_bytes($downloaded)); | |
| out('Current offset: ' . format_bytes($state['offset'])); | |
| out('Total size: ' . format_bytes($state['size'])); | |
| out('Progress: ' . ($percent !== null ? $percent . '%' : 'unknown')); | |
| out(''); | |
| if ($state['done']) { | |
| out('Completed.'); | |
| out('Now restore from All-in-One WP Migration β Backups'); | |
| } else { | |
| out('Continuing automatically...'); | |
| out('Do not close this page until it finishes.'); | |
| out(''); | |
| out('If it stops unexpectedly, just reopen this URL.'); | |
| } | |
| echo '</pre></body></html>'; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment