logsPath = rtrim(WRITEPATH . 'logs', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } public function index(): string { return view('logs/index'); } public function list(): ResponseInterface { $search = trim((string) ($this->request->getGet('q') ?? '')); $page = max(1, (int) ($this->request->getGet('page') ?? 1)); $limit = max(1, min(50, (int) ($this->request->getGet('limit') ?? 10))); $all = $this->listLogFiles($search !== '' ? $search : null); $total = count($all); $offset = ($page - 1) * $limit; $files = array_slice($all, $offset, $limit); $pages = $total > 0 ? (int) ceil($total / $limit) : 0; return $this->respond([ 'status' => 'success', 'files' => $files, 'page' => $page, 'limit' => $limit, 'total' => $total, 'total_pages'=> $pages, 'has_more' => $page < $pages, ]); } public function view(): string|ResponseInterface { $file = $this->resolveLogFile((string) ($this->request->getGet('file') ?? '')); if ($file === null) { return $this->response->setStatusCode(404)->setBody('Log file not found.'); } $content = @file_get_contents($file['path']); if ($content === false) { return $this->response->setStatusCode(500)->setBody('Unable to read log file.'); } $maxBytes = 2 * 1024 * 1024; $truncated = false; if (strlen($content) > $maxBytes) { $content = substr($content, -$maxBytes); $truncated = true; } return view('logs/view', [ 'file' => $file, 'content' => $content, 'truncated' => $truncated, 'maxBytes' => $maxBytes, ]); } public function download(): DownloadResponse|ResponseInterface { $file = $this->resolveLogFile((string) ($this->request->getGet('file') ?? '')); if ($file === null) { return $this->response->setStatusCode(404)->setBody('Log file not found.'); } return $this->response->download($file['path'], null)->setFileName($file['name']); } /** * @return list */ private function listLogFiles(?string $search = null): array { if (! is_dir($this->logsPath)) { return []; } $files = glob($this->logsPath . 'log-*.log') ?: []; $result = []; $needle = $search !== null ? strtolower($search) : null; foreach ($files as $path) { if (! is_file($path)) { continue; } $name = basename($path); if ($needle !== null && ! str_contains(strtolower($name), $needle)) { continue; } $size = (int) filesize($path); $mtime = (int) filemtime($path); $result[] = [ 'name' => $name, 'size' => $size, 'size_human' => $this->formatBytes($size), 'modified' => date('Y-m-d H:i:s', $mtime), 'modified_ts' => $mtime, ]; } usort($result, static fn (array $a, array $b): int => $b['modified_ts'] <=> $a['modified_ts']); return $result; } /** * @return array{name: string, path: string, size: int, size_human: string, modified: string}|null */ private function resolveLogFile(string $name): ?array { $name = basename(trim($name)); if ($name === '' || ! preg_match('/^log-\d{4}-\d{2}-\d{2}\.log$/', $name)) { return null; } $path = $this->logsPath . $name; $realPath = realpath($path); $realLogs = realpath($this->logsPath); if ($realPath === false || $realLogs === false || ! str_starts_with($realPath, $realLogs) || ! is_file($realPath)) { return null; } $size = (int) filesize($realPath); return [ 'name' => $name, 'path' => $realPath, 'size' => $size, 'size_human' => $this->formatBytes($size), 'modified' => date('Y-m-d H:i:s', (int) filemtime($realPath)), ]; } private function formatBytes(int $bytes): string { if ($bytes < 1024) { return $bytes . ' B'; } $units = ['KB', 'MB', 'GB']; $value = (float) $bytes; foreach ($units as $unit) { $value /= 1024; if ($value < 1024) { return round($value, 2) . ' ' . $unit; } } return round($value / 1024, 2) . ' TB'; } }