#coding:utf-8 #!/usr/bin/python3 # # Copyright (c) 2007-2009 Canonical Ltd. # # Author: Oliver Grawert # # Modifications 2013 from papoteur # and Geiger David # # 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' SERVICE = 'org.freedesktop.UDisks2' def __init__(self): self.bus = SystemBus() try: self.iface = self.bus.get(self.SERVICE) 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.*?/drives/(?P.*))') re_block = re.compile('(?P.*?/block_devices/(?P.*))') 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 def eject(self, device): ''' device is expected like /dev/sda''' block = os.path.basename(device) iface = self.bus.get(self.SERVICE,'/org/freedesktop/UDisks2/block_devices/' + block) drive = iface.Get(self.BLOCK,"Drive") i_drive = self.bus.get(self.SERVICE, drive) i_drive.Eject({}) 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): 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")) #Eject self.u.eject(target) 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
\ ----------------
\ 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
contents of the flash drive onto the hard disc, and restore the flash drive \ to its previous state subsequently.
\ It gives also a feature for formatting the USB device.
\
\ IsoDumper can be launched either from the menus, or a user or root console with the command 'isodumper'.
\ For normal users, the root password is solicited; this is necessary for the program's operation.
\ 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.
\ (You may have to close any automatically opened File Manager window).
\
\ The fields of the main window are as follows:
\ - Device to work on: the device of the USB flash drive, a drop-down list to choose from.
\ - Write Image: to choose the source ISO image *.iso (or flash drive backup file *.img) to write out.
\ - Write to device: This button launches the operation - with a prior warning dialogue.
\ The operation is shown in the progress bar beneath.
\ - 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.
\ - Backup the device: launch the backup operation.
\ - 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.
")) 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 do not unplug\ 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
Papoteur
Pictures : Timothée Giet"), _("A tool for writing ISO images to a device")+"
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()