예제 #1
0
파일: verify.py 프로젝트: tussock/Vault
class Verify():
    def __init__(self, backup_name, run_date):
        '''
        Verify a run to ensure it is valid
        
        '''
        self.config = Config.get_config()
        self.backup = self.config.backups[backup_name]
        self.store = self.config.storage[self.backup.store].copy()

        self.db = DB()

        #    Find the run
        runs = self.db.runs(self.backup.name, run_date)
        if len(runs) == 0:
            raise Exception(_("Verify failed: Backup run does not exist"))
        self.vrun = runs[0]


    def run(self):

        self.test_store()
        #    Get config and packages
        self.fetch_config()

        #    We only check the data if there is actually something stored there.
        if self.vrun.nfiles == 0 and self.vrun.nfolders == 0:
            return True

        self.prepare_input(self.vrun, self.backup, self.store)
        try:
            #    Only check for tar data if there are files backed up
            #    Otherwise the tar will simply return an error
            tarinfo = self.tarfile.next()
            while tarinfo:
                tarinfo = self.tarfile.next()

        finally:
            self.close_input(self.backup)

        store_size, store_hash, = self.store_thread.get_hash()
        run_hash = self.vrun.hash
        run_size = self.vrun.size
        if store_size == run_size and store_hash == run_hash:
            return True
#        print(store_size, store_hash, run_size, run_hash)
        raise Exception(_("Verify failed - Run data is corrupt"))

    def test_store(self):
        store = self.config.storage[self.store.name].copy()
        store.connect()
        try:
            store.test()
        finally:
            store.disconnect()

    def fetch_config(self):
        store = self.config.storage[self.store.name].copy()
        store.connect()
        try:
            encrypted = False
            config = os.path.join(self.vrun.folder, const.ConfigName)
            if not store.exists(config):
                encrypted = True
                config = config + const.EncryptionSuffix
                if not store.exists(config):
                    raise Exception(_("Configuration file missing. Bad run"))

            store.get(config, os.path.join(tempfile.gettempdir(), "__vault__tmp__"))
            os.remove(os.path.join(tempfile.gettempdir(), "__vault__tmp__"))
            if self.backup.include_packages:
                packages = os.path.join(self.vrun.folder, const.PackageFile)
                if encrypted:
                    packages = packages + const.EncryptionSuffix
                store.get(packages, os.path.join(tempfile.gettempdir(), "__vault__tmp__"))
                os.remove(os.path.join(tempfile.gettempdir(), "__vault__tmp__"))
        finally:
            store.disconnect()

    def prepare_input(self, run, backup, store):
        '''
        Open the tar file.
        Connect the output of the tar to either:
        a) the storage handler
        b) to encryption (openssl), THEN the storage handler
        
        '''
        log.trace("Setting up input processes")

        #    Set up the encryptor (use TEE for now)
        self.crypt_proc = None
        if backup.encrypt:
            log.debug("Creating crypt objects")
            self.crypto = cryptor.DecryptStream(self.config.data_passphrase)
        else:
            self.crypto = cryptor.Buffer()

        #    Set up the storage handler
        log.debug("Starting storage thread")

        self.store_thread = StreamIn(self.crypto, store, run.folder)
        self.store_thread.start()

        log.debug("Connecting tar object")
        self.tarfile = tarfile.open(mode="r|gz", fileobj=self.crypto, bufsize=const.BufferSize)

        log.trace("Completed input preparation")

    def close_input(self, backup):
        log.trace("Closing output managers")
        #    If we are using an external save command, we do nothing here
        try:
            self.tarfile.close()
            self.crypto.close()
            #    Now we are ready to wait for the storage.
            self.store_thread.join()
            if self.store_thread.error:
                log.error("Closing store. Got error", str(self.store_thread.error))
#                self.db.save_message("Error saving backup: %s" % str(self.store_thread.error))
                raise self.store_thread.error

        finally:
            pass
        log.debug("All input closed")
예제 #2
0
class ConfigPanel(gui.ConfigPanel):
    '''
    classdocs
    '''


    def __init__(self, parent):
        '''
        Constructor
        '''
        log.info("***ConfigPanel.init")

        gui.ConfigPanel.__init__(self, parent)
        self.config = Config.get_config()
        self.db = DB()        

        self.state = ViewState
        self.update_filetype_list()
        self.clear_filetype()
        self.image = wx.Bitmap(os.path.join(const.PixmapDir, "configure.png"))
        self.title = _("Configuration")
        if self.lstFileTypes.GetCount() > 0:
            self.lstFileTypes.SetSelection(0)
            self.onFileType(None)
        self.show_mail()
        self.txtMailServer.SetFocus()
        self.nb_config.SetSelection(0)

        self.show_security()
        self.pwd_hidden = True
        self.mail_hidden = True

        log.trace("Done ConfigPanel.init")

    def update_data(self):
        pass

    def update_filetype_list(self):
        self.lstFileTypes.Clear()
        self.lstFileTypes.AppendItems(self.config.file_types.keys())


######################################################################3
#
#        FILE TYPE EVENTS
#
######################################################################3


    def onSaveTypes(self, event):
        self.save_filetype()
        self.state = ViewState
        self.update_state()
        self.lstFileTypes.SetStringSelection(self.txtName.GetValue())

    def onFileType(self, event):
        #    Get the name to be showed
        name = self.lstFileTypes.GetStringSelection()
        if len(name) == 0:
            return
        #    Load it
        try:
            list = self.config.file_types[name]
            self.state = ViewState
            self.show_filetype(name, list)
        except Exception:
            #   Missing backup!
            dlg.Warn(self, _("That File Type seems to be missing or corrupt."))
            self.update_filetype_list()
            self.state = ViewState
            self.clear_filetype()
            return


    def onDelete(self, event):
        #    Get the name to be showed
        name = self.lstFileTypes.GetStringSelection()
        if len(name) == 0:
            return
        if dlg.OkCancel(self, _("Delete File Type definition %s and all its data! Are you sure?") % name) == wx.ID_OK:
            self.delete_filetype(name)
            self.clear_filetype()
            self.state = ViewState


    def onNew(self, event):
        log.info("New!")
        self.state = NewState
        self.clear_filetype()
        self.txtName.SetFocus()


    def onName(self, event):
        self.update_state()

    def onSSL(self, event):
        if self.chkMailSSL.GetValue():
            self.txtMailPort.SetValue("465")
        else:
            self.txtMailPort.SetValue("25")


######################################################################3
#
#        EMAIL EVENTS
#
######################################################################3

    def onHideMailPassword(self, event):
        if self.mail_hidden:
            self.txtMailPassword.SetWindowStyle(wx.NORMAL)
            self.mail_hidden = False
            self.btnHideMailPassword.SetLabel("Hide")
        else:
            self.txtMailPassword.SetWindowStyle(wx.TE_PASSWORD)
            self.mail_hidden = True
            self.btnHideMailPassword.SetLabel("Show")

    def onMailAuth(self, event):
        auth = self.chkMailAuth.GetValue()
        self.txtMailLogin.Enable(auth)
        self.txtMailPassword.Enable(auth)

    def onMailSave(self, event):
        self.config.mail_server = self.txtMailServer.GetValue()
        self.config.mail_port = self.txtMailPort.GetValue()
        self.config.mail_ssl = self.chkMailSSL.GetValue()
        self.config.mail_auth = self.chkMailAuth.GetValue()
        self.config.mail_login = self.txtMailLogin.GetValue()
        self.config.mail_password = self.txtMailPassword.GetValue()

        self.config.mail_from = self.txtMailFrom.GetValue()
        self.config.mail_to = self.txtMailTo.GetValue()
        self.config.save()

    def onMailTest(self, event):

        try:
            if not self.txtMailServer.GetValue() \
                    or not self.txtMailFrom.GetValue() \
                    or not self.txtMailTo.GetValue():
                raise Exception(_("Mail server, from address and to address are required."))

            with ProgressDialog(self, _("Sending"), _("Sending a test email.\nPlease wait...")):
                import time
                time.sleep(1)
                log.debug("Doing send")
                sendemail.sendemail2(self.txtMailServer.GetValue(),
                       int(self.txtMailPort.GetValue()),
                       self.chkMailSSL.GetValue(),
                       self.txtMailFrom.GetValue(),
                       self.txtMailTo.GetValue(),
                       self.chkMailAuth.GetValue(),
                       self.txtMailLogin.GetValue(),
                       self.txtMailPassword.GetValue(),
                       _('The Vault Backup System - Test Message'),
                       _("This is a test message from The Vault Backup System.\n"
                       "If you have received this, then email is correctly configured."))
            dlg.Info(self, _("Mail was sent successfully. Please check it arrived."))
        except Exception as e:
            dlg.Warn(self, str(e))


    def show_mail(self):
        self.txtMailServer.SetValue(self.config.mail_server)
        self.txtMailPort.SetValue(str(self.config.mail_port))
        self.chkMailSSL.SetValue(self.config.mail_ssl)
        self.chkMailAuth.SetValue(self.config.mail_auth)
        self.txtMailLogin.SetValue(self.config.mail_login)
        self.txtMailPassword.SetValue(self.config.mail_password)

        self.txtMailFrom.SetValue(self.config.mail_from)
        self.txtMailTo.SetValue(self.config.mail_to)
        self.onMailAuth(None)

######################################################################3
#
#        Security EVENTS
#
######################################################################3

    def onHidePassword(self, event):
        if self.pwd_hidden:
            self.txtMasterPassword.SetWindowStyle(wx.NORMAL)
            self.pwd_hidden = False
            self.btnHidePassword.SetLabel("Hide")
        else:
            self.txtMasterPassword.SetWindowStyle(wx.TE_PASSWORD)
            self.pwd_hidden = True
            self.btnHidePassword.SetLabel("Show")

    def show_security(self):
        if not self.config.data_passphrase:
            self.txtMasterPassword.SetValue("")
        else:
            self.txtMasterPassword.SetValue(self.config.data_passphrase)
        self.onMasterPasswordChar(None)

    def onMasterPasswordChar(self, event):
        """Recalculate entropy any time the password changes."""
        pwd = self.txtMasterPassword.GetValue()
        e = int(cryptor.entropy(pwd))
        if e < 0:
            e = 0
        if e > 100:
            e = 100
        self.strength.SetValue(e)
        if event:
            event.Skip()

    def onSavePassword(self, event):
        pwd = self.txtMasterPassword.GetValue()
        if pwd != self.config.data_passphrase:
            #    Password has changed. Do we have any stored backups? 
            #    If so, they should be deleted.
            runs = self.db.runs()
            num_runs = len(runs)
            if num_runs > 0:
                size = 0
                for run in runs:
                    size += run.size                
                #    Check with the user.
                msg = _("You current have {numruns} backup runs stored, " \
                        "totalling {size} of remote data.\n" \
                        "Changing the Master Password means old encrypted backups cannot be used.\n" \
                        "Note that they can be kept for disaster recovery if needed,\n" \
                        "but we suggest you simply start fresh.").format(\
                        numruns=num_runs, size=utils.readable_form(size))
                mbox = OptionDialog(self, msg, _("Delete Backup Runs"),
                                    _("Also delete all encrypted backup data stored remotely."), 
                                    default=True)
                if mbox.ShowModal() != wx.ID_OK:
                    return
                delete_offsite_data = mbox.chkOption.GetValue()

                #    TODO skip if no runs
                #    We keep track of all errors
                errors = ""
                with ProgressDialog(self, _("Deleting"), _("Deleting old encrypted backup data.\nPlease wait...")):
                    for backup in self.config.backups.itervalues():
                        #    If its encrypted
                        if backup.encrypt:
                            #    If the option set - delete all offline data at the store
                            if delete_offsite_data:
                                try:
                                    #    Get the list of unique stores used by runs of this backup
                                    runs = self.db.runs(backup.name)
                                    stores = set([r.store for r in runs]) 
                                    #    Get the store and delete all data.
                                    for storename in stores:
                                        store = self.config.storage[storename].copy()
                                        store.delete_backup_data(backup.name)
                                except Exception as e:
                                    errors += "\nDelete offline data for %s failed: %s" % (backup.name, str(e))
                            #    Now delete the database records of the run abd backup
                            try:
                                self.db.delete_backup(backup.name)
                            except Exception as e:
                                errors += "\nDelete local backup information for %s failed: " % (backup.name, str(e))
                
                if len(errors) > 0:
                    dlg.Error(self, errors)
                
        if not pwd:
            self.config.data_passphrase = None
            app.show_message('Password cleared')
        else:
            self.config.data_passphrase = pwd
            app.show_message('Password set')
        self.config.save()
        #    Now delete all the backups and offsite data.
        
        
        

