333 lines
14 KiB
PHP
333 lines
14 KiB
PHP
<?php
|
||
/**
|
||
* n8n S3 Migration Endpoint (JSON Output)
|
||
*
|
||
* Этот скрипт предназначен для вызова из n8n через SSH
|
||
* для автоматической миграции новых файлов в S3
|
||
* Выводит только JSON для удобного парсинга
|
||
*/
|
||
|
||
ini_set('memory_limit', '512M');
|
||
set_time_limit(0);
|
||
date_default_timezone_set('Europe/Moscow');
|
||
|
||
// Параметры по умолчанию
|
||
$defaults = [
|
||
'limit' => 20,
|
||
'dry_run' => 0
|
||
];
|
||
|
||
// Получение параметров из командной строки или переменных окружения
|
||
$limit = isset($argv[1]) ? (int)$argv[1] : (int)($_ENV['LIMIT'] ?? $defaults['limit']);
|
||
$dryRun = isset($argv[2]) ? (int)$argv[2] : (int)($_ENV['DRY_RUN'] ?? $defaults['dry_run']);
|
||
|
||
// Валидация параметров
|
||
$limit = max(1, $limit); // Минимум 1 файл
|
||
$dryRun = $dryRun ? 1 : 0;
|
||
|
||
// Подключение к базе данных
|
||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||
$mysqli = new mysqli($dbconfig['db_server'], $dbconfig['db_username'], $dbconfig['db_password'], $dbconfig['db_name']);
|
||
if ($mysqli->connect_error) {
|
||
echo json_encode([
|
||
'status' => 'error',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'error' => 'Database connection failed: ' . $mysqli->connect_error,
|
||
'exit_code' => 3
|
||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||
exit(3);
|
||
}
|
||
$mysqli->set_charset("utf8");
|
||
|
||
// Подключение S3 сервиса
|
||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/include/Storage/S3StorageService.php';
|
||
$s3Service = new S3StorageService();
|
||
$s3Bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
|
||
|
||
// Поиск локальных файлов без S3 метаданных (исключая очищенные файлы)
|
||
$query = "SELECT n.notesid, n.filename, n.filesize, n.filetype, n.filelocationtype,
|
||
a.attachmentsid, a.path, a.storedname
|
||
FROM vtiger_notes n
|
||
INNER JOIN vtiger_seattachmentsrel s ON s.crmid = n.notesid
|
||
INNER JOIN vtiger_attachments a ON a.attachmentsid = s.attachmentsid
|
||
WHERE (n.s3_key IS NULL OR n.s3_key = '')
|
||
AND n.filelocationtype = 'I'
|
||
AND NOT (n.filename IS NULL OR n.filename = '')
|
||
AND NOT (n.filesize = 0)
|
||
AND NOT (n.filename LIKE 'file_15_%')
|
||
AND NOT (n.filename = '7 заявление потребителя')
|
||
ORDER BY n.notesid ASC
|
||
LIMIT ?";
|
||
|
||
$stmt = $mysqli->prepare($query);
|
||
if (!$stmt) {
|
||
echo json_encode([
|
||
'status' => 'error',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'error' => 'Query prepare failed: ' . $mysqli->error,
|
||
'exit_code' => 3
|
||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||
exit(3);
|
||
}
|
||
|
||
$stmt->bind_param('i', $limit);
|
||
$stmt->execute();
|
||
$result = $stmt->get_result();
|
||
|
||
$totalProcessed = 0;
|
||
$successfullyMigrated = 0;
|
||
$failedMigrations = 0;
|
||
$skippedFiles = 0;
|
||
$migratedFiles = [];
|
||
$problematicFiles = [];
|
||
|
||
if ($result->num_rows > 0) {
|
||
while ($row = $result->fetch_assoc()) {
|
||
$totalProcessed++;
|
||
$notesid = $row['notesid'];
|
||
$filename = $row['filename'];
|
||
$attachmentsid = $row['attachmentsid'];
|
||
$path = $row['path'];
|
||
$storedname = $row['storedname'];
|
||
|
||
// Поиск локальных файлов (используем данные из vtiger_attachments)
|
||
$storageDir = '/var/www/fastuser/data/www/crm.clientright.ru/';
|
||
$localFilePath = null;
|
||
|
||
// Основной путь из vtiger_attachments
|
||
$mainPath = $storageDir . $path . $attachmentsid . '_' . $storedname;
|
||
if (file_exists($mainPath) && is_readable($mainPath)) {
|
||
$localFilePath = $mainPath;
|
||
}
|
||
|
||
// Если не найден по основному пути, используем старую логику поиска
|
||
if ($localFilePath === null) {
|
||
// Основные пути для поиска (приоритетные)
|
||
$priorityPaths = [
|
||
$storageDir . 'storage/' . $filename, // Прямо в корне storage
|
||
$storageDir . 'storage/' . $notesid . '_' . $filename, // С префиксом notesid
|
||
];
|
||
|
||
// Дополнительный поиск по имени файла (без ID)
|
||
$cleanFilename = $filename;
|
||
if (preg_match('/^\d+_(.+)$/', $filename, $matches)) {
|
||
$cleanFilename = $matches[1];
|
||
$priorityPaths[] = $storageDir . 'storage/' . $cleanFilename;
|
||
}
|
||
|
||
// Проверяем приоритетные пути
|
||
foreach ($priorityPaths as $path) {
|
||
if (file_exists($path) && is_readable($path)) {
|
||
$localFilePath = $path;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Если не найден в приоритетных путях, ищем в структурированном хранилище
|
||
if ($localFilePath === null) {
|
||
// Поиск по годам (только текущий и предыдущий)
|
||
$currentYear = date('Y');
|
||
for ($year = $currentYear - 1; $year <= $currentYear; $year++) {
|
||
$yearPath = $storageDir . 'storage/' . $year . '/';
|
||
if (!is_dir($yearPath)) continue;
|
||
|
||
// Поиск по месяцам (только последние 3 месяца)
|
||
$months = ['January', 'February', 'March', 'April', 'May', 'June',
|
||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||
$currentMonth = (int)date('n') - 1; // 0-based
|
||
|
||
for ($i = 0; $i < 3; $i++) {
|
||
$monthIndex = ($currentMonth - $i + 12) % 12;
|
||
$month = $months[$monthIndex];
|
||
$monthPath = $yearPath . $month . '/';
|
||
|
||
if (!is_dir($monthPath)) continue;
|
||
|
||
// Проверяем основные варианты в месяце
|
||
$monthPaths = [
|
||
$monthPath . $filename,
|
||
$monthPath . $notesid . '_' . $filename,
|
||
];
|
||
|
||
// Добавляем поиск по чистому имени файла
|
||
if (isset($cleanFilename) && $cleanFilename !== $filename) {
|
||
$monthPaths[] = $monthPath . $cleanFilename;
|
||
}
|
||
|
||
// Поиск по близким ID (±10 от текущего)
|
||
for ($idOffset = -10; $idOffset <= 10; $idOffset++) {
|
||
$searchId = $notesid + $idOffset;
|
||
$monthPaths[] = $monthPath . $searchId . '_' . $filename;
|
||
if (isset($cleanFilename) && $cleanFilename !== $filename) {
|
||
$monthPaths[] = $monthPath . $searchId . '_' . $cleanFilename;
|
||
}
|
||
}
|
||
|
||
foreach ($monthPaths as $path) {
|
||
if (file_exists($path) && is_readable($path)) {
|
||
$localFilePath = $path;
|
||
break 3; // Выходим из всех циклов
|
||
}
|
||
}
|
||
|
||
// Поиск по неделям (все 4 недели)
|
||
for ($week = 1; $week <= 4; $week++) {
|
||
$weekPath = $monthPath . 'week' . $week . '/';
|
||
if (!is_dir($weekPath)) continue;
|
||
|
||
$weekPaths = [
|
||
$weekPath . $filename,
|
||
$weekPath . $notesid . '_' . $filename,
|
||
];
|
||
|
||
foreach ($weekPaths as $path) {
|
||
if (file_exists($path) && is_readable($path)) {
|
||
$localFilePath = $path;
|
||
break 4; // Выходим из всех циклов
|
||
}
|
||
}
|
||
|
||
// Поиск по близким ID в неделях
|
||
for ($idOffset = -10; $idOffset <= 10; $idOffset++) {
|
||
$searchId = $notesid + $idOffset;
|
||
$closeIdPath = $weekPath . $searchId . '_' . $filename;
|
||
if (file_exists($closeIdPath) && is_readable($closeIdPath)) {
|
||
$localFilePath = $closeIdPath;
|
||
break 4;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Проверка существования файла
|
||
if ($localFilePath === null) {
|
||
// Помечаем файл как проблемный
|
||
$problematic_filename = "[PROBLEM_" . date('Y-m-d_H-i-s') . "] " . $filename;
|
||
|
||
if (!$dryRun) {
|
||
$update_query = "UPDATE vtiger_notes SET filename = ? WHERE notesid = ?";
|
||
$update_stmt = $mysqli->prepare($update_query);
|
||
if ($update_stmt) {
|
||
$update_stmt->bind_param('si', $problematic_filename, $notesid);
|
||
if ($update_stmt->execute()) {
|
||
$problematicFiles[] = [
|
||
'notesid' => $notesid,
|
||
'original_filename' => $filename,
|
||
'problematic_filename' => $problematic_filename,
|
||
'reason' => 'Physical file not found'
|
||
];
|
||
$skippedFiles++;
|
||
} else {
|
||
$failedMigrations++;
|
||
}
|
||
$update_stmt->close();
|
||
} else {
|
||
$failedMigrations++;
|
||
}
|
||
} else {
|
||
$problematicFiles[] = [
|
||
'notesid' => $notesid,
|
||
'original_filename' => $filename,
|
||
'reason' => 'Physical file not found (dry run)'
|
||
];
|
||
$skippedFiles++;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (!$dryRun) {
|
||
try {
|
||
// Загрузка в S3 (используем правильную сигнатуру)
|
||
$s3_result = $s3Service->put($localFilePath, $notesid, $filename);
|
||
|
||
if ($s3_result && isset($s3_result['url'])) {
|
||
$s3_etag = isset($s3_result['etag']) ? trim($s3_result['etag'], '"') : '';
|
||
$s3_url = $s3_result['url'];
|
||
$s3_key = $s3_result['key'];
|
||
|
||
// Обновление базы данных
|
||
$update_query = "UPDATE vtiger_notes SET
|
||
s3_key = ?,
|
||
s3_bucket = ?,
|
||
s3_etag = ?,
|
||
filename = ?,
|
||
filelocationtype = 'E'
|
||
WHERE notesid = ?";
|
||
|
||
$update_stmt = $mysqli->prepare($update_query);
|
||
if (!$update_stmt) {
|
||
$failedMigrations++;
|
||
continue;
|
||
}
|
||
|
||
$update_stmt->bind_param('ssssi', $s3_key, $s3Bucket, $s3_etag, $s3_url, $notesid);
|
||
|
||
if ($update_stmt->execute()) {
|
||
$migratedFiles[] = [
|
||
'notesid' => $notesid,
|
||
'filename' => $filename,
|
||
's3_url' => $s3_url,
|
||
's3_key' => $s3_key,
|
||
'local_file_deleted' => unlink($localFilePath)
|
||
];
|
||
$successfullyMigrated++;
|
||
} else {
|
||
$failedMigrations++;
|
||
}
|
||
$update_stmt->close();
|
||
} else {
|
||
$failedMigrations++;
|
||
}
|
||
} catch (Exception $e) {
|
||
$failedMigrations++;
|
||
}
|
||
} else {
|
||
$migratedFiles[] = [
|
||
'notesid' => $notesid,
|
||
'filename' => $filename,
|
||
'local_file_path' => $localFilePath,
|
||
'dry_run' => true
|
||
];
|
||
$successfullyMigrated++;
|
||
}
|
||
}
|
||
}
|
||
|
||
$stmt->close();
|
||
$mysqli->close();
|
||
|
||
// JSON вывод для n8n
|
||
$jsonOutput = [
|
||
'status' => 'success',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'summary' => [
|
||
'total_processed' => $totalProcessed,
|
||
'successfully_migrated' => $successfullyMigrated,
|
||
'failed' => $failedMigrations,
|
||
'marked_problematic' => $skippedFiles,
|
||
'dry_run' => $dryRun ? true : false
|
||
],
|
||
'migrated_files' => $migratedFiles,
|
||
'problematic_files' => $problematicFiles,
|
||
'exit_code' => 0
|
||
];
|
||
|
||
// Определяем статус и код выхода
|
||
if ($failedMigrations > 0) {
|
||
$jsonOutput['status'] = 'partial_error';
|
||
$jsonOutput['exit_code'] = 2;
|
||
} elseif ($skippedFiles > 0) {
|
||
$jsonOutput['status'] = 'warning';
|
||
$jsonOutput['exit_code'] = 1;
|
||
}
|
||
|
||
// Выводим JSON
|
||
echo json_encode($jsonOutput, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||
|
||
// Возвращаем код выхода для n8n
|
||
exit($jsonOutput['exit_code']);
|
||
?>
|