같은 cafe24 DB의 다른 시스템 테이블과 혼동/충돌을 막기 위해 5개 테이블을 leak_ 네임스페이스로 격리: sensor_log→leak_sensor_log, sensor_status→leak_sensor_status, sensor_metric→leak_sensor_metric, sms_log→leak_sms_log, ingest_nonce→leak_ingest_nonce (leak_incident/leak_photo는 이미 접두사). 컬럼명은 불변(sensor_id, is_leak 등) — 테이블명만 변경. sql/migration_add_leak.sql, migration_drop_leak.sql 삭제: 리네임 후 구버전 무접두 테이블 대상이라 실행 즉시 에러. 신규 설치는 schema_sht30.sql 하나로 완결. BREAKING CHANGE: 기존 무접두 테이블 배포와 호환 안 됨(신규 설치 전용). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821XP36Q6t9eWKTPSEumnC
122 lines
5 KiB
PHP
122 lines
5 KiB
PHP
<?php
|
|
// =============================================================================
|
|
// retention_cleanup.php - 로그/측정 보관기간 정리 (SMS 로그, 센서 로그, 측정 이력,
|
|
// 관리자 감사로그)
|
|
//
|
|
// Usage:
|
|
// php retention_cleanup.php --dry-run
|
|
// php retention_cleanup.php --execute
|
|
// =============================================================================
|
|
|
|
if (php_sapi_name() !== 'cli') {
|
|
http_response_code(403);
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
echo "Forbidden\n";
|
|
exit;
|
|
}
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
require_once __DIR__ . '/admin_security.php';
|
|
|
|
$execute = in_array('--execute', $argv, true);
|
|
$dryRun = !$execute || in_array('--dry-run', $argv, true);
|
|
|
|
function retention_cutoff(int $days): string {
|
|
return date('Y-m-d H:i:s', time() - ($days * 86400));
|
|
}
|
|
|
|
function retention_count(PDO $db, string $sql, array $params): int {
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return (int)$stmt->fetchColumn();
|
|
}
|
|
|
|
function retention_delete(PDO $db, string $sql, array $params, bool $dryRun): int {
|
|
if ($dryRun) {
|
|
return 0;
|
|
}
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return $stmt->rowCount();
|
|
}
|
|
|
|
$summary = [
|
|
'mode' => $dryRun ? 'dry-run' : 'execute',
|
|
'sms_log_retention_days' => SMS_LOG_RETENTION_DAYS,
|
|
'sensor_log_retention_days' => SENSOR_LOG_RETENTION_DAYS,
|
|
'sensor_metric_retention_days' => SENSOR_METRIC_RETENTION_DAYS,
|
|
'admin_audit_retention_days' => ADMIN_AUDIT_RETENTION_DAYS,
|
|
'leak_sms_log' => ['matched' => 0, 'rows_deleted' => 0],
|
|
'leak_sensor_log' => ['matched' => 0, 'rows_deleted' => 0],
|
|
'leak_sensor_metric' => ['matched' => 0, 'rows_deleted' => 0],
|
|
'admin_audit' => ['kept_lines' => null, 'removed_lines' => null],
|
|
];
|
|
|
|
try {
|
|
$db = get_db();
|
|
} catch (Throwable $e) {
|
|
$summary['error'] = 'DB connection failed: ' . $e->getMessage();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
|
exit(2);
|
|
}
|
|
|
|
$smsCutoff = retention_cutoff(SMS_LOG_RETENTION_DAYS);
|
|
$summary['leak_sms_log']['matched'] = retention_count($db, "SELECT COUNT(*) FROM leak_sms_log WHERE sent_at < :cutoff", [':cutoff' => $smsCutoff]);
|
|
$summary['leak_sms_log']['rows_deleted'] = retention_delete($db, "DELETE FROM leak_sms_log WHERE sent_at < :cutoff", [':cutoff' => $smsCutoff], $dryRun);
|
|
|
|
$sensorCutoff = retention_cutoff(SENSOR_LOG_RETENTION_DAYS);
|
|
$summary['leak_sensor_log']['matched'] = retention_count($db, "SELECT COUNT(*) FROM leak_sensor_log WHERE created_at < :cutoff", [':cutoff' => $sensorCutoff]);
|
|
$summary['leak_sensor_log']['rows_deleted'] = retention_delete($db, "DELETE FROM leak_sensor_log WHERE created_at < :cutoff", [':cutoff' => $sensorCutoff], $dryRun);
|
|
|
|
$metricCutoff = retention_cutoff(SENSOR_METRIC_RETENTION_DAYS);
|
|
$summary['leak_sensor_metric']['matched'] = retention_count($db, "SELECT COUNT(*) FROM leak_sensor_metric WHERE created_at < :cutoff", [':cutoff' => $metricCutoff]);
|
|
$summary['leak_sensor_metric']['rows_deleted'] = retention_delete($db, "DELETE FROM leak_sensor_metric WHERE created_at < :cutoff", [':cutoff' => $metricCutoff], $dryRun);
|
|
|
|
$auditPath = admin_audit_log_path();
|
|
if (is_file($auditPath)) {
|
|
$cutoffTs = time() - (ADMIN_AUDIT_RETENTION_DAYS * 86400);
|
|
$kept = [];
|
|
$removed = 0;
|
|
// 감사로그 회전은 동시 append(admin_audit_log: file_put_contents FILE_APPEND|LOCK_EX)와
|
|
// 경합한다. 읽기+필터+재기록 전체를 배타 락(flock LOCK_EX)으로 묶어, 회전 도중
|
|
// 들어온 기록이 유실되지 않게 한다(append 는 락 해제까지 대기 후 새 파일에 추가).
|
|
$fh = fopen($auditPath, 'c+');
|
|
if ($fh !== false && flock($fh, LOCK_EX)) {
|
|
while (($line = fgets($fh)) !== false) {
|
|
$line = rtrim($line, "\r\n");
|
|
if ($line === '') {
|
|
continue;
|
|
}
|
|
$record = json_decode($line, true);
|
|
$recordTs = isset($record['time']) ? strtotime((string)$record['time']) : false;
|
|
if ($recordTs !== false && $recordTs < $cutoffTs) {
|
|
$removed++;
|
|
continue;
|
|
}
|
|
$kept[] = $line;
|
|
}
|
|
if (!$dryRun && $removed > 0) {
|
|
ftruncate($fh, 0);
|
|
rewind($fh);
|
|
fwrite($fh, $kept ? implode(PHP_EOL, $kept) . PHP_EOL : '');
|
|
fflush($fh);
|
|
}
|
|
flock($fh, LOCK_UN);
|
|
}
|
|
if ($fh !== false) {
|
|
fclose($fh);
|
|
}
|
|
$summary['admin_audit']['kept_lines'] = count($kept);
|
|
$summary['admin_audit']['removed_lines'] = $removed;
|
|
}
|
|
|
|
admin_audit_log('retention_cleanup', 'system', true, [
|
|
'mode' => $summary['mode'],
|
|
'sms_matches' => $summary['leak_sms_log']['matched'],
|
|
'sensor_matches' => $summary['leak_sensor_log']['matched'],
|
|
'metric_matches' => $summary['leak_sensor_metric']['matched'],
|
|
]);
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL;
|