######################################################################
#
#        Save and Load
#
######################################################################
    def update_state(self):
        if self.state == ViewState:
            self.lblName.Show(True)
            self.txtName.Show(False)
        if self.state == NewState:
            self.lblName.Show(False)
            self.txtName.Show(True)

        #self.pnlDetails.Fit()
        self.pnlDetails.Refresh()


    def clear_filetype(self):
        self.show_filetype("<name>", [])


    def show_filetype(self, name, list):
        try:
            #    General Information
            self.txtName.SetValue(name)
            self.lblName.SetLabel(name)


            # TODO!
            self.txtExtensions.Clear()
            list.sort()
            self.txtExtensions.AppendText("\n".join(list))

            self.update_state()
        except Exception as e:
            log.error("Error showing File Type:", str(e))


    def save_filetype(self):

        #    BUILD THE Storage
        if len(self.txtName.GetValue()) == 0:
            dlg.Warn(self, _("File Type name cannot be blank"))
            return
        list = self.txtExtensions.GetValue().split("\n")
        try:
            #    Create the new file_type object
            name = self.txtName.GetValue()
            #    We already have list from above

            #    ensure the list is clean
            cleanlist = []
            for item in list:
                item = item.strip()
                while len(item) > 0 and item[0] == ".":
                    item = item[1:]
                if len(item) == 0:
                    continue
                if item not in cleanlist:
                    cleanlist.append(item)
            cleanlist.sort()
        except Exception as e:
            dlg.Warn(self, str(e))
            return
        if self.state == ViewState:
            #    Delete the old name
            oldname = self.lstFileTypes.GetStringSelection()
            try:
                del self.config.file_types[oldname]
            except:
                pass
        self.config.file_types[name] = cleanlist

        self.config.save()

        self.update_filetype_list()
        self.show_filetype(name, cleanlist)

######################################################################3
#
#        Misc Routines
#
######################################################################3
    def delete_filetype(self, name):
        del self.config.file_types[name]
        self.config.save()
        self.update_filetype_list()
예제 #3
0
class BackupPanel(EditPanel, gui.BackupPanel):
    '''
    classdocs
    '''


    def __init__(self, parent):
        '''
        Constructor
        '''
        log.info("***BackupPanel.init")
        gui.BackupPanel.__init__(self, parent)
        self.btnAddFolder.SetBitmapLabel(wx.Bitmap(os.path.join(const.PixmapDir, "add.png")))
        
        self.db = DB()
        self.config = Config.get_config()

        self.state = ViewState
        self.update_data(False)
        self.nbBackup.SetSelection(0)
        self.clear()
        self.nbBackup.Layout()
        self.Fit()
        self.radSchedDailyWeekly.SetValue(True)

        if self.lstItems.GetCount() > 0:
            self.lstItems.SetSelection(0)
            self.onItemSelected(None)
#        self.onNotifyEmail(None)
        self.image = wx.Bitmap(os.path.join(const.PixmapDir, "backup.png"))
        self.title = _("Backups")

        self.onBackupSchedule(None)
        log.trace("Done BackupPanel.init")



    def update_data(self, set_selection=True):
        #    The next line should be 
        #        for child in self.pnlScheduleTab.GetChildren():
        #            if child.GetName().find("cboTime") == 0:
        #    but there is a bug in wxFormBuilder. It doesn't set the name attribute.
        #    See http://sourceforge.net/tracker/?func=detail&aid=3187563&group_id=135521&atid=733136
        for name in dir(self):
            if name.find("cboTime") == 0:
                child = self.__getattribute__(name)
                child.Clear()
                child.AppendItems(const.HoursOfDay)
                child.SetSelection(0)

            if name.find("cboDay") == 0:
                child = self.__getattribute__(name)
                child.Clear()
                child.AppendItems(const.ShortDaysOfWeek)
                child.SetSelection(0)

            if name.find("cboMonthDay") == 0:
                child = self.__getattribute__(name)
                child.Clear()
                child.AppendItems([str(i) for i in xrange(1, 32)])
                child.SetSelection(0)

        self.txtFolders.Clear()

        self.lstExcludeTypes.Clear()
        self.lstExcludeTypes.AppendItems(self.config.file_types.keys())

        #    Lets update this in a smart fasion. Need to keep the current selection if possible
        old_sel = self.cboStore.GetStringSelection()
        self.cboStore.Clear()
        self.cboStore.AppendItems(self.config.storage.keys())
        self.cboStore.SetStringSelection(old_sel)

        #    Lastly - lets reload the backup list
        self.update_backup_list(set_selection)

    def update_backup_list(self, set_selection=True):
        sel = self.lstItems.GetStringSelection()
        #    Look for new items
        backups = self.lstItems.GetItems()
        keys = self.config.backups.keys()
        keys.sort()
        for item in keys:
            if not item in backups:
                #    new item becomes selected (hopefully the first)
                sel = item
                break

        self.lstItems.Clear()
        self.lstItems.AppendItems(keys)

        if set_selection:
            self.lstItems.SetStringSelection(sel)
            self.onItemSelected(None)


######################################################################3
#
#        EVENTS
#
######################################################################3


    def onHistory(self, event):
        name = self.lstItems.GetStringSelection()
        if len(name) > 0:
            self.history(name)

    def onRun(self, event):
        name = self.lstItems.GetStringSelection()
        if len(name) > 0:
            self.run_backup(name)


    def onAddFolder(self, event):
        dlog = wx.DirDialog(self, _("Select a folder to back up"), "/home")
        ret = dlog.ShowModal()
        if ret == wx.ID_OK:
            folders = self.text_to_list(self.txtFolders.GetValue())
            folders.append(dlog.GetPath())
            self.txtFolders.Clear()
            self.txtFolders.AppendText("\n".join(folders))

    def onBackupSchedule(self, event):
        if self.radSchedAdvanced.GetValue():
            self.pnlAdvanced.Show()
        else:
            self.pnlAdvanced.Hide()


######################################################################
#
#        Save and Load
#
######################################################################
    def update_state(self):
        if self.state == ViewState:
            self.lblName.Show(True)
            self.txtName.Show(False)
        if self.state == NewState:
            self.lblName.Show(False)
            self.txtName.Show(True)
        self.onBackupSchedule(None)
        self.Fit()
        self.Refresh()

    def clear(self):
        b = Backup(EmptyName)
        self.show_backup(b)
        self.nbBackup.SetSelection(0)

    def delete(self, name):
        #    Lets get some statistics
        runs = self.db.runs(backupname=name)
        num_runs = len(runs)
        size = 0
        for run in runs:
            size += run.size

        if num_runs > 0:
            msg = _("Backup '{backup}' has {numruns} runs stored, " \
                    "totalling {size} of remote data.\n" \
                    "Are you sure you want to delete the backup definition?\n" \
                    "(hint - its usually better to just deactivate the backup)").format(\
                    backup=name, numruns=num_runs, size=utils.readable_form(size))
            mbox = OptionDialog(self, msg, _("Delete Backup Definition"),
                                _("Also delete all backup data stored remotely\nNote that this cannot be undone."))
            if mbox.ShowModal() != wx.ID_OK:
                return
            delete_offsite_data = mbox.chkOption.GetValue()

        else:
            msg = _("Backup '{backup}' has never run. Are you " \
                    "sure you want to delete the backup definition?").format(backup=name)
            if dlg.OkCancel(self, msg, _("Confirm Delete")) != wx.ID_OK:
                return
            delete_offsite_data = False


        with ProgressDialog(self, _("Deleting"), 
                            _("Deleting backup %s%s.\nPlease wait...") % 
                            (name, " and all offsite data" if delete_offsite_data else "")):
            self.delete_backup(name, delete_offsite_data)
            import time
            time.sleep(3)
        self.clear()
        self.state = ViewState
        app.broadcast_update()



    def show(self, name):
        try:
            backup = self.config.backups[name]
            self.state = ViewState
            self.show_backup(backup)
        except Exception as e:
            #   Missing backup!
            dlg.Warn(self, _("The backup '{backup}' seems to be corrupt. {error}").format(backup=name, error=str(e)))
#            self.update_backup_list()
#            self.state = ViewState
#            self.clear()


    def show_backup(self, b):

        #    General Information
        self.txtName.SetValue(b.name)
        self.lblName.SetLabel(b.name)

        self.chkActive.SetValue(b.active)

        #    Folder Information
        self.txtFolders.Clear()
        self.txtFolders.AppendText("\n".join(b.include_folders))
        self.chkPackages.SetValue(b.include_packages)
        #    Exclusions
        self.lstExcludeTypes.SetCheckedStrings(b.exclude_types)
        self.txtExcludePatterns.Clear()
        self.txtExcludePatterns.AppendText("\n".join(b.exclude_patterns))

        #    Destination        
        self.cboStore.SetStringSelection(b.store)

        self.chkEncrypt.SetValue(b.encrypt)
        self.chkVerify.SetValue(b.verify)

        #    Schedule
        if b.sched_type == "custom":
            self.radSchedAdvanced.SetValue(True)
            incr, full = b.sched_times.split("\n")
            self.txtCronIncr.SetValue(incr)
            self.txtCronFull.SetValue(full)
        else:
