*/ function podman_read_installed_versions(): array { $path = '/usr/local/share/unraid-podman/installed-versions.env'; if (!is_readable($path)) { return []; } $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $out = []; foreach ($lines as $line) { $line = trim($line); if ($line === '' || str_starts_with($line, '#')) { continue; } if (preg_match('/^([A-Z_][A-Z0-9_]*)="?([^"]*)"?$/', $line, $m)) { $out[$m[1]] = $m[2]; } } return $out; } function podman_format_bytes(int $bytes): string { if ($bytes <= 0) { return '0 B'; } $units = ['B', 'KB', 'MB', 'GB', 'TB']; $i = (int) floor(log($bytes, 1024)); $i = min($i, count($units) - 1); $value = $bytes / (1024 ** $i); return sprintf($value >= 100 || $i === 0 ? '%.0f %s' : '%.1f %s', $value, $units[$i]); } /** Formats a duration in seconds as a compact "14d 6h" / "6h 12m" / "38m" style string. */ function podman_format_duration(int $seconds): string { if ($seconds < 60) { return $seconds . 's'; } $days = intdiv($seconds, 86400); $hours = intdiv($seconds % 86400, 3600); $minutes = intdiv($seconds % 3600, 60); if ($days > 0) { return "{$days}d {$hours}h"; } if ($hours > 0) { return "{$hours}h {$minutes}m"; } return "{$minutes}m"; } /** * Converts a libpod timestamp to a Unix timestamp, or null if unparsable. * Accepts both the RFC3339 strings `inspect`-style endpoints return and * the raw Unix-epoch integers `list`-style endpoints return for the same * logical field (e.g. containers/json's StartedAt/Created vs. inspect's) * — verified live against a real podman system service, not just docs. */ function podman_parse_time(string|int|null $value): ?int { if ($value === null || $value === '') { return null; } if (is_int($value)) { return $value > 0 ? $value : null; } if (str_starts_with($value, '0001-01-01')) { return null; } $ts = strtotime($value); return $ts === false ? null : $ts; } /** * Shortens a full image/container ID to the 12-character form Docker/ * Podman CLIs conventionally display, matching what users expect to see * (and copy-paste into `podman inspect `, which accepts short IDs). */ function podman_short_id(string $id): string { // Some APIs prefix with "sha256:" for image IDs. $id = str_starts_with($id, 'sha256:') ? substr($id, 7) : $id; return substr($id, 0, 12); } /** * Sends a JSON response and terminates the request — every ajax/*.php * endpoint's single exit point, so response shape (envelope with "ok" and * either "data" or "error") is consistent for the frontend's shared AJAX * helper (javascript/app.js's request() function) to rely on. */ function podman_json_response(mixed $data, int $httpStatus = 200): never { http_response_code($httpStatus); header('Content-Type: application/json'); echo json_encode(['ok' => $httpStatus < 400, 'data' => $data], JSON_UNESCAPED_SLASHES); exit; } function podman_json_error(string $message, int $httpStatus = 500): never { http_response_code($httpStatus); header('Content-Type: application/json'); echo json_encode(['ok' => false, 'error' => $message], JSON_UNESCAPED_SLASHES); exit; } /** * Reads and JSON-decodes the request body for POST/DELETE actions that * take parameters (e.g. {"id": "..."}), with a friendly error on * malformed input instead of a fatal error deep inside an endpoint. * * @return array */ function podman_read_json_body(): array { $raw = file_get_contents('php://input'); if ($raw === false || trim($raw) === '') { return []; } $decoded = json_decode($raw, true); if (!is_array($decoded)) { podman_json_error('Request body must be a JSON object', 400); } return $decoded; }