예제 #1
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