Files

165 lines
5.0 KiB
Python

"""
AUR RPC API service for ArchStore.
Handles all interactions with the Arch User Repository via the official RPC API
and yay for installations.
"""
import asyncio
import httpx
from typing import Optional
from utils.sanitize import sanitize_package_name, sanitize_search_query
AUR_RPC_BASE = "https://aur.archlinux.org/rpc/v5"
AUR_PACKAGE_URL = "https://aur.archlinux.org/packages"
async def _run_command(cmd: list[str], timeout: int = 30) -> tuple[str, str, int]:
"""Run a shell command safely."""
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(
proc.communicate(), timeout=timeout
)
return (
stdout.decode("utf-8", errors="replace"),
stderr.decode("utf-8", errors="replace"),
proc.returncode,
)
except asyncio.TimeoutError:
proc.kill()
return "", "Command timed out", -1
except Exception as e:
return "", str(e), -1
async def search_packages(query: str) -> list[dict]:
"""Search AUR packages using the RPC API."""
query = sanitize_search_query(query)
async with httpx.AsyncClient(timeout=15) as client:
try:
response = await client.get(
f"{AUR_RPC_BASE}/search/{query}",
params={"by": "name-desc"},
)
response.raise_for_status()
data = response.json()
if data.get("type") != "search":
return []
packages = []
for pkg in data.get("results", []):
packages.append(_normalize_aur_package(pkg))
# Sort by popularity descending
packages.sort(key=lambda p: p.get("popularity", 0), reverse=True)
return packages[:100] # Limit results
except (httpx.HTTPError, Exception):
return []
async def get_package_info(name: str) -> Optional[dict]:
"""Get detailed info about an AUR package."""
name = sanitize_package_name(name)
async with httpx.AsyncClient(timeout=10) as client:
try:
response = await client.get(
f"{AUR_RPC_BASE}/info",
params={"arg[]": name},
)
response.raise_for_status()
data = response.json()
results = data.get("results", [])
if not results:
return None
pkg = _normalize_aur_package(results[0])
# Check if installed
_, _, code = await _run_command(["pacman", "-Q", name], timeout=5)
pkg["installed"] = code == 0
return pkg
except (httpx.HTTPError, Exception):
return None
async def install_package(name: str) -> dict:
"""Install an AUR package using yay."""
name = sanitize_package_name(name)
stdout, stderr, code = await _run_command(
["yay", "-S", "--noconfirm", name], timeout=600
)
return {
"success": code == 0,
"message": stdout if code == 0 else stderr,
"package": name,
}
async def check_updates() -> list[dict]:
"""Check for AUR package updates."""
stdout, _, code = await _run_command(
["yay", "-Qua"], timeout=30
)
if code != 0:
return []
updates = []
for line in stdout.strip().split("\n"):
if line.strip():
parts = line.split()
if len(parts) >= 4:
updates.append({
"name": parts[0],
"current_version": parts[1],
"new_version": parts[3],
"source": "aur",
})
return updates
async def get_pkgbuild(name: str) -> Optional[str]:
"""Fetch the PKGBUILD content for an AUR package."""
name = sanitize_package_name(name)
async with httpx.AsyncClient(timeout=15) as client:
try:
url = f"https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h={name}"
response = await client.get(url)
if response.status_code == 200:
return response.text
return None
except httpx.HTTPError:
return None
def _normalize_aur_package(pkg: dict) -> dict:
"""Convert AUR RPC response to our standard package format."""
return {
"name": pkg.get("Name", ""),
"version": pkg.get("Version", ""),
"description": pkg.get("Description", ""),
"maintainer": pkg.get("Maintainer", "Orphaned"),
"url": pkg.get("URL", ""),
"votes": pkg.get("NumVotes", 0),
"popularity": pkg.get("Popularity", 0),
"out_of_date": pkg.get("OutOfDate") is not None,
"first_submitted": pkg.get("FirstSubmitted", 0),
"last_modified": pkg.get("LastModified", 0),
"source": "aur",
"repository": "aur",
"aur_url": f"{AUR_PACKAGE_URL}/{pkg.get('Name', '')}",
"package_base": pkg.get("PackageBase", ""),
"installed": False,
}