Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 7 additions & 23 deletions astrbot/cli/utils/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,36 +63,20 @@ def download_repository(

Raises:
ValueError: If the repository URL is unsupported or invalid.
httpx.HTTPError: If repository metadata or the archive cannot be downloaded.
httpx.HTTPError: If the repository archive cannot be downloaded.
"""
from astrbot.core.repository import GitHubRepository

temp_dir = Path(tempfile.mkdtemp())
try:
repository = GitHubRepository.parse(url)
if not repository.branch:
try:
with httpx.Client(follow_redirects=True, trust_env=True) as client:
response = client.get(repository.default_branch_api_url)
response.raise_for_status()
default_branch = str(
response.json().get("default_branch") or ""
).strip()
except httpx.HTTPError as exc:
default_branch = ""
click.echo(
f"Failed to resolve the default GitHub branch: {exc}. Trying main."
)
branch = default_branch or "main"
repository = GitHubRepository(
repository.owner,
repository.name,
branch,
)

reference = (
f"branch {repository.branch}"
if repository.branch
else "default reference HEAD"
)
click.echo(
f"Downloading {repository.owner}/{repository.name} "
f"from GitHub branch {repository.branch}"
f"Downloading {repository.owner}/{repository.name} from GitHub {reference}"
)
download_url = repository.archive_url
if proxy:
Expand Down
28 changes: 8 additions & 20 deletions astrbot/core/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class GitHubRepository:
Args:
owner: GitHub repository owner.
name: GitHub repository name.
branch: Explicit or resolved source branch.
branch: Explicit source branch, or None to use the default HEAD reference.
"""

owner: str
Expand Down Expand Up @@ -91,22 +91,15 @@ def parse(cls, url: str) -> "GitHubRepository":
raise ValueError("Invalid GitHub repository URL")
return cls(owner, name, branch)

@property
def default_branch_api_url(self) -> str:
"""Return the GitHub repository metadata API URL."""
owner = quote(self.owner, safe="")
name = quote(self.name, safe="")
return f"https://api.github.com/repos/{owner}/{name}"

@property
def archive_url(self) -> str:
"""Return the source ZIP URL for the resolved branch.
"""Return the source ZIP URL for a branch or the default HEAD reference.

Raises:
ValueError: If the source branch has not been resolved.
Returns:
Branch archive URL, or the repository HEAD archive when unspecified.
"""
if not self.branch:
raise ValueError("GitHub source branch has not been resolved")
return self.revision_archive_url("HEAD")
owner = quote(self.owner, safe="")
name = quote(self.name, safe="")
branch = quote(self.branch, safe="/")
Expand All @@ -130,22 +123,17 @@ def revision_archive_url(
return f"https://github.com/{owner}/{name}/archive/{encoded_revision}.zip"

def raw_file_url(self, path: str) -> str:
"""Return a raw file URL in the resolved branch.
"""Return a raw file URL in a branch or the default HEAD reference.

Args:
path: Repository-relative file path.

Returns:
GitHub raw file URL.

Raises:
ValueError: If the source branch has not been resolved.
GitHub raw file URL, using HEAD when no branch is specified.
"""
if not self.branch:
raise ValueError("GitHub source branch has not been resolved")
owner = quote(self.owner, safe="")
name = quote(self.name, safe="")
branch = quote(self.branch, safe="/")
branch = quote(self.branch or "HEAD", safe="/")
encoded_path = quote(path.lstrip("/"), safe="/")
return (
f"https://raw.githubusercontent.com/{owner}/{name}/{branch}/{encoded_path}"
Expand Down
3 changes: 2 additions & 1 deletion astrbot/core/star/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from astrbot.core import logger
from astrbot.core.repository import (
GitHubRepository,
GitUnavailableError,
normalize_repository_url,
parse_repository_url,
Expand Down Expand Up @@ -140,7 +141,7 @@ async def inspect_repository(
await self._clone_repository(normalized_url, checkout_path)
metadata = self.inspect_plugin_directory(checkout_path)["metadata"]
else:
source = await self._resolve_repository_source(normalized_url)
source = GitHubRepository.parse(normalized_url)
proxy = proxy.strip().removesuffix("/")
async with self._create_httpx_client(
timeout=PLUGIN_REPOSITORY_TIMEOUT_SECONDS
Expand Down
93 changes: 18 additions & 75 deletions astrbot/core/zip_updater.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import inspect
import os
import re
Expand Down Expand Up @@ -59,7 +60,6 @@ def __init__(
Args:
verify: TLS certificate verification configuration for HTTPX.
"""
self._rm_on_error = on_error
self._httpx_verify = certifi.where() if verify is None else verify

def _create_httpx_client(self, timeout: float = 30.0) -> httpx.AsyncClient:
Expand All @@ -76,72 +76,6 @@ def _truncate_response_body(body: str, max_len: int = 1000) -> str:
return body
return body[:max_len] + "...[truncated]"

async def _fetch_repository_default_branch(
self,
repository: GitHubRepository,
) -> str | None:
"""Fetch the default branch for a repository.

Args:
repository: Parsed GitHub repository.

Returns:
The default branch name, or None if it cannot be resolved.
"""
url = repository.default_branch_api_url
try:
async with self._create_httpx_client(timeout=10.0) as client:
response = await client.get(url)
response.raise_for_status()
repo_info = response.json()
except Exception as exc:
logger.debug(
"Failed to get the default %s branch for %s/%s: %s",
"github",
repository.owner,
repository.name,
exc,
)
return None

default_branch = str(repo_info.get("default_branch") or "").strip()
return default_branch or None

async def _resolve_repository_source(
self,
repo_url: str,
) -> GitHubRepository:
"""Resolve a repository URL to a downloadable source archive.

Args:
repo_url: Repository URL, optionally with an explicit tree branch.

Returns:
Resolved provider adapter and repository branch.

Raises:
ValueError: If the repository URL is unsupported or invalid.
"""
repository = GitHubRepository.parse(repo_url)
if repository.branch:
return repository

default_branch = await self._fetch_repository_default_branch(repository)
branch = default_branch or "main"
if not default_branch:
logger.info(
"Could not get the default %s branch for %s/%s; trying %s.",
"github",
repository.owner,
repository.name,
branch,
)
return GitHubRepository(
repository.owner,
repository.name,
branch,
)

async def _download_file(
self,
url: str,
Expand Down Expand Up @@ -201,10 +135,18 @@ async def _emit_progress(payload: dict) -> None:
"speed": 0,
},
)
except Exception as e:
logger.error(f"Failed to download file: {url} -> {target_path}: {e}")
if self._rm_on_error and target_path.exists():
target_path.unlink()
except (asyncio.CancelledError, Exception) as error:
if not isinstance(error, asyncio.CancelledError):
logger.error(
f"Failed to download file: {url} -> {target_path}: {error}"
)
try:
target_path.unlink(missing_ok=True)
except OSError as cleanup_error:
logger.warning(
"Failed to remove partial download: "
f"{url} -> {target_path}: {cleanup_error}"
)
raise

async def _fetch_release_info(self, url: str, latest: bool = True) -> list:
Expand Down Expand Up @@ -287,15 +229,16 @@ async def _check_update(
async def _download_repository(
self, target_path: str, repo_url: str, proxy=""
) -> None:
repository = await self._resolve_repository_source(repo_url)
repository = GitHubRepository.parse(repo_url)

logger.info(f"Downloading update for {repository.name} ...")
logger.info(
"Downloading %s/%s from %s branch %s",
"Downloading %s/%s from github %s",
repository.owner,
repository.name,
"github",
repository.branch,
f"branch {repository.branch}"
if repository.branch
else "default reference HEAD",
)
release_url = repository.archive_url

Expand Down
16 changes: 16 additions & 0 deletions tests/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ def test_github_repository_resolves_branch_with_slashes() -> None:
assert repository.archive_url == (
"https://github.com/AstrBotDevs/AstrBot/archive/refs/heads/feature/updater.zip"
)
assert repository.raw_file_url("metadata.yaml") == (
"https://raw.githubusercontent.com/AstrBotDevs/AstrBot/"
"feature/updater/metadata.yaml"
)


def test_github_repository_uses_head_when_branch_is_unspecified() -> None:
repository = GitHubRepository.parse("https://github.com/AstrBotDevs/AstrBot")

assert repository.branch is None
assert repository.archive_url == (
"https://github.com/AstrBotDevs/AstrBot/archive/HEAD.zip"
)
assert repository.raw_file_url("metadata.yaml") == (
"https://raw.githubusercontent.com/AstrBotDevs/AstrBot/HEAD/metadata.yaml"
)


def test_non_github_http_repository_uses_git_transport() -> None:
Expand Down
Loading