OSDN Git Service

Initial commit
[rebornos/cnchi-gnome-osdn.git] / Cnchi / mirrors.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # mirrors.py
5 #
6 # Copyright © 2013-2018 Antergos
7 #
8 # This file is part of Cnchi.
9 #
10 # Cnchi is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Cnchi is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # The following additional terms are in effect as per Section 7 of the license:
21 #
22 # The preservation of all legal notices and author attributions in
23 # the material or in the Appropriate Legal Notices displayed
24 # by works containing it is required.
25 #
26 # You should have received a copy of the GNU General Public License
27 # along with Cnchi; If not, see <http://www.gnu.org/licenses/>.
28
29
30 """ Let advanced users manage mirrorlist files """
31
32 import logging
33 import multiprocessing
34 import shutil
35
36 import gi
37 gi.require_version('Gtk', '3.0')
38 from gi.repository import Gtk, Gdk, GObject
39
40 try:
41     gi.require_foreign("cairo")
42 except ImportError:
43     print("No pycairo integration")
44
45 import cairo
46 from pages.gtkbasebox import GtkBaseBox
47 from rank_mirrors import RankMirrors
48
49 if __name__ == '__main__':
50     import sys
51     sys.path.append('/usr/share/cnchi/src')
52
53 # When testing, no _() is available
54 try:
55     _("")
56 except NameError as err:
57     def _(message):
58         return message
59
60
61 class MirrorListBoxRow(Gtk.ListBoxRow):
62     """ Represents a mirror """
63     def __init__(self, url, active, switch_cb, drag_cbs):
64         super(Gtk.ListBoxRow, self).__init__()
65         #self.data = data
66         # self.add(Gtk.Label(data))
67
68         self.data = url
69
70         box = Gtk.Box(spacing=20)
71
72         self.handle = Gtk.EventBox.new()
73         self.handle.add(Gtk.Image.new_from_icon_name("open-menu-symbolic", 1))
74         box.pack_start(self.handle, False, False, 0)
75
76         # Add mirror url label
77         self.label = Gtk.Label.new()
78         self.label.set_halign(Gtk.Align.START)
79         self.label.set_justify(Gtk.Justification.LEFT)
80         self.label.set_name(url)
81         # Only show site address
82         url_parts = url.split('/')
83         text_url = url_parts[0] + "//" + url_parts[2]
84         self.label.set_text(text_url)
85         box.pack_start(self.label, False, True, 0)
86
87         # Add mirror switch
88         self.switch = Gtk.Switch.new()
89         self.switch.set_name("switch_" + url)
90         self.switch.set_property('margin_top', 2)
91         self.switch.set_property('margin_bottom', 2)
92         self.switch.set_property('margin_end', 10)
93         self.switch.connect("notify::active", switch_cb)
94         self.switch.set_active(active)
95         box.pack_end(self.switch, False, False, 0)
96
97         self.add(box)
98
99         self.set_selectable(True)
100
101         # Drag and drop
102         # Source
103         self.handle.drag_source_set(
104             Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.MOVE)
105         self.handle.drag_source_add_text_targets()
106         self.handle.connect("drag-begin", drag_cbs['drag-begin'])
107         self.handle.connect("drag-data-get", drag_cbs['drag-data-get'])
108         #self.handle.connect("drag-data-delete", self.drag_data_delete)
109         #self.handle.connect("drag-end", self.drag_end)
110
111         # Destination
112         self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.MOVE)
113         self.drag_dest_add_text_targets()
114         self.connect("drag-data-received", drag_cbs['drag-data-received'])
115         #self.connect("drag-motion", self.drag_motion);
116         #self.connect("drag-crop", self.drag_crop);
117
118     def is_active(self):
119         """ Returns if the mirror is active """
120         return self.switch.get_active()
121
122
123 class MirrorListBox(Gtk.ListBox):
124     """ List that stores all mirrors """
125     __gsignals__ = {
126         'switch-activated': (GObject.SignalFlags.RUN_FIRST, None, ())
127     }
128
129     # 6 mirrors for Arch repos and 6 for Antergos repos
130     MAX_MIRRORS = 7
131     # DND_ID_LISTBOX_ROW = 6791
132
133     def __init__(self, mirrors_file_path, settings):
134         super(Gtk.ListBox, self).__init__()
135         self.mirrors_file_path = mirrors_file_path
136         self.set_selection_mode(Gtk.SelectionMode.NONE)
137         # self.set_selection_mode(Gtk.SelectionMode.BROWSE)
138         # self.connect("row-selected", self.on_listbox_row_selected)
139         # self.sort_func(self.listbox_sort_by_name, None)
140
141         self.settings = settings
142
143         # List. Each element is a tuple (url, active)
144         self.mirrors = []
145
146         self.load_mirrors()
147         self.fillme()
148
149     @staticmethod
150     def uncommented_mirrors_first(mirrors):
151         """ Put uncommented mirrors first. Order is not guaranteed """
152         commented = []
153         uncommented = []
154         for mirror in mirrors:
155             if mirror.startswith("#"):
156                 commented.append(mirror)
157             else:
158                 uncommented.append(mirror)
159         return uncommented + commented
160
161     def load_mirrors(self):
162         """ Load mirrors from text file """
163         lines = []
164
165         # Load mirror file contents
166         with open(self.mirrors_file_path) as mfile:
167             lines = mfile.readlines()
168
169         # Discard lines that are not server lines
170         tmp_lines = lines
171         lines = []
172         for line in tmp_lines:
173             line = line.strip()
174             if line.startswith("Server") or line.startswith("#Server"):
175                 lines.append(line)
176         tmp_lines = []
177
178         lines = self.uncommented_mirrors_first(lines)
179
180         # Use MAX_MIRRORS at max
181         if len(lines) > MirrorListBox.MAX_MIRRORS:
182             lines = lines[0:MirrorListBox.MAX_MIRRORS]
183
184         # Read mirror info and create mirrors list
185         for line in lines:
186             if line.startswith("#Server"):
187                 active = False
188                 line = line[1:]
189             else:
190                 active = True
191
192             try:
193                 url = line.split("=")[1].strip()
194                 logging.debug(url)
195                 self.mirrors.append((url, active))
196             except KeyError:
197                 pass
198
199     def fillme(self):
200         """ Fill listbox with mirrors info """
201         for listboxrow in self.get_children():
202             listboxrow.destroy()
203
204         drag_cbs = {
205             'drag-begin': self.drag_begin,
206             'drag-data-get': self.drag_data_get,
207             'drag-data-received': self.drag_data_received
208         }
209
210         for (url, active) in self.mirrors:
211             box = Gtk.Box(spacing=20)
212             box.set_name(url)
213             row = MirrorListBoxRow(url, active, self.switch_activated, drag_cbs)
214             self.add(row)
215
216     def set_mirror_active(self, url, active):
217         """ Changes the active status in our mirrors list """
218         for index, item in enumerate(self.mirrors):
219             murl, _mact = item
220             if url == murl:
221                 self.mirrors[index] = (url, active)
222
223     def get_active_mirrors(self):
224         """ Returns a list with all active mirrors """
225         active_mirrors = []
226         for (url, active) in self.mirrors:
227             if active:
228                 active_mirrors.append(url)
229         return active_mirrors
230
231     def switch_activated(self, switch, _gparam):
232         """ Mirror activated """
233         row = switch.get_ancestor(Gtk.ListBoxRow)
234         if row:
235             self.set_mirror_active(row.data, switch.get_active())
236             self.emit("switch-activated")
237
238     def drag_begin(self, widget, drag_context):
239         """ User starts a drag """
240         row = widget.get_ancestor(Gtk.ListBoxRow)
241         alloc = row.get_allocation()
242         surface = cairo.ImageSurface(
243             cairo.FORMAT_ARGB32, alloc.width, alloc.height)
244         ctx = cairo.Context(surface)
245
246         row.get_style_context().add_class("drag-icon")
247         row.draw(ctx)
248         row.get_style_context().remove_class("drag-icon")
249
250         pos_x, pos_y = widget.translate_coordinates(row, 0, 0)
251
252         surface.set_device_offset(-pos_x, -pos_y)
253         Gtk.drag_set_icon_surface(drag_context, surface)
254
255         hand_cursor = Gdk.Cursor(Gdk.CursorType.HAND1)
256         self.get_window().set_cursor(hand_cursor)
257
258     def drag_data_get(self, widget, _drag_context, selection_data, _info, _time):
259         """ When drag data is requested by the destination """
260         row = widget.get_ancestor(Gtk.ListBoxRow)
261         listbox_str = str(self)
262         row_index = row.get_index()
263         data = "{0}|{1}".format(listbox_str, row_index)
264         selection_data.set_text(data, len(data))
265         self.get_window().set_cursor(None)
266
267     def drag_data_received(
268         self, widget, _drag_context, _pos_x, _pos_y, selection_data, _info, _time):
269         """ When drag data is received by the destination """
270         data = selection_data.get_text()
271         try:
272             listbox_str = data.split('|')[0]
273             if listbox_str == str(self):
274                 old_index = int(data.split('|')[1])
275                 new_index = widget.get_index()
276                 self.mirrors.insert(new_index, self.mirrors.pop(old_index))
277                 self.fillme()
278                 self.show_all()
279         except (KeyError, ValueError) as err:
280             logging.warning(err)
281
282     @staticmethod
283     def trim_mirror_url(server_line):
284         """ Get url from full mirrorlist line """
285
286         if not server_line.startswith("Server") or '=' not in server_line:
287             return server_line
288
289         # Get url part
290         url = server_line.split('=')[1].strip()
291
292         # Remove end part to get the FDQN only
293         url = url.replace("/$repo/os/$arch", "")
294
295         return url
296
297     def save_changes(self, use_rankmirrors=False):
298         """ Save mirrors in mirrors list file """
299         # Save a backup if possible
300         src = self.mirrors_file_path
301         dst = src + ".cnchi-backup"
302
303         try:
304             shutil.copy2(src, dst)
305         except (FileNotFoundError, FileExistsError, OSError) as err:
306             logging.warning(err)
307
308         # ok, now save our changes
309         with open(src, 'w') as mfile:
310             line = "# Mirrorlist file modified by Cnchi\n\n"
311             mfile.write(line)
312             for (url, active) in self.mirrors:
313                 line = "Server = {}\n".format(url)
314                 if not active:
315                     line = "#" + line
316                 mfile.write(line)
317
318         # If this is the Arch mirrorlist and we are not using rankmirrors,
319         # we need to save it to rankmirrors_result in settings
320         arch_mirrors = []
321         if not use_rankmirrors and self.mirrors_file_path == "/etc/pacman.d/mirrorlist":
322             for (url, active) in self.mirrors:
323                 if active:
324                     arch_mirrors.append(self.trim_mirror_url(url))
325             self.settings.set('rankmirrors_result', arch_mirrors)
326
327
328 class Mirrors(GtkBaseBox):
329     """ Page that shows mirrolists so the user can arrange them manually """
330     """ Remove antergos-mirrorlist, and add reborn-mirrorlist (Rafael) """
331
332     MIRRORLISTS = [
333         "/etc/pacman.d/mirrorlist",
334         "/etc/pacman.d/reborn-mirrorlist"]
335
336     def __init__(self, params, prev_page="cache", next_page="installation_ask"):
337         super().__init__(self, params, "mirrors", prev_page, next_page)
338
339         # Set up lists
340         self.listboxes = []
341         self.scrolledwindows = []
342
343         self.scrolledwindows.append(self.gui.get_object("scrolledwindow1"))
344         self.scrolledwindows.append(self.gui.get_object("scrolledwindow2"))
345
346         for mirror_file in Mirrors.MIRRORLISTS:
347             mirror_listbox = MirrorListBox(mirror_file, self.settings)
348             mirror_listbox.connect("switch-activated", self.switch_activated)
349             self.listboxes.append(mirror_listbox)
350
351         for index, scrolled_window in enumerate(self.scrolledwindows):
352             scrolled_window.add(self.listboxes[index])
353
354         self.listboxes_box = self.gui.get_object("listboxes_box")
355
356         self.use_rankmirrors = True
357         self.use_listboxes = False
358
359         # Boolean variable to check if rank_mirrors has already been run
360         self.rank_mirrors_launched = False
361
362     def switch_activated(self, _widget):
363         """ A mirror has been activated/deactivated. We must check if
364         at least there is one mirror active for each list """
365         self.check_active_mirrors()
366
367     def check_active_mirrors(self):
368         """ Checks if at least there is one mirror active for each list """
369         ok_to_proceed = True
370         for listbox in self.listboxes:
371             if not listbox.get_active_mirrors():
372                 ok_to_proceed = False
373         self.forward_button.set_sensitive(ok_to_proceed)
374
375     def rank_radiobutton_toggled(self, _widget):
376         """ Use rankmirrors """
377         self.use_rankmirrors = True
378         self.use_listboxes = False
379         self.forward_button.set_sensitive(True)
380         # self.listboxes_box.hide()
381         self.listboxes_box.set_sensitive(False)
382
383     def leave_radiobutton_toggled(self, _widget):
384         """ Do not modify mirror lists """
385         self.use_rankmirrors = False
386         self.use_listboxes = False
387         self.forward_button.set_sensitive(True)
388         # self.listboxes_box.hide()
389         self.listboxes_box.set_sensitive(False)
390
391     def user_radiobutton_toggled(self, _widget):
392         """ Let user choose mirrorlist ordering """
393         self.use_rankmirrors = False
394         self.use_listboxes = True
395         self.show_all()
396         self.check_active_mirrors()
397         self.listboxes_box.set_sensitive(True)
398
399     def start_rank_mirrors(self):
400         """ Launch rank mirrors process to optimize Arch and Antergos mirrorlists
401             As user can come and go from/to this screen, we must get sure he/she
402             has not already run Rankmirrors before """
403
404         if not self.rank_mirrors_launched:
405             logging.debug("Cnchi is ranking your mirrors lists...")
406             parent_conn, child_conn = multiprocessing.Pipe(duplex=False)
407             # Store parent_conn for later use in summary.py (rankmirrors wait dialog)
408             #proc = RankMirrors(fraction_pipe=child_conn)
409             proc = RankMirrors(child_conn, self.settings)
410             proc.daemon = True
411             proc.name = "rankmirrors"
412             proc.start()
413             self.rank_mirrors_launched = True
414             self.settings.set('rankmirrors_pipe', parent_conn)
415
416     def prepare(self, direction):
417         """ Prepares screen """
418         self.translate_ui()
419         self.show_all()
420         # self.listboxes_box.hide()
421         self.listboxes_box.set_sensitive(False)
422         self.forward_button.set_sensitive(True)
423
424     def translate_ui(self):
425         """ Translates screen before showing it """
426         self.header.set_subtitle(_("Mirrors Selection"))
427
428         self.forward_button.set_always_show_image(True)
429         self.forward_button.set_sensitive(True)
430
431         #bold_style = '<span weight="bold">{0}</span>'
432
433         radio = self.gui.get_object("rank_radiobutton")
434         txt = _("Let Cnchi sort the mirrors lists (recommended)")
435         radio.set_label(txt)
436         radio.set_name('rank_radio_btn')
437
438         radio = self.gui.get_object("leave_radiobutton")
439         txt = _("Leave the mirrors lists as they are")
440         radio.set_label(txt)
441         radio.set_name('leave_radio_btn')
442
443         radio = self.gui.get_object("user_radiobutton")
444         txt = _("Manage the mirrors lists manually (advanced)")
445         radio.set_label(txt)
446         radio.set_name('user_radio_btn')
447
448         intro_txt = _("How would you like to proceed?")
449         intro_label = self.gui.get_object("introduction")
450         intro_label.set_text(intro_txt)
451         intro_label.set_name("intro_label")
452         intro_label.set_hexpand(False)
453         intro_label.set_line_wrap(True)
454
455         intro_label.set_max_width_chars(80)
456
457         lbl = self.gui.get_object("arch_mirrors_label")
458         lbl.set_text(_("Arch Mirrors"))
459
460         lbl = self.gui.get_object("antergos_mirrors_label")
461         lbl.set_text(_("RebornOS Mirrors"))
462
463     def store_values(self):
464         """ Store selected values """
465         if self.use_rankmirrors:
466             self.start_rank_mirrors()
467         if self.use_listboxes:
468             for listbox in self.listboxes:
469                 listbox.save_changes(self.use_rankmirrors)
470         return True
471
472     def get_next_page(self):
473         """ Returns next page """
474         return self.next_page