Files
crm.clientright.ru/crm_extensions/file_storage/update_db_phase2.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());