#            itime, dummy = incr.split("/")       # iday not used
#            ftime, fday = full.split("/")
            time, day = b.sched_times.split("/")
            if b.sched_type == "daily/weekly":
                self.radSchedDailyWeekly.SetValue(True)
                self.cboTime1.SetStringSelection(time)
                self.cboDay1.SetStringSelection(day)
            elif b.sched_type == "daily/monthly":
                self.radSchedDailyMonthly.SetValue(True)
                self.cboTime2.SetStringSelection(time)
                self.cboMonthDay2.SetStringSelection(day)
            elif b.sched_type == "hourly/weekly":
                self.radSchedHourlyWeekly.SetValue(True)
                self.cboTime3.SetStringSelection(time)
                self.cboDay3.SetStringSelection(day)
            elif b.sched_type == "none/daily":
                self.radSchedNoneDaily.SetValue(True)
                self.cboTime4.SetStringSelection(time)
            elif b.sched_type == "none/weekly":
                self.radSchedNoneWeekly.SetValue(True)
                self.cboDay5.SetStringSelection(day)
                self.cboTime5.SetStringSelection(time)
            else:
                raise Exception(_("This backup is corrupt. Invalid schedule type"))

        #    Notifications
        self.chkNotifyMsg.SetValue(b.notify_msg)
        self.chkNotifyEmail.SetValue(b.notify_email)
        self.chkShutdown.SetValue(b.shutdown_after)

        self.update_state()

    def text_to_list(self, text):
        list = [item.strip() for item in text.split("\n") if len(item.strip()) > 0]
        return list

    def get_time_str(self, cronitem):
        hour = cronitem.hour().render()
        if not hour.isdigit():
            hour = "19"
        if len(hour) == 1:
            hour = '0' + hour
        min = cronitem.minute().render()
        if not min.isdigit():
            min = "00"
        if len(min) == 1:
            min = '0' + min
        time = "%s:%s" % (hour, min)
        return time

    def get_dow(self, cronitem):
        dow = cronitem.dow().render()
        if not dow.isdigit():
            dow = "0"
        return int(dow)
    def get_dom(self, cronitem):
        dom = cronitem.dom().render()
        if not dom.isdigit():
            dom = "0"
        return int(dom)


    def save(self):
        #    BUILD THE BACKUP
        if len(self.txtName.GetValue()) == 0:
            raise Exception(_("Backup name cannot be blank"))
        if self.chkEncrypt.GetValue() and not self.config.data_passphrase:
            raise Exception(_("You cannot select encryption when the passphrase is blank (see Configuration page)."))
        if self.txtName.GetValue() == EmptyName:
            raise Exception(_("You need to provide a proper backup name"))
        try:
            #    Create the new backup object
            b = Backup(self.txtName.GetValue())
            #    General Information
            b.active = self.chkActive.GetValue()

            #    Folder Information
            b.include_folders = self.text_to_list(self.txtFolders.GetValue())
            b.include_packages = self.chkPackages.GetValue()

            #    Exclusions
            b.exclude_types = list(self.lstExcludeTypes.GetCheckedStrings()) # returns a tuple, convert to array
            b.exclude_patterns = self.text_to_list(self.txtExcludePatterns.GetValue())

            #    Destination
            b.store = self.cboStore.GetStringSelection()
            b.encrypt = self.chkEncrypt.GetValue()
            b.verify = self.chkVerify.GetValue()

            #    Schedule
            if self.radSchedAdvanced.GetValue():
                b.sched_type = "custom"
                b.sched_times = "%s\n%s" % (self.txtCronIncr.GetValue(), self.txtCronFull.GetValue())
            else:
                if self.radSchedDailyWeekly.GetValue():
                    b.sched_type = "daily/weekly"
                    time = self.cboTime1.GetStringSelection()
                    day = self.cboDay1.GetStringSelection()
                elif self.radSchedDailyMonthly.GetValue():
                    b.sched_type = "daily/monthly"
                    time = self.cboTime2.GetStringSelection()
                    day = self.cboMonthDay2.GetStringSelection()
                elif self.radSchedHourlyWeekly.GetValue():
                    b.sched_type = "hourly/weekly"
                    time = self.cboTime3.GetStringSelection()
                    day = self.cboDay3.GetStringSelection()
                elif self.radSchedNoneDaily.GetValue():
                    b.sched_type = "none/daily"
                    time = self.cboTime4.GetStringSelection()
                    day = "*"
                elif self.radSchedNoneWeekly.GetValue():
                    b.sched_type = "none/weekly"
                    time = self.cboTime5.GetStringSelection()
                    day = self.cboDay5.GetStringSelection()
                else:
                    raise Exception(_("Corrupt backup"))

                b.sched_times = time + "/" + day

            #    Notifications
            b.notify_msg = self.chkNotifyMsg.GetValue()
            b.notify_email = self.chkNotifyEmail.GetValue()
            b.shutdown_after = self.chkShutdown.GetValue()

            b.check()
        except Exception as e:
            raise e
        if self.state == ViewState:
            #    Delete the old name
            oldname = self.lstItems.GetStringSelection()
            try:
                del self.config.backups[oldname]
            except:
                pass
        self.config.backups[b.name] = b
        self.config.save()
        self.update_backup_list()

        #    Attempt to save the crontab. If this fails, the backup was corrupt.
        #    But it has been saved. So that is a problem
        update_crontab(self.config.backups)

######################################################################3
#
#        Misc Routines
#
######################################################################3
    def hour_min_from_str(self, str):
        hour, min = str.split(":")
        return int(hour), int(min)

    def delete_backup(self, name, delete_offsite_data):
        #    Delete the database runs.
        backup = self.config.backups[name]
        #    Read the runs
        dummy = self.db.runs(name)
        success = True
        try:
            if delete_offsite_data:
                wx.Yield()
                store = self.config.storage[backup.store].copy()
                store.delete_backup_data(name)
            wx.Yield()
            self.db.delete_backup(name)
        except:
            #    Most likely this will happen with a corrupt backup object.
            #    We dont want that corruption to stop the deletion.
            success = False
        #    Now delete the configuration.
        wx.Yield()
        del self.config.backups[name]
        update_crontab(self.config.backups)
        self.config.save()
        self.update_backup_list()
        if not success:
            dlg.Warn(self, _("There were errors during the delete. You should check/delete the offsite store manually."),
                     _("Error During Delete"))


    def history(self, default_name):
        #    Open the file list window
        win = HistoryWindow(self, default_name)
        win.Show()

    def run_backup(self, backup_name):
        win = RunBackupWindow(self, backup_name)
        win.Show()
예제 #4
0
class RestorePanel(gui.RestorePanel):
    '''
    classdocs
    '''


    def __init__(self, parent):
        '''
        Constructor
        '''
        log.info("***RestorePanel.init")

        gui.RestorePanel.__init__(self, parent)
        self.db = DB()
        self.config = Config.get_config()
        self.images = wx.ImageList(16, 16)
        self.images.Add(
                        wx.Bitmap(os.path.join(const.PixmapDir, "folder.png"), 
                                  wx.BITMAP_TYPE_PNG)
                        )
        self.images.Add(
                        wx.Bitmap(os.path.join(const.PixmapDir, "document.png"), 
                                  wx.BITMAP_TYPE_PNG)
                        )
        self.fs_tree.SetImageList(self.images)
        #    Looks better if this is blank.
        self.set_selected_file("")
        self.force_rebuild()

        self.image = wx.Bitmap(os.path.join(const.PixmapDir, "review.png"))
        self.title = _("Restore")

        #    Ensure the right page is showing
        self.nb_restore.SetSelection(0)
        log.trace("Done RestorePanel.init")

    def prepare_static_data(self):
        #    Load all runs for the date slider
        self.runs = self.db.runs()

        self.date_slider.SetMin(0)
        log.debug("Date Slider: %d runs" % len(self.runs))
        if len(self.runs) in [0, 1]:
            #    Cannot set a slider with 0 or 1 positions. So disable it.
            self.date_slider.Enable(False)
            self.date_slider.SetMax(1)
            self.date_slider.SetValue(0)
        else:
            self.date_slider.SetMax(len(self.runs) - 1)
            self.date_slider.SetValue(len(self.runs) - 1)
            self.date_slider.Enable(True)

        self.cboBackup.Clear()
        self.cboBackup.AppendItems([name for name in self.config.backups.iterkeys()])
        if self.cboBackup.Count > 0:
            self.cboBackup.SetSelection(0)

    def force_rebuild(self):
        log.debug("Forcing complete rebuild")
        self.displayed_run = None
        self.prepare_static_data()
        #    Prepare the tree
        self.fs_tree.DeleteAllItems()
        self.root_node = self.fs_tree.AddRoot(text="/", image=0)
        self.fs_tree.SetItemPyData(self.root_node, node_info(0, 0, "D", False, "/"))
        self.expand_node(self.root_node)
        self.onSliderScroll(None)
        self.pnlRestore.Layout()

    def update_data(self):
        # TODO! This could be dangerously time consuming!
        self.prepare_static_data()
        self.onSliderScroll(None)
        self.rebuild_tree()
        self.pnlRestore.Layout()

    def get_current_run(self):
        if len(self.runs) == 0:
            return None
        if len(self.runs) == 1:
            return self.runs[0]

        idx = self.date_slider.GetValue()
        if idx >= len(self.runs):
            raise Exception("Invalid date slider position")
        return self.runs[idx]

    def expand_node(self, parent_node):
        log.trace("expand_node", parent_node)
        run = self.get_current_run()
        if not run:
            return
        #    Get the folder - which is in data
        parent_info = self.fs_tree.GetItemPyData(parent_node)
        log.debug("expanding node", parent_info)
        if parent_info.expanded:
            log.debug("Already expanded")
            return
        if parent_info.type == "F":
            log.debug("File node (no children)")
            return
        #    Clean out the dummy sub-node
        self.fs_tree.DeleteChildren(parent_node)

        #    Now add the subnodes (Only up to the currently selected run
        files = self.db.list_dir_id(parent_info.fs_id, run_id=run.run_id)
        for name, item in files.iteritems():
            if parent_info.fs_id == 0 and name == "/":
                continue
            #    The type may be None because this could be a folder that
            #    we log, but dont back up.
            type = item.type
            if type is None:
                type = "D"
            #    We dont add in deleted files
            if type != "X":
                node_name = utils.display_escape(name)
                new_node = self.fs_tree.AppendItem(parent_node, node_name, image=0 if type == "D" else 1)
                new_info = node_info(parent_info.fs_id, item.fs_id, type, False, os.path.join(parent_info.path, name))
                log.debug("New node: ", new_info)
                self.fs_tree.SetItemPyData(new_node, new_info)
                #    For any folders, add a dummy sub-node so we can expand it later
                if type == "D":
                    self.fs_tree.AppendItem(new_node, DummyTreeNode)

        #    Update the parent node to show that its been expanded.
        upd_info = node_info(parent_info.parent_id, parent_info.fs_id, parent_info.type, True, parent_info.path)
        self.fs_tree.SetItemPyData(parent_node, upd_info)
        self.fs_tree.SortChildren(parent_node)

