mirror of
https://github.com/0x5t4l1n/AURHub.git
synced 2026-05-26 11:25:50 +00:00
Initial commit: ArchStore package manager for Arch Linux
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user