> /path/maintain.log 2>&1 * * 지우는 것과 지우지 않는 것이 이 도구의 핵심이다: * * 스냅샷 이미지 — 지운다. PIPA 대상 개인정보이고 보존기간이 정해져 있다. * 성능 시계열 — 지운다. 장치당 하루 2,880행이라 공유호스팅 용량을 가장 먼저 잡아먹는다. * nonce — 지운다. 만료된 재전송 방지 토큰은 아무 의미가 없다. * 이벤트 기록 — **기본적으로 지우지 않는다**. 산업안전 조사나 노사 분쟁에서 수년 뒤에도 * 필요할 수 있는 사고 기록이라, 시스템이 임의로 폐기하면 안 된다. * config.php 의 event_retention_days 를 명시적으로 설정한 현장만 정리한다. * * --dry-run 으로 무엇이 지워질지 먼저 확인할 수 있다. */ declare(strict_types=1); require __DIR__ . '/bootstrap.php'; use App\Repository\Pdo\{PdoEventRepository, PdoSnapshotRepository, PdoDeviceMetricRepository}; [$cfg, $pdo] = tool_boot(); $args = tool_args($argv); $dry = isset($args['dry-run']); $now = time(); function say(string $m): void { fwrite(STDOUT, date('c') . ' ' . $m . "\n"); } $done = []; // ---- 스냅샷 이미지 (PIPA) ------------------------------------------------------------ $snapDays = $cfg->snapshotRetentionDays(); if ($snapDays > 0) { $cutoff = $now - $snapDays * 86400; $snaps = new PdoSnapshotRepository($pdo, $cfg->snapshotDir()); if ($dry) { $st = $pdo->prepare('SELECT COUNT(*) FROM move_event_snapshots WHERE created_at < ?'); $st->execute([$cutoff]); $done[] = 'snapshots(dry) ' . (int)$st->fetchColumn(); } else { // 기존 보존 로직을 그대로 쓴다 (파일 + 행을 함께 지운다). 메서드가 없는 구버전 // 저장소에서도 도구가 죽지 않도록 방어한다. $purge = method_exists($snaps, 'purgeOlderThan') ? $snaps->purgeOlderThan($cutoff) : 0; $done[] = "snapshots $purge"; } } else { $done[] = 'snapshots disabled'; } // ---- 성능 시계열 --------------------------------------------------------------------- $metricDays = $cfg->metricRetentionDays(); if ($metricDays > 0) { $cutoff = $now - $metricDays * 86400; if ($dry) { $st = $pdo->prepare('SELECT COUNT(*) FROM move_device_metrics WHERE ts < ?'); $st->execute([$cutoff]); $done[] = 'metrics(dry) ' . (int)$st->fetchColumn(); } else { $done[] = 'metrics ' . (new PdoDeviceMetricRepository($pdo))->prune($cutoff); } } // ---- nonce --------------------------------------------------------------------------- if ($dry) { $st = $pdo->prepare('SELECT COUNT(*) FROM move_nonces WHERE expires_at < ?'); $st->execute([$now]); $done[] = 'nonces(dry) ' . (int)$st->fetchColumn(); } else { $st = $pdo->prepare('DELETE FROM move_nonces WHERE expires_at < ?'); $st->execute([$now]); $done[] = 'nonces ' . $st->rowCount(); } // ---- rate limit 버킷 ------------------------------------------------------------------ if (!$dry) { $st = $pdo->prepare('DELETE FROM move_rate_limits WHERE window_start < ?'); $st->execute([intdiv($now, 60) - 1440]); // 하루치만 남긴다 $done[] = 'rate_limits ' . $st->rowCount(); } // ---- 이벤트 기록 (기본 보존) ---------------------------------------------------------- $eventDays = $cfg->eventRetentionDays(); if ($eventDays > 0) { $cutoff = (float)($now - $eventDays * 86400); if ($dry) { $st = $pdo->prepare('SELECT COUNT(*) FROM move_events WHERE occurred_at < ?'); $st->execute([sprintf('%.6f', $cutoff)]); $done[] = 'events(dry) ' . (int)$st->fetchColumn(); } else { // 한 번에 다 지우면 공유호스팅에서 락이 길어져 ingest 가 밀린다. 배치로 끊는다. $events = new PdoEventRepository($pdo); $total = 0; for ($i = 0; $i < 20; $i++) { $n = $events->purgeOlderThan($cutoff, 500); $total += $n; if ($n === 0) break; } $done[] = "events $total"; } } else { $done[] = 'events kept (retention disabled — 사고 기록은 기본 영구 보존)'; } // ---- 용량 경고 ------------------------------------------------------------------------- $minFree = $cfg->storageMinFreeMb(); if ($minFree > 0 && is_dir($cfg->snapshotDir())) { $free = @disk_free_space($cfg->snapshotDir()); if ($free !== false && $free !== null) { $freeMb = (int)floor($free / 1048576); if ($freeMb < $minFree) { // 정리를 마친 뒤에도 부족하다면 사람이 개입해야 한다. 종료 코드로도 알린다. say("WARNING: 스냅샷 볼륨 여유 {$freeMb}MB (기준 {$minFree}MB) — 보존기간을 줄이거나 용량을 확보하세요."); say(implode(' · ', $done) . ($dry ? ' (dry-run)' : '')); exit(3); } } } say(implode(' · ', $done) . ($dry ? ' (dry-run)' : '')); exit(0);