##################################################################
#
#    Event Handlers
#
##################################################################
    def onRefresh(self, event):
        self.force_rebuild()

    def onTreeItemExpanding(self, event):
        log.trace("onTreeItemExpanding")
        item = event.GetItem()
        self.expand_node(item)

    def onTreeSelChanged(self, event):
        log.trace("onTreeSelChanged")
        item = event.GetItem()
        print(item)
        info = self.fs_tree.GetItemPyData(item)
        self.set_selected_file(info.path)

    def onSliderScroll(self, event):
        #    Get the run
        log.trace("Slider Scroll")
        run = self.get_current_run()
        if not run:
            self.date_label.SetLabel("")
            self.time_label.SetLabel("")
            self.pnlRestore.Layout()
            self.lblTreeTitle.SetLabel("No backups have run yet.")
            return

        date = run.start_time
        self.date_label.SetLabel(date.strftime(const.ShortDateFormat))
        self.time_label.SetLabel(date.strftime(const.ShortTimeFormat))
        self.pnlRestore.Layout()
        self.date_slider.SetToolTipString(date.strftime(const.ShortDateTimeFormat))
        self.lblTreeTitle.SetLabel("File System as at " + date.strftime(const.ShortDateTimeFormat))

        self.rebuild_tree()

    def onRunDetails(self, event):
        run = self.get_current_run()
        if not run:
            return
        dummy = RunDetailsWindow(self, run)

    def onRestore(self, event):
        sel = self.fs_tree.GetSelection()
        data = self.fs_tree.GetItemPyData(sel)
        dummy = RunRestoreWindow(self, data.path)

    def onReload(self, event):
        #    Reload the configuration
        do_recover(self)

    def onRebuild(self, event):
        #    Rebuild the local database of saved files.
        do_rebuilddb(self)

    def onRestoreTab(self, event):
        #    Refresh the restore tab, then switch to it...
        self.onRefresh(event)
        self.nb_restore.SetSelection(0)

    def onShowPackages(self, event):
        #    Fetch the package list from the last run of the selected backup
        if self.cboBackup.Count == 0:
            return
        #    Get the backup and store
        bname = self.cboBackup.GetStringSelection()
        backup = self.config.backups[bname]
        store = self.config.storage[backup.store].copy()
        #    Figure out the last run
        runs = self.db.runs(bname)
        if len(runs) == 0:
            dlg.Info(self, _("The selected backup has not run"))
            return

        #    last is the most recent
        run = runs[-1]
        #    Get the folder
        folder = os.path.join(bname, run.start_time_str + ' ' + run.type)
        src = os.path.join(folder, const.PackageFile)

        if backup.encrypt:
            src = src + const.EncryptionSuffix
        workfolder = tempfile.mkdtemp()
        store.connect()
        store.copy_from(src, workfolder)
        store.disconnect()
        package_path = os.path.join(workfolder, const.PackageFile)
        if backup.encrypt:
            crypt_path = package_path + const.EncryptionSuffix
            cryptor.decrypt_file(self.config.data_passphrase, crypt_path, package_path)

        package_list = open(package_path).read().split('\n')
        #    Cleanup
        shutil.rmtree(workfolder)

        win = PackageWindow(self, package_list)

    def onSelectedFileSize(self, event):
        self.update_truncated_text(self.lblSelectedFile)
##################################################################
#
#    Utilities
#
##################################################################

    def set_selected_file(self, text):
        """
        We dont want the SelectedFile expanding, nor do we want it resizing.
        
        We save the full text in the HelpText field.
        We set the text to a reduced text field that will fit.
        """
        self.lblSelectedFile.SetToolTipString(text)
        self.update_truncated_text(self.lblSelectedFile)
        
    def update_truncated_text(self, field):
        dc = wx.ClientDC(field)
        maxWidth = self.lblSelectedFile.GetSize().x
        tt = field.GetToolTip()
        text = tt.GetTip() if tt else ""
        newText = self.truncate_text(dc, text, maxWidth)
        if newText == None:
            newText = ""
        self.lblSelectedFile.SetLabel(newText)
        
    def truncate_text(self, dc, text, maxWidth):
        """
        Truncates a given string to fit given width size. if the text does not fit
        into the given width it is truncated to fit. the format of the fixed text
        is <truncate text ..>.
        """
    
        textLen = len(text)
        tempText = text
        rectSize = maxWidth
    
        fixedText = ""
    
        textW, textH = dc.GetTextExtent(text)
    
        if rectSize >= textW:
            return text
    
        # The text does not fit in the designated area,
        # so we need to truncate it a bit
        suffix = "..."
        border = 5
        w, h = dc.GetTextExtent(suffix)
        rectSize -= w
    
        for i in xrange(textLen, -1, -1):
    
            textW, textH = dc.GetTextExtent(tempText)
            if rectSize >= textW + border:
                fixedText = tempText
                fixedText += suffix
                return fixedText
    
            tempText = tempText[:-1]         

    def rebuild_tree(self):
        #    The date has changed.
        #    We correct the tree for the current date.
        #    Assume a number of nodes are visible/created
        #        1: if that node was not backed up in the run (or prior) then delete it
        #            unless that node has never been backed up (i.e we just record its FS position)
        #        2: if that node changed type, fix it
        #        3: if a new node should be added, add it.
        #    We must do this for all loaded nodes, not just visible.

        run = self.get_current_run() #self.runs[self.date_slider.GetValue()]
        if not run:
            return
        if run != self.displayed_run:
            log.info("Rebuilding tree for run:", run)
            self.rebuild_node(run, self.root_node)
            log.debug("Done rebuild tree")
            self.displayed_run = run
        else:
            log.debug("Already showing this run.")

    def rebuild_node(self, run, node):
        '''
        Given a node, we want to correct it's state as at the given run.
        We do this as non-destructively as possible, so that the tree remains
        in shape.
        
        We are correcting the CHILDREN of the current node, so if the node is
        a leaf - we quit (it should have been corrected when correcting its parent)
        
        @param run:
        @type run:
        @param node:
        @type node:
        '''
        node_data = self.fs_tree.GetItemPyData(node)
        #    If this is NOT a folder node, exit.
        if node_data.type != "D":
            log.debug("Node not directory")
            return
        #    If this node has not been expanded, exit
        if not node_data.expanded:
            log.debug("Node not expanded")
            return
        log.debug("rebuild_node", run, node, node_data)

        #    From the DB, get all fs entries under the current one
        dbentries = self.db.list_dir_id(node_data.fs_id, run.run_id)
        if not dbentries:
            #    This node has no children
            self.fs_tree.DeleteChildren(node)
            log.debug("Node has no children")
            return
#        #    Get the child list - we have to get this in advance because our changes to
#        #    the tree will cause iterators to fail
#        (child, cookie) = self.fs_tree.GetFirstChild(node)

        #    For each of its children
        children_ids = []
        (child, cookie) = self.fs_tree.GetFirstChild(node)
        while child:
            children_ids.append(child)
            child, cookie = self.fs_tree.GetNextChild(node, cookie)

        for child in children_ids:
            #    Get information on this node.
            child_name = self.fs_tree.GetItemText(child)
            log.debug("Got ", child_name)
            child_data = self.fs_tree.GetItemPyData(child)
            if not child_data:
                raise Exception("Illegal: empty child node")

            log.debug("Visiting node:", child_name, child_data)
            #    Fix the node.
            #    If the node type is right, then we will leave it in.
            #    Get the DB node:
            try:
                if child_name in dbentries:
                    #    Exists in tree AND in DB. Make sure the types are right
                    db_data = dbentries[child_name]
                    if db_data.type == 'X':
                        log.debug("Type X = delete", child_name)
                        self.fs_tree.Delete(child)
                    elif db_data.type == 'D' and child_data.type == 'F':
                        new_info = node_info(db_data.parent_id, db_data.fs_id, db_data.type, False, os.path.join(node_data.path, child_name))
                        log.debug("New node: ", new_info)
                        self.fs_tree.SetItemPyData(child, new_info)
                        #    Adding a DIR node, so add a dummy to ensure it shows properly.
                        self.fs_tree.AppendItem(child, DummyTreeNode)
                        #    TODO. change the icon from dir to file.
                    elif db_data.type == 'F' and child_data.type == 'D':
                        #    Fix
                        new_info = node_info(db_data.parent_id, db_data.fs_id, db_data.type, False, os.path.join(node_data.path, child_name))
                        log.debug("New node: ", new_info)
                        self.fs_tree.SetItemPyData(child, new_info)
                        self.fs_tree.DeleteChildren(child)
                        #    TODO. change the icon from file to dir.
                    else:
                        #    The tree and DB nodes agree
                        pass
                    #    Check on the children
                    log.debug("Recursing")
                    self.rebuild_node(run, child)
                    log.debug("Completed node ", child_name)
                    #    This node has been completely fixed
                    del dbentries[child_name]
                else:   #    NOTE IN DB
                    log.debug("Entry no longer exists. Removing from tree")
                    self.fs_tree.Delete(child)
            except Exception as e:
                log.error("Exception on %s: %s" % (child_name, str(e)))
            #    Next node
        log.debug("Items Left:", dbentries)
        #    Now add any missing items back in
        for name, db_data in dbentries.iteritems():
            #    Special case - watch for root
            if db_data.fs_id == 0:
                continue
            if db_data.type is None:
                type = 'D'
            else:
                type = db_data.type

            if db_data.type == 'X':
                continue

            node_name = utils.display_escape(name)
            new_node = self.fs_tree.AppendItem(node, node_name, image=0 if type == "D" else 1)
            new_info = node_info(db_data.parent_id, db_data.fs_id, type, False, os.path.join(node_data.path, name))
            log.debug("New node: ", new_info)
            self.fs_tree.SetItemPyData(new_node, new_info)
            #    For any folders, add a dummy sub-node so we can expand it later
            if type == "D":
                self.fs_tree.AppendItem(new_node, DummyTreeNode)

        #    Ensure the nodes keep roughly the same position/order
        self.fs_tree.SortChildren(node)


    def get_path(self, node):
        '''
        Walk back up the tree to work out the full path for the current node
        
        @param node:
        '''
        if node == self.root_node:
            return "/"

        path = os.path.join(self.get_path(self.fs_tree.GetItemParent(node)),
                            self.fs_tree.GetItemText(node))
        return path
