285 lines
11 KiB
PHP
285 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Phase 2: Update vtiger_notes to point to S3 URLs for given notesids.
|
|
*
|
|
* Usage examples:
|
|
* php update_db_phase2.php --notesid=392975 --dry-run=1
|
|
* php update_db_phase2.php --notesid=392975 --dry-run=0
|
|
* php update_db_phase2.php --notesid=392975 --url="https://s3.../key"
|
|
*
|
|
* Behavior:
|
|
* - Finds S3 URL for the note from migration_results_*.json (fields: copied[]|uploaded[]: { notesid, url, ... })
|
|
* - Backs up current DB row values to backups/phase2_notes_backup_{notesid}_{ts}.json
|
|
* - If --dry-run=1 (default): only prints planned changes and writes backup; no DB changes.
|
|
* - If --dry-run=0: updates vtiger_notes S3 fields. Optional switch to External link via --activate-external=1 (sets filelocationtype='E' and filename to S3 URL).
|
|
*/
|
|
|
|
// Enable debug output via env PHASE2_DEBUG=1
|
|
$__phase2Debug = getenv('PHASE2_DEBUG');
|
|
if ($__phase2Debug === '1') {
|
|
ini_set('display_errors', '1');
|
|
error_reporting(E_ALL);
|
|
} else {
|
|
ini_set('display_errors', '0');
|
|
error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING & ~E_DEPRECATED & ~E_STRICT);
|
|
}
|
|
|
|
// Root discovery (this file lives under crm_extensions/file_storage)
|
|
$rootDir = realpath(__DIR__ . '/../../');
|
|
if ($rootDir === false) {
|
|
fwrite(STDERR, "[FATAL] Cannot resolve project root.\n");
|
|
exit(2);
|
|
}
|
|
|
|
require_once $rootDir . '/config.inc.php';
|
|
|
|
function parseArgs(array $argv): array {
|
|
$result = [
|
|
'notesid' => 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=<id> [--dry-run=1|0] [--url=<S3 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='<key>', s3_etag='<etag>', filelocationtype='E', filename='<S3 URL>' WHERE notesid={$notesId};\n";
|
|
} else {
|
|
echo "[PLAN] UPDATE vtiger_notes SET s3_bucket='{$plannedBucket}', s3_key='<key>', s3_etag='<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());
|
|
|
|
|