176 lines
5.1 KiB
PHP
176 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use CodeIgniter\API\ResponseTrait;
|
|
use CodeIgniter\HTTP\DownloadResponse;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
class LogViewerController extends BaseController
|
|
{
|
|
use ResponseTrait;
|
|
|
|
private string $logsPath;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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<array{name: string, size: int, size_human: string, modified: string, modified_ts: int}>
|
|
*/
|
|
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';
|
|
}
|
|
}
|