예제 #5
0
class VerifyTestCase(unittest.TestCase):

    def setUp(self):
        self.config = Config.get_config()

        self.db = DB()
        self.db.check_upgrade()
        self.mark_db_ids()

        self.test_folder = tempfile.mkdtemp()
        self.files_folder = os.path.join(self.test_folder, "files")
        self.store_folder = os.path.join(self.test_folder, "store")
        self.restore_folder = os.path.join(self.test_folder, "restore")
        utils.makedirs(self.files_folder)
        utils.makedirs(self.store_folder)
        utils.makedirs(self.restore_folder)

        utils.build_file_structure(self.files_folder, 50 * const.Kilobyte, 500 * const.Kilobyte)

        #    Build a store object (dont save config)
        #    Note the careful size selection - we want backups to overflow the FolderStore.
        self.store = FolderStore("teststore", "2MB", True, self.store_folder)
        self.config.storage[self.store.name] = self.store

        #    Build the backup object (dont save config)
        self.backup = Backup("testbackup")
        self.backup.include_folders = [self.files_folder]
        self.backup.store = self.store.name
        self.backup.notify_msg = False
        self.include_packages = True
        self.config.backups[self.backup.name] = self.backup

        #    build an options object for use with the backup
        self.options = BlankClass()
        self.options.dry_run = False
        self.options.message = False
        self.options.email = False
        self.options.shutdown = False
        self.options.norecurse = False

        self.old_pass = self.config.data_passphrase
        self.config.data_passphrase = "banana"


    def tearDown(self):
        self.config.data_passphrase = self.old_pass
        #    Remove all DB records created during this test
        self.clean_db()
        shutil.rmtree(self.test_folder)
        self.assertFalse(os.path.isdir(self.test_folder))


    def testVerify(self):
        #    Run a full backup
        b = Run("testbackup", const.FullBackup, self.options)
        b.run()

        #    Get the times
        runs = self.db.runs("testbackup")
        run = runs[0]


        v = Verify("testbackup", run.start_time)
        self.assertTrue(v.run())


    def testBadVerify(self):
        #    Run a full backup
        b = Run("testbackup", const.FullBackup, self.options)
        b.run()

        #    Get the times
        runs = self.db.runs("testbackup")
        run = runs[0]

        #    Get the location of the data file from the streamer
        streamer = StreamOut(None, self.store, b.backup_folder)
        datafile = os.path.join(self.store.root, streamer.get_path(0))
        size = os.path.getsize(datafile)
        #    Now corrupt the data file a little
        with open(datafile, "r+b") as f:
            f.seek(size // 2, 0)
            f.write("X")


        v = Verify("testbackup", run.start_time)
        self.assertRaises(Exception, v.run)


    def testBadConfig(self):
        #    Run a full backup
        b = Run("testbackup", const.FullBackup, self.options)
        b.run()

        #    Get the times
        runs = self.db.runs("testbackup")
        run = runs[0]

        #    Delete The Config File
        configfile = os.path.join(run.folder, const.ConfigName)
        self.store.remove_file(configfile)


        v = Verify("testbackup", run.start_time)
        self.assertRaises(Exception, v.run)

    def testBadVerifyEncrypted(self):
        backup = self.config.backups[self.backup.name]
        backup.encrypt = True
        self.config.backups[backup.name] = backup

        #    Run a full backup
        b = Run("testbackup", const.FullBackup, self.options)
        b.run()

        #    Get the times
        runs = self.db.runs("testbackup")
        run = runs[0]

        #    Get the location of the data file from the streamer
        streamer = StreamOut(None, self.store, b.backup_folder)
        datafile = os.path.join(self.store.root, streamer.get_path(0))
        #    Now corrupt the data file a little
        size = os.path.getsize(datafile)
        with open(datafile, "r+b") as f:
            f.seek(size // 2, 0)
            f.write("X")


        v = Verify("testbackup", run.start_time)
        self.assertRaises(Exception, v.run)



############################################################################
#
#    Utility Routines
#
############################################################################

    def mark_db_ids(self):
        self.max_fs_id = self.db.query("select max(fs_id) from fs", ())[0][0]
        if self.max_fs_id is None:
            self.max_fs_id = 0
        self.max_version_id = self.db.query("select max(version_id) from versions", ())[0][0]
        if self.max_version_id is None:
            self.max_version_id = 0
        self.max_run_id = self.db.query("select max(run_id) from runs", ())[0][0]
        if self.max_run_id is None:
            self.max_run_id = 0
        self.max_message_id = self.db.query("select max(message_id) from messages", ())[0][0]
        if self.max_message_id is None:
            self.max_message_id = 0

    def clean_db(self):
        self.db.execute("delete from messages where message_id > ?", (self.max_message_id,))
        self.db.execute("delete from versions where version_id > ?", (self.max_version_id,))
        self.db.execute("delete from fs where fs_id > ?", (self.max_fs_id,))
        self.db.execute("delete from runs where run_id > ?", (self.max_run_id,))
예제 #6
0
class ServerTestCase(unittest.TestCase):

    def setUp(self):
        self.config = Config.get_config()
        self.db = DB()
        self.db.check_upgrade()
        self.mark_db_ids()

        self.test_folder = tempfile.mkdtemp()
        self.files_folder = os.path.join(self.test_folder, "files")
        self.store_folder = os.path.join(self.test_folder, "store")
        self.restore_folder = os.path.join(self.test_folder, "restore")
        utils.makedirs(self.files_folder)
        utils.makedirs(self.store_folder)
        utils.makedirs(self.restore_folder)

        utils.build_file_structure(self.files_folder, 50 * const.Kilobyte, 500 * const.Kilobyte)

        #    Build a store object (dont save the config)
        #    Note the careful size selection - we want backups to overflow the FolderStore.
        self.store = FolderStore("teststore", "2MB", True, self.store_folder)
        self.config.storage[self.store.name] = self.store

        #    Build the backup object (dont save config)
        self.backup = Backup("testbackup")
        self.backup.include_folders = [self.files_folder]
        self.backup.store = self.store.name
        self.backup.notify_msg = False
        self.old_pass = self.config.data_passphrase
        self.config.data_passphrase = "goofy"
        self.backup.encrypt = True
        self.config.backups[self.backup.name] = self.backup

        #    build an options object for use with the backup
        self.options = BlankClass()
        self.options.dry_run = False
        self.options.message = False
        self.options.email = False
        self.options.shutdown = False
        self.options.norecurse = False


    def tearDown(self):
        self.config.data_passphrase = self.old_pass
        #    Remove all DB records created during this test
        self.clean_db()
        shutil.rmtree(self.test_folder)
        self.assertFalse(os.path.isdir(self.test_folder))


    def testBackupRestore(self):
        self.backup_restore_compare()

    def testCheckFiles(self):
        self.backup.include_packages = True
        b = Run("testbackup", const.FullBackup, self.options)
        b.run()
        #    Check that all the right files are there.
        runs = self.db.runs(self.backup.name)
        self.assertEqual(len(runs), 1)
        run = runs[0]
        folder = run.folder
        self.assertTrue(self.store.exists(os.path.join(folder, const.PackageFile + const.EncryptionSuffix)))
        self.assertTrue(self.store.exists(os.path.join(folder, const.LOFFile + const.EncryptionSuffix)))
        self.assertTrue(self.store.exists(os.path.join(folder, const.ConfigName + const.EncryptionSuffix)))

        self.assertTrue(self.store.exists)

    def testAutoManagementOfStore1(self):
        #    Run a set of backups that will overload the store. 
        #    The automanaged store should continue to archive old backups as required.
        #    Store space reclaimation happens across all backups (i.e. any run).
        #    We should see older runs from the first backup disappear.
        max_size, dummy, dummy = self.store.limit_details()

        filesize = utils.du(self.backup.include_folders[0])

        #    Lets make sure we are going to do enough backups that
        #    the older ones will be removed.
        RunCount = (max_size // filesize) + 2


        last_start = None
        for cycle in xrange(RunCount):
            if last_start:
                #    Make sure we have ticked to another second since the start of the last backup.
                while datetime.now() - last_start < timedelta(seconds=1):
                    time.sleep(0.01)

            backup = Backup(self.backup.name + str(cycle))
            backup.include_folders = self.backup.include_folders
            backup.store = self.backup.store
            backup.notify_msg = False
            self.config.backups[backup.name] = backup

            #    Run a full backup
            b = Run(backup.name, const.FullBackup, self.options)
            b.run()
            last_start = b.start_time

            #    Assert that the store is still of an appropriate size
            size, used, avail = self.store.current_usage()
            self.assertTrue(avail >= 0)
            self.assertTrue(used <= max_size)

            #    Confirm that's true on disk
            disksize = utils.du(self.store.root)
            self.assertTrue(disksize <= max_size)


        #    Check that some runs have actually been deleted
        runs = self.db.runs(self.backup.name + "0")
        self.assertTrue(len(runs) == 0)
        runs = self.db.runs(self.backup.name + "1")
        self.assertTrue(len(runs) == 0)

    def testAutoManagementOfStore2(self):
        #    Run one backup multiple times to overload a store
        max_size, dummy, dummy = self.store.limit_details()

        filesize = utils.du(self.backup.include_folders[0])

        #    Lets make sure we are going to do enough backups that
        #    the older ones will be removed.
        RunCount = (max_size // filesize) + 2

        last_start = None
        for cycle in xrange(RunCount):

            if last_start:
                #    Make sure we have ticked to another second since the start of the last backup.
                while datetime.now() - last_start < timedelta(seconds=1):
                    time.sleep(0.01)

            #    Run a full backup
            b = Run(self.backup.name, const.FullBackup, self.options)
            b.run()

            last_start = b.start_time

            #    Assert that the store is still of an appropriate size
            size, used, avail = self.store.current_usage()
            self.assertTrue(avail >= 0)
            self.assertTrue(used <= max_size)

            #    Confirm that's true on disk
            disksize = utils.du(self.store.root)
            self.assertTrue(disksize <= max_size)


        #    Check that some runs have actually been deleted
        runs = self.db.runs(self.backup.name)
        self.assertTrue(len(runs) < RunCount)

    def testChanges(self):
        pass
        #    Full Backup
        #    change a file
        #    Incremental backup
        #    Restore most recent. ensure you get latest file
        #    Restore to just prior to incremental, ensure you get earlier file
        #    Run a full backup
        file = os.path.join(self.files_folder, "changer")
        restore_file = os.path.join(self.restore_folder, file[1:])

        #    t=0 - file does not exist
        b = Run("testbackup", const.FullBackup, self.options)
        b.run()

        #    Make sure we have ticked to another second since the start of the last backup.
        while datetime.now() - b.start_time < timedelta(seconds=1):
            time.sleep(0.01)

        #    t=1 - file exists
        with open(file, "w") as f:
            f.write("1")
        b = Run("testbackup", const.IncrBackup, self.options)
        b.run()

        #    Make sure we have ticked to another second since the start of the last backup.
        while datetime.now() - b.start_time < timedelta(seconds=1):
            time.sleep(0.01)

        #    t=2 - file changed
        with open(file, "w") as f:
            f.write("2")
        b = Run("testbackup", const.IncrBackup, self.options)
        b.run()

        #    Get the times
        runs = self.db.runs("testbackup")
        t0 = runs[0].start_time
        t1 = runs[1].start_time
        t2 = runs[2].start_time

        for t, exists, contents in [(t0, False, None), (t1, True, "1"), (t2, True, "2"), (None, True, "2")]:
            #    Attempt to restore most recent of ALL files
            #    This tests the default restore.
            r = Restore(self.restore_folder, [self.files_folder], t, self.options)
            r.run()
            if exists:
                with open(restore_file, "r") as f:
                    self.assertEqual(f.read(), contents)
            else:
                self.assertFalse(os.path.exists(restore_file))
            #    clean
            shutil.rmtree(self.restore_folder)
            utils.makedirs(self.restore_folder)


    def test7bitFilenames(self):
        #    Make some 7 bit filenames
        strange_folder = os.path.join(self.files_folder, "strange")
        utils.makedirs(strange_folder)
        for i in xrange(1, 117, 10):
            name = "".join([chr(j) for j in xrange(i, i + 10) if chr(j) != "/"])
            path = os.path.join(strange_folder, name)
            with open(path, "w") as f:
                f.write(os.urandom(100))

        self.backup_restore_compare()

    def testUnicodeFilenames(self):
        #    Make some unicode bit filenames
        #    Clean out the ordinary files
        shutil.rmtree(self.files_folder)
        utils.makedirs(self.files_folder)
        unicode_folder = os.path.join(unicode(self.files_folder), u"unicode")
        utils.makedirs(unicode_folder)
        for i in xrange(1000, 1200, 10):
            name = u"".join([unichr(j) for j in xrange(i, i + 10) if unichr(j) != u"/"])
            path = os.path.join(unicode_folder, name)
            with open(path, "w") as f:
                f.write(os.urandom(10))

        self.backup_restore_compare()

    def backup_restore_compare(self):
        #    Run a full backup
        b = Run("testbackup", const.FullBackup, self.options)
        b.run()

        #    Make sure we have ticked to another second since the start of the last backup.
        while datetime.now() - b.start_time < timedelta(seconds=1):
            time.sleep(0.01)


        #    Attempt to restore every file
        r = Restore(self.restore_folder, [self.files_folder],
                                          datetime.now(), self.options)
        r.run()

        #    Check that the restored folder and original folder are identical
        left = unicode(self.files_folder)
        right = unicode(os.path.join(self.restore_folder, self.files_folder[1:]))
        d = utils.dircmp(left, right)

        self.assertEqual(d.left_only, set())
        self.assertEqual(d.right_only, set())
        self.assertEqual(d.diff_files, set())
        self.assertTrue(len(d.same_files) > 0)

        #    Check that all files are in the DB
        for folder, _, local_files in os.walk(self.files_folder):
            for file in local_files:
                path = os.path.join(file, folder)
                #    This will raise an exception if it does not exist
                self.db.select_path(path, build=False)

############################################################################
#
#    Utility Routines
#
############################################################################

    def mark_db_ids(self):
        self.max_fs_id = self.db.query("select max(fs_id) from fs", ())[0][0]
        if self.max_fs_id is None:
            self.max_fs_id = 0
        self.max_version_id = self.db.query("select max(version_id) from versions", ())[0][0]
        if self.max_version_id is None:
            self.max_version_id = 0
        self.max_run_id = self.db.query("select max(run_id) from runs", ())[0][0]
        if self.max_run_id is None:
            self.max_run_id = 0
        self.max_message_id = self.db.query("select max(message_id) from messages", ())[0][0]
        if self.max_message_id is None:
            self.max_message_id = 0

    def clean_db(self):
        self.db.execute("delete from messages where message_id > ?", (self.max_message_id,))
        self.db.execute("delete from versions where version_id > ?", (self.max_version_id,))
        self.db.execute("delete from fs where fs_id > ?", (self.max_fs_id,))
        self.db.execute("delete from runs where run_id > ?", (self.max_run_id,))
예제 #7
0
파일: run.py 프로젝트: tussock/Vault
class Run():
    def __init__(self, name, type, options):
        '''
        Prepare to run a backup event
        
        @param name: name of the backup
        @param type: type (Full/Incr)
        @param type: dry_run
        
        If dry_run is True, then we will print the files we *would have* backed
        up to stdout.
        
        '''
        self.type = type
        self.dry_run = options.dry_run
        self.options = options
        self.config = Config.get_config()

        try:
            self.backup = self.config.backups[name]
        except:
            raise Exception(_("Backup is missing or corrupt. Please reconfigure backup."))

        try:
            #    Get a fresh store (a copy of the config version
            self.store = self.config.storage[self.backup.store].copy()
        except:
            raise Exception(_("Storage definition is missing. Please reconfigure backup."))

        self.db = DB()
        self.start_time = None
        self.nfiles = None
        self.nfolders = None
        self.bytes = None
        self.run_id = None
        self.backup_folder = None

        #    Make sure there are no other backups running of this name
        self.lock = locking.InterProcessLock(name="Vault-%s" % self.backup.name)

        #    Build a quick file exclusion list, to speed up exclusion checking
        self.excl_ext = self.build_excl_exts()
        log.debug("Exclusion List:", ",".join(self.excl_ext))


    def run(self):
        '''
        Execute the backup
        '''
        try:
            self.lock.acquire()
        except:
            msg = _("Backup '%s' is already running. New backup run cannot start") \
                    % (self.backup.name)

            if not self.dry_run:
                #    Since this is a real backup, we create the run, write to the log and fail immediately.
                self.db.start_run(self.backup.name, self.backup.store, self.type, datetime.now())
                self.db.save_message(msg)
                self.db.update_run_status(const.StatusFailed)
            else:
                #    We dont need to do anything for a dry run. The message will
                #    be returned to the user.
                pass
            raise Exception(msg)

        #    We have the lock now...
        try:
            self.orig_type = self.type
            self.check_old_backups()
            self.do_backup()
        finally:
            self.lock.release()

    def check_old_backups(self):
        '''
        We have got the lock, but if there was a crash, there may be a "running"
        state backup left behind. Note that we *know* its not running because
        the lock is gone.
        
        Check for it and fail it if there is.
        '''
        log.debug("Checking for dead backups")
        runs = self.db.runs(self.backup.name)
        runs = [run for run in runs if run.status == const.StatusRunning]

        #    It looks like there is a run that is still running.
        for run in runs:
            log.warn("A prior run crashed. Cleaning up %s/%s" % (run.name, run.start_time_str))
            #    Get the store
            log.debug("Attempting to delete remote run data")
            try:
                self.store.delete_run_data(run)
            except:
                pass
            #    Delete the entries in the database (but not the failed run itself)
            #    This means the messages will persist, so we can see the usage.
            log.debug("Attempting to delete DB run data")
            self.db.delete_run_versions(self.run_id)
            #    Update the status
            log.debug("Setting status to failed and logging")
            self.db.execute("update runs set status = ? where run_id = ?", (const.StatusFailed, run.run_id))
            self.db.execute("insert into messages (run_id, message, time) values (?, ?, ?)",
                            (run.run_id, _("Backup run crashed. It was cleaned up."),
                             datetime.now().strftime(const.DateTimeFormat)))


        #        If there are *no* full backups in the history, then this run MUST be a full,
        #        even if an incremental is requested. Actually a request for incremental will
        #        grab all files anyway, but still... lets make the name match the contents.
        runs = self.db.runs(self.backup.name)
        full_count = len([run for run in runs
                            if run.type == const.FullBackup and
                            run.status == const.StatusSuccess])
        log.debug("Full backups: %d" % full_count)
        if full_count == 0 and self.type != const.FullBackup:
            log.debug("Resetting type to Full")
            self.type = const.FullBackup


    def do_backup(self)      :
        self.start_time = datetime.now()
        self.nfiles = 0
        self.nfolders = 0
        self.bytes = 0
        success = False
        message = ""

        self.backup_folder = os.path.join(self.backup.name, self.start_time.strftime(const.DateTimeFormat) + " " + self.type)
        if not self.dry_run:
            self.run_id = self.db.start_run(self.backup.name, self.backup.store, self.type, self.start_time)
            msg = _("Backup {server}/{backup}/{type} beginning").format(
                                                            server=utils.get_hostname(), 
                                                            backup=self.backup.name, 
                                                            type=self.type)
            if self.dry_run:
                msg += _(" (Dry Run)")
            log.info(msg)
            self.db.save_message(msg)
            if self.orig_type != self.type:
                #    The backup type was switched
                self.db.save_message(_("NOTE: Backup type switched to {newtype} from {oldtype}").format(
                                                        newtype=self.type, oldtype=self.orig_type))

        #    After here we have a run set up in the database, and can begin logging errors.
        try:
            #    Check that if ENCRYPTION is enabled, that there is a password defined.
            if self.backup.encrypt and not self.config.data_passphrase:
                raise Exception("Backup encryption required, but no passphrase has been configured. Backup cancelled.")
            
            self.prepare_store()

            #    Prepare output/destinations/encryption
            self.prepare_output()
            try:
                #    Now we actually DO the backup, for each listed folder
                for folder in self.backup.include_folders:
                    self.recursive_backup_folder(folder)

                log.debug("Committing saved fs entries...")
                self.db.fs_saved_commit()

                log.debug("Closing...")
                self.close_output(success=True)
            #raise Exception("Test Exception")
            except Exception as e:
                log.warn("Exception during backup:", str(e))
                #    We are going to fail. But we need to try and close
                #    whatever we can. Closing may fail, but in this case
                #    we ignore that error.
                try:
                    self.close_output(success=False)
                except:
                    pass
                raise e

            if self.backup.verify and not self.dry_run:
                log.info("Starting verify phase")
                msg = _("Backup {server}/{backup}/{type} verification starting").format(
                                                            server=utils.get_hostname(), 
                                                            backup=self.backup.name, 
                                                            type=self.type)
                
                self.db.save_message(msg)
                v = Verify(self.backup.name, self.start_time)
                v.run()
                msg = _("Backup {server}/{backup}/{type} verification succeeded").format(
                                                            server=utils.get_hostname(), 
                                                            backup=self.backup.name, 
                                                            type=self.type)
                self.db.save_message(msg)
#                self.do_verify()

            #    Messaging...
            #    If its a dry run, the command line specifies messaging.
            #    Otherwise both the command line AND backup spec do.
            if not self.dry_run:
                self.db.update_run_status(const.StatusSuccess)
            message = _("Backup {server}/{backup}/{type} completed").format(
                                                            server=utils.get_hostname(), 
                                                            backup=self.backup.name, 
                                                            type=self.type)
            if self.dry_run:
                message += " " + _("(Dry Run)")
            success = True
            if not self.dry_run:
                self.db.save_message(message)

        except Exception as e:
            log.error("Exception in backup. Recording. ", e)
            message = _("Backup {server}/{backup}/{type} failed. {error}").format(
                                                            server=utils.get_hostname(), 
                                                            backup=self.backup.name, 
                                                            type=self.type, 
                                                            error=str(e))
            success = False
            if not self.dry_run:
                self.db.update_run_status(const.StatusFailed)

                #    After a failed backup - we must remove the backup data because it 
                #    cannot be trusted.
                run = self.db.run_details(self.run_id)
                #    Delete the remote data
                log.debug("Attempting to delete remote run data")
                self.store.delete_run_data(run)
                #    Delete the entries in the database (but not the failed run itself)
                #    This means the messages will persist, so we can see the usage.
                log.debug("Attempting to delete DB run data")
                self.db.delete_run_versions(self.run_id)

                self.db.save_message(message)

        if self.options.message or (self.backup.notify_msg and not self.dry_run):
            try:
                from lib.dlg import Notify
                Notify(const.AppTitle, message)
            except Exception as e:
                #    This one is not really an error... there is probably no-one logged in.
                msg = _("Unable to send notification message (no-one logged in)")
                if not self.dry_run:
                    self.db.save_message(message)
                log.info(msg)

        if self.options.email or (self.backup.notify_email and not self.dry_run):
            try:
                self.send_email(success, message)
            except Exception as e:
                msg = _("Unable to email notification message: {error}").format(
                                                error=str(e))
                if not self.dry_run:
                    self.db.save_message(message)
                log.error(msg)
        if self.options.shutdown or (self.backup.shutdown_after and not self.dry_run):
            try:
                cmd = ["zenity", "--question",
                       "--ok-label", _("Shutdown Now"),
                       "--cancel-label", _("Cancel Shutdown"),
                       "--text",
                       _("Backup {backup} complete. Computer will shut down in 2 minutes").format(backup=self.backup.name),
                       "--timeout", "120"]
                status = subprocess.call(cmd)
                log.debug("Shutdown query. status=%d" % status)
                if status == 0 or status == 5:
                    print("Running shutdown")
                    subprocess.Popen(["shutdown", "-P", "now"])
                    print("Done running shutdown")
            except Exception as e:
                msg = _("Unable to shutdown PC: {error}").format(
                                                error=str(e))
                if not self.dry_run:
                    self.db.save_message(message)
                log.error(msg)

    def send_email(self, result, head):

        '''
        Send a message to the appropriate users.
        If result is False (failure) then error message will contain the reason.
        
        @param result:
        @param error_message:
        '''
        log.debug("send_email: ", result, head)
        if result:
            message_text = head + \
                    _("\n\nStatistics:\n    {files} files backed up.\n    "
                      "{folders} folders backed up.\n    {size} copied.\n"
                      ).format(files=self.nfiles, folders=self.nfolders, size=utils.readable_form(self.bytes))

            subject = _("Backup {server}/{backup}/{type} completed").format(server=utils.get_hostname(), backup=self.backup.name, type=self.type)
        else:
            message_text = head
            subject = _("Backup {server}/{backup}/{type} failed").format(server=utils.get_hostname(), backup=self.backup.name, type=self.type)

        if not self.options.dry_run:
            messages = "    " + "\n    ".join([message.time + " " + message.message for message in self.db.run_messages(self.run_id)])
            message_text += _("\nBackup messages:\n") + messages
        else:
            message_text = "\n"

        log.debug("Starting mail send")
        try:
            sendemail.sendemail(subject, message_text)
        except Exception as e:
            msg = _("Unable to email results. {error}").format(error=str(e))
            if not self.dry_run:
                self.db.save_message(msg)
            else:
                print(msg)

        log.trace("send_email completed")



    def backup_packages(self):
        '''
        Build the package list. Then send to the backup server
        '''
        log.info("Backing up packages")
        package_list = utils.get_packages()
        f = tempfile.NamedTemporaryFile(delete=False)
        f.write("\n".join(package_list))
        f.close()

        self.copy_file(f.name, const.PackageFile)
        os.remove(f.name)


    def copy_file(self, path, name=None):
        log.debug("CopyFile: ", path, name)
        if not name:
            name = os.path.basename(path)
        if self.dry_run:
            print(utils.escape(name))
            sys.stdout.flush()
        else:
            if self.backup.encrypt:
                if name:
                    name = name + ".enc"    #    Otherwise left as None
                enc_path = path + ".enc"
                cryptor.encrypt_file(self.config.data_passphrase, path, enc_path)
                self.store.send(enc_path, os.path.join(self.backup_folder, name))
                os.remove(enc_path)
            else:
                self.store.send(path, os.path.join(self.backup_folder, name))


    def build_excl_exts(self):
        list = []
        for name in self.backup.exclude_types:
            try:
                for ext in self.config.file_types[name]:
                    list.append(ext)
            except:
                self.db.save_message(_("Exclusion Type %s is no longer recongnised. Ignored.") % name)
        return list

    def list_to_unicode(self, l):
        return [utils.path_to_unicode(p) for p in l]




    def recursive_backup_folder(self, root):
        '''
        Backup a folder and all its sub-folders.
        This routine REQUIRES an absolute path.
        
        @param folder:
        '''
        log.trace("recursive_backup_folder", root)
        #    Before we interact with the FS - convert to utf-8
        root = root.encode('utf-8')
        if len(root) == 0:
            raise Exception(_("Backup_folder called on empty folder name"))
        if root[0] != "/":
            raise Exception(_("Backup_folder requires absolute paths"))
        for folder, local_folders, local_files in os.walk(root):
            #    Lets get everything to unicode
#            local_folders = self.list_to_unicode(local_folders)
#            local_files = self.list_to_unicode(local_files)
            log.debug("os.walk", folder, local_folders, local_files)
            #    First: Check if this is specifically excluded
            if self.check_exclusion(folder):
                log.info("Excluding Dir:", folder)
                continue

            log.info("Backing up folder: %s" % folder)

#            local_files.sort()
#            local_folders.sort()

            #    Get the data on this folder from the db
            db_files = self.db.list_dir(folder)
            log.debug("Backing up folder", folder)
            log.debug("local files:", local_files)
            log.debug("local folders:", local_folders)
            log.debug("DB files:", db_files)

            for local_file in local_files:
                try:
                    local_path = os.path.join(folder, local_file)
                    if self.check_backup(local_path, local_file, db_files):
                        self.do_backup_file(folder, local_file)
                except StoreFullException as e:
                    log.error(str(e))
                    raise e
                except Exception as e:
                    log.warn("Skipping file %s: %s" % (local_file, str(e)))

            #    Convert to unicode for checks below...
            local_folders = self.list_to_unicode(local_folders)
            local_files = self.list_to_unicode(local_files)
            #    Have backed up all the local files. Now look for DB files
            #    that exist, but are not local (i.e. they have been deleted)
            #    Make sure we are only looking for 'F' and 'D' (ignore 'X')
            for db_file in db_files.itervalues():
                try:
                    uname = utils.path_to_unicode(db_file.name)
                    if db_file.type in ['D', 'F'] and not uname in local_files and not uname in local_folders:
                        self.do_backup_deleted(folder, db_file.name)
                except Exception as e:
                    log.warn("Ignoring exception logging deleted file %s: %s" % (db_file.name, e))

            for local_folder in local_folders:
                try:
                    local_path = os.path.join(folder, local_folder)
                    if self.check_backup(local_path, local_folder, db_files):
                        self.do_backup_folder(folder, local_folder)
                except Exception as e:
                    log.warn("Ignoring exception backing up folder %s: %s" % (local_path, e))

#            #    At the completion of a folder - we update the DB storage usage
            if not self.dry_run:
                self.bytes, self.hash = self.store_thread.get_hash()
                self.db.update_run_stats(self.bytes, self.nfiles, self.nfolders, self.backup.include_packages, self.hash)

    def lof_record(self, folder, name, type, mod_time=None, size=None):
        #    Save the entry in the LOF
        log.trace("lof_record", folder, name)
        if folder != self.lof_folder:
            self.lof.write("\n%s\n" % utils.escape(folder))
            self.lof_folder = folder
        self.lof.write("%s,%s" % (type, utils.escape(name)))
        if mod_time:
            self.lof.write(',%s,%d' % (mod_time, size))
        self.lof.write("\n")


    def check_exclusion(self, path):
        _, ext = os.path.splitext(path)
        ext = ext[1:].lower()           #    Remove the '.'
        #    Is this file excluded by type
        if ext in self.excl_ext:
            return True

        #    Is this file excluded by filename/folder/glob
        ancestors = utils.ancestor_paths(path)
        #log.debug("Ancestor Pathlist:", ",".join(ancestors))
        for patt in self.backup.exclude_patterns:
            for path in ancestors:
                if fnmatch.fnmatch(path, patt):
                    return True

        return False



    def check_backup(self, path, basename, db_files):
        log.trace("check_backup", path, basename)
        #    Check for exclusions
        if self.check_exclusion(path):
            log.debug("Excluding", path)
            return False

        #    If this is a full backup, then its included
        if self.type == const.FullBackup:
            log.debug("Include", path, "because full backup")
            return True

        #    Its an incremental - check timestamps.
        #    Find it in the database...
        if not basename in db_files:
            log.debug("Include", path, "because new file")
            return True

        db_file = db_files[basename]
        #    Was the last entry a delete? If so, its back and we back it up
        if db_file.type == 'X':
            log.debug("Include", path, "because file was deleted")
            return True

        if db_file.mod_time == None:
            #    The last version record for this file must have been removed.
            #    This can happen when the storage is too small for a complete
            #    cycle of FULL and INCREMENTALS. The FULL has been deleted and
            #    so there are FS entries, but no version entries.
            return True

        #    Check the timestamp
        #local_modtime = self.get_file_time_str(path)
        local_modtime = datetime.fromtimestamp(os.path.getmtime(path))
        #    The file datetime is to the nearest microsecond. The DB time is not.
        local_modtime -= timedelta(microseconds=local_modtime.microsecond)
        log.debug("MOD TIME CHECK: db: %s local: %s" % (str(db_file.mod_time), str(local_modtime)))
        if local_modtime > db_file.mod_time:
            log.debug("Include", path, "because changed")
            return True

        #    Check if the type has changed
        log.debug("TYPE CHECK: db type = %s isfile=%d" % (db_file.type, os.path.isfile(path)))
        if db_file.type == 'F' and not os.path.isfile(path):
            log.debug("Include", path, "because changed type")
            return True
        if db_file.type == 'D' and not os.path.isdir(path):
            log.debug("Include", path, "because changed type")
            return True

        #    One extra check - just in case. Only for files
        if db_file.type == 'F':
            size = os.path.getsize(path)
            log.debug("SIZE CHECK: db: %s local: %s" % (db_file.size, size))
            if size != db_file.size:
                log.debug("Include", path, "because changed size")
                return True

        return False

    def get_file_time_str(self, path):
        return datetime.fromtimestamp(os.path.getmtime(path)).strftime(const.DateTimeFormat)

    def do_backup_file(self, folder, name):
        log.trace("do_backup_file", folder, name)
        path = os.path.join(folder, name)
        if self.dry_run:
            print("F - %s" % utils.escape(path))
            sys.stdout.flush()
            return

        #    Add it to the tar
        try:
            #    What's the encoding on the file?
            upath = utils.path_to_unicode(path)
            uname = utils.path_to_unicode(name)
            ufolder = utils.path_to_unicode(folder)
            #    Due to issues with encoding... I'll open the file myself and pass to tar
            with open(path, "rb") as f:
                info = self.tarfile.gettarinfo(arcname=upath, fileobj=f)
                self.tarfile.addfile(info, f)
            #self.tarfile.addfile(name=path, recursive=False)

            mod_time = self.get_file_time_str(path)
            size = os.path.getsize(path)
            type = 'F'
            self.db.fs_saved(upath, mod_time, size, type)
            self.nfiles += 1

            #    Save the entry in the LOF
            self.lof_record(ufolder, uname, "F", mod_time, size)

        except Exception as e:
            #    If the exception is in the store - we crash and burn
            #    as we cannot save
            if self.store_thread.error:
                raise self.store_thread.error

            #    Otherwise log it and keep going...
            msg = "Unable to backup %s: %s" % (path, str(e))
            self.db.save_message(msg)
            log.warn(msg)

    def do_backup_folder(self, folder, name):
        log.trace("do_backup_folder", folder, name)
        path = os.path.join(folder, name)
        if self.dry_run:
            print("D - %s" % utils.escape(path))
            sys.stdout.flush()
            return

        #    We dont need to add it to the 
        try:
            self.tarfile.add(name=path, recursive=False)
            mod_time = self.get_file_time_str(path)
            size = 0
            type = 'D'
            self.db.fs_saved(path, mod_time, size, type)
            self.nfolders += 1

            #    Save the entry in the LOF
            self.lof_record(folder, name, "D", mod_time, size)
        except Exception as e:
            log.warn("Exception backing up folder %s: %s" % (path, str(e)))
            #    If the exception is in the store - we crash and burn
            #    as we cannot save
            if self.store_thread.error:
                raise self.store_thread.error

            #    Otherwise log it and keep going...
            msg = "Unable to backup %s: %s" % (path, str(e))
            self.db.save_message(msg)
            log.warn(msg)

    def do_backup_deleted(self, folder, name):
        path = os.path.join(folder, name)
        if self.dry_run:
            print("X - %s" % utils.escape(path))
            sys.stdout.flush()
            return
        try:
            log.debug("FILE/FOLDER DELETED:", name)
            self.db.fs_deleted(path)
            #    We keep track of deletions
            self.lof_record(folder, name, "X")
        except Exception as e:
            #    If the exception is in the store - we crash and burn
            #    as we cannot save
            if self.store_thread.error:
                raise self.store_thread.error

            #    Otherwise log it and keep going...
            msg = "Unable to backup %s: %s" % (path, str(e))
            self.db.save_message(msg)
            log.warn(msg)


    def prepare_store(self):
        #    Test that the storage is available (this will connect and disconnect)
        self.store.test()

        #    Connect to the store
        self.store.connect()

        try:
            log.info("Preparing store")
            #    Ensure the store is properly marked and configured.
            self.store.setup_store()

            if self.backup.include_packages:
                self.backup_packages()

            #    Backup the configuration
            self.copy_file(const.ConfigFile, const.ConfigName)
        finally:
            self.store.disconnect()


    def prepare_output(self):
        '''
        Open the tar file.
        Connect the output of the tar to either:
        a) the storage handler
        b) to encryption (openssl), THEN the storage handler
        
        '''
        log.trace("Setting up output processes")

        #    If we are using an external save command, we do nothing here
        if self.dry_run:
            log.info("No output processes: Dry run")
            return

        #    Set up the encryptor (use TEE for now)
        self.crypt_proc = None
        if self.backup.encrypt:
            log.debug("Creating crypto stream")

            self.crypto = cryptor.EncryptStream(self.config.data_passphrase)

        else:
            self.crypto = cryptor.Buffer()

        #    Set up the storage handler
        log.debug("Starting storage thread")

        self.store_thread = StreamOut(self.crypto, self.store, self.backup_folder)
        self.store_thread.start()

        #    Now set up the tar file which will feed all this
        log.debug("Connecting tar object")
        self.tarfile = tarfile.open(mode="w|gz", fileobj=self.crypto, format=tarfile.PAX_FORMAT, encoding="utf-8",
                                    dereference=False, bufsize=const.BufferSize)

        #    Now set up a zipped temp file to record the list of files/folders saved
        tmp = tempfile.NamedTemporaryFile(delete=False)
        self.lof = gzip.GzipFile(tmp.name, mode="wb", compresslevel=9)
        tmp.close()
        #    Used to keep track of the current folder
        self.lof_folder = ""

        log.trace("Completed output preparation")

    def close_output(self, success):
        '''
        Close as much as we can. This means we may need to ignore some
        errors as we go to ensure everything gets closed.
        
        If success is True, then we have been successful up to this point.
        '''
        log.trace("Closing output managers")
        #    If we are using an external save command, we do nothing here
        if self.dry_run:
            log.info("No output managers: Dry Run")
            return
        #    First exception will be returned.
        error = None
        try:
            self.tarfile.close()
            #    Tar object tries to write after delete. Seems that in an error state,
            #    we get more data after the crypto pipe is closed.
            #    Its an ignorable error, that only occurs when output write fails.
#            del self.tarfile            
        except Exception as e:
            if not error:
                error = e
        try:
            #    Must manually close this (tarfile wont
            #    close a file_obj
            self.crypto.close()
        except Exception as e:
            if not error:
                error = e
        try:
            #    Now we are ready to wait for the storage.
            self.store_thread.join()
            if self.store_thread.error:
                msg = _("Error saving backup: %s") % str(self.store_thread.error)
                log.error(msg)
                self.db.save_message(msg)
                raise self.store_thread.error
        except Exception as e:
            #    This one we will propogate
            error = e
        try:
            self.lof.close()
            if not error and success:
                #    Send the lof only on success
                self.copy_file(self.lof.name, const.LOFFile)
            os.remove(self.lof.name)
        except Exception as e:
            if not error:
                error = e
        try:

            self.store.disconnect()

            #    Update run data, ONLY if this was successful.
            if not error and success:
                self.bytes, self.hash = self.store_thread.get_hash()
                self.db.update_run_stats(self.bytes, self.nfiles, self.nfolders, self.backup.include_packages, self.hash)
        except Exception as e:
            if not error:
                error = e

        log.debug("All output closed. ")
        if error:
            raise error
예제 #8
0
class HistoryWindow(gui.HistoryWindow):
    '''
    classdocs
    '''


    def __init__(self, parent, default_name=None):
        '''
        Constructor
        '''
        gui.HistoryWindow.__init__(self, parent)

        log.trace("Starting up a history panel")
        self.db = DB()
        self.config = Config.get_config()

        self.order = const.ASC
        
        self.update_data(default_name)

#        self.imgList = wx.ImageList(16, 16)
#        self.img_up = self.imgList.Add(wx.Bitmap("images/go-up.png", wx.BITMAP_TYPE_PNG))
#        self.img_down = self.imgList.Add(wx.Bitmap("images/go-down.png", wx.BITMAP_TYPE_PNG))
#        self.lstRuns.SetImageList(self.imgList, wx.IMAGE_LIST_SMALL)

        icon = wx.Icon(os.path.join(const.PixmapDir, "storage.png"), wx.BITMAP_TYPE_ANY)
        self.SetIcon(icon)

#        listmix.ColumnSorterMixin.__init__(self, 7)
#        self.Bind(wx.EVT_LIST_COL_CLICK, self.onColClick, self.lstRuns)

#        self.SortListItems(2, 1)

        #    Ensure the right page is showing
        self.nb_history.SetSelection(0)
        self.Show()


    def update_data(self, default_name = None):
        all = _("***All Backups***")
        if default_name:
            old_sel = default_name
        else:
            old_sel = self.cboBackup.GetStringSelection()
        if not old_sel:
            old_sel = all
        self.cboBackup.Clear()
        self.cboBackup.AppendItems([all])
        self.cboBackup.AppendItems(self.config.backups.keys())
        self.cboBackup.SetStringSelection(old_sel)

        self.update_runs()
        self.update_messages()

    def update_runs(self):
        if self.cboBackup.GetSelection() == 0:
            runs = self.db.runs()
        else:
            backup_name = self.cboBackup.GetStringSelection()
            runs = self.db.runs(backupname=backup_name)
            
        if self.order == const.ASC:
            runs.sort(key=lambda x : x.start_time_str, reverse=False)
            self.txtOrder.SetLabel("Order: Oldest First")
        else:
            runs.sort(key=lambda x : x.start_time_str, reverse=True)
            self.txtOrder.SetLabel("Order: Newest First")

        self.lstRuns.DeleteAllColumns()
        self.lstRuns.DeleteAllItems()
        self.lstRuns.InsertColumn(0, _("Name"), wx.LIST_FORMAT_LEFT)
        self.lstRuns.InsertColumn(1, _("Type"), wx.LIST_FORMAT_CENTER)
        self.lstRuns.InsertColumn(2, _("Time"), wx.LIST_FORMAT_CENTER)
        self.lstRuns.InsertColumn(3, _("Status"), wx.LIST_FORMAT_CENTER)
        self.lstRuns.InsertColumn(4, _("Files"), wx.LIST_FORMAT_CENTER)
        self.lstRuns.InsertColumn(5, _("Folders"), wx.LIST_FORMAT_CENTER)
        self.lstRuns.InsertColumn(6, _("Size"), wx.LIST_FORMAT_CENTER)

        self.itemDataMap = {}
        idx = 0
        for run in runs:
            row = [run.name, run.type, run.start_time_str, run.status, str(run.nfiles), str(run.nfolders), utils.readable_form(run.size)]
            self.lstRuns.Append(row)
            self.lstRuns.SetItemData(idx, run.run_id)
            self.itemDataMap[idx + 1] = row
            idx = idx + 1
        self.itemIndexMap = self.itemDataMap.keys()

        self.lstRuns.SetColumnWidth(0, 100)
        self.lstRuns.SetColumnWidth(1, 50)
        self.lstRuns.SetColumnWidth(2, wx.LIST_AUTOSIZE)
        self.lstRuns.SetColumnWidth(3, 80)
        self.lstRuns.SetColumnWidth(4, 120)
        self.lstRuns.SetColumnWidth(5, 100)
        self.lstRuns.SetColumnWidth(6, wx.LIST_AUTOSIZE)

    # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
    def GetListCtrl(self):
        return self.lstRuns

    # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
    def GetSortImages(self):
        return (self.img_down, self.img_up)


    def onColClick(self, event):
        event.Skip()



    def update_messages(self):

        self.lstMessages.DeleteAllColumns()
        self.lstMessages.DeleteAllItems()
        self.lstMessages.InsertColumn(0, _("Time"))
        self.lstMessages.InsertColumn(1, _("Message"))


        if self.cboBackup.GetSelection() == 0:
            messages = self.db.messages()
        else:
            backup_name = self.cboBackup.GetStringSelection()
            messages = self.db.backup_messages(backup_name)

        if self.order == const.ASC:
            messages.sort(reverse=False, key=lambda msg: msg.time)
        else:
            messages.sort(reverse=True, key=lambda msg: msg.time)

        for msg in messages:
            item = (msg.time, msg.message)
            self.lstMessages.Append(item)

        self.lstMessages.SetColumnWidth(0, wx.LIST_AUTOSIZE)
        self.lstMessages.SetColumnWidth(1, wx.LIST_AUTOSIZE)

    def onBackup(self, event):
        self.update_data()

    def onRefresh(self, event):
        self.update_data()

    def onDetails(self, event):
        #    Get the selected item
        sel = self.lstRuns.GetFirstSelected()
        if sel == -1:
            return
        run_id = self.lstRuns.GetItemData(sel)
        #    Will raise an exception if no run
        run = self.db.run_details(run_id)
        RunDetailsWindow(self, run)

    def onLeftDClick(self, event):
        self.onDetails(event)

    def onOrder(self, event):
        if self.order == const.ASC:
            self.order = const.DESC
        else:
            self.order = const.ASC
        self.update_data()