*/ private const KINDS = ['c2', 'xxbb']; private const PER_PAGE = 40; private const MAX_PER_PAGE = 100; /** Display cap so a single keystore/census blob cannot blow the HTML layout. */ private const MAX_ENTRY_CHARS = 16384; private const CHUNK_BYTES = 262144; /** Bound the backward-scan carry if a record has no separator. */ private const MAX_CARRY_BYTES = 524288; public function index(Request $request) { $kind = $this->kind($request); $path = trim((string) $request->query('path', '')); $device = trim((string) $request->query('device', '')); $groups = []; foreach (self::KINDS as $k) { $groups[$k] = $this->listFiles($k); } return view('admin.logs.index', compact('groups', 'kind', 'path', 'device')); } public function show(Request $request, string $kind, string $file) { $kind = in_array($kind, self::KINDS, true) ? $kind : 'c2'; $safe = basename($file); if (! preg_match('/^\d{8}\.log$/', $safe)) { abort(404); } $full = public_path('log/'.$kind.'/'.$safe); abort_unless(is_file($full), 404); $path = trim((string) $request->query('path', '')); $device = trim((string) $request->query('device', '')); $page = max(1, (int) $request->query('page', 1)); $perPage = (int) $request->query('per_page', self::PER_PAGE); $perPage = min(self::MAX_PER_PAGE, max(10, $perPage)); $read = $this->tailEntries($full, $path, $device, $page, $perPage); return view('admin.logs.show', [ 'kind' => $kind, 'file' => $safe, 'entries' => $read['entries'], 'path' => $path, 'device' => $device, 'page' => $read['page'], 'perPage' => $read['perPage'], 'hasOlder' => $read['hasOlder'], 'hasNewer' => $read['hasNewer'], 'fileSize' => $read['fileSize'], ]); } private function kind(Request $request): string { $kind = (string) $request->query('kind', 'c2'); return in_array($kind, self::KINDS, true) ? $kind : 'c2'; } /** * Newest-first page of records, scanning backward so a multi-MB day file * never lands in memory as one string. * * @return array{ * entries: list, * page: int, * perPage: int, * hasOlder: bool, * hasNewer: bool, * fileSize: int * } */ private function tailEntries(string $full, string $path, string $device, int $page, int $perPage): array { $fileSize = (int) filesize($full); $skip = ($page - 1) * $perPage; $want = $skip + $perPage + 1; $matched = []; $fh = fopen($full, 'rb'); if ($fh === false) { return [ 'entries' => [], 'page' => $page, 'perPage' => $perPage, 'hasOlder' => false, 'hasNewer' => $page > 1, 'fileSize' => $fileSize, ]; } try { $pos = $fileSize; $carry = ''; while ($pos > 0 && count($matched) < $want) { $read = (int) min(self::CHUNK_BYTES, $pos); $pos -= $read; if (fseek($fh, $pos) !== 0) { break; } $chunk = fread($fh, $read); if (! is_string($chunk) || $chunk === '') { break; } $data = $chunk.$carry; if (strlen($data) > self::MAX_CARRY_BYTES) { $data = substr($data, -self::MAX_CARRY_BYTES); } $parts = preg_split("/\r\n\r\n/", $data) ?: [$data]; $carry = $parts[0]; for ($i = count($parts) - 1; $i >= 1; $i--) { if ($this->collectEntry($matched, $parts[$i], $path, $device, $want)) { break 2; } } } if (count($matched) < $want && $pos === 0 && $carry !== '') { $this->collectEntry($matched, $carry, $path, $device, $want); } } finally { fclose($fh); } $hasOlder = count($matched) > $skip + $perPage; $entries = array_slice($matched, $skip, $perPage); return [ 'entries' => $entries, 'page' => $page, 'perPage' => $perPage, 'hasOlder' => $hasOlder, 'hasNewer' => $page > 1, 'fileSize' => $fileSize, ]; } /** * @param list $matched */ private function collectEntry(array &$matched, string $chunk, string $path, string $device, int $want): bool { $entry = trim($chunk); if ($entry === '') { return false; } if ($path !== '' && ! str_contains($entry, $path)) { return false; } if ($device !== '' && ! str_contains($entry, $device)) { return false; } $matched[] = $this->truncateEntry($entry); return count($matched) >= $want; } private function truncateEntry(string $entry): string { if (strlen($entry) <= self::MAX_ENTRY_CHARS) { return $entry; } return substr($entry, 0, self::MAX_ENTRY_CHARS)."\n… (truncated)"; } /** * @return list */ private function listFiles(string $kind): array { $dir = public_path('log/'.$kind); $files = []; if (! is_dir($dir)) { return $files; } foreach (File::files($dir) as $file) { if ($file->getExtension() !== 'log') { continue; } $files[] = [ 'name' => $file->getFilename(), 'size' => $file->getSize(), 'mtime' => date('Y-m-d H:i:s', $file->getMTime()), ]; } usort($files, fn ($a, $b) => strcmp($b['name'], $a['name'])); return $files; } }