import requests
import yaml
import json
import re
from bs4 import BeautifulSoup
import os
import sys
from collections import OrderedDict
import glob
from urllib.parse import urlparse, urlunparse
import time
ALPINE_MIRRORS_URL = "https://dl-cdn.alpinelinux.org/MIRRORS.txt"
DEBIAN_MIRRORS_URL = "https://www.debian.org/mirror/list"
UBUNTU_MIRRORS_URL = "https://launchpad.net/ubuntu/+archivemirrors"
FEDORA_MIRRORS_URL = "https://mirrormanager.fedoraproject.org/mirrors?page_size=500"
ARCH_HTML_MIRRORLIST_URL = "https://archlinux.org/mirrorlist/all/"
OPENSUSE_MIRRORS_URL = "https://mirrors.opensuse.org/"
ALPINE_CACHE_TXT = "mirrors-alpine.txt"
ARCH_CACHE_TXT = "mirrors-archlinux.txt"
DEBIAN_CACHE_HTML = "mirrors-debian.html"
OPENSUSE_CACHE_HTML = "mirrors-opensuse.html"
FEDORA_CACHE_HTML = "mirrors-fedora.html"
UBUNTU_CACHE_HTML = "mirrors-ubuntu.html"
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
INPUT_DIR = os.path.join(BASE_DIR, 'input')
OUTPUT_DIR = os.path.join(BASE_DIR, 'output')
FEDORA_MIRRORS_PATH = os.path.join(INPUT_DIR, FEDORA_CACHE_HTML)
UBUNTU_MIRRORS_PATH = os.path.join(INPUT_DIR, UBUNTU_CACHE_HTML)
OPENEULER_MIRRORS_PATH = os.path.join(INPUT_DIR, 'mirrors-openeuler.html')
OFFICIAL_MIRRORS_OUTPUT_PATH = os.path.join(OUTPUT_DIR, 'official-mirrors.json')
from common import load_distro_configs, get_distro_configs, debug_print, get_valid_dirs
def load_url_blacklist(filepath):
"""Load URL blacklist from file, extracting hostnames from URLs.
File format: one entry per line, can be full URL (http://host/path) or hostname.
Lines starting with '#' are treated as comments and ignored.
Returns list of hostnames (netloc).
"""
blacklist = set()
if not os.path.exists(filepath):
return blacklist
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '://' in line:
hostname = urlparse(line).netloc
else:
hostname = line
blacklist.add(hostname)
except Exception as e:
print(f"Warning: Failed to load blacklist from {filepath}: {e}")
return blacklist
URL_BLACKLIST = load_url_blacklist(os.path.join(BASE_DIR, 'blacklist-mirrors.txt'))
def should_update_cache(cache_file_path, max_age_days=30):
"""Check if cache file should be updated based on age.
Returns True if cache file doesn't exist or is older than max_age_days.
Returns False if cache file exists and is recent (younger than max_age_days).
"""
if not os.path.exists(cache_file_path):
return True
try:
mtime = os.path.getmtime(cache_file_path)
age_days = (time.time() - mtime) / (24 * 3600)
return age_days >= max_age_days
except OSError:
return True
def update_cache(url, cache_filename, base_dir, is_json=False, timeout=15):
"""Fetch content from URL and update cache file.
Returns True if fetch succeeded and cache was updated, False otherwise.
"""
cache_filepath = os.path.join(base_dir, cache_filename)
debug_print(f"Fetching from {url} to update cache at {cache_filepath}", "cache")
try:
response = requests.get(url, timeout=timeout)
response.raise_for_status()
os.makedirs(base_dir, exist_ok=True)
if is_json:
try:
parsed_json = response.json()
with open(cache_filepath, 'w', encoding='utf-8') as f:
json.dump(parsed_json, f, indent=4)
debug_print(f"Cached JSON to {cache_filepath}", "cache")
return True
except json.JSONDecodeError as e:
debug_print(f"Downloaded content from {url} is not valid JSON: {e}")
return False
else:
with open(cache_filepath, 'wb') as f:
f.write(response.content)
debug_print(f"Cached HTML to {cache_filepath}", "cache")
return True
except requests.RequestException as e:
debug_print(f"Failed to fetch {url}: {e}")
return False
except IOError as e:
debug_print(f"File I/O error for {cache_filepath}: {e}")
return False
def read_cache(cache_filename, base_dir, is_json=False):
"""Read content from cache file.
Returns content (JSON dict or bytes) if successful, None otherwise.
"""
cache_filepath = os.path.join(base_dir, cache_filename)
if not os.path.exists(cache_filepath):
debug_print(f"Cache file does not exist: {cache_filepath}", "cache")
return None
try:
if is_json:
with open(cache_filepath, 'r', encoding='utf-8') as f:
return json.load(f)
else:
with open(cache_filepath, 'rb') as f:
return f.read()
except Exception as e:
debug_print(f"Error reading cache file {cache_filepath}: {e}")
return None
def maybe_update_cache(url, cache_filename, base_dir, is_json=False, timeout=15):
"""Update cache if it's stale or missing.
Returns True if cache is fresh or update succeeded, False otherwise.
"""
cache_filepath = os.path.join(base_dir, cache_filename)
if should_update_cache(cache_filepath):
debug_print(f"Cache stale or missing: attempting to update from {url}", "cache")
return update_cache(url, cache_filename, base_dir, is_json, timeout)
else:
debug_print(f"Cache is fresh: {cache_filepath}", "cache")
return True
def parse_bandwidth(bandwidth_text):
"""Parse bandwidth text (like '10 Gbps') to a numerical value in Mbps.
For example:
- '10 Gbps' -> 10000 (10 Gbps = 10,000 Mbps)
- '10 Mbps' -> 10 (10 Mbps = 10 Mbps)
- '1 Gbps' -> 1000 (1 Gbps = 1,000 Mbps)
- '100 Mbps' -> 100 (100 Mbps = 100 Mbps)
"""
if not bandwidth_text:
return None
bandwidth_text = str(bandwidth_text).lower()
number_match = re.search(r'([\d.]+)', bandwidth_text)
if not number_match:
return None
value = float(number_match.group(1))
if 'gbps' in bandwidth_text or ' gb' in bandwidth_text or 'gbit' in bandwidth_text:
value *= 1000
elif 'tbps' in bandwidth_text or ' tb' in bandwidth_text or 'tbit' in bandwidth_text:
value *= 1000000
elif 'kbps' in bandwidth_text or ' kb' in bandwidth_text or 'kbit' in bandwidth_text:
value *= 0.001
return int(value)
def add_mirror(temp_mirror_groups, original_url, canonical_distro_name_from_parser, metadata_from_parser):
"""
Processes a mirror URL, attempts to strip known distro prefixes, and aggregates it
into temp_mirror_groups. Grouping is based on (netloc, common_path_after_stripping).
"""
debug_print(f"Called for {canonical_distro_name_from_parser}. URL='{original_url}'", "mirror")
original_url_cleaned = original_url.rstrip('/.').rstrip('/')
parsed_url = urlparse(original_url_cleaned)
netloc = parsed_url.netloc
original_path = parsed_url.path
if parsed_url.scheme.lower() == 'rsync':
pass
if netloc in URL_BLACKLIST:
print(f"Skipping blacklisted URL: {original_url}")
return
common_path_for_grouping = original_path
path_stripped = None
distro_configs = get_distro_configs()
prefixes_from_config = distro_configs.get(canonical_distro_name_from_parser, [])
prefixes_from_config.append(canonical_distro_name_from_parser)
prefixes_from_config.sort(key=len, reverse=True)
for prefix_to_try in prefixes_from_config:
if original_path.endswith(f"/{prefix_to_try}"):
common_path_for_grouping = original_path[:-len(prefix_to_try)-1]
if not common_path_for_grouping:
common_path_for_grouping = "/"
path_stripped = prefix_to_try
debug_print(f"Stripped trailing '{prefix_to_try}' from URL path: '{original_path}' -> '{common_path_for_grouping}'", "strip")
break
group_key = (netloc, common_path_for_grouping)
if group_key not in temp_mirror_groups:
temp_mirror_groups[group_key] = {
'distro_dirs': set(),
'protocols': set(),
'original_urls': set(),
'metadata_store': {},
'representative_scheme': parsed_url.scheme.lower(),
'distros': set()
}
entry = temp_mirror_groups[group_key]
if path_stripped:
if entry['distros']:
print("Skipping unexpected mixup with top_level {original_url}")
return
entry['distro_dirs'].add(path_stripped)
debug_print(f"Added stripped directory '{path_stripped}' to distro_dirs", "strip")
else:
entry['metadata_store']['top_level'] = True
entry['distros'].add(canonical_distro_name_from_parser)
entry['protocols'].add(parsed_url.scheme.lower())
entry['original_urls'].add(original_url_cleaned)
current_scheme_lower = parsed_url.scheme.lower()
if current_scheme_lower == 'https':
entry['representative_scheme'] = 'https'
elif current_scheme_lower == 'http' and entry['representative_scheme'] != 'https':
entry['representative_scheme'] = 'http'
for meta_key, meta_val in metadata_from_parser.items():
if meta_key in ['distro_dirs', 'protocols', 'top_level']:
continue
if meta_key == 'internet2' and meta_val is False:
continue
if meta_key == 'priority':
continue
if meta_val is not None:
entry['metadata_store'][meta_key] = meta_val
def get_content_from_url_or_cache(url, cache_filename, base_dir, is_json=False, timeout=15):
"""Get content from URL using cache.
First updates cache if stale or missing, then reads from cache.
Returns content (JSON dict or bytes) if successful, None otherwise.
"""
maybe_update_cache(url, cache_filename, base_dir, is_json, timeout)
return read_cache(cache_filename, base_dir, is_json)
def parse_alpine_mirrors(temp_mirror_groups):
debug_print("Fetching and parsing Alpine Linux mirrors...")
html_content = get_content_from_url_or_cache(ALPINE_MIRRORS_URL, ALPINE_CACHE_TXT, BASE_DIR)
alpine_added_count = 0
if not html_content:
print("Failed to get Alpine mirrors content.")
return
debug_print(f"Alpine HTML content length: {len(html_content) if html_content else 0}")
soup = BeautifulSoup(html_content, 'html.parser')
with open(os.path.join(BASE_DIR, ALPINE_CACHE_TXT), 'r', encoding='utf-8', errors='replace') as f:
debug_print(f"Reading Alpine mirrors from cache file...")
try:
lines = f.readlines()
debug_print(f"Alpine text file has {len(lines)} lines")
for line in lines:
line = line.strip()
if line.startswith('http://') or line.startswith('https://'):
mirror_url = line.strip()
debug_print(f"Alpine: Found mirror URL: {mirror_url}")
metadata = {
'country': None,
'country_code': None
}
add_mirror(temp_mirror_groups, mirror_url, "alpine", metadata)
alpine_added_count += 1
except Exception as e:
debug_print(f"Error reading Alpine mirror file: {e}")
try:
for country_header in soup.select('h4'):
country_name_text = country_header.get_text(strip=True)
country_name = country_name_text
country_code = None
img_tag = country_header.find('img', src=True)
if img_tag and 'flags/' in img_tag['src']:
try:
country_code = img_tag['src'].split('flags/')[-1].split('.')[0].upper()
except IndexError:
pass
ul_tag = country_header.find_next_sibling('ul')
if ul_tag:
for li_tag in ul_tag.find_all('li'):
a_tag = li_tag.find('a', href=True)
if a_tag:
mirror_url = a_tag['href']
if mirror_url.startswith("http://") or mirror_url.startswith("https://") or mirror_url.startswith("rsync://"):
debug_print(f"Alpine: Found mirror URL in HTML: {mirror_url}")
metadata = {
'country': country_name,
'country_code': country_code.upper() if country_code else None
}
add_mirror(temp_mirror_groups, mirror_url, "alpine", metadata)
alpine_added_count += 1
except Exception as e:
debug_print(f"Error parsing Alpine HTML content: {e}")
print(f"Alpine: Total mirrors passed to add_mirror: {alpine_added_count}")
def parse_debian_mirrors(temp_mirror_groups):
debian_added_count = 0
debug_print("Fetching and parsing Debian mirrors...")
html_content = get_content_from_url_or_cache(DEBIAN_MIRRORS_URL, DEBIAN_CACHE_HTML, BASE_DIR)
if not html_content:
print("Failed to get Debian mirrors content.")
return
debug_print(f"Debian HTML content length: {len(html_content) if html_content else 0}")
soup = BeautifulSoup(html_content, 'html.parser')
country_table_heading = soup.find('h2', id='per-country')
if not country_table_heading:
debug_print("Debian: Could not find 'Debian Mirrors per Country' heading (h2 id='per-country').")
debug_print(f"Debian: Total mirrors passed to add_mirror: {debian_added_count}")
return
mirror_table = country_table_heading.find_next_sibling('table')
if not mirror_table:
debug_print("Debian: Could not find table immediately following 'per-country' heading.")
debug_print(f"Debian: Total mirrors passed to add_mirror: {debian_added_count}")
return
for row in mirror_table.find_all('tr'):
cols = row.find_all('td')
if len(cols) >= 2:
country_name_text = cols[0].get_text(strip=True)
site_anchor = cols[1].find('a', href=True)
if site_anchor:
href = site_anchor.get('href')
if href and (href.startswith('http://') or href.startswith('https://')):
mirror_url_base = href.rstrip('/')
country_code = None
metadata = {
'country': country_name_text,
'country_code': country_code
}
debug_print(f"Debian: Attempting to add mirror: {mirror_url_base}, Metadata: {metadata}")
add_mirror(temp_mirror_groups, mirror_url_base, "debian", metadata)
debian_added_count += 1
complete_list_heading = soup.find('h2', id='complete-list')
if complete_list_heading:
debug_print("Debian: Found 'Complete List of Mirrors' heading. Attempting to parse...")
complete_list_table = complete_list_heading.find_next('table')
if complete_list_table:
current_country_name = None
debug_print("Debian: Found Complete List table")
for row in complete_list_table.find_all('tr'):
big_tag = row.find('big')
if big_tag and big_tag.find('strong'):
country_name = big_tag.get_text(strip=True)
debug_print(f"Debian: Found country header: '{country_name}'")
current_country_name = country_name
continue
if not current_country_name:
continue
tds = row.find_all('td')
if len(tds) < 2:
continue
row_text = " | ".join(td.get_text(strip=True) for td in tds)
if '163.com' in row_text:
debug_print(f"Debian (Complete List Row Debug for 163.com): Country '{current_country_name}', Row content: [{row_text}]")
hostname = tds[0].get_text(strip=True)
for td_idx, td in enumerate(tds):
if '163.com' in td.get_text():
debug_print(f"Debian (Complete List Cell Debug for 163.com): TD {td_idx}: '{td.get_text(strip=True)}'")
for anchor in td.find_all('a', href=True):
href = anchor.get('href')
if href and (href.startswith('http://') or href.startswith('https://')):
if '163.com' in href:
debug_print(f"Debian (Complete List 163.com Found): URL='{href}', Country='{current_country_name}'")
mirror_url_base = href.rstrip('/')
metadata = {
'country': current_country_name,
'country_code': None
}
debug_print(f"Debian (Complete List): Adding mirror: {mirror_url_base}, Metadata: {metadata}")
add_mirror(temp_mirror_groups, mirror_url_base, "debian", metadata)
debian_added_count += 1
debug_print(f"Debian: Total mirrors passed to add_mirror: {debian_added_count}")
def parse_arch_mirrors(temp_mirror_groups):
debug_print("Fetching and parsing Arch Linux mirrors from HTML mirrorlist...")
content = get_content_from_url_or_cache(ARCH_HTML_MIRRORLIST_URL, ARCH_CACHE_TXT, BASE_DIR, is_json=False)
if not content:
print("Failed to get Arch Linux mirrorlist content.")
return
try:
mirror_list_text = content.decode('utf-8')
except UnicodeDecodeError:
debug_print("Failed to decode Arch Linux mirrorlist content as UTF-8.")
return
count = 0
current_country = None
for line in mirror_list_text.splitlines():
stripped_line = line.strip()
if stripped_line.startswith('## '):
current_country = stripped_line[3:].strip()
debug_print(f"Arch: Found country header: {current_country}")
elif stripped_line.startswith('#Server = '):
mirror_url_template = stripped_line.split('=', 1)[1].strip()
base_url = mirror_url_template.split('$repo/os/$arch')[0]
if not base_url.endswith('/'):
if mirror_url_template.endswith('$repo/os/$arch'):
base_url += '/'
if base_url:
metadata = {
'country': current_country,
'country_code': None,
}
debug_print(f"Arch: Adding mirror {base_url} with country {current_country}")
add_mirror(temp_mirror_groups, base_url, "archlinux", metadata)
count += 1
print(f"Processed {count} Arch Linux mirrors from HTML mirrorlist.")
def parse_opensuse_mirrors(temp_mirror_groups):
debug_print("Fetching and parsing openSUSE mirrors...")
url = "https://mirrors.opensuse.org/"
html_content = get_content_from_url_or_cache(url, "mirrors-opensuse.html", BASE_DIR)
debug_print(f"openSUSE HTML content length: {len(html_content) if html_content else 0}")
opensuse_added_count = 0
if not html_content:
print("Could not get openSUSE mirror list.")
debug_print(f"openSUSE: Total mirrors passed to add_mirror: {opensuse_added_count}")
return
soup = BeautifulSoup(html_content, 'html.parser')
debug_print("openSUSE: Searching for mirror entries in HTML structure")
rows = soup.find_all('tr')
for row in rows:
country_div = row.select_one('td div.country')
hostname_div = row.select_one('td div.hostname')
url_divs = row.select('td div.url')
if country_div and (hostname_div or url_divs):
country_code = country_div.get_text(strip=True)
if '.jp' in str(hostname_div) or '.au' in str(hostname_div) or 'netspace.net.au' in str(hostname_div) or 'kddilabs.jp' in str(hostname_div):
debug_print(f"openSUSE: Found country code '{country_code}' for {hostname_div.get_text(strip=True) if hostname_div else 'unknown'}", "country")
hostname = None
url_hostname = None
if hostname_div:
hostname_link = hostname_div.find('a')
if hostname_link:
hostname = hostname_link.get_text(strip=True)
href = hostname_link.get('href', '')
if href:
try:
url_hostname = urlparse(href).netloc
if '163.com' in href:
debug_print(f"openSUSE: Found 163.com in hostname href: {href}, extracted hostname: {url_hostname}")
except Exception as e:
debug_print(f"Error parsing URL: {e}")
pass
if '163.com' in str(row):
debug_print(f"openSUSE: Found row with 163.com, Country code: {country_code}, Hostname: {hostname}")
debug_print(f"openSUSE: Row HTML: {row}")
for url_div in url_divs:
url_link = url_div.find('a', href=True)
if url_link and url_link.get('href'):
mirror_url = url_link['href'].rstrip('/')
if mirror_url.startswith('http://') or mirror_url.startswith('https://') or mirror_url.startswith('rsync://'):
try:
mirror_netloc = urlparse(mirror_url).netloc
except:
mirror_netloc = ""
if country_code is None:
country_code = ''
if '.au' in mirror_netloc or 'netspace.net.au' in mirror_netloc:
debug_print(f"openSUSE: Processing Australian URL: {mirror_url}, Country Code from HTML: {country_code}", "country")
elif '.jp' in mirror_netloc or 'kddilabs.jp' in mirror_netloc:
debug_print(f"openSUSE: Processing Japanese URL: {mirror_url}, Country Code from HTML: {country_code}", "country")
metadata = {
'country': None,
'country_code': country_code.upper() if country_code else None
}
debug_print(f"openSUSE: Attempting to add mirror: {mirror_url}, Metadata: {metadata}")
add_mirror(temp_mirror_groups, mirror_url, "opensuse", metadata)
opensuse_added_count += 1
if opensuse_added_count == 0:
debug_print("openSUSE: First parsing pattern found no mirrors. Trying alternative patterns...")
for link in soup.find_all('a', href=True):
href = link.get('href')
if ('opensuse' in href.lower() or 'suse' in href.lower()) and \
(href.startswith('http://') or href.startswith('https://') or href.startswith('rsync://')):
parsed_url = urlparse(href)
netloc = parsed_url.netloc.lower()
country_code = ''
tld_match = re.search(r'\.([a-z]{2})$', netloc)
if tld_match and tld_match.group(1) not in ['com', 'net', 'org', 'edu', 'gov', 'mil', 'int']:
country_code = tld_match.group(1)
debug_print(f"openSUSE: Extracted country code '{country_code}' from domain: {netloc}")
metadata = {'country': None, 'country_code': country_code}
mirror_url = href.rstrip('/')
debug_print(f"openSUSE: (Fallback) Attempting to add mirror: {mirror_url}")
add_mirror(temp_mirror_groups, mirror_url, "opensuse", metadata)
opensuse_added_count += 1
debug_print(f"openSUSE: Total mirrors passed to add_mirror: {opensuse_added_count}")
def parse_fedora_mirrors(temp_mirror_groups):
debug_print(f"Fetching and parsing Fedora mirrors from {FEDORA_MIRRORS_URL}")
fedora_added_count = 0
centos_added_count = 0
rocky_added_count = 0
html_content = get_content_from_url_or_cache(FEDORA_MIRRORS_URL, FEDORA_CACHE_HTML, BASE_DIR)
if not html_content:
print("Failed to get Fedora mirrors content.")
return
debug_print(f"Fedora HTML content length: {len(html_content) if html_content else 0}")
if len(html_content) < 100:
debug_print("Fedora content appears to be empty or corrupted.")
return
soup = BeautifulSoup(html_content, 'html.parser')
mirror_rows = soup.select('tr.mirror-row')
debug_print(f"Found {len(mirror_rows)} mirror rows in Fedora HTML")
for row in mirror_rows:
actual_country_code = row.select_one('td:nth-of-type(1)')
actual_country_code = actual_country_code.get_text(strip=True) if actual_country_code else None
site_name = row.select_one('td:nth-of-type(2)')
site_name = site_name.get_text(strip=True) if site_name else "Unknown Site"
country_name_detail = row.select_one('td:nth-of-type(3)')
country_name_detail = country_name_detail.get_text(strip=True) if country_name_detail else actual_country_code
bandwidth_text = row.select_one('td:nth-of-type(5)')
bandwidth_text = bandwidth_text.get_text(strip=True) if bandwidth_text else None
internet2_text = row.select_one('td:nth-of-type(6)')
internet2_text = internet2_text.get_text(strip=True).lower() if internet2_text else 'no'
internet2 = internet2_text == 'yes'
categories_cell = row.select_one('td:nth-of-type(4)')
if not categories_cell:
continue
for li in categories_cell.select('ul.list-unstyled > li'):
full_text = li.get_text(strip=True)
category_name = ""
for node in li.contents:
if isinstance(node, str):
category_name += node.strip()
elif node.name == "a":
break
category_name = category_name.strip()
if not category_name:
link_texts = [a.get_text(strip=True) for a in li.find_all('a')]
for link_text in link_texts:
full_text = full_text.replace(link_text, '')
category_name = full_text.strip()
debug_print(f"Fedora: Found category: '{category_name}' from {site_name}")
canonical_distro_name_for_config = None
if 'EPEL' in category_name.upper():
canonical_distro_name_for_config = "EPEL"
elif 'CentOS' in category_name:
canonical_distro_name_for_config = "centos"
elif 'Fedora' in category_name:
canonical_distro_name_for_config = "fedora"
if canonical_distro_name_for_config:
debug_print(f"Fedora: Processing {canonical_distro_name_for_config} links from {site_name}")
for link_tag in li.find_all('a', href=True):
href = link_tag.get('href')
if not href:
continue
protocol_text = link_tag.get_text(strip=True).lower()
if not protocol_text:
if href.startswith('https://'):
protocol_text = 'https'
elif href.startswith('http://'):
protocol_text = 'http'
elif href.startswith('rsync://'):
protocol_text = 'rsync'
elif href.startswith('ftp://'):
protocol_text = 'ftp'
debug_print(f"Fedora: Found {canonical_distro_name_for_config} link: {protocol_text} -> {href}")
if protocol_text in ['http', 'https', 'rsync', 'ftp']:
corrected_country_code = actual_country_code.upper() if actual_country_code else None
bandwidth_mbps = parse_bandwidth(bandwidth_text)
metadata = {
'country_code': corrected_country_code,
'internet2': internet2,
'bandwidth': bandwidth_mbps
}
if bandwidth_mbps and bandwidth_text:
debug_print(f"Fedora: Converted bandwidth '{bandwidth_text}' to {bandwidth_mbps} Mbps")
debug_print(f"Fedora: Adding {canonical_distro_name_for_config} mirror: {href}")
if re.search(r'\balt\b', href):
debug_print(f"Fedora: Skipping fedora-alt mirror: {href}")
continue
if re.search(r'\beln\b', href):
debug_print(f"Fedora: Skipping fedora-eln mirror: {href}")
continue
if re.search(r'\barchive\b', href):
debug_print(f"Fedora: Skipping fedora-archive mirror: {href}")
continue
add_mirror(temp_mirror_groups, href, canonical_distro_name_for_config, metadata)
if canonical_distro_name_for_config == "fedora":
fedora_added_count += 1
elif canonical_distro_name_for_config == "centos":
centos_added_count += 1
elif canonical_distro_name_for_config == "EPEL":
rocky_added_count += 1
print(f"Fedora: Total mirrors passed to add_mirror: Fedora={fedora_added_count}, CentOS={centos_added_count}, EPEL={rocky_added_count}")
def parse_ubuntu_mirrors(temp_mirror_groups):
debug_print(f"Fetching and parsing Ubuntu mirrors from {UBUNTU_MIRRORS_URL}")
ubuntu_added_count = 0
content_bytes = get_content_from_url_or_cache(UBUNTU_MIRRORS_URL, UBUNTU_CACHE_HTML, BASE_DIR)
if not content_bytes:
debug_print(f"Failed to fetch Ubuntu mirrors from URL. Trying local file {UBUNTU_MIRRORS_PATH}")
try:
with open(UBUNTU_MIRRORS_PATH, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
debug_print(f"Ubuntu content loaded from local file, size: {len(content)} bytes")
except FileNotFoundError:
debug_print(f"Ubuntu mirror file not found at {UBUNTU_MIRRORS_PATH}.")
content = None
except Exception as e:
debug_print(f"Error reading Ubuntu mirror file: {e}")
content = None
else:
if isinstance(content_bytes, bytes):
try:
content = content_bytes.decode('utf-8')
except UnicodeDecodeError:
try:
content = content_bytes.decode('latin-1')
except Exception as e:
debug_print(f"Error decoding content: {e}")
content = None
else:
content = content_bytes
debug_print(f"Successfully fetched Ubuntu mirrors from URL, size: {len(content) if content else 0} bytes")
if not content:
debug_print("Ubuntu: No mirror content available.")
return
try:
mirror_data = json.loads(content)
debug_print(f"Successfully parsed Ubuntu data as JSON: {len(mirror_data)} entries")
except json.JSONDecodeError as e:
debug_print(f"Error parsing Ubuntu mirror JSON: {e}")
html_indicators = ["<html", "<body", "<!DOCTYPE", "<head"]
is_likely_html = any(indicator in content for indicator in html_indicators) if isinstance(content, str) else False
if is_likely_html:
debug_print("Trying to parse Ubuntu data as HTML...")
soup = BeautifulSoup(content, 'html.parser')
links_found = False
current_country = None
country_code_map = {}
current_country = None
rows = soup.find_all('tr')
debug_print(f"Ubuntu: Sequentially processing {len(rows)} table rows in HTML content")
for row in rows:
th_cell = row.find('th', attrs={'colspan': '2'})
if th_cell is not None:
country_name = th_cell.get_text(strip=True)
if country_name:
current_country = country_name
country_code_map[country_name] = {'name': country_name, 'code': None}
debug_print(f"Ubuntu: Switched current country to: {country_name}")
continue
if current_country is None:
continue
cells = row.find_all('td')
if len(cells) < 3:
continue
links_cell = cells[1]
mirror_links = links_cell.find_all('a', href=True)
bandwidth_cell = cells[2] if len(cells) > 2 else None
bandwidth_text = bandwidth_cell.get_text(strip=True) if bandwidth_cell else None
if bandwidth_text and any(x in bandwidth_text.lower() for x in ['gbps', 'mbps', 'tb', 'gb', 'mb']):
debug_print(f"Ubuntu: Found row with bandwidth: {bandwidth_text}")
for a_tag in mirror_links:
href = a_tag['href']
if href.startswith(('http://', 'https://', 'rsync://')) and ("/ubuntu" in href or ".ubuntu.com" in href or "archive.ubuntu.com" in href):
mirror_url = href.rstrip('/')
country_info = country_code_map.get(current_country, {'name': current_country, 'code': None})
debug_print(f"Ubuntu: Found mirror URL from HTML table: {mirror_url}, Bandwidth: {bandwidth_text}, Country: {country_info['name']}")
bandwidth_mbps = parse_bandwidth(bandwidth_text)
metadata = {
'country': country_info['name'],
'country_code': country_info['code'],
'bandwidth': bandwidth_mbps
}
if bandwidth_mbps and bandwidth_text:
debug_print(f"Ubuntu: Converted bandwidth '{bandwidth_text}' to {bandwidth_mbps} Mbps")
add_mirror(temp_mirror_groups, mirror_url, "ubuntu", metadata)
ubuntu_added_count += 1
links_found = True
if not links_found:
print("Ubuntu: No mirrors found in table rows, trying to find individual links...")
for a_tag in soup.find_all('a', href=True):
href = a_tag['href']
if href.startswith("http://") or href.startswith("https://"):
if "/ubuntu" in href or ".ubuntu.com" in href or "archive.ubuntu.com" in href:
mirror_url = href.rstrip('/')
debug_print(f"Ubuntu: Found mirror URL from HTML: {mirror_url}")
metadata = {'country': None, 'country_code': None}
add_mirror(temp_mirror_groups, mirror_url, "ubuntu", metadata)
ubuntu_added_count += 1
links_found = True
if links_found:
debug_print(f"Ubuntu: Added {ubuntu_added_count} mirrors from HTML content")
return
if isinstance(content, str):
lines = content.splitlines()
debug_print(f"Trying to parse Ubuntu mirror file as text: {len(lines)} lines")
for line in lines:
line = line.strip()
if line.startswith('http://') or line.startswith('https://'):
parts = line.split()
mirror_url = parts[0].strip()
debug_print(f"Ubuntu: Found mirror URL from text: {mirror_url}")
metadata = {'country': None, 'country_code': None}
add_mirror(temp_mirror_groups, mirror_url, "ubuntu", metadata)
ubuntu_added_count += 1
if ubuntu_added_count > 0:
print(f"Ubuntu: Added {ubuntu_added_count} mirrors from text file")
return
print("Ubuntu: No mirrors found in file.")
return
except FileNotFoundError:
debug_print(f"Ubuntu mirror file not found at {UBUNTU_MIRRORS_PATH}. Skipping.")
return
except Exception as e:
debug_print(f"Error reading Ubuntu mirror file: {e}")
return
debug_print("Processing Ubuntu mirror JSON data:")
for idx, mirror_entry in enumerate(mirror_data):
if 'url' not in mirror_entry:
debug_print(f"Ubuntu: Entry {idx} missing URL, skipping")
continue
mirror_url = mirror_entry['url']
country_code = mirror_entry.get('country_code', None)
country_name = mirror_entry.get('country', None)
debug_print(f"Ubuntu: Processing mirror: {mirror_url}, Country: {country_code or 'Unknown'}")
metadata = {
'country': country_name,
'country_code': country_code.upper() if country_code else None,
'speed': mirror_entry.get('speed', None),
'official': mirror_entry.get('official', False)
}
if not mirror_url.startswith(('http://', 'https://', 'rsync://')):
debug_print(f"Ubuntu: Skipping non-http/https/rsync URL: {mirror_url}")
continue
add_mirror(temp_mirror_groups, mirror_url, "ubuntu", metadata)
ubuntu_added_count += 1
print(f"Ubuntu: Total mirrors passed to add_mirror: {ubuntu_added_count}")
def parse_openeuler_mirrors(temp_mirror_groups):
print(f"Parsing openEuler mirrors from local file: {OPENEULER_MIRRORS_PATH}")
if not os.path.exists(OPENEULER_MIRRORS_PATH):
print(f"openEuler mirror file not found: {OPENEULER_MIRRORS_PATH}")
return
try:
with open(OPENEULER_MIRRORS_PATH, 'rb') as f:
soup = BeautifulSoup(f, 'html.parser')
except IOError as e:
debug_print(f"Failed to read or parse {OPENEULER_MIRRORS_PATH}: {e}")
return
count = 0
mirror_table_div = soup.find('div', class_='o-table mirror-pc')
if not mirror_table_div:
debug_print(f"Could not find the main mirror table div ('o-table mirror-pc') in {OPENEULER_MIRRORS_PATH}.")
return
table = mirror_table_div.find('table')
if not table:
debug_print(f"Could not find table within 'o-table mirror-pc' div in {OPENEULER_MIRRORS_PATH}.")
return
tbody = table.find('tbody')
if not tbody:
debug_print(f"Could not find tbody in the mirror table in {OPENEULER_MIRRORS_PATH}.")
return
for row in tbody.find_all('tr'):
cells = row.find_all('td')
if len(cells) < 2:
continue
site_cell = cells[0]
link_tag = site_cell.find('a', href=True)
if not link_tag:
continue
mirror_url = link_tag['href'].rstrip('/')
if not (mirror_url.startswith('http://') or mirror_url.startswith('https://') or mirror_url.startswith('rsync://')):
continue
location_cell = cells[1]
country_name = location_cell.get_text(strip=True)
if not country_name:
country_name = None
metadata = {
'country': country_name,
'country_code': None
}
add_mirror(temp_mirror_groups, mirror_url, "openeuler", metadata)
count += 1
if count > 0:
print(f"Processed {count} openEuler mirrors from {OPENEULER_MIRRORS_PATH}.")
else:
print("No openEuler mirrors found or parsed from {OPENEULER_MIRRORS_PATH}. Check HTML structure and parsing logic.")
def collect_mirrors():
"""Collect mirrors from all distro sources and return temporary groups."""
temp_mirror_groups = {}
parse_alpine_mirrors(temp_mirror_groups)
parse_debian_mirrors(temp_mirror_groups)
parse_arch_mirrors(temp_mirror_groups)
parse_opensuse_mirrors(temp_mirror_groups)
parse_fedora_mirrors(temp_mirror_groups)
parse_ubuntu_mirrors(temp_mirror_groups)
parse_openeuler_mirrors(temp_mirror_groups)
debug_print("\n--- DEBUG: Checking temp_mirror_groups for all distros ---")
distro_counts = {}
for key_tuple, group_data_val in temp_mirror_groups.items():
for distro in group_data_val.get('distro_dirs', set()):
distro_counts[distro] = distro_counts.get(distro, 0) + 1
if distro_counts[distro] <= 2:
debug_print(f"DEBUG_TEMP: Found {distro} in temp_mirror_groups: Key={key_tuple}")
if '163.com' in key_tuple[0] or 'ustc.edu' in key_tuple[0]:
debug_print(f"DEBUG_TEMP: Notable mirror found: {key_tuple}, Data={group_data_val}")
print("\nMirror counts by distro:")
for distro, count in distro_counts.items():
print(f" {distro}: {count} mirrors")
debug_print("")
rsync_only_count = 0
for key_tuple, group_data in temp_mirror_groups.items():
protocols = group_data['protocols']
if len(protocols) == 1 and 'rsync' in protocols:
rsync_only_count += 1
if rsync_only_count <= 3:
debug_print(f"DEBUG_RSYNC_ONLY: {key_tuple} - Distros: {group_data['distros']}")
print(f"Total: {rsync_only_count} rsync-only mirrors will be skipped\n")
return temp_mirror_groups
def process_mirror_groups(temp_mirror_groups, valid_dirs):
"""Process temporary mirror groups into final mirror structure."""
def find_matching_distro_dir(path, valid_dirs):
"""Find the longest suffix of path that matches a valid directory.
Given a path like '/pub/fedora/linux', check if the entire path
(without leading slash) is in valid_dirs, then 'fedora/linux', then 'linux'.
Returns (matched_suffix, parent_segments) where parent_segments are the
segments before the matched suffix (original case).
If no match, returns (None, None).
"""
path_stripped = path.lstrip('/')
if not path_stripped:
return None, None
segments = [seg for seg in path_stripped.split('/') if seg]
for i in range(len(segments)):
candidate = '/'.join(segments[i:])
if candidate in valid_dirs:
matched_suffix = '/'.join(segments[i:])
parent_segments = segments[:i] if i > 0 else []
return matched_suffix, parent_segments
return None, None
official_mirrors = {}
for (netloc, common_path), group_data in temp_mirror_groups.items():
protocols = group_data['protocols']
if len(protocols) == 1 and 'rsync' in protocols:
debug_print(f"Skipping rsync-only mirror: {netloc}{common_path}")
continue
final_url_key = urlunparse((group_data['representative_scheme'], netloc, common_path.rstrip('/'), '', '', ''))
if common_path == '/':
final_url_key = urlunparse((group_data['representative_scheme'], netloc, '', '', '', '')).rstrip('/')
else:
final_url_key = final_url_key.rstrip('/')
final_entry = {
'distro_dirs': sorted(list(group_data['distro_dirs'])),
'protocols': sorted(list(group_data['protocols'])),
'original_urls': sorted(list(group_data['original_urls'])),
'distros': sorted(list(group_data['distros']))
}
final_entry.update(group_data['metadata_store'])
official_mirrors[final_url_key] = final_entry
from collections import defaultdict
base_groups = defaultdict(list)
to_delete = []
for url_key, entry in list(official_mirrors.items()):
if entry.get('top_level'):
parsed = urlparse(url_key)
path = parsed.path
distro_dir, parent_segments = find_matching_distro_dir(path, valid_dirs)
if distro_dir is not None:
debug_print(f"Transforming top-level entry: {url_key} -> will move '{distro_dir}' to distro_dirs", "transform")
parent_path = '/' + '/'.join(parent_segments) if parent_segments else '/'
base_url = urlunparse((parsed.scheme, parsed.netloc, parent_path, '', '', '')).rstrip('/')
if parent_path == '/':
base_url = urlunparse((parsed.scheme, parsed.netloc, '', '', '', '')).rstrip('/')
base_groups[base_url].append((url_key, entry, distro_dir))
to_delete.append(url_key)
for base_url, items in base_groups.items():
distro_dirs = []
all_protocols = set()
all_original_urls = set()
template_entry = items[0][1]
for _, entry, distro_dir in items:
if distro_dir not in distro_dirs:
distro_dirs.append(distro_dir)
all_protocols.update(entry.get('protocols', []))
all_original_urls.update(entry.get('original_urls', []))
distro_dirs.sort()
if base_url in official_mirrors and base_url not in to_delete:
existing = official_mirrors[base_url]
for dd in distro_dirs:
if dd not in existing.get('distro_dirs', []):
existing.setdefault('distro_dirs', []).append(dd)
existing['distro_dirs'].sort()
existing_protocols = set(existing.get('protocols', []))
existing['protocols'] = sorted(list(existing_protocols | all_protocols))
existing_original = set(existing.get('original_urls', []))
existing['original_urls'] = sorted(list(existing_original | all_original_urls))
existing.pop('top_level', None)
existing.pop('distros', None)
else:
new_entry = template_entry.copy()
new_entry.pop('top_level', None)
new_entry.pop('distros', None)
new_entry['distro_dirs'] = distro_dirs
new_entry['protocols'] = sorted(list(all_protocols))
new_entry['original_urls'] = sorted(list(all_original_urls))
official_mirrors[base_url] = new_entry
for url in to_delete:
if url in official_mirrors:
del official_mirrors[url]
debug_print("\n--- DEBUG: Checking final official_mirrors for all distros ---")
final_distro_counts = {}
for url_key_final, entry_data_final in official_mirrors.items():
for distro in entry_data_final.get('distro_dirs', []):
final_distro_counts[distro] = final_distro_counts.get(distro, 0) + 1
if final_distro_counts[distro] <= 1:
debug_print(f"DEBUG_FINAL: Found {distro} in official_mirrors: Key={url_key_final}")
if distro in ['fedora', 'ubuntu', 'alpine'] or '163.com' in url_key_final or 'ustc.edu' in url_key_final:
debug_print(f"DEBUG_FINAL: Notable mirror found: {url_key_final}, Data={entry_data_final}")
print("\nFinal mirror counts by distro:")
for distro, count in final_distro_counts.items():
print(f" {distro}: {count} entries")
debug_print("")
for target_mirror in ['mirrors.163.com', 'mirrors.ustc.edu.cn']:
found = False
for url_key_final in official_mirrors.keys():
if target_mirror in url_key_final:
found = True
debug_print(f"DEBUG_FINAL: {target_mirror} IS present in the final output: {url_key_final}")
break
if not found:
debug_print(f"DEBUG_FINAL: WARNING! {target_mirror} is NOT present in the final output!")
debug_print("")
for expected_distro in ['debian', 'opensuse', 'fedora', 'ubuntu', 'alpine', 'archlinux']:
if final_distro_counts.get(expected_distro, 0) == 0:
debug_print(f"DEBUG_FINAL: WARNING! No {expected_distro} mirrors in final output!")
return official_mirrors
def write_mirrors_output(official_mirrors, output_path):
"""Write new mirrors dictionary to JSON file."""
print(f"Writing {len(official_mirrors)} new/updated mirrors to {output_path}")
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'w') as f:
json.dump(official_mirrors, f, indent=4, sort_keys=True)
print("New mirror generation complete.")
def main():
print("Starting new mirror generation...")
load_distro_configs(BASE_DIR)
valid_dirs = get_valid_dirs(BASE_DIR)
temp_mirror_groups = collect_mirrors()
official_mirrors = process_mirror_groups(temp_mirror_groups, valid_dirs)
write_mirrors_output(official_mirrors, OFFICIAL_MIRRORS_OUTPUT_PATH)
if __name__ == "__main__":
main()