retry_timeouts
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
@@ -31,6 +34,40 @@ GOOGLE_FONT_FAMILIES = (
|
|||||||
"Source Sans 3",
|
"Source Sans 3",
|
||||||
)
|
)
|
||||||
GOOGLE_FONT_WEIGHTS = "400;500;600;700;800"
|
GOOGLE_FONT_WEIGHTS = "400;500;600;700;800"
|
||||||
|
DOWNLOAD_RETRIES = int(os.environ.get("PYTORRENT_DOWNLOAD_RETRIES", "4"))
|
||||||
|
DOWNLOAD_RETRY_DELAY = int(os.environ.get("PYTORRENT_DOWNLOAD_RETRY_DELAY", "10"))
|
||||||
|
DOWNLOAD_TIMEOUT = int(os.environ.get("PYTORRENT_DOWNLOAD_TIMEOUT", "180"))
|
||||||
|
|
||||||
|
|
||||||
|
def retry_countdown(seconds: int) -> None:
|
||||||
|
for remaining in range(seconds, 0, -1):
|
||||||
|
print(f"Retrying in {remaining}s...", end="\r", flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
if seconds > 0:
|
||||||
|
print(" " * 40, end="\r", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def candidate_urls(url: str) -> list[str]:
|
||||||
|
candidates = [url]
|
||||||
|
replacements = (
|
||||||
|
("https://cdn.jsdelivr.net/npm/bootstrap@", "https://unpkg.com/bootstrap@"),
|
||||||
|
("https://cdn.jsdelivr.net/npm/bootswatch@", "https://unpkg.com/bootswatch@"),
|
||||||
|
("https://cdn.jsdelivr.net/npm/swagger-ui-dist@", "https://unpkg.com/swagger-ui-dist@"),
|
||||||
|
("https://cdn.jsdelivr.net/gh/lipis/flag-icons@", "https://cdn.jsdelivr.net/npm/flag-icons@"),
|
||||||
|
("https://cdn.jsdelivr.net/gh/DevExpress/bootstrap-themes@master/", "https://raw.githubusercontent.com/DevExpress/bootstrap-themes/master/"),
|
||||||
|
("https://cdn.socket.io/", "https://cdnjs.cloudflare.com/ajax/libs/socket.io/"),
|
||||||
|
("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/", "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@"),
|
||||||
|
)
|
||||||
|
for old, new in replacements:
|
||||||
|
if url.startswith(old):
|
||||||
|
candidates.append(url.replace(old, new, 1))
|
||||||
|
# font-awesome has a different path layout on npm/jsDelivr.
|
||||||
|
candidates = [item.replace("/css/all.min.css", "/css/all.min.css") for item in candidates]
|
||||||
|
unique = []
|
||||||
|
for item in candidates:
|
||||||
|
if item not in unique:
|
||||||
|
unique.append(item)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
def google_fonts_css_url() -> str:
|
def google_fonts_css_url() -> str:
|
||||||
@@ -147,15 +184,31 @@ def bootstrap_css_asset(theme: str) -> dict[str, str]:
|
|||||||
|
|
||||||
def download(url: str, dest: Path) -> None:
|
def download(url: str, dest: Path) -> None:
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
req = Request(url, headers={"User-Agent": "pyTorrent installer"})
|
last_error: Exception | None = None
|
||||||
with urlopen(req, timeout=60) as response:
|
for candidate in candidate_urls(url):
|
||||||
data = response.read()
|
for attempt in range(1, DOWNLOAD_RETRIES + 1):
|
||||||
if not data:
|
try:
|
||||||
raise RuntimeError(f"Empty response for {url}")
|
req = Request(candidate, headers={"User-Agent": "pyTorrent installer"})
|
||||||
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
with urlopen(req, timeout=DOWNLOAD_TIMEOUT) as response:
|
||||||
tmp.write_bytes(data)
|
data = response.read()
|
||||||
tmp.replace(dest)
|
if not data:
|
||||||
print(f"OK {dest.relative_to(ROOT)}")
|
raise RuntimeError(f"Empty response for {candidate}")
|
||||||
|
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
||||||
|
tmp.write_bytes(data)
|
||||||
|
tmp.replace(dest)
|
||||||
|
if candidate != url:
|
||||||
|
print(f"OK {dest.relative_to(ROOT)} from fallback {candidate}")
|
||||||
|
else:
|
||||||
|
print(f"OK {dest.relative_to(ROOT)}")
|
||||||
|
return
|
||||||
|
except (HTTPError, URLError, TimeoutError, OSError, RuntimeError) as exc:
|
||||||
|
last_error = exc
|
||||||
|
print(f"Download failed ({attempt}/{DOWNLOAD_RETRIES}) for {candidate}: {exc}")
|
||||||
|
if attempt < DOWNLOAD_RETRIES:
|
||||||
|
retry_countdown(DOWNLOAD_RETRY_DELAY)
|
||||||
|
if candidate != candidate_urls(url)[-1]:
|
||||||
|
print(f"Trying alternative source: {candidate_urls(url)[candidate_urls(url).index(candidate) + 1]}")
|
||||||
|
raise RuntimeError(f"Failed to download {url}: {last_error}")
|
||||||
|
|
||||||
|
|
||||||
def download_css_with_assets(url: str, dest: Path) -> None:
|
def download_css_with_assets(url: str, dest: Path) -> None:
|
||||||
@@ -184,10 +237,22 @@ def download_google_fonts_css(url: str, dest: Path) -> None:
|
|||||||
"Accept": "text/css,*/*;q=0.1",
|
"Accept": "text/css,*/*;q=0.1",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
with urlopen(req, timeout=60) as response:
|
last_error: Exception | None = None
|
||||||
css = response.read().decode("utf-8", errors="ignore")
|
css = ""
|
||||||
|
for attempt in range(1, DOWNLOAD_RETRIES + 1):
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=DOWNLOAD_TIMEOUT) as response:
|
||||||
|
css = response.read().decode("utf-8", errors="ignore")
|
||||||
|
if not css.strip():
|
||||||
|
raise RuntimeError(f"Empty response for {url}")
|
||||||
|
break
|
||||||
|
except (HTTPError, URLError, TimeoutError, OSError, RuntimeError) as exc:
|
||||||
|
last_error = exc
|
||||||
|
print(f"Download failed ({attempt}/{DOWNLOAD_RETRIES}) for {url}: {exc}")
|
||||||
|
if attempt < DOWNLOAD_RETRIES:
|
||||||
|
retry_countdown(DOWNLOAD_RETRY_DELAY)
|
||||||
if not css.strip():
|
if not css.strip():
|
||||||
raise RuntimeError(f"Empty response for {url}")
|
raise RuntimeError(f"Failed to download {url}: {last_error}")
|
||||||
|
|
||||||
def replace_url(match: re.Match[str]) -> str:
|
def replace_url(match: re.Match[str]) -> str:
|
||||||
quote = match.group(1) or ""
|
quote = match.group(1) or ""
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ REPO_URL="${PYTORRENT_REPO_URL:-https://github.com/zdzichu6969/pyTorrent}"
|
|||||||
REPO_BRANCH="${PYTORRENT_REPO_BRANCH:-master}"
|
REPO_BRANCH="${PYTORRENT_REPO_BRANCH:-master}"
|
||||||
WORK_DIR="${PYTORRENT_BOOTSTRAP_DIR:-/tmp/pytorrent-only-installer}"
|
WORK_DIR="${PYTORRENT_BOOTSTRAP_DIR:-/tmp/pytorrent-only-installer}"
|
||||||
KEEP_WORK_DIR="${PYTORRENT_KEEP_BOOTSTRAP_DIR:-0}"
|
KEEP_WORK_DIR="${PYTORRENT_KEEP_BOOTSTRAP_DIR:-0}"
|
||||||
|
DOWNLOAD_RETRIES="${PYTORRENT_DOWNLOAD_RETRIES:-4}"
|
||||||
|
DOWNLOAD_RETRY_DELAY="${PYTORRENT_DOWNLOAD_RETRY_DELAY:-10}"
|
||||||
|
DOWNLOAD_CONNECT_TIMEOUT="${PYTORRENT_DOWNLOAD_CONNECT_TIMEOUT:-30}"
|
||||||
|
DOWNLOAD_MAX_TIME="${PYTORRENT_DOWNLOAD_MAX_TIME:-600}"
|
||||||
|
|
||||||
default_archive_url() {
|
default_archive_url() {
|
||||||
case "${REPO_URL%/}" in
|
case "${REPO_URL%/}" in
|
||||||
@@ -61,13 +65,65 @@ prepare_downloader() {
|
|||||||
fail "curl or wget is required."
|
fail "curl or wget is required."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retry_countdown() {
|
||||||
|
local seconds="$1"
|
||||||
|
local remaining
|
||||||
|
for ((remaining=seconds; remaining>0; remaining--)); do
|
||||||
|
printf 'Retrying in %ss...\r' "${remaining}"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
[[ "${seconds}" -gt 0 ]] && printf '%*s\r' 40 ''
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_url_candidates() {
|
||||||
|
local url="$1"
|
||||||
|
printf '%s\n' "${url}"
|
||||||
|
case "${url}" in
|
||||||
|
https://github.com/*/archive/refs/heads/*.tar.gz)
|
||||||
|
local rest owner repo branch
|
||||||
|
rest="${url#https://github.com/}"
|
||||||
|
owner="${rest%%/*}"
|
||||||
|
rest="${rest#*/}"
|
||||||
|
repo="${rest%%/*}"
|
||||||
|
branch="${url##*/}"
|
||||||
|
branch="${branch%.tar.gz}"
|
||||||
|
printf 'https://codeload.github.com/%s/%s/tar.gz/refs/heads/%s\n' "${owner}" "${repo}" "${branch}"
|
||||||
|
;;
|
||||||
|
https://github.com/*/archive/*.tar.gz)
|
||||||
|
local rest owner repo ref
|
||||||
|
rest="${url#https://github.com/}"
|
||||||
|
owner="${rest%%/*}"
|
||||||
|
rest="${rest#*/}"
|
||||||
|
repo="${rest%%/*}"
|
||||||
|
ref="${url##*/}"
|
||||||
|
ref="${ref%.tar.gz}"
|
||||||
|
printf 'https://codeload.github.com/%s/%s/tar.gz/%s\n' "${owner}" "${repo}" "${ref}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
download_file() {
|
download_file() {
|
||||||
local url="$1" destination="$2"
|
local url="$1"
|
||||||
if [[ "${DOWNLOADER}" == "curl" ]]; then
|
local destination="$2"
|
||||||
curl -fL "${url}" -o "${destination}"
|
local candidate attempt status
|
||||||
else
|
while IFS= read -r candidate; do
|
||||||
wget -O "${destination}" "${url}"
|
[[ -n "${candidate}" ]] || continue
|
||||||
fi
|
for ((attempt=1; attempt<=DOWNLOAD_RETRIES; attempt++)); do
|
||||||
|
if [[ "${DOWNLOADER}" == "curl" ]]; then
|
||||||
|
curl -fL --connect-timeout "${DOWNLOAD_CONNECT_TIMEOUT}" --max-time "${DOWNLOAD_MAX_TIME}" "${candidate}" -o "${destination}" && return 0
|
||||||
|
status=$?
|
||||||
|
else
|
||||||
|
wget --timeout="${DOWNLOAD_CONNECT_TIMEOUT}" --read-timeout="${DOWNLOAD_MAX_TIME}" --tries=1 -O "${destination}" "${candidate}" && return 0
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
log "Download failed (${attempt}/${DOWNLOAD_RETRIES}) from ${candidate} (exit ${status})."
|
||||||
|
if [[ "${attempt}" -lt "${DOWNLOAD_RETRIES}" ]]; then
|
||||||
|
retry_countdown "${DOWNLOAD_RETRY_DELAY}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log "Trying alternative source if available after: ${candidate}"
|
||||||
|
done < <(archive_url_candidates "${url}")
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ REPO_URL="${PYTORRENT_REPO_URL:-https://github.com/zdzichu6969/pyTorrent}"
|
|||||||
REPO_BRANCH="${PYTORRENT_REPO_BRANCH:-master}"
|
REPO_BRANCH="${PYTORRENT_REPO_BRANCH:-master}"
|
||||||
WORK_DIR="${PYTORRENT_BOOTSTRAP_DIR:-/tmp/pytorrent-stack-installer}"
|
WORK_DIR="${PYTORRENT_BOOTSTRAP_DIR:-/tmp/pytorrent-stack-installer}"
|
||||||
KEEP_WORK_DIR="${PYTORRENT_KEEP_BOOTSTRAP_DIR:-0}"
|
KEEP_WORK_DIR="${PYTORRENT_KEEP_BOOTSTRAP_DIR:-0}"
|
||||||
|
DOWNLOAD_RETRIES="${PYTORRENT_DOWNLOAD_RETRIES:-4}"
|
||||||
|
DOWNLOAD_RETRY_DELAY="${PYTORRENT_DOWNLOAD_RETRY_DELAY:-10}"
|
||||||
|
DOWNLOAD_CONNECT_TIMEOUT="${PYTORRENT_DOWNLOAD_CONNECT_TIMEOUT:-30}"
|
||||||
|
DOWNLOAD_MAX_TIME="${PYTORRENT_DOWNLOAD_MAX_TIME:-600}"
|
||||||
|
|
||||||
default_archive_url() {
|
default_archive_url() {
|
||||||
case "${REPO_URL%/}" in
|
case "${REPO_URL%/}" in
|
||||||
@@ -105,14 +109,65 @@ prepare_downloader() {
|
|||||||
fail "curl or wget is required and no supported package manager was found."
|
fail "curl or wget is required and no supported package manager was found."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retry_countdown() {
|
||||||
|
local seconds="$1"
|
||||||
|
local remaining
|
||||||
|
for ((remaining=seconds; remaining>0; remaining--)); do
|
||||||
|
printf 'Retrying in %ss...\r' "${remaining}"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
[[ "${seconds}" -gt 0 ]] && printf '%*s\r' 40 ''
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_url_candidates() {
|
||||||
|
local url="$1"
|
||||||
|
printf '%s\n' "${url}"
|
||||||
|
case "${url}" in
|
||||||
|
https://github.com/*/archive/refs/heads/*.tar.gz)
|
||||||
|
local rest owner repo branch
|
||||||
|
rest="${url#https://github.com/}"
|
||||||
|
owner="${rest%%/*}"
|
||||||
|
rest="${rest#*/}"
|
||||||
|
repo="${rest%%/*}"
|
||||||
|
branch="${url##*/}"
|
||||||
|
branch="${branch%.tar.gz}"
|
||||||
|
printf 'https://codeload.github.com/%s/%s/tar.gz/refs/heads/%s\n' "${owner}" "${repo}" "${branch}"
|
||||||
|
;;
|
||||||
|
https://github.com/*/archive/*.tar.gz)
|
||||||
|
local rest owner repo ref
|
||||||
|
rest="${url#https://github.com/}"
|
||||||
|
owner="${rest%%/*}"
|
||||||
|
rest="${rest#*/}"
|
||||||
|
repo="${rest%%/*}"
|
||||||
|
ref="${url##*/}"
|
||||||
|
ref="${ref%.tar.gz}"
|
||||||
|
printf 'https://codeload.github.com/%s/%s/tar.gz/%s\n' "${owner}" "${repo}" "${ref}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
download_file() {
|
download_file() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local destination="$2"
|
local destination="$2"
|
||||||
if [[ "${DOWNLOADER}" == "curl" ]]; then
|
local candidate attempt status
|
||||||
curl -fL "${url}" -o "${destination}"
|
while IFS= read -r candidate; do
|
||||||
else
|
[[ -n "${candidate}" ]] || continue
|
||||||
wget -O "${destination}" "${url}"
|
for ((attempt=1; attempt<=DOWNLOAD_RETRIES; attempt++)); do
|
||||||
fi
|
if [[ "${DOWNLOADER}" == "curl" ]]; then
|
||||||
|
curl -fL --connect-timeout "${DOWNLOAD_CONNECT_TIMEOUT}" --max-time "${DOWNLOAD_MAX_TIME}" "${candidate}" -o "${destination}" && return 0
|
||||||
|
status=$?
|
||||||
|
else
|
||||||
|
wget --timeout="${DOWNLOAD_CONNECT_TIMEOUT}" --read-timeout="${DOWNLOAD_MAX_TIME}" --tries=1 -O "${destination}" "${candidate}" && return 0
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
log "Download failed (${attempt}/${DOWNLOAD_RETRIES}) from ${candidate} (exit ${status})."
|
||||||
|
if [[ "${attempt}" -lt "${DOWNLOAD_RETRIES}" ]]; then
|
||||||
|
retry_countdown "${DOWNLOAD_RETRY_DELAY}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log "Trying alternative source if available after: ${candidate}"
|
||||||
|
done < <(archive_url_candidates "${url}")
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_os_family() {
|
detect_os_family() {
|
||||||
|
|||||||
@@ -24,6 +24,54 @@ DEFAULT_CURL_REF = "8.19.0"
|
|||||||
DEFAULT_SERVICE_PATH = "/etc/systemd/system/rtorrent@.service"
|
DEFAULT_SERVICE_PATH = "/etc/systemd/system/rtorrent@.service"
|
||||||
DEFAULT_SCGI_PORT = 5000
|
DEFAULT_SCGI_PORT = 5000
|
||||||
DEFAULT_TORRENT_PORT = 51300
|
DEFAULT_TORRENT_PORT = 51300
|
||||||
|
DOWNLOAD_RETRIES = int(os.environ.get("PYTORRENT_DOWNLOAD_RETRIES", "4"))
|
||||||
|
DOWNLOAD_RETRY_DELAY = int(os.environ.get("PYTORRENT_DOWNLOAD_RETRY_DELAY", "10"))
|
||||||
|
DOWNLOAD_CONNECT_TIMEOUT = int(os.environ.get("PYTORRENT_DOWNLOAD_CONNECT_TIMEOUT", "30"))
|
||||||
|
DOWNLOAD_MAX_TIME = int(os.environ.get("PYTORRENT_DOWNLOAD_MAX_TIME", "600"))
|
||||||
|
|
||||||
|
|
||||||
|
def retry_countdown(seconds):
|
||||||
|
for remaining in range(seconds, 0, -1):
|
||||||
|
print(f"Retrying in {remaining}s...", end="\r", flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
if seconds > 0:
|
||||||
|
print(" " * 40, end="\r", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run_with_retry(cmd, *, retries=DOWNLOAD_RETRIES, retry_delay=DOWNLOAD_RETRY_DELAY, retry_label=None, **kwargs):
|
||||||
|
last_error = None
|
||||||
|
label = retry_label or " ".join(str(x) for x in cmd[:3])
|
||||||
|
for attempt in range(1, retries + 1):
|
||||||
|
try:
|
||||||
|
return run(cmd, **kwargs)
|
||||||
|
except InstallError as exc:
|
||||||
|
last_error = exc
|
||||||
|
print(f"{label} failed ({attempt}/{retries}): {exc}")
|
||||||
|
if attempt < retries:
|
||||||
|
retry_countdown(retry_delay)
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
|
||||||
|
def download_url_candidates(url):
|
||||||
|
candidates = [url]
|
||||||
|
if url.startswith("https://github.com/c-ares/c-ares/releases/download/v") and url.endswith(".tar.gz"):
|
||||||
|
version = url.rsplit("/c-ares-", 1)[-1].removesuffix(".tar.gz")
|
||||||
|
candidates.append(f"https://codeload.github.com/c-ares/c-ares/tar.gz/refs/tags/v{version}")
|
||||||
|
if url.startswith("https://curl.se/download/curl-") and url.endswith(".tar.gz"):
|
||||||
|
version = url.rsplit("/curl-", 1)[-1].removesuffix(".tar.gz")
|
||||||
|
tag = "curl-" + version.replace(".", "_")
|
||||||
|
candidates.append(f"https://github.com/curl/curl/releases/download/{tag}/curl-{version}.tar.gz")
|
||||||
|
candidates.append(f"https://codeload.github.com/curl/curl/tar.gz/refs/tags/{tag}")
|
||||||
|
if "sourceforge.net/projects/xmlrpc-c/files/latest/download" in url:
|
||||||
|
candidates.append("https://downloads.sourceforge.net/project/xmlrpc-c/latest/download")
|
||||||
|
if url.startswith("https://downloads.sourceforge.net/project/xmlrpc-c/"):
|
||||||
|
candidates.append(url.replace("https://downloads.sourceforge.net/", "https://sourceforge.net/projects/").replace("project/xmlrpc-c/", "xmlrpc-c/files/"))
|
||||||
|
|
||||||
|
unique = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate not in unique:
|
||||||
|
unique.append(candidate)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
class InstallError(Exception):
|
class InstallError(Exception):
|
||||||
@@ -220,17 +268,35 @@ def clone_or_update_repo(repo_url, repo_dir, ref, *, debug=False):
|
|||||||
repo_dir = Path(repo_dir)
|
repo_dir = Path(repo_dir)
|
||||||
if not repo_dir.exists():
|
if not repo_dir.exists():
|
||||||
with Spinner(f"Cloning {repo_dir.name}", enabled=not debug):
|
with Spinner(f"Cloning {repo_dir.name}", enabled=not debug):
|
||||||
run(["git", "clone", repo_url, str(repo_dir)], debug=debug)
|
run_with_retry(["git", "clone", repo_url, str(repo_dir)], debug=debug, retry_label=f"git clone {repo_url}")
|
||||||
else:
|
else:
|
||||||
print(f"Repository already exists: {repo_dir}")
|
print(f"Repository already exists: {repo_dir}")
|
||||||
with Spinner(f"Checking out {repo_dir.name} -> {ref}", enabled=not debug):
|
with Spinner(f"Checking out {repo_dir.name} -> {ref}", enabled=not debug):
|
||||||
run(["git", "fetch", "--all", "--tags"], cwd=str(repo_dir), debug=debug)
|
run_with_retry(["git", "fetch", "--all", "--tags"], cwd=str(repo_dir), debug=debug, retry_label=f"git fetch {repo_dir.name}")
|
||||||
run(["git", "checkout", ref], cwd=str(repo_dir), debug=debug)
|
run(["git", "checkout", ref], cwd=str(repo_dir), debug=debug)
|
||||||
run(["git", "pull", "--ff-only"], cwd=str(repo_dir), check=False, debug=debug)
|
run_with_retry(["git", "pull", "--ff-only"], cwd=str(repo_dir), check=False, debug=debug, retry_label=f"git pull {repo_dir.name}")
|
||||||
|
|
||||||
|
|
||||||
def download_file(url, destination, *, debug=False):
|
def download_file(url, destination, *, debug=False):
|
||||||
run(["curl", "-fL", url, "-o", str(destination)], debug=debug)
|
last_error = None
|
||||||
|
for candidate in download_url_candidates(url):
|
||||||
|
for attempt in range(1, DOWNLOAD_RETRIES + 1):
|
||||||
|
try:
|
||||||
|
return run([
|
||||||
|
"curl",
|
||||||
|
"-fL",
|
||||||
|
"--connect-timeout", str(DOWNLOAD_CONNECT_TIMEOUT),
|
||||||
|
"--max-time", str(DOWNLOAD_MAX_TIME),
|
||||||
|
candidate,
|
||||||
|
"-o", str(destination),
|
||||||
|
], debug=debug)
|
||||||
|
except InstallError as exc:
|
||||||
|
last_error = exc
|
||||||
|
print(f"Download failed ({attempt}/{DOWNLOAD_RETRIES}) from {candidate}: {exc}")
|
||||||
|
if attempt < DOWNLOAD_RETRIES:
|
||||||
|
retry_countdown(DOWNLOAD_RETRY_DELAY)
|
||||||
|
print(f"Trying alternative source if available after: {candidate}")
|
||||||
|
raise last_error or InstallError(f"Download failed: {url}")
|
||||||
|
|
||||||
|
|
||||||
def extract_tarball(tarball, destination, *, debug=False):
|
def extract_tarball(tarball, destination, *, debug=False):
|
||||||
|
|||||||
@@ -24,6 +24,54 @@ DEFAULT_CURL_REF = "8.19.0"
|
|||||||
DEFAULT_SERVICE_PATH = "/etc/systemd/system/rtorrent@.service"
|
DEFAULT_SERVICE_PATH = "/etc/systemd/system/rtorrent@.service"
|
||||||
DEFAULT_SCGI_PORT = 5000
|
DEFAULT_SCGI_PORT = 5000
|
||||||
DEFAULT_TORRENT_PORT = 51300
|
DEFAULT_TORRENT_PORT = 51300
|
||||||
|
DOWNLOAD_RETRIES = int(os.environ.get("PYTORRENT_DOWNLOAD_RETRIES", "4"))
|
||||||
|
DOWNLOAD_RETRY_DELAY = int(os.environ.get("PYTORRENT_DOWNLOAD_RETRY_DELAY", "10"))
|
||||||
|
DOWNLOAD_CONNECT_TIMEOUT = int(os.environ.get("PYTORRENT_DOWNLOAD_CONNECT_TIMEOUT", "30"))
|
||||||
|
DOWNLOAD_MAX_TIME = int(os.environ.get("PYTORRENT_DOWNLOAD_MAX_TIME", "600"))
|
||||||
|
|
||||||
|
|
||||||
|
def retry_countdown(seconds):
|
||||||
|
for remaining in range(seconds, 0, -1):
|
||||||
|
print(f"Retrying in {remaining}s...", end="\r", flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
if seconds > 0:
|
||||||
|
print(" " * 40, end="\r", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run_with_retry(cmd, *, retries=DOWNLOAD_RETRIES, retry_delay=DOWNLOAD_RETRY_DELAY, retry_label=None, **kwargs):
|
||||||
|
last_error = None
|
||||||
|
label = retry_label or " ".join(str(x) for x in cmd[:3])
|
||||||
|
for attempt in range(1, retries + 1):
|
||||||
|
try:
|
||||||
|
return run(cmd, **kwargs)
|
||||||
|
except InstallError as exc:
|
||||||
|
last_error = exc
|
||||||
|
print(f"{label} failed ({attempt}/{retries}): {exc}")
|
||||||
|
if attempt < retries:
|
||||||
|
retry_countdown(retry_delay)
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
|
||||||
|
def download_url_candidates(url):
|
||||||
|
candidates = [url]
|
||||||
|
if url.startswith("https://github.com/c-ares/c-ares/releases/download/v") and url.endswith(".tar.gz"):
|
||||||
|
version = url.rsplit("/c-ares-", 1)[-1].removesuffix(".tar.gz")
|
||||||
|
candidates.append(f"https://codeload.github.com/c-ares/c-ares/tar.gz/refs/tags/v{version}")
|
||||||
|
if url.startswith("https://curl.se/download/curl-") and url.endswith(".tar.gz"):
|
||||||
|
version = url.rsplit("/curl-", 1)[-1].removesuffix(".tar.gz")
|
||||||
|
tag = "curl-" + version.replace(".", "_")
|
||||||
|
candidates.append(f"https://github.com/curl/curl/releases/download/{tag}/curl-{version}.tar.gz")
|
||||||
|
candidates.append(f"https://codeload.github.com/curl/curl/tar.gz/refs/tags/{tag}")
|
||||||
|
if "sourceforge.net/projects/xmlrpc-c/files/latest/download" in url:
|
||||||
|
candidates.append("https://downloads.sourceforge.net/project/xmlrpc-c/latest/download")
|
||||||
|
if url.startswith("https://downloads.sourceforge.net/project/xmlrpc-c/"):
|
||||||
|
candidates.append(url.replace("https://downloads.sourceforge.net/", "https://sourceforge.net/projects/").replace("project/xmlrpc-c/", "xmlrpc-c/files/"))
|
||||||
|
|
||||||
|
unique = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate not in unique:
|
||||||
|
unique.append(candidate)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
class InstallError(Exception):
|
class InstallError(Exception):
|
||||||
@@ -221,17 +269,35 @@ def clone_or_update_repo(repo_url, repo_dir, ref, *, debug=False):
|
|||||||
repo_dir = Path(repo_dir)
|
repo_dir = Path(repo_dir)
|
||||||
if not repo_dir.exists():
|
if not repo_dir.exists():
|
||||||
with Spinner(f"Cloning {repo_dir.name}", enabled=not debug):
|
with Spinner(f"Cloning {repo_dir.name}", enabled=not debug):
|
||||||
run(["git", "clone", repo_url, str(repo_dir)], debug=debug)
|
run_with_retry(["git", "clone", repo_url, str(repo_dir)], debug=debug, retry_label=f"git clone {repo_url}")
|
||||||
else:
|
else:
|
||||||
print(f"Repository already exists: {repo_dir}")
|
print(f"Repository already exists: {repo_dir}")
|
||||||
with Spinner(f"Checking out {repo_dir.name} -> {ref}", enabled=not debug):
|
with Spinner(f"Checking out {repo_dir.name} -> {ref}", enabled=not debug):
|
||||||
run(["git", "fetch", "--all", "--tags"], cwd=str(repo_dir), debug=debug)
|
run_with_retry(["git", "fetch", "--all", "--tags"], cwd=str(repo_dir), debug=debug, retry_label=f"git fetch {repo_dir.name}")
|
||||||
run(["git", "checkout", ref], cwd=str(repo_dir), debug=debug)
|
run(["git", "checkout", ref], cwd=str(repo_dir), debug=debug)
|
||||||
run(["git", "pull", "--ff-only"], cwd=str(repo_dir), check=False, debug=debug)
|
run_with_retry(["git", "pull", "--ff-only"], cwd=str(repo_dir), check=False, debug=debug, retry_label=f"git pull {repo_dir.name}")
|
||||||
|
|
||||||
|
|
||||||
def download_file(url, destination, *, debug=False):
|
def download_file(url, destination, *, debug=False):
|
||||||
run(["curl", "-fL", url, "-o", str(destination)], debug=debug)
|
last_error = None
|
||||||
|
for candidate in download_url_candidates(url):
|
||||||
|
for attempt in range(1, DOWNLOAD_RETRIES + 1):
|
||||||
|
try:
|
||||||
|
return run([
|
||||||
|
"curl",
|
||||||
|
"-fL",
|
||||||
|
"--connect-timeout", str(DOWNLOAD_CONNECT_TIMEOUT),
|
||||||
|
"--max-time", str(DOWNLOAD_MAX_TIME),
|
||||||
|
candidate,
|
||||||
|
"-o", str(destination),
|
||||||
|
], debug=debug)
|
||||||
|
except InstallError as exc:
|
||||||
|
last_error = exc
|
||||||
|
print(f"Download failed ({attempt}/{DOWNLOAD_RETRIES}) from {candidate}: {exc}")
|
||||||
|
if attempt < DOWNLOAD_RETRIES:
|
||||||
|
retry_countdown(DOWNLOAD_RETRY_DELAY)
|
||||||
|
print(f"Trying alternative source if available after: {candidate}")
|
||||||
|
raise last_error or InstallError(f"Download failed: {url}")
|
||||||
|
|
||||||
|
|
||||||
def extract_tarball(tarball, destination, *, debug=False):
|
def extract_tarball(tarball, destination, *, debug=False):
|
||||||
|
|||||||
Reference in New Issue
Block a user