Skip to content

Commit 1764b49

Browse files
authored
Refactor mirrors (#4274)
1 parent 8d6c56c commit 1764b49

8 files changed

Lines changed: 176 additions & 172 deletions

File tree

archinstall/lib/global_menu.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from archinstall.lib.interactions.system_conf import select_kernel, select_swap
1212
from archinstall.lib.locale.locale_menu import LocaleMenu
1313
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
14-
from archinstall.lib.mirrors import MirrorListHandler, MirrorMenu
14+
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
15+
from archinstall.lib.mirror.mirror_menu import MirrorMenu
1516
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
1617
from archinstall.lib.models.authentication import AuthenticationConfiguration
1718
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration

archinstall/lib/installer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from archinstall.lib.hardware import SysInfo
3131
from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout
3232
from archinstall.lib.luks import Luks2, unlock_luks2_dev
33-
from archinstall.lib.mirrors import MirrorListHandler
33+
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
3434
from archinstall.lib.models.application import ZramAlgorithm
3535
from archinstall.lib.models.bootloader import Bootloader
3636
from archinstall.lib.models.device import (

archinstall/lib/mirror/__init__.py

Whitespace-only changes.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import time
2+
import urllib
3+
from pathlib import Path
4+
5+
from archinstall.lib.models import MirrorRegion
6+
from archinstall.lib.models.mirrors import MirrorStatusEntryV3, MirrorStatusListV3
7+
from archinstall.lib.networking import fetch_data_from_url
8+
from archinstall.lib.output import debug, info
9+
10+
11+
class MirrorListHandler:
12+
def __init__(
13+
self,
14+
local_mirrorlist: Path = Path('/etc/pacman.d/mirrorlist'),
15+
offline: bool = False,
16+
verbose: bool = False,
17+
) -> None:
18+
self._local_mirrorlist = local_mirrorlist
19+
self._status_mappings: dict[str, list[MirrorStatusEntryV3]] | None = None
20+
self._fetched_remote: bool = False
21+
self.offline = offline
22+
self.verbose = verbose
23+
24+
def _mappings(self) -> dict[str, list[MirrorStatusEntryV3]]:
25+
if self._status_mappings is None:
26+
self.load_mirrors()
27+
28+
assert self._status_mappings is not None
29+
return self._status_mappings
30+
31+
def get_mirror_regions(self) -> list[MirrorRegion]:
32+
available_mirrors = []
33+
mappings = self._mappings()
34+
35+
for region_name, status_entry in mappings.items():
36+
urls = [entry.server_url for entry in status_entry]
37+
region = MirrorRegion(region_name, urls)
38+
available_mirrors.append(region)
39+
40+
return available_mirrors
41+
42+
def load_mirrors(self) -> None:
43+
if self.offline:
44+
self._fetched_remote = False
45+
self.load_local_mirrors()
46+
else:
47+
self._fetched_remote = self.load_remote_mirrors()
48+
debug(f'load mirrors: {self._fetched_remote}')
49+
if not self._fetched_remote:
50+
self.load_local_mirrors()
51+
52+
def load_remote_mirrors(self) -> bool:
53+
url = 'https://archlinux.org/mirrors/status/json/'
54+
attempts = 3
55+
56+
for attempt_nr in range(attempts):
57+
try:
58+
mirrorlist = fetch_data_from_url(url)
59+
self._status_mappings = self._parse_remote_mirror_list(mirrorlist)
60+
return True
61+
except Exception as e:
62+
debug(f'Error while fetching mirror list: {e}')
63+
time.sleep(attempt_nr + 1)
64+
65+
debug('Unable to fetch mirror list remotely, falling back to local mirror list')
66+
return False
67+
68+
def load_local_mirrors(self) -> None:
69+
with self._local_mirrorlist.open('r') as fp:
70+
mirrorlist = fp.read()
71+
self._status_mappings = self._parse_local_mirrors(mirrorlist)
72+
73+
def get_status_by_region(self, region: str, speed_sort: bool) -> list[MirrorStatusEntryV3]:
74+
mappings = self._mappings()
75+
region_list = mappings[region]
76+
77+
# Only sort if we have remote mirror data with score/speed info
78+
# Local mirrors lack this data and can be modified manually before-hand
79+
# Or reflector potentially ran already
80+
if self._fetched_remote and speed_sort:
81+
info('Sorting your selected mirror list based on the speed between you and the individual mirrors (this might take a while)')
82+
# Sort by speed descending (higher is better in bitrate form core.db download)
83+
return sorted(region_list, key=lambda mirror: -mirror.speed)
84+
# just return as-is without sorting?
85+
return region_list
86+
87+
def _parse_remote_mirror_list(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]:
88+
context = {'verbose': self.verbose}
89+
mirror_status = MirrorStatusListV3.model_validate_json(mirrorlist, context=context)
90+
91+
sorting_placeholder: dict[str, list[MirrorStatusEntryV3]] = {}
92+
93+
for mirror in mirror_status.urls:
94+
# We filter out mirrors that have bad criteria values
95+
if any(
96+
[
97+
mirror.active is False, # Disabled by mirror-list admins
98+
mirror.last_sync is None, # Has not synced recently
99+
# mirror.score (error rate) over time reported from backend:
100+
# https://github.com/archlinux/archweb/blob/31333d3516c91db9a2f2d12260bd61656c011fd1/mirrors/utils.py#L111C22-L111C66
101+
(mirror.score is None or mirror.score >= 100),
102+
]
103+
):
104+
continue
105+
106+
if mirror.country == '':
107+
# TODO: This should be removed once RFC!29 is merged and completed
108+
# Until then, there are mirrors which lacks data in the backend
109+
# and there is no way of knowing where they're located.
110+
# So we have to assume world-wide
111+
mirror.country = 'Worldwide'
112+
113+
if mirror.url.startswith('http'):
114+
sorting_placeholder.setdefault(mirror.country, []).append(mirror)
115+
116+
sorted_by_regions: dict[str, list[MirrorStatusEntryV3]] = dict(
117+
{region: unsorted_mirrors for region, unsorted_mirrors in sorted(sorting_placeholder.items(), key=lambda item: item[0])}
118+
)
119+
120+
return sorted_by_regions
121+
122+
def _parse_local_mirrors(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]:
123+
lines = mirrorlist.splitlines()
124+
125+
# remove empty lines
126+
# lines = [line for line in lines if line]
127+
128+
mirror_list: dict[str, list[MirrorStatusEntryV3]] = {}
129+
130+
current_region = ''
131+
132+
for line in lines:
133+
line = line.strip()
134+
135+
if line.startswith('## '):
136+
current_region = line.replace('## ', '').strip()
137+
mirror_list.setdefault(current_region, [])
138+
139+
if line.startswith('Server = '):
140+
if not current_region:
141+
current_region = 'Local'
142+
mirror_list.setdefault(current_region, [])
143+
144+
url = line.removeprefix('Server = ')
145+
146+
mirror_entry = MirrorStatusEntryV3(
147+
url=url.removesuffix('$repo/os/$arch'),
148+
protocol=urllib.parse.urlparse(url).scheme,
149+
active=True,
150+
country=current_region or 'Worldwide',
151+
# The following values are normally populated by
152+
# archlinux.org mirror-list endpoint, and can't be known
153+
# from just the local mirror-list file.
154+
country_code='WW',
155+
isos=True,
156+
ipv4=True,
157+
ipv6=True,
158+
details='Locally defined mirror',
159+
)
160+
161+
mirror_list[current_region].append(mirror_entry)
162+
163+
return mirror_list
Lines changed: 2 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
1-
import time
2-
import urllib.parse
3-
from pathlib import Path
41
from typing import override
52

63
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
74
from archinstall.lib.menu.helpers import Input, Loading, Selection
85
from archinstall.lib.menu.list_manager import ListManager
6+
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
97
from archinstall.lib.models.mirrors import (
108
CustomRepository,
119
CustomServer,
1210
MirrorConfiguration,
1311
MirrorRegion,
14-
MirrorStatusEntryV3,
15-
MirrorStatusListV3,
1612
SignCheck,
1713
SignOption,
1814
)
1915
from archinstall.lib.models.packages import Repository
20-
from archinstall.lib.networking import fetch_data_from_url
21-
from archinstall.lib.output import FormattedOutput, debug, info
16+
from archinstall.lib.output import FormattedOutput
2217
from archinstall.lib.translationhandler import tr
2318
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
2419
from archinstall.tui.ui.result import ResultType
@@ -206,161 +201,6 @@ async def _add_custom_server(self, preset: CustomServer | None = None) -> Custom
206201
return None
207202

208203

209-
class MirrorListHandler:
210-
def __init__(
211-
self,
212-
local_mirrorlist: Path = Path('/etc/pacman.d/mirrorlist'),
213-
offline: bool = False,
214-
verbose: bool = False,
215-
) -> None:
216-
self._local_mirrorlist = local_mirrorlist
217-
self._status_mappings: dict[str, list[MirrorStatusEntryV3]] | None = None
218-
self._fetched_remote: bool = False
219-
self.offline = offline
220-
self.verbose = verbose
221-
222-
def _mappings(self) -> dict[str, list[MirrorStatusEntryV3]]:
223-
if self._status_mappings is None:
224-
self.load_mirrors()
225-
226-
assert self._status_mappings is not None
227-
return self._status_mappings
228-
229-
def get_mirror_regions(self) -> list[MirrorRegion]:
230-
available_mirrors = []
231-
mappings = self._mappings()
232-
233-
for region_name, status_entry in mappings.items():
234-
urls = [entry.server_url for entry in status_entry]
235-
region = MirrorRegion(region_name, urls)
236-
available_mirrors.append(region)
237-
238-
return available_mirrors
239-
240-
def load_mirrors(self) -> None:
241-
if self.offline:
242-
self._fetched_remote = False
243-
self.load_local_mirrors()
244-
else:
245-
self._fetched_remote = self.load_remote_mirrors()
246-
debug(f'load mirrors: {self._fetched_remote}')
247-
if not self._fetched_remote:
248-
self.load_local_mirrors()
249-
250-
def load_remote_mirrors(self) -> bool:
251-
url = 'https://archlinux.org/mirrors/status/json/'
252-
attempts = 3
253-
254-
for attempt_nr in range(attempts):
255-
try:
256-
mirrorlist = fetch_data_from_url(url)
257-
self._status_mappings = self._parse_remote_mirror_list(mirrorlist)
258-
return True
259-
except Exception as e:
260-
debug(f'Error while fetching mirror list: {e}')
261-
time.sleep(attempt_nr + 1)
262-
263-
debug('Unable to fetch mirror list remotely, falling back to local mirror list')
264-
return False
265-
266-
def load_local_mirrors(self) -> None:
267-
with self._local_mirrorlist.open('r') as fp:
268-
mirrorlist = fp.read()
269-
self._status_mappings = self._parse_local_mirrors(mirrorlist)
270-
271-
def get_status_by_region(self, region: str, speed_sort: bool) -> list[MirrorStatusEntryV3]:
272-
mappings = self._mappings()
273-
region_list = mappings[region]
274-
275-
# Only sort if we have remote mirror data with score/speed info
276-
# Local mirrors lack this data and can be modified manually before-hand
277-
# Or reflector potentially ran already
278-
if self._fetched_remote and speed_sort:
279-
info('Sorting your selected mirror list based on the speed between you and the individual mirrors (this might take a while)')
280-
# Sort by speed descending (higher is better in bitrate form core.db download)
281-
return sorted(region_list, key=lambda mirror: -mirror.speed)
282-
# just return as-is without sorting?
283-
return region_list
284-
285-
def _parse_remote_mirror_list(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]:
286-
context = {'verbose': self.verbose}
287-
mirror_status = MirrorStatusListV3.model_validate_json(mirrorlist, context=context)
288-
289-
sorting_placeholder: dict[str, list[MirrorStatusEntryV3]] = {}
290-
291-
for mirror in mirror_status.urls:
292-
# We filter out mirrors that have bad criteria values
293-
if any(
294-
[
295-
mirror.active is False, # Disabled by mirror-list admins
296-
mirror.last_sync is None, # Has not synced recently
297-
# mirror.score (error rate) over time reported from backend:
298-
# https://github.com/archlinux/archweb/blob/31333d3516c91db9a2f2d12260bd61656c011fd1/mirrors/utils.py#L111C22-L111C66
299-
(mirror.score is None or mirror.score >= 100),
300-
]
301-
):
302-
continue
303-
304-
if mirror.country == '':
305-
# TODO: This should be removed once RFC!29 is merged and completed
306-
# Until then, there are mirrors which lacks data in the backend
307-
# and there is no way of knowing where they're located.
308-
# So we have to assume world-wide
309-
mirror.country = 'Worldwide'
310-
311-
if mirror.url.startswith('http'):
312-
sorting_placeholder.setdefault(mirror.country, []).append(mirror)
313-
314-
sorted_by_regions: dict[str, list[MirrorStatusEntryV3]] = dict(
315-
{region: unsorted_mirrors for region, unsorted_mirrors in sorted(sorting_placeholder.items(), key=lambda item: item[0])}
316-
)
317-
318-
return sorted_by_regions
319-
320-
def _parse_local_mirrors(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]:
321-
lines = mirrorlist.splitlines()
322-
323-
# remove empty lines
324-
# lines = [line for line in lines if line]
325-
326-
mirror_list: dict[str, list[MirrorStatusEntryV3]] = {}
327-
328-
current_region = ''
329-
330-
for line in lines:
331-
line = line.strip()
332-
333-
if line.startswith('## '):
334-
current_region = line.replace('## ', '').strip()
335-
mirror_list.setdefault(current_region, [])
336-
337-
if line.startswith('Server = '):
338-
if not current_region:
339-
current_region = 'Local'
340-
mirror_list.setdefault(current_region, [])
341-
342-
url = line.removeprefix('Server = ')
343-
344-
mirror_entry = MirrorStatusEntryV3(
345-
url=url.removesuffix('$repo/os/$arch'),
346-
protocol=urllib.parse.urlparse(url).scheme,
347-
active=True,
348-
country=current_region or 'Worldwide',
349-
# The following values are normally populated by
350-
# archlinux.org mirror-list endpoint, and can't be known
351-
# from just the local mirror-list file.
352-
country_code='WW',
353-
isos=True,
354-
ipv4=True,
355-
ipv6=True,
356-
details='Locally defined mirror',
357-
)
358-
359-
mirror_list[current_region].append(mirror_entry)
360-
361-
return mirror_list
362-
363-
364204
class MirrorMenu(AbstractSubMenu[MirrorConfiguration]):
365205
def __init__(
366206
self,

0 commit comments

Comments
 (0)