null, 'dry_run' => 1, 'url' => null, 'activate_external' => 0, ]; foreach ($argv as $arg) { if (strpos($arg, '--notesid=') === 0) { $result['notesid'] = (int)substr($arg, 10); } elseif (strpos($arg, '--dry-run=') === 0) { $result['dry_run'] = (int)substr($arg, 10); } elseif (strpos($arg, '--url=') === 0) { $result['url'] = substr($arg, 6); } elseif (strpos($arg, '--activate-external=') === 0) { $result['activate_external'] = (int)substr($arg, 20); } } return $result; } function ensureDir(string $dir): void { if (!is_dir($dir)) { if (!mkdir($dir, 0775, true) && !is_dir($dir)) { throw new RuntimeException("Cannot create directory: {$dir}"); } } } function getPdo(array $dbconfig): PDO { $host = $dbconfig['db_server'] ?? 'localhost'; $port = $dbconfig['db_port'] ?? ':3306'; // vtiger style ":3306" $portClean = ltrim((string)$port, ':'); $dbName = $dbconfig['db_name']; $user = $dbconfig['db_username']; $pass = $dbconfig['db_password']; $dsn = "mysql:host={$host};port={$portClean};dbname={$dbName};charset=utf8"; $pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); return $pdo; } function findS3InfoForNotesId(int $notesId, ?string $overrideUrl = null): ?array { // Returns ['url'=>..., 'bucket'=>..., 'key'=>..., 'etag'=>...] $dir = __DIR__; $pattern = $dir . '/migration_results_*.json'; $files = glob($pattern, GLOB_NOSORT) ?: []; // Sort by mtime desc usort($files, static function ($a, $b) { return (filemtime($b) <=> filemtime($a)); }); foreach ($files as $file) { $json = file_get_contents($file); if ($json === false) { continue; } $data = json_decode($json, true); if (!is_array($data)) { continue; } foreach (['uploaded', 'copied'] as $bucket) { if (!empty($data[$bucket]) && is_array($data[$bucket])) { foreach ($data[$bucket] as $row) { $nid = (int)($row['notesid'] ?? 0); if ($nid === $notesId) { return [ 'url' => (string)($overrideUrl ?? ($row['url'] ?? '')), 'bucket' => $row['bucket'] ?? null, 'key' => $row['key'] ?? null, 'etag' => $row['etag'] ?? null, ]; } } } } } return null; } function backupRow(array $row, int $notesId): string { $backupDir = __DIR__ . '/backups'; ensureDir($backupDir); $ts = date('Ymd_His'); $file = sprintf('%s/phase2_notes_backup_%d_%s.json', $backupDir, $notesId, $ts); $payload = [ 'notesid' => $notesId, 'backup_taken_at' => date(DATE_ATOM), 'vtiger_notes' => $row, ]; file_put_contents($file, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); return $file; } function main(): int { global $dbconfig; $args = parseArgs(array_slice($_SERVER['argv'], 1)); $notesId = (int)($args['notesid'] ?? 0); $dryRun = (int)($args['dry_run'] ?? 1); $overrideUrl = $args['url'] ?? null; $activateExternal = (int)($args['activate_external'] ?? 0); if ($notesId <= 0) { fwrite(STDERR, "Usage: php update_db_phase2.php --notesid= [--dry-run=1|0] [--url=]\n"); return 2; } echo "[INFO] Phase 2 update for notesid={$notesId} (dry-run={$dryRun}, activate-external={$activateExternal})\n"; // Debug markers if (getenv('PHASE2_DEBUG') === '1') { @file_put_contents('/tmp/phase2_debug.log', "after_info\n", FILE_APPEND); } $pdo = getPdo($dbconfig); if (getenv('PHASE2_DEBUG') === '1') { @file_put_contents('/tmp/phase2_debug.log', "after_get_pdo\n", FILE_APPEND); } // Fetch current row try { $sql = 'SELECT notesid, filename, filelocationtype, filetype, filesize, s3_bucket, s3_key, s3_etag FROM vtiger_notes WHERE notesid = :id'; $stmt = $pdo->prepare($sql); $stmt->execute([':id' => $notesId]); $current = $stmt->fetch(); } catch (Throwable $e) { fwrite(STDERR, "[FATAL] SELECT failed: " . $e->getMessage() . "\n"); return 6; } if (getenv('PHASE2_DEBUG') === '1') { @file_put_contents('/tmp/phase2_debug.log', "after_select\n", FILE_APPEND); } if (!$current) { fwrite(STDERR, "[ERROR] vtiger_notes not found for notesid={$notesId}\n"); return 3; } echo "[INFO] Current: filelocationtype=" . ($current['filelocationtype'] ?? 'NULL') . ", s3_bucket=" . ($current['s3_bucket'] ?? 'NULL') . ", s3_key=" . ($current['s3_key'] ?? 'NULL') . "\n"; if (getenv('PHASE2_DEBUG') === '1') { @file_put_contents('/tmp/phase2_debug.log', "after_info_current\n", FILE_APPEND); } // Find S3 info from migration reports $s3Info = findS3InfoForNotesId($notesId, $overrideUrl); if (getenv('PHASE2_DEBUG') === '1') { @file_put_contents('/tmp/phase2_debug.log', "after_find_url\n", FILE_APPEND); } if ($s3Info === null) { fwrite(STDERR, "[ERROR] S3 info not found in migration_results_*.json. Optionally pass --url=... to override URL.\n"); return 4; } $plannedBucket = (string)($s3Info['bucket'] ?? ''); $plannedKey = (string)($s3Info['key'] ?? ''); $plannedEtag = (string)($s3Info['etag'] ?? ''); echo "[INFO] S3: bucket={$plannedBucket}, key={$plannedKey}, etag={$plannedEtag}\n"; // Prefer URL from migration report; fallback compose from bucket/key using known public endpoint $plannedUrl = (string)($s3Info['url'] ?? ''); if ($plannedUrl === '' && $plannedBucket !== '' && $plannedKey !== '') { // Try to read endpoint from file_storage config $endpoint = null; try { $fsConfig = require __DIR__ . '/config.php'; $endpoint = $fsConfig['s3']['endpoint'] ?? null; } catch (Throwable $e) { $endpoint = null; } if (is_string($endpoint) && $endpoint !== '') { $endpoint = rtrim($endpoint, '/'); $plannedUrl = $endpoint . '/' . $plannedBucket . '/' . str_replace('%2F', '/', rawurlencode($plannedKey)); } } if ($activateExternal === 1) { echo "[INFO] External link planned (filename will point to S3 URL).\n"; } if (getenv('PHASE2_DEBUG') === '1') { @file_put_contents('/tmp/phase2_debug.log', "after_info_url\n", FILE_APPEND); } // Backup $backupFile = backupRow($current, $notesId); echo "[INFO] Backup saved: {$backupFile}\n"; if (getenv('PHASE2_DEBUG') === '1') { @file_put_contents('/tmp/phase2_debug.log', "after_backup\n", FILE_APPEND); } // Planned changes: set S3 fields (and optionally activate external link) $planned = [ 's3_bucket' => $plannedBucket, 's3_key' => $plannedKey, 's3_etag' => $plannedEtag, ]; if ($activateExternal === 1) { echo "[PLAN] UPDATE vtiger_notes SET s3_bucket='{$plannedBucket}', s3_key='', s3_etag='', filelocationtype='E', filename='' WHERE notesid={$notesId};\n"; } else { echo "[PLAN] UPDATE vtiger_notes SET s3_bucket='{$plannedBucket}', s3_key='', s3_etag='' WHERE notesid={$notesId};\n"; } if ($dryRun === 1) { echo "[DRY-RUN] No DB changes applied.\n"; echo "[ROLLBACK] Backup JSON contains original s3_* (and filename/filelocationtype) values.\n"; return 0; } // Apply update try { $pdo->beginTransaction(); if ($activateExternal === 1) { $upd = $pdo->prepare('UPDATE vtiger_notes SET s3_bucket = :bucket, s3_key = :s3key, s3_etag = :etag, filelocationtype = \'E\', filename = :fname WHERE notesid = :id'); $upd->execute([ ':bucket' => $planned['s3_bucket'], ':s3key' => $planned['s3_key'], ':etag' => $planned['s3_etag'], ':fname' => $plannedUrl, ':id' => $notesId, ]); } else { $upd = $pdo->prepare('UPDATE vtiger_notes SET s3_bucket = :bucket, s3_key = :s3key, s3_etag = :etag WHERE notesid = :id'); $upd->execute([ ':bucket' => $planned['s3_bucket'], ':s3key' => $planned['s3_key'], ':etag' => $planned['s3_etag'], ':id' => $notesId, ]); } $pdo->commit(); echo "[OK] Updated notesid={$notesId} S3 fields" . ($activateExternal === 1 ? " and activated External link." : ".") . "\n"; $rollback = "UPDATE vtiger_notes SET s3_bucket=" . (isset($current['s3_bucket']) && $current['s3_bucket'] !== null ? ("'" . addslashes((string)$current['s3_bucket']) . "'") : 'NULL') . ", s3_key=" . (isset($current['s3_key']) && $current['s3_key'] !== null ? ("'" . addslashes((string)$current['s3_key']) . "'") : 'NULL') . ", s3_etag=" . (isset($current['s3_etag']) && $current['s3_etag'] !== null ? ("'" . addslashes((string)$current['s3_etag']) . "'") : 'NULL'); if ($activateExternal === 1) { $rollback .= ", filelocationtype='" . ($current['filelocationtype'] ?? '') . "'"; $rollback .= ", filename=" . (isset($current['filename']) && $current['filename'] !== null ? ("'" . addslashes((string)$current['filename']) . "'") : 'NULL'); } $rollback .= " WHERE notesid={$notesId};"; echo "[ROLLBACK_SQL] {$rollback}\n"; echo "[BACKUP_FILE] {$backupFile}\n"; return 0; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } fwrite(STDERR, "[FATAL] Update failed: " . $e->getMessage() . "\n"); return 5; } } exit(main());