Source

AutomaticBackups / automaticbackups.py

Full commit
#
# AutomaticBackupsPlugin.py
# (c) Steve Cooper, 2008
# This software comes with NO WARRANTIES.
#

#                   ^--- Good old days :)

import sublime, sublime_plugin, os, shutil, re
from subprocess import Popen

import backups

WINMERGE = '"D:\portable_sync\PortableApps\WinMergePortable\WinMergePortable.exe"'
KEY      = "AutoBackups"

class StateManager(sublime_plugin.EventListener):
    def on_close(self, view):
        NavigateBackups.forget(view)

    def on_post_save(self, view):
        NavigateBackups(view).teardown()
        
    def on_modified(self, view):
        state = NavigateBackups(view)
        if NavigateBackups(view).index is not None:
            if view.command_history(0)[0] == 'revert':
                state.teardown()

class NavigateBackups(sublime_plugin.TextCommand):
    cmds = {}

    class defaults:
        # view          = 'indexed here, but set in __new__'
        index              = None
        just_reverted      = False
        found_backup_files = None
        current_file       = None

    def default_state(self):
        self.__dict__.update(self.defaults.__dict__)

    def teardown(self):
        self.default_state()
        self.view.erase_status(KEY)

    def __new__(cls, view):
        "There's only ever one instance per view"
        cmds           = cls.cmds
        vid            = view.id()
        instance       = cmds.get(vid)

        if not vid in cmds:
            instance = cmds[vid] = object.__new__(cls)
            instance.default_state()
            instance.view  = view

        return instance

    @classmethod
    def forget(cls, view):
        cmds = cls.cmds
        if view.id() in cmds: del cmds[view.id()]

    def run(self, edit, cmd):
        view       = self.view

        if self.index is None: self.find_backups(view)

        if   cmd == 'forward':  self.nav_forwards()
        elif cmd == 'backward': self.nav_backwards()

        if self.found_backup_files:
            print len(self.found_backup_files), self.index, view

            if self.navigated_to_end_of_backups():
                if cmd != 'merge' and not self.just_reverted:
                    self.revert(view)
            else:
                self.backup = self.found_backup_files[self.index]
                self.backup_full_path = os.path.join( self.backup_path,
                                                      self.backup )

                if cmd == 'merge': self.merge()
                else: self.buffer(view, edit)

    def nav_forwards(self):
        self.index +=1
        self.index = min(len(self.found_backup_files)-1, self.index)

    def nav_backwards(self):
        self.index -=1
        self.index = max(0, self.index)
        self.just_reverted = False

    def find_backups(self, view):
        fn = view.file_name()
        self.current_file = fn

        f, ext = os.path.splitext(os.path.split(fn)[1])
        self.backup_path = backups.backup_file_path(view, just_dir=True)
        dir_listing = os.listdir(self.backup_path)

        date = "-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}"
        pattern = "%s%s%s" % (f, date, ext)
        matcher = re.compile(pattern)

        self.found_backup_files = \
                filter(lambda x: matcher.match(x), dir_listing)

        self.index = len(self.found_backup_files)-1

    def merge(self):
        #TODO: make the merge tool configurable
        CMD = '%s /e /wr /s /x /ul /ur /dr "AUTOMATIC BACKUP: %s" "%s" "%s"' %\
              (WINMERGE, self.backup, self.current_file, self.backup_full_path)

        Popen(CMD)

    def buffer(self, view, edit):
        line, _  = view.rowcol(view.visible_region().begin())

        region = view.sel()[0] if view.sel() else None
        if region is not None: text = view.substr(region)
        view.sel().clear()

        with file(self.backup_full_path) as old_file:
            view.erase(edit, sublime.Region(0, view.size()))
            view.insert(edit, 0, unicode(old_file.read(), 'utf8'))
            view.set_status('AutoBackups', "%s [%s of %s]" %\
                (self.backup, self.index+1, len(self.found_backup_files)-1))

            def inner(t=0):
                # These 3 lines should force sublime to complete `loading`
                # basically synchronisation.
                for pt in range(view.size()): view.scope_name(pt)
                view.layout_extent()
                view.viewport_extent()

                pt = view.text_point(line, 0)
                y  = view.text_to_layout(pt)[1]
                view.set_viewport_position((0, y))

                if region:
                    found = view.find(text, view.visible_region().begin(),
                                            sublime.LITERAL)
                    if found:
                        view.sel().add(found)
                        view.show_at_center(found)

            sublime.set_timeout(inner, 0)

    def navigated_to_end_of_backups(self):
        return self.index == len(self.found_backup_files)-1

    def revert(self, view):
        def inner():
            view.run_command('revert')
            self.teardown()
            self.just_reverted = True

        sublime.set_timeout(inner, 50)

    def is_enabled(self, **args):
        return self.view.file_name()

class AutomaticBackupsPlugin(sublime_plugin.EventListener):
    """Creates an automatic backup of every file you save. This
    gives you a rudimentary mechanism for making sure you don't lose
    information while working."""

    def on_post_save(self, view):
        """When a file is saved, put a copy of the file into the
        backup directory"""

        buffer_file_name = view.file_name()

        # if buffer_file_name.endswith('.lnk'): return
        newname = backups.backup_file_path(view)
        if newname == None:
            return

        backup_dir, file_to_write = os.path.split(newname)

        # make sure that we have a directory to write into
        if (os.access(backup_dir, os.F_OK) == False):
            os.makedirs(backup_dir)

        backups.log("backing up to " + newname)
        shutil.copy(buffer_file_name, newname)