#coding:utf-8 #!/usr/bin/python3 # # Copyright (c) 2007-2009 Canonical Ltd. # # Author: Oliver Grawert <ogra@ubuntu.com> # # Modifications 2013 from papoteur <papoteur@mageialinux-online.org> # and Geiger David <david.david@mageialinux-online.org> # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. # Requires python3-parted # ensure we're using the latest build, if called from our build environment import sys import imp sys.path.insert(0,'../../../build/swig/python3') imp.reload(sys) ########### # imports # ########### import yui import time #log = yui.YUILog.instance() #log.setLogFileName("isodumper.log") #log.enableDebugLogging( True ) import os import re import gettext from subprocess import call, Popen, PIPE from pydbus import SystemBus from gi.repository import GLib import psutil class NoUDisks2(Exception): pass class UDisks2(object): BLOCK = 'org.freedesktop.UDisks2.Block' DRIVE = 'org.freedesktop.UDisks2.Drive' def __init__(self): self.bus = SystemBus() try: self.iface = self.bus.get('org.freedesktop.UDisks2') except : raise NoUDisks2() def find_devices(self): _udisks2_obj_manager = self.iface["org.freedesktop.DBus.ObjectManager"] objects=_udisks2_obj_manager.GetManagedObjects() re_drive = re.compile('(?P<path>.*?/drives/(?P<id>.*))') re_block = re.compile('(?P<path>.*?/block_devices/(?P<id>.*))') devs= [m.groupdict() for m in [re_drive.match(path) for path in (objects.keys())] if m] blocks = [m.groupdict() for m in [re_block.match(path) for path in (objects.keys())] if m] list=[] for block in blocks: if not block['path'][-1].isdigit(): for dev in devs: if dev['path'] == objects[block['path']][self.BLOCK]['Drive']: dev_obj =objects[dev['path']][self.DRIVE] if (dev_obj['ConnectionBus'] == 'usb' or dev_obj['ConnectionBus'] == 'sdio') and \ (dev_obj['Removable'] == 1 or dev_obj['MediaRemovable'] == 1 ): item=[] vend = dev_obj['Vendor'] if vend == "": name = dev_obj['Model'] else: name = vend + " "+ dev_obj['Model'] dev_name = block['path'].split('/')[-1] path = '/dev/'+block['path'].split('/')[-1] size = dev_obj['Size'] item.append(name) item.append(path) item.append(size) list.append(item) return list class Info(object): def __init__(self,title,richtext,text): self.title=title self.richtext=richtext self.text=text class IsoDumper(object): RELEASE="v1.15" def get_devices(self): self.list=self.u.find_devices() while len(self.list)==0: if self.nodevDialog(): self.list = self.u.find_devices() else: return False break self.devicelist.addItem("",False) if len(self.list)>0: for name, path, size in self.list: if size != 0 : self.devicelist.addItem(str(name+' ('+path.lstrip()+') ' + self.sizeof_fmt(size)),False) return True def update_list(self ): self.devicelist.deleteAllItems() self.get_devices() self.img_name = "" self.ima.setLabel(self.ChooseImage) self.backup_select.setLabel(self.ChooseImage) self.dialog.recalcLayout() self.restore() def device_selected(self): selitem = self.devicelist.selectedItem() if selitem != None: self.dev = selitem.label() for name, path, size in self.list: if self.dev.startswith(name): self.deviceSize=size self.device_name=name.rstrip().replace(' ', '') self.logger(_('Target Device: ')+ self.dev) self.formatbt.setEnabled() self.ima.setEnabled() self.backup_select.setEnabled() break def sizeof_fmt(self, num): #I18N these are units for files size for unit in [_('B'),_('KiB'),_('MiB'),_('GiB'),_('TiB')]: if abs(num) < 1024.0: return "%3.3f %s" % (num, unit) num /= 1024.0 return "%.1f %s" % (num, _('TiB')) def backup_choosed(self): # Add .img if not specified if not self.backup_img_name.lower().endswith('.img'): self.backup_img_name=self.backup_img_name+".img" head, tail = os.path.split(self.backup_img_name) self.backup_dest=self.backup_img_name self.backup_select.setLabel(tail) self.dialog.recalcLayout() self.backupbt.setEnabled() def do_format(self,format_type,name): target = self.dev.split('(')[1].split(')')[0] info = Info(_("Formatting confirmation"),True,self.warning) if self.ask_YesOrNo(info): rc=self.raw_format(target, format_type, name) self.operation=False if rc == 0: message = _('The device was formatted successfully.') self.logger(message) self.success() elif rc == 5: message = _("An error occurred while creating a partition.") self.logger(message) self.emergency(message) elif rc == 127: message = _('Authentication error.') self.logger(message) self.emergency(message) else: message = _('An error occurred.') self.emergency(message) def restore(self): self.backup_select.setDisabled() self.backupbt.setDisabled() self.formatbt.setDisabled() self.ima.setDisabled() self.writebt.setDisabled() self.devicelist.setEnabled() self.progress.setLabel("") self.progress.setValue(0) self.progress.setDisabled() self.refreshbt.setEnabled() self.persistencecb.setDisabled() def onProgress(self, frac): self.logger(_('Wrote: {}% '.format(frac))) self.progress.setValue(frac) self.dialog.pollEvent() def raw_format(self, usb_path, fstype, label): self.operation=True if os.geteuid() > 0: launcher='pkexec' self.process = Popen([launcher,'/usr/bin/python3', '-u', '/usr/lib/isodumper/raw_format.py','-d',usb_path,'-f',fstype, '-l', label, '-u', str(os.geteuid()), '-g', str(os.getgid())], shell=False, stdout=PIPE, preexec_fn=os.setsid) else: self.process = Popen(['/usr/bin/python3', '-u', '/usr/lib/isodumper/raw_format.py','-d',usb_path,'-f',fstype, '-l', label, '-u', str(os.geteuid()), '-g', str(os.getgid())], shell=False, stdout=PIPE, preexec_fn=os.setsid) working=True while working: time.sleep(0.5) self.process.poll() rc=self.process.returncode if rc is None: working=True else: self.process = None working= False return rc def backup_go(self): dest = self.backup_img_name if os.path.exists(dest): info = Info(_("Backup confirmation"),True,_("Do you want to overwrite the file?")) if not(self.ask_YesOrNo(info)): self.returncode = 1 return True st = os.statvfs(os.path.dirname(dest)) free = st.f_bavail * st.f_frsize if free<self.deviceSize : sizeM=str(self.deviceSize/(1024*1024)) message=_("The destination directory is too small to receive the backup (%s Mb needed)")%(sizeM) self.logger(message) self.emergency(message) else: self.returncode=0 source = self.dev.split('(')[1].split(')')[0] self.logger(_('Backup to:')+' '+dest) bus = SystemBus() iface = bus.get("org.mageia.Magiback","Isodumper") #Writing step iface.do_write(source, dest, self.deviceSize) progress = iface.progress while not iface.done : progress = iface.progress self.progress.setValue(progress) self.dialog.pollEvent() time.sleep(.2) success, message = iface.end() if success: self.logger(_('{source} successfully written to {target}').format( source=source.split('/')[-1], target=dest)) self.progress.setEnabled() self.progress.setValue(100) self.dialog.pollEvent() self.logger(message) self.success() else: self.emergency(message) def do_write(self): self.writebt.setDisabled() self.devicelist.setDisabled() self.formatbt.setDisabled() self.backupbt.setDisabled() self.backup_select.setDisabled() self.progress.setEnabled() source = self.img_name target = self.dev.split('(')[1].split(')')[0] # self.logger(_('Image: ')+source) # self.logger(_('Target Device: ')+self.dev) b = os.path.getsize(source) if b > (self.deviceSize): message = _('The device is too small to contain the ISO file.') self.logger(message) self.emergency(message) else: info = Info(_("Writing confirmation"),True,self.warning) if self.ask_YesOrNo(info): if self.deviceSize> 1024*1024*1024*32 : info = Info(_("Warning"),True,_('The device is bigger than 32 Gbytes. Are you sure you want use it?')) if self.ask_YesOrNo(info): pass else: return self.ima.setDisabled() self.writebt.setDisabled() self.progress.setLabel(_('Writing {source} to {target}').format(source=source.split('/')[-1],target=target.split('/')[-1])) self.logger(_('Executing copy from ')+source+_(' to ')+target) bus = SystemBus() iface = bus.get("org.mageia.Magiback","Isodumper") success, message = iface.do_unmount(target) if success: #Writing step #Dump mode iface.do_write(source, target, b) iface.get_sum(source) progress = iface.progress while not iface.done : progress = iface.progress self.progress.setValue(progress) self.dialog.pollEvent() time.sleep(.5) success, message = iface.end() if success: self.logger(_('Image {source} successfully written to {target}').format( source=source.split('/')[-1], target=target)) self.logger(_('Bytes written: ')+str(b)) self.progress.setLabel(_('Checking ')+target.split('/')[-1]) self.progress.setValue(0) self.dialog.pollEvent() # Checking iface.check_write(target, source) progress = iface.progress while progress < 100 : progress = iface.progress self.progress.setValue(progress) self.dialog.pollEvent() time.sleep(.5) success, message = iface.end() self.progress.setEnabled() self.progress.setValue(100) self.dialog.pollEvent() self.logger(message) # Add persistent partition if asked if self.persistencecb.value(): self.logger(_("Adding persistent partition")) iface.do_persistence(target,"mgalive-persist") self.logger(_("Added persistent partition")) self.success() else: self.emergency(message) else: self.emergency(message) self.restore() else: self.restore() def success(self): self.operation=False self.final_unsensitive() info = Info(_("Success"),True,_("The operation completed successfully.\n\ You are free to unplug it now, a logfile \n\ (/home/-user- or /root)/.isodumper/isodumper.log will be saved when\n\ you close the application.")) if self.ask_OK(info) : return def confirm_close(self, *args): if self.operation==False: # no writing , backup nor format running self.close() else: # writing , backup or format running info = Info(_("Warning"),True,_("Writing is in progress. Exiting during writing \n\ will make the device or the backup unusable.\n\ Are you sure you want to quit during writing?")) if self.ask_YesOrNo(info) : return True else: return False def emergency(self,message): self.operation=False self.returncode=1 self.final_unsensitive() info = Info(_("Error"),True,message) self.ask_OK(info) def final_unsensitive(self): self.ima.setDisabled() self.devicelist.setDisabled() self.writebt.setDisabled() self.progress.setDisabled() self.backup_select.setDisabled() self.persistencecb.setDisabled() def wip_unsensitive(self): self.ima.setDisabled() self.devicelist.setDisabled() self.writebt.setDisabled() self.backup_select.setDisabled() self.backupbt.setDisabled() self.refreshbt.setDisabled() self.persistencecb.setDisabled() def close(self): self.write_logfile() self.dialog.destroy() self.dialog = None def write_logfile(self): logpath = os.path.join(os.path.expanduser('~'),'.isodumper') if not(os.path.isdir(logpath)): os.mkdir(logpath) logfile=open(logpath+'/isodumper.log',"w", encoding="utf-8") logfile.write(self.logview.logText()) logfile.close() print((self.logview.logText())) def logger(self, text): self.logview.appendLines(text+"\n") def activate_devicelist(self): self.devicelist.setEnabled() self.logger(_('Image ')+": "+ self.img_name) # self.chooser.set_tooltip_text(self.img_name) def help_dialog(self): info = Info(_("IsoDumper"),True,_("Mageia IsoDumper<BR />\ ----------------<BR />\ This GUI program is primarily for safely writing a bootable ISO image to a USB flash drive, \ an operation devious & potentially hazardous when done by hand. As a bonus, it can also back up the\ entire previous<BR />contents of the flash drive onto the hard disc, and restore the flash drive \ to its previous state subsequently.<BR />\ It gives also a feature for formatting the USB device.<BR />\ <BR />\ IsoDumper can be launched either from the menus, or a user or root console with the command 'isodumper'.<BR />\ For normal users, the root password is solicited; this is necessary for the program's operation. <BR />\ The flash drive can be inserted beforehand or once the program is started. In the latter case, a \ dialogue will say that there is no flash drive inserted, and allow a 'retry' to find it once it is. <BR />\ (You may have to close any automatically opened File Manager window).<BR />\ <BR />\ The fields of the main window are as follows:<BR />\ - Device to work on: the device of the USB flash drive, a drop-down list to choose from.<BR />\ - Write Image: to choose the source ISO image *.iso (or flash drive backup file *.img) to write out.<BR />\ - Write to device: This button launches the operation - with a prior warning dialogue. <BR />\ The operation is shown in the progress bar beneath.<BR />\ - Backup to: define the name and placement of the backup image file. The current flash drive \ will be backed up to a disc file. Note that the entire flash drive is preserved, regardless of its \ actual contents; ensure that you have the necessary free disc space (the same size as the USB device). \ This backup file can be used later to restore the flash drive by selecting it as the source *.img file to write out.<BR />\ - Backup the device: launch the backup operation.<BR />\ - Format the device: create an unique partition on the entire volume in the specified format in FAT, \ NTFS or ext. You can specify a volume name and the format in a new dialog box.<BR />")) if self.ask_OK(info) : return def __init__(self): APP="isodumper" DIR="/usr/share/locale" # Call translation catalog gettext.install(APP, localedir=DIR, names=('ngettext',)) #Check that there is no other instance running current_pid = psutil.Process().pid for pid in psutil.pids(): p = psutil.Process(pid) if p.name() == "isodumper" and p.pid != current_pid : info = Info(_("Error"),True,_("There is another instance of Isodumper already running.")) self.ask_OK(info) yui.YUILoader.deleteUI() exit() #TODO Read log level from command line # define size of the selected device self.deviceSize=0 # Operation running self.operation=False self.ChooseImage = _("Choose an image") self.warning = _("Warning\nThis will destroy all data on the target device,\n\ are you sure you want to proceed?\n\ If you say ok here, please <b>do not unplug</b>\ the device during the following operation.") """ Init/Constructor for the 'widgets' """ yui.YUI.app().setApplicationTitle(_("IsoDumper")+" "+self.RELEASE) yui.YUI.app().setApplicationIcon("/usr/share/icons/isodumper.png") self.atelier = yui.YUI.widgetFactory() self.dialog = self.atelier.createPopupDialog() # create the main gui # +---+-----------------+ # + banner + # +---+-----------------+ # | devicelist | # +---+-----+------+----+ # + L | ima | writebt | # +---+-----+------+----+ # + L | backup_select | backupbt | # +---+-----+------+----+ # + F | formatbt | # +----------------+----+ # | progress | # +---------------------+ # | report | # +---------------------+ # | refreshbt | aboutbt | helpbt | quitbt | # +---------------------+ self.ancrage = self.atelier.createReplacePoint(self.dialog) self.box = self.atelier.createVBox(self.ancrage) self.bannerbox = self.atelier.createHBox(self.box) self.banner=self.atelier.createImage(self.bannerbox,"/usr/share/isodumper/header.svg") self.devicebox = self.atelier.createHBox(self.box) self.devicelist=self.atelier.createComboBox(self.devicebox,_("Device to work on:"),False) self.devicelist.setNotify() self.devicelist.setStretchable(0,True) self.writebox = self.atelier.createHBox(self.box) self.atelier.createLabel(self.writebox,_("Write Image:")) # add file picker for image file self.ima=self.atelier.createPushButton(self.writebox,self.ChooseImage) self.ima.setStretchable(0,True) self.ima.setDisabled() self.atelier.createHStretch(self.writebox) self.writebt = self.atelier.createPushButton(self.writebox, _("&Write to device" )) self.writebt.setDisabled() self.persistencebox = self.atelier.createHBox(self.box) self.persistencecb = self.atelier.createCheckBox(self.persistencebox, _("Add a persistent partition in the remaining space")) self.persistencecb.setDisabled() self.backupbox = self.atelier.createHBox(self.box) self.atelier.createLabel(self.backupbox,_("Backup to:")) # add file picker for backup file name self.backup_select=self.atelier.createPushButton(self.backupbox,self.ChooseImage) self.backup_select.setStretchable(0,True) self.backup_select.setDisabled() self.atelier.createHStretch(self.backupbox) self.backupbt = self.atelier.createPushButton(self.backupbox, _("Backup the device" )) self.backupbt.setDisabled() self.formatbox = self.atelier.createHBox(self.box) self.atelier.createLabel(self.formatbox,_("Format the device in FAT, NTFS or ext:")) self.atelier.createHStretch(self.formatbox) self.formatbt = self.atelier.createPushButton(self.formatbox, _("Format the device" )) self.formatbt.setDisabled() self.progressbox = self.atelier.createHBox(self.box) self.progress = self.atelier.createProgressBar(self.progressbox,_("Progress"),100) self.progress.setStretchable(0,True) self.reportbox = self.atelier.createHBox(self.box) self.reportbox.setWeight(1,30) self.logview = self.atelier.createLogView(self.reportbox,_("Report"), 10) self.logview.setStretchable(0,True) self.buttonsbox = self.atelier.createHBox(self.box) self.refreshbt = self.atelier.createPushButton(self.devicebox, _("Refresh" )) self.refreshbt.setStretchable(0,True) self.aboutbt = self.atelier.createPushButton(self.buttonsbox, _("About" )) self.aboutbt.setStretchable(0,True) self.helpbt = self.atelier.createPushButton(self.buttonsbox, _("Help" )) self.helpbt.setStretchable(0,True) self.quitbt = self.atelier.createPushButton(self.buttonsbox, _("Quit" )) self.quitbt.setStretchable(0,True) self.u = None try: self.u = UDisks2() except : message = _('UDisks2 is not available on your system') self.logger(message) self.emergency(message) if not self.get_devices(): self.dialog.destroy() yui.YUILoader.deleteUI() exit() self.device_selected() self.dialog.recalcLayout() self.ancrage.showChild() def ask_format(self): atelier = yui.YUI.widgetFactory() dialog = atelier.createPopupDialog() # dialog.setTitle(_("Choose format")) vb=atelier.createVBox(dialog) label = atelier.createInputField(vb,_("Label for the device:")) cr = atelier.createRadioButtonGroup(vb) vb_c = atelier.createVBox(cr) vb_c1 = atelier.createHBox(vb_c) format_fat = atelier.createRadioButton(atelier.createLeft(vb_c1),_("FAT 32 (Windows)")) vb_c2 = atelier.createHBox(vb_c) format_ntfs = atelier.createRadioButton(atelier.createLeft(vb_c2),_("NTFS (Windows)")) vb_c3 = atelier.createHBox(vb_c) format_ext = atelier.createRadioButton(atelier.createLeft(vb_c3),_("ext4 (Linux)")) bb = atelier.createHBox(vb) executebt = atelier.createPushButton(bb,_("Execute")) cancelbt = atelier.createPushButton(bb,_("Cancel")) dialog.open() returncode = True while True: event = dialog.waitForEvent() if event.eventType() == yui.YEvent.CancelEvent: returncode = False break if event.widget() == executebt: if format_fat.value(): format_type = 'fat32' format_label = label.value().upper()[:11] break if format_ntfs.value(): format_type = 'ntfs' format_label = label.value()[:32] break if format_ext.value(): format_type = 'ext4' format_label = label.value() break if event.widget() == cancelbt: returncode = False break dialog.destroy() if returncode: return True,format_type,format_label else: return False," "," " def ask_OK(self, info): yui.YUI.widgetFactory mgafactory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(yui.YExternalWidgets.externalWidgetFactory("mga")) dlg = mgafactory.createDialogBox(yui.YMGAMessageBox.B_ONE) dlg.setTitle(info.title) dlg.setText(info.text, info.richtext) dlg.setButtonLabel(_("OK"), yui.YMGAMessageBox.B_ONE) dlg.setMinSize(60, 10); return dlg.show() == yui.YMGAMessageBox.B_ONE def ask_YesOrNo(self, info): yui.YUI.widgetFactory mgafactory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(yui.YExternalWidgets.externalWidgetFactory("mga")) dlg = mgafactory.createDialogBox(yui.YMGAMessageBox.B_TWO) dlg.setTitle(info.title) dlg.setText(info.text, info.richtext) dlg.setButtonLabel(_("Yes"), yui.YMGAMessageBox.B_ONE) dlg.setButtonLabel(_("No"), yui.YMGAMessageBox.B_TWO) dlg.setMinSize(60, 8); return dlg.show() == yui.YMGAMessageBox.B_ONE def aboutDialog(self): yui.YUI.widgetFactory; mgafactory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(yui.YExternalWidgets.externalWidgetFactory("mga")) dlg = mgafactory.createAboutDialog("Isodumper", self.RELEASE, "GPLv2", _("Oliver Grawert<BR />Papoteur<BR />Pictures : Timothée Giet"), _("A tool for writing ISO images to a device")+"<BR />http://gitweb.mageia.org/software/isodumper", "") dlg.show(); def nodevDialog(self): yui.YUI.widgetFactory mgafactory = yui.YMGAWidgetFactory.getYMGAWidgetFactory(yui.YExternalWidgets.externalWidgetFactory("mga")) dlg = mgafactory.createDialogBox(yui.YMGAMessageBox.B_TWO) dlg.setTitle("IsoDumper") dlg.setText(_("Warning\nNo target devices were found.\nYou need to plug in a USB Key to which the image can be written.")) dlg.setButtonLabel(_("Refresh"), yui.YMGAMessageBox.B_ONE) dlg.setButtonLabel(_("Cancel"), yui.YMGAMessageBox.B_TWO) dlg.setMinSize(60, 8); return dlg.show() == yui.YMGAMessageBox.B_ONE def handleevent(self): self.traitement=None while True: event = self.dialog.waitForEvent() if event.eventType() == yui.YEvent.CancelEvent: self.close() break if event.widget() == self.quitbt: self.close() break if event.widget() == self.ima: self.img_name=yui.YUI.app().askForExistingFile("","*.iso *.img",self.ChooseImage) if self.img_name != "" : self.ima.setLabel(os.path.basename(self.img_name)) self.dialog.recalcLayout() self.writebt.setEnabled() self.activate_devicelist() self.persistencecb.setEnabled() if event.widget() == self.writebt: self.wip_unsensitive() self.do_write() self.restore() if event.widget() == self.backupbt: self.wip_unsensitive() self.backup_go() self.restore() if event.widget() == self.backup_select: self.backup_img_name=yui.YUI.app().askForSaveFileName("","*.img",_("Backup to:")) if self.backup_img_name != '': self.backup_choosed() if event.widget() == self.formatbt: code,format_type,name = self.ask_format() if code: self.do_format(format_type,name) self.restore() if event.widget() == self.devicelist: self.device_selected() try: if event.widget() == self.refreshbt: self.update_list() except: pass try: if event.widget() == self.helpbt: self.help_dialog() except: pass try: if event.widget() == self.quitbt: self.confirm_close() except: pass if event.widget() == self.aboutbt: self.aboutDialog() def run(self): try: self.handleevent() except Exception as e: print(str(e)) yui.YDialog.deleteAllDialogs() if __name__ == "__main__": app = IsoDumper() app.run() # next line seems to be a workaround to prevent the qt-app from crashing # see https://github.com/libyui/libyui-qt/issues/41 yui.YUILoader.deleteUI()