Ejemplo n.º 1
0
class Action:
    def __init__(self, key, action):
        self.key = key
        self.action = action
        self.log = Logger()

    def execute(self):
        self.log.sub('executing \"%s\"' % (self.action))
        try:
            subprocess.call(self.action, shell=True)
        except KeyboardInterrupt:
            self.log.warn('action interrupted')

    def __str__(self):
        return 'key:%s -> \"%s\"' % (self.key, self.action)

    def __eq__(self, other):
        return self.__dict__ == other.__dict__

    def __hash__(self):
        return hash(self.key) ^ hash(self.action)
Ejemplo n.º 2
0
class Action:
    def __init__(self, key, action):
        self.key = key
        self.action = action
        self.log = Logger()

    def execute(self):
        ret = 1
        self.log.sub('executing \"%s\"' % (self.action))
        try:
            ret = subprocess.call(self.action, shell=True)
        except KeyboardInterrupt:
            self.log.warn('action interrupted')
        return ret == 0

    def transform(self, arg0, arg1):
        '''execute transformation with {0} and {1}
        where {0} is the file to transform and
        {1} is the result file'''
        if os.path.exists(arg1):
            msg = 'transformation destination exists: %s'
            self.log.warn(msg % (arg1))
            return False
        ret = 1
        cmd = self.action.format(arg0, arg1)
        self.log.sub('transforming with \"%s\"' % (cmd))
        try:
            ret = subprocess.call(cmd, shell=True)
        except KeyboardInterrupt:
            self.log.warn('action interrupted')
        return ret == 0

    def __str__(self):
        return 'key:%s -> \"%s\"' % (self.key, self.action)

    def __eq__(self, other):
        return self.__dict__ == other.__dict__

    def __hash__(self):
        return hash(self.key) ^ hash(self.action)
Ejemplo n.º 3
0
class Action:

    def __init__(self, key, action):
        self.key = key
        self.action = action
        self.log = Logger()

    def execute(self):
        self.log.sub('executing \"%s\"' % (self.action))
        try:
            subprocess.call(self.action, shell=True)
        except KeyboardInterrupt:
            self.log.warn('action interrupted')

    def __str__(self):
        return 'key:%s -> \"%s\"' % (self.key, self.action)

    def __eq__(self, other):
        return self.__dict__ == other.__dict__

    def __hash__(self):
        return hash(self.key) ^ hash(self.action)
Ejemplo n.º 4
0
class Installer:

    def __init__(self, base='.', create=True, backup=True,
                 dry=False, safe=False, workdir='~/.config/dotdrop',
                 debug=False, diff=True, totemp=None, showdiff=False,
                 backup_suffix='.dotdropbak', diff_cmd=''):
        """constructor
        @base: directory path where to search for templates
        @create: create directory hierarchy if missing when installing
        @backup: backup existing dotfile when installing
        @dry: just simulate
        @safe: ask for any overwrite
        @workdir: where to install template before symlinking
        @debug: enable debug
        @diff: diff when installing if True
        @totemp: deploy to this path instead of dotfile dst if not None
        @showdiff: show the diff before overwriting (or asking for)
        @backup_suffix: suffix for dotfile backup file
        @diff_cmd: diff command to use
        """
        self.create = create
        self.backup = backup
        self.dry = dry
        self.safe = safe
        self.workdir = os.path.expanduser(workdir)
        self.base = base
        self.debug = debug
        self.diff = diff
        self.totemp = totemp
        self.showdiff = showdiff
        self.backup_suffix = backup_suffix
        self.diff_cmd = diff_cmd
        self.comparing = False
        self.action_executed = False
        self.log = Logger()

    def _log_install(self, boolean, err):
        if not self.debug:
            return boolean, err
        if boolean:
            self.log.dbg('install: SUCCESS')
        else:
            if err:
                self.log.dbg('install: ERROR: {}'.format(err))
            else:
                self.log.dbg('install: IGNORED')
        return boolean, err

    def install(self, templater, src, dst,
                actionexec=None, noempty=False,
                ignore=[], template=True):
        """
        install src to dst using a template
        @templater: the templater object
        @src: dotfile source path in dotpath
        @dst: dotfile destination path in the FS
        @actionexec: action executor callback
        @noempty: render empty template flag
        @ignore: pattern to ignore when installing
        @template: template this dotfile

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        """
        if self.debug:
            self.log.dbg('installing \"{}\" to \"{}\"'.format(src, dst))
        if not dst or not src:
            if self.debug:
                self.log.dbg('empty dst for {}'.format(src))
            return self._log_install(True, None)
        self.action_executed = False
        src = os.path.join(self.base, os.path.expanduser(src))
        if not os.path.exists(src):
            err = 'source dotfile does not exist: {}'.format(src)
            return self._log_install(False, err)
        dst = os.path.expanduser(dst)
        if self.totemp:
            dst = self._pivot_path(dst, self.totemp)
        if utils.samefile(src, dst):
            # symlink loop
            err = 'dotfile points to itself: {}'.format(dst)
            return self._log_install(False, err)
        isdir = os.path.isdir(src)
        if self.debug:
            self.log.dbg('install {} to {}'.format(src, dst))
            self.log.dbg('is a directory \"{}\": {}'.format(src, isdir))
        if isdir:
            b, e = self._install_dir(templater, src, dst,
                                     actionexec=actionexec,
                                     noempty=noempty, ignore=ignore,
                                     template=template)
            return self._log_install(b, e)
        b, e = self._install_file(templater, src, dst,
                                  actionexec=actionexec,
                                  noempty=noempty, ignore=ignore,
                                  template=template)
        return self._log_install(b, e)

    def link(self, templater, src, dst, actionexec=None, template=True):
        """
        set src as the link target of dst
        @templater: the templater
        @src: dotfile source path in dotpath
        @dst: dotfile destination path in the FS
        @actionexec: action executor callback
        @template: template this dotfile

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        """
        if self.debug:
            self.log.dbg('link \"{}\" to \"{}\"'.format(src, dst))
        if not dst or not src:
            if self.debug:
                self.log.dbg('empty dst for {}'.format(src))
            return self._log_install(True, None)
        self.action_executed = False
        src = os.path.normpath(os.path.join(self.base,
                                            os.path.expanduser(src)))
        if not os.path.exists(src):
            err = 'source dotfile does not exist: {}'.format(src)
            return self._log_install(False, err)
        dst = os.path.normpath(os.path.expanduser(dst))
        if self.totemp:
            # ignore actions
            b, e = self.install(templater, src, dst, actionexec=None,
                                template=template)
            return self._log_install(b, e)

        if template and Templategen.is_template(src):
            if self.debug:
                self.log.dbg('dotfile is a template')
                self.log.dbg('install to {} and symlink'.format(self.workdir))
            tmp = self._pivot_path(dst, self.workdir, striphome=True)
            i, err = self.install(templater, src, tmp, actionexec=actionexec,
                                  template=template)
            if not i and not os.path.exists(tmp):
                return self._log_install(i, err)
            src = tmp
        b, e = self._link(src, dst, actionexec=actionexec)
        return self._log_install(b, e)

    def link_children(self, templater, src, dst, actionexec=None,
                      template=True):
        """
        link all files under a given directory
        @templater: the templater
        @src: dotfile source path in dotpath
        @dst: dotfile destination path in the FS
        @actionexec: action executor callback
        @template: template this dotfile

        return
        - True, None: success
        - False, error_msg: error
        - False, None, ignored
        """
        if self.debug:
            self.log.dbg('link_children \"{}\" to \"{}\"'.format(src, dst))
        if not dst or not src:
            if self.debug:
                self.log.dbg('empty dst for {}'.format(src))
            return self._log_install(True, None)
        self.action_executed = False
        parent = os.path.join(self.base, os.path.expanduser(src))

        # Fail if source doesn't exist
        if not os.path.exists(parent):
            err = 'source dotfile does not exist: {}'.format(parent)
            return self._log_install(False, err)

        # Fail if source not a directory
        if not os.path.isdir(parent):
            if self.debug:
                self.log.dbg('symlink children of {} to {}'.format(src, dst))

            err = 'source dotfile is not a directory: {}'.format(parent)
            return self._log_install(False, err)

        dst = os.path.normpath(os.path.expanduser(dst))
        if not os.path.lexists(dst):
            self.log.sub('creating directory "{}"'.format(dst))
            os.makedirs(dst)

        if os.path.isfile(dst):
            msg = ''.join([
                'Remove regular file {} and ',
                'replace with empty directory?',
            ]).format(dst)

            if self.safe and not self.log.ask(msg):
                err = 'ignoring "{}", nothing installed'.format(dst)
                return self._log_install(False, err)
            os.unlink(dst)
            os.mkdir(dst)

        children = os.listdir(parent)
        srcs = [os.path.normpath(os.path.join(parent, child))
                for child in children]
        dsts = [os.path.normpath(os.path.join(dst, child))
                for child in children]

        installed = 0
        for i in range(len(children)):
            src = srcs[i]
            dst = dsts[i]

            if self.debug:
                self.log.dbg('symlink child {} to {}'.format(src, dst))

            if template and Templategen.is_template(src):
                if self.debug:
                    self.log.dbg('dotfile is a template')
                    self.log.dbg('install to {} and symlink'
                                 .format(self.workdir))
                tmp = self._pivot_path(dst, self.workdir, striphome=True)
                r, e = self.install(templater, src, tmp, actionexec=actionexec,
                                    template=template)
                if not r and e and not os.path.exists(tmp):
                    continue
                src = tmp

            ret, err = self._link(src, dst, actionexec=actionexec)
            if ret:
                installed += 1
                # void actionexec if dotfile installed
                # to prevent from running actions multiple times
                actionexec = None
            else:
                if err:
                    return self._log_install(ret, err)

        return self._log_install(installed > 0, None)

    def _link(self, src, dst, actionexec=None):
        """
        set src as a link target of dst

        return
        - True, None: success
        - False, error_msg: error
        - False, None, ignored
        """
        overwrite = not self.safe
        if os.path.lexists(dst):
            if os.path.realpath(dst) == os.path.realpath(src):
                msg = 'ignoring "{}", link already exists'.format(dst)
                if self.debug:
                    self.log.dbg(msg)
                return False, None
            if self.dry:
                self.log.dry('would remove {} and link to {}'.format(dst, src))
                return True, None
            if self.showdiff:
                self._diff_before_write(src, dst, quiet=False)
            msg = 'Remove "{}" for link creation?'.format(dst)
            if self.safe and not self.log.ask(msg):
                err = 'ignoring "{}", link was not created'.format(dst)
                return False, err
            overwrite = True
            try:
                utils.removepath(dst)
            except OSError as e:
                err = 'something went wrong with {}: {}'.format(src, e)
                return False, err
        if self.dry:
            self.log.dry('would link {} to {}'.format(dst, src))
            return True, None
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            err = 'error creating directory for {}'.format(dst)
            return False, err
        r, e = self._exec_pre_actions(actionexec)
        if not r:
            return False, e
        # re-check in case action created the file
        if os.path.lexists(dst):
            msg = 'Remove "{}" for link creation?'.format(dst)
            if self.safe and not overwrite and not self.log.ask(msg):
                err = 'ignoring "{}", link was not created'.format(dst)
                return False, err
            try:
                utils.removepath(dst)
            except OSError as e:
                err = 'something went wrong with {}: {}'.format(src, e)
                return False, err
        os.symlink(src, dst)
        self.log.sub('linked {} to {}'.format(dst, src))
        return True, None

    def _get_tmp_file_vars(self, src, dst):
        tmp = {}
        tmp['_dotfile_sub_abs_src'] = src
        tmp['_dotfile_sub_abs_dst'] = dst
        return tmp

    def _install_file(self, templater, src, dst,
                      actionexec=None, noempty=False,
                      ignore=[], template=True):
        """install src to dst when is a file"""
        if self.debug:
            self.log.dbg('deploy file: {}'.format(src))
            self.log.dbg('ignore empty: {}'.format(noempty))
            self.log.dbg('ignore pattern: {}'.format(ignore))
            self.log.dbg('template: {}'.format(template))
            self.log.dbg('no empty: {}'.format(noempty))

        if utils.must_ignore([src, dst], ignore, debug=self.debug):
            if self.debug:
                self.log.dbg('ignoring install of {} to {}'.format(src, dst))
            return False, None

        if utils.samefile(src, dst):
            # symlink loop
            err = 'dotfile points to itself: {}'.format(dst)
            return False, err

        if not os.path.exists(src):
            err = 'source dotfile does not exist: {}'.format(src)
            return False, err

        # handle the file
        content = None
        if template:
            # template the file
            saved = templater.add_tmp_vars(self._get_tmp_file_vars(src, dst))
            try:
                content = templater.generate(src)
            except UndefinedException as e:
                return False, str(e)
            finally:
                templater.restore_vars(saved)
            if noempty and utils.content_empty(content):
                if self.debug:
                    self.log.dbg('ignoring empty template: {}'.format(src))
                return False, None
            if content is None:
                err = 'empty template {}'.format(src)
                return False, err
        ret, err = self._write(src, dst,
                               content=content,
                               actionexec=actionexec,
                               template=template)

        # build return values
        if ret < 0:
            # error
            return False, err
        if ret > 0:
            # already exists
            if self.debug:
                self.log.dbg('ignoring {}'.format(dst))
            return False, None
        if ret == 0:
            # success
            if not self.dry and not self.comparing:
                self.log.sub('copied {} to {}'.format(src, dst))
            return True, None
        # error
        err = 'installing {} to {}'.format(src, dst)
        return False, err

    def _install_dir(self, templater, src, dst,
                     actionexec=None, noempty=False,
                     ignore=[], template=True):
        """install src to dst when is a directory"""
        if self.debug:
            self.log.dbg('install dir {}'.format(src))
            self.log.dbg('ignore empty: {}'.format(noempty))
        # default to nothing installed and no error
        ret = False, None
        if not self._create_dirs(dst):
            err = 'creating directory for {}'.format(dst)
            return False, err
        # handle all files in dir
        for entry in os.listdir(src):
            f = os.path.join(src, entry)
            if not os.path.isdir(f):
                # is file
                res, err = self._install_file(templater, f,
                                              os.path.join(dst, entry),
                                              actionexec=actionexec,
                                              noempty=noempty,
                                              ignore=ignore,
                                              template=template)
                if not res and err:
                    # error occured
                    ret = res, err
                    break
                elif res:
                    # something got installed
                    ret = True, None
            else:
                # is directory
                res, err = self._install_dir(templater, f,
                                             os.path.join(dst, entry),
                                             actionexec=actionexec,
                                             noempty=noempty,
                                             ignore=ignore,
                                             template=template)
                if not res and err:
                    # error occured
                    ret = res, err
                    break
                elif res:
                    # something got installed
                    ret = True, None
        return ret

    def _fake_diff(self, dst, content):
        """
        fake diff by comparing file content with content
        returns True if same
        """
        cur = ''
        with open(dst, 'br') as f:
            cur = f.read()
        return cur == content

    def _write(self, src, dst, content=None,
               actionexec=None, template=True):
        """
        copy dotfile / write content to file
        return  0, None:  for success,
                1, None:  when already exists
               -1, err: when error
        content is always empty if template is False
        and is to be ignored
        """
        overwrite = not self.safe
        if self.dry:
            self.log.dry('would install {}'.format(dst))
            return 0, None
        if os.path.lexists(dst):
            rights = os.stat(src).st_mode
            samerights = False
            try:
                samerights = os.stat(dst).st_mode == rights
            except OSError as e:
                if e.errno == errno.ENOENT:
                    # broken symlink
                    err = 'broken symlink {}'.format(dst)
                    return -1, err
            diff = None
            if self.diff:
                diff = self._diff_before_write(src, dst,
                                               content=content,
                                               quiet=True)
                if not diff and samerights:
                    if self.debug:
                        self.log.dbg('{} is the same'.format(dst))
                    return 1, None
            if self.safe:
                if self.debug:
                    self.log.dbg('change detected for {}'.format(dst))
                if self.showdiff:
                    if diff is None:
                        # get diff
                        diff = self._diff_before_write(src, dst,
                                                       content=content,
                                                       quiet=True)
                        if diff:
                            self._print_diff(src, dst, diff)
                if not self.log.ask('Overwrite \"{}\"'.format(dst)):
                    self.log.warn('ignoring {}'.format(dst))
                    return 1, None
                overwrite = True
        if self.backup and os.path.lexists(dst):
            self._backup(dst)
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            err = 'creating directory for {}'.format(dst)
            return -1, err
        r, e = self._exec_pre_actions(actionexec)
        if not r:
            return -1, e
        if self.debug:
            self.log.dbg('install dotfile to \"{}\"'.format(dst))
        # re-check in case action created the file
        if self.safe and not overwrite and os.path.lexists(dst):
            if not self.log.ask('Overwrite \"{}\"'.format(dst)):
                self.log.warn('ignoring {}'.format(dst))
                return 1, None

        if template:
            # write content the file
            try:
                with open(dst, 'wb') as f:
                    f.write(content)
                shutil.copymode(src, dst)
            except NotADirectoryError as e:
                err = 'opening dest file: {}'.format(e)
                return -1, err
            except Exception as e:
                return -1, str(e)
        else:
            # copy file
            try:
                shutil.copyfile(src, dst)
                shutil.copymode(src, dst)
            except Exception as e:
                return -1, str(e)
        return 0, None

    def _diff_before_write(self, src, dst, content=None, quiet=False):
        """
        diff before writing
        using a temp file if content is not None
        returns diff string ('' if same)
        """
        tmp = None
        if content:
            tmp = utils.write_to_tmpfile(content)
            src = tmp
        diff = utils.diff(modified=src, original=dst, raw=False,
                          diff_cmd=self.diff_cmd)
        if tmp:
            utils.removepath(tmp, logger=self.log)

        if not quiet and diff:
            self._print_diff(src, dst, diff)
        return diff

    def _print_diff(self, src, dst, diff):
        """show diff to user"""
        self.log.log('diff \"{}\" VS \"{}\"'.format(dst, src))
        self.log.emph(diff)

    def _create_dirs(self, directory):
        """mkdir -p <directory>"""
        if not self.create and not os.path.exists(directory):
            if self.debug:
                self.log.dbg('no mkdir as \"create\" set to false in config')
            return False
        if os.path.exists(directory):
            return True
        if self.dry:
            self.log.dry('would mkdir -p {}'.format(directory))
            return True
        if self.debug:
            self.log.dbg('mkdir -p {}'.format(directory))
        os.makedirs(directory)
        return os.path.exists(directory)

    def _backup(self, path):
        """backup file pointed by path"""
        if self.dry:
            return
        dst = path.rstrip(os.sep) + self.backup_suffix
        self.log.log('backup {} to {}'.format(path, dst))
        os.rename(path, dst)

    def _pivot_path(self, path, newdir, striphome=False):
        """change path to be under newdir"""
        if self.debug:
            self.log.dbg('pivot new dir: \"{}\"'.format(newdir))
            self.log.dbg('strip home: {}'.format(striphome))
        if striphome:
            path = utils.strip_home(path)
        sub = path.lstrip(os.sep)
        new = os.path.join(newdir, sub)
        if self.debug:
            self.log.dbg('pivot \"{}\" to \"{}\"'.format(path, new))
        return new

    def _exec_pre_actions(self, actionexec):
        """execute action executor"""
        if self.action_executed:
            return True, None
        if not actionexec:
            return True, None
        ret, err = actionexec()
        self.action_executed = True
        return ret, err

    def _install_to_temp(self, templater, src, dst, tmpdir, template=True):
        """install a dotfile to a tempdir"""
        tmpdst = self._pivot_path(dst, tmpdir)
        r = self.install(templater, src, tmpdst, template=template)
        return r, tmpdst

    def install_to_temp(self, templater, tmpdir, src, dst, template=True):
        """install a dotfile to a tempdir"""
        ret = False
        tmpdst = ''
        # save some flags while comparing
        self.comparing = True
        drysaved = self.dry
        self.dry = False
        diffsaved = self.diff
        self.diff = False
        createsaved = self.create
        self.create = True
        # normalize src and dst
        src = os.path.expanduser(src)
        dst = os.path.expanduser(dst)
        if self.debug:
            self.log.dbg('tmp install {} (defined dst: {})'.format(src, dst))
        # install the dotfile to a temp directory for comparing
        r, tmpdst = self._install_to_temp(templater, src, dst, tmpdir,
                                          template=template)
        ret, err = r
        if self.debug:
            self.log.dbg('tmp installed in {}'.format(tmpdst))
        # reset flags
        self.dry = drysaved
        self.diff = diffsaved
        self.comparing = False
        self.create = createsaved
        return ret, err, tmpdst
Ejemplo n.º 5
0
class Installer:

    BACKUP_SUFFIX = '.dotdropbak'

    def __init__(self,
                 base='.',
                 create=True,
                 backup=True,
                 dry=False,
                 safe=False,
                 quiet=False,
                 diff=True):
        self.create = create
        self.backup = backup
        self.dry = dry
        self.safe = safe
        self.base = base
        self.quiet = quiet
        self.diff = diff
        self.comparing = False
        self.log = Logger()

    def install(self, templater, profile, src, dst):
        '''Install the dotfile for profile "profile"'''
        src = os.path.join(self.base, os.path.expanduser(src))
        dst = os.path.join(self.base, os.path.expanduser(dst))
        if os.path.isdir(src):
            return self._handle_dir(templater, profile, src, dst)
        return self._handle_file(templater, profile, src, dst)

    def link(self, src, dst):
        '''Sets src as the link target of dst'''
        src = os.path.join(self.base, os.path.expanduser(src))
        dst = os.path.join(self.base, os.path.expanduser(dst))
        if os.path.exists(dst):
            if os.path.realpath(dst) == os.path.realpath(src):
                self.log.sub('ignoring "%s", link exists' % dst)
                return []
            if self.dry:
                self.log.dry('would remove %s and link it to %s' % (dst, src))
                return []
            if self.safe and \
                    not self.log.ask('Remove "%s" for link creation?' % dst):
                self.log.warn('ignoring "%s", link was not created' % dst)
                return []
            try:
                utils.remove(dst)
            except OSError:
                self.log.err('something went wrong with %s' % src)
                return []
        if self.dry:
            self.log.dry('would link %s to %s' % (dst, src))
            return []
        os.symlink(src, dst)
        self.log.sub('linked %s to %s' % (dst, src))
        # Follows original developer's behavior
        return [(src, dst)]

    def _handle_file(self, templater, profile, src, dst):
        '''Install a file using templater for "profile"'''
        content = templater.generate(src, profile)
        if content is None:
            self.log.err('generate from template \"%s\"' % (src))
            return []
        if not os.path.exists(src):
            self.log.err('source dotfile does not exist: \"%s\"' % (src))
            return []
        st = os.stat(src)
        ret = self._write(dst, content, st.st_mode)
        if ret < 0:
            self.log.err('installing \"%s\" to \"%s\"' % (src, dst))
            return []
        if ret > 0:
            if not self.quiet:
                self.log.sub('ignoring \"%s\", same content' % (dst))
            return []
        if ret == 0:
            if not self.dry and not self.comparing:
                self.log.sub('copied \"%s\" to \"%s\"' % (src, dst))
            return [(src, dst)]
        return []

    def _handle_dir(self, templater, profile, src, dst):
        '''Install a folder using templater for "profile"'''
        ret = []
        for entry in os.listdir(src):
            f = os.path.join(src, entry)
            if not os.path.isdir(f):
                res = self._handle_file(templater, profile, f,
                                        os.path.join(dst, entry))
                ret.extend(res)
            else:
                res = self._handle_dir(templater, profile, f,
                                       os.path.join(dst, entry))
                ret.extend(res)
        return ret

    def _fake_diff(self, dst, content):
        '''Fake diff by comparing file content with "content"'''
        cur = ''
        with open(dst, 'br') as f:
            cur = f.read()
        return cur == content

    def _write(self, dst, content, rights):
        '''Write file'''
        if self.dry:
            self.log.dry('would install %s' % (dst))
            return 0
        if os.path.exists(dst):
            if self.diff and self._fake_diff(dst, content):
                return 1
            if self.safe and not self.log.ask('Overwrite \"%s\"' % (dst)):
                self.log.warn('ignoring \"%s\", already present' % (dst))
                return 1
        if self.backup and os.path.exists(dst):
            self._backup(dst)
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            self.log.err('creating directory for \"%s\"' % (dst))
            return -1
        with open(dst, 'wb') as f:
            f.write(content)
        os.chmod(dst, rights)
        return 0

    def _create_dirs(self, folder):
        '''mkdir -p "folder"'''
        if not self.create and not os.path.exists(folder):
            return False
        if os.path.exists(folder):
            return True
        os.makedirs(folder)
        return os.path.exists(folder)

    def _backup(self, path):
        '''Backup the file'''
        if self.dry:
            return
        dst = path.rstrip(os.sep) + self.BACKUP_SUFFIX
        self.log.log('backup %s to %s' % (path, dst))
        os.rename(path, dst)

    def _install_to_temp(self, templater, profile, src, dst, tmpfolder):
        '''Install a dotfile to a tempfolder for comparing'''
        sub = dst
        if dst[0] == os.sep:
            sub = dst[1:]
        tmpdst = os.path.join(tmpfolder, sub)
        return self.install(templater, profile, src, tmpdst), tmpdst

    def compare(self, templater, tmpfolder, profile, src, dst, opts=''):
        '''Compare temporary generated dotfile with local one'''
        self.comparing = True
        retval = False, ''
        drysaved = self.dry
        self.dry = False
        diffsaved = self.diff
        self.diff = False
        createsaved = self.create
        self.create = True
        src = os.path.expanduser(src)
        dst = os.path.expanduser(dst)
        if not os.path.exists(dst):
            retval = False, '\"%s\" does not exist on local\n' % (dst)
        else:
            ret, tmpdst = self._install_to_temp(templater, profile, src, dst,
                                                tmpfolder)
            if ret:
                diff = utils.diff(tmpdst, dst, log=False, raw=False, opts=opts)
                if diff == '':
                    retval = True, ''
                else:
                    retval = False, diff
        self.dry = drysaved
        self.diff = diffsaved
        self.comparing = False
        self.create = createsaved
        return retval
Ejemplo n.º 6
0
class Installer:

    BACKUP_SUFFIX = '.dotdropbak'

    def __init__(self,
                 base='.',
                 create=True,
                 backup=True,
                 dry=False,
                 safe=False,
                 debug=False,
                 diff=True):
        self.create = create
        self.backup = backup
        self.dry = dry
        self.safe = safe
        self.base = base
        self.debug = debug
        self.diff = diff
        self.comparing = False
        self.log = Logger(debug=self.debug)

    def install(self, templater, profile, src, dst):
        """install the src to dst using a template"""
        src = os.path.join(self.base, os.path.expanduser(src))
        dst = os.path.join(self.base, os.path.expanduser(dst))
        self.log.dbg('install {} to {}'.format(src, dst))
        if os.path.isdir(src):
            return self._handle_dir(templater, profile, src, dst)
        return self._handle_file(templater, profile, src, dst)

    def link(self, src, dst):
        """set src as the link target of dst"""
        src = os.path.join(self.base, os.path.expanduser(src))
        dst = os.path.join(self.base, os.path.expanduser(dst))
        if os.path.exists(dst):
            if os.path.realpath(dst) == os.path.realpath(src):
                self.log.sub('ignoring "{}", link exists'.format(dst))
                return []
            if self.dry:
                self.log.dry('would remove {} and link to {}'.format(dst, src))
                return []
            msg = 'Remove "{}" for link creation?'.format(dst)
            if self.safe and not self.log.ask(msg):
                msg = 'ignoring "{}", link was not created'
                self.log.warn(msg.format(dst))
                return []
            try:
                utils.remove(dst)
            except OSError:
                self.log.err('something went wrong with {}'.format(src))
                return []
        if self.dry:
            self.log.dry('would link {} to {}'.format(dst, src))
            return []
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            self.log.err('creating directory for \"{}\"'.format(dst))
            return []
        os.symlink(src, dst)
        self.log.sub('linked {} to {}'.format(dst, src))
        return [(src, dst)]

    def _handle_file(self, templater, profile, src, dst):
        """install src to dst when is a file"""
        self.log.dbg('generate template for {}'.format(src))
        content = templater.generate(src, profile)
        if content is None:
            self.log.err('generate from template \"{}\"'.format(src))
            return []
        if not os.path.exists(src):
            self.log.err('source dotfile does not exist: \"{}\"'.format(src))
            return []
        st = os.stat(src)
        ret = self._write(dst, content, st.st_mode)
        if ret < 0:
            self.log.err('installing \"{}\" to \"{}\"'.format(src, dst))
            return []
        if ret > 0:
            self.log.dbg('ignoring \"{}\", same content'.format(dst))
            return []
        if ret == 0:
            if not self.dry and not self.comparing:
                self.log.sub('copied \"{}\" to \"{}\"'.format(src, dst))
            return [(src, dst)]
        return []

    def _handle_dir(self, templater, profile, src, dst):
        """install src to dst when is a directory"""
        ret = []
        for entry in os.listdir(src):
            f = os.path.join(src, entry)
            if not os.path.isdir(f):
                res = self._handle_file(templater, profile, f,
                                        os.path.join(dst, entry))
                ret.extend(res)
            else:
                res = self._handle_dir(templater, profile, f,
                                       os.path.join(dst, entry))
                ret.extend(res)
        return ret

    def _fake_diff(self, dst, content):
        """fake diff by comparing file content with content"""
        cur = ''
        with open(dst, 'br') as f:
            cur = f.read()
        return cur == content

    def _write(self, dst, content, rights):
        """write content to file
        return  0 for success,
                1 when already exists
               -1 when error"""
        if self.dry:
            self.log.dry('would install {}'.format(dst))
            return 0
        if os.path.exists(dst):
            samerights = os.stat(dst).st_mode == rights
            if self.diff and self._fake_diff(dst, content) and samerights:
                self.log.dbg('{} is the same'.format(dst))
                return 1
            if self.safe and not self.log.ask('Overwrite \"{}\"'.format(dst)):
                self.log.warn('ignoring \"{}\", already present'.format(dst))
                return 1
        if self.backup and os.path.exists(dst):
            self._backup(dst)
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            self.log.err('creating directory for \"{}\"'.format(dst))
            return -1
        self.log.dbg('write content to {}'.format(dst))
        try:
            with open(dst, 'wb') as f:
                f.write(content)
        except NotADirectoryError as e:
            self.log.err('opening dest file: {}'.format(e))
            return -1
        os.chmod(dst, rights)
        return 0

    def _create_dirs(self, directory):
        """mkdir -p <directory>"""
        if not self.create and not os.path.exists(directory):
            return False
        if os.path.exists(directory):
            return True
        self.log.dbg('mkdir -p {}'.format(directory))
        os.makedirs(directory)
        return os.path.exists(directory)

    def _backup(self, path):
        """backup file pointed by path"""
        if self.dry:
            return
        dst = path.rstrip(os.sep) + self.BACKUP_SUFFIX
        self.log.log('backup {} to {}'.format(path, dst))
        os.rename(path, dst)

    def _install_to_temp(self, templater, profile, src, dst, tmpdir):
        """install a dotfile to a tempdir for comparing"""
        sub = dst
        if dst[0] == os.sep:
            sub = dst[1:]
        tmpdst = os.path.join(tmpdir, sub)
        return self.install(templater, profile, src, tmpdst), tmpdst

    def compare(self, templater, tmpdir, profile, src, dst, opts=''):
        """compare a temporary generated dotfile with the local one"""
        self.comparing = True
        retval = False, ''
        drysaved = self.dry
        self.dry = False
        diffsaved = self.diff
        self.diff = False
        createsaved = self.create
        self.create = True
        src = os.path.expanduser(src)
        dst = os.path.expanduser(dst)
        self.log.dbg('comparing {} and {}'.format(src, dst))
        if not os.path.exists(dst):
            retval = False, '\"{}\" does not exist on local\n'.format(dst)
        else:
            ret, tmpdst = self._install_to_temp(templater, profile, src, dst,
                                                tmpdir)
            if ret:
                self.log.dbg('diffing {} and {}'.format(tmpdst, dst))
                diff = utils.diff(tmpdst, dst, raw=False, opts=opts)
                if diff == '':
                    retval = True, ''
                else:
                    retval = False, diff
        self.dry = drysaved
        self.diff = diffsaved
        self.comparing = False
        self.create = createsaved
        return retval
Ejemplo n.º 7
0
class Installer:

    BACKUP_SUFFIX = '.dotdropbak'

    def __init__(self, base='.', create=True, backup=True,
                 dry=False, safe=False, quiet=False, diff=True):
        self.create = create
        self.backup = backup
        self.dry = dry
        self.safe = safe
        self.base = base
        self.quiet = quiet
        self.diff = diff
        self.comparing = False
        self.log = Logger()

    def install(self, templater, profile, src, dst):
        '''Install the dotfile for profile "profile"'''
        src = os.path.join(self.base, os.path.expanduser(src))
        dst = os.path.join(self.base, os.path.expanduser(dst))
        if os.path.isdir(src):
            return self._handle_dir(templater, profile, src, dst)
        return self._handle_file(templater, profile, src, dst)

    def link(self, src, dst):
        '''Sets src as the link target of dst'''
        src = os.path.join(self.base, os.path.expanduser(src))
        dst = os.path.join(self.base, os.path.expanduser(dst))
        if os.path.exists(dst):
            if os.path.realpath(dst) == os.path.realpath(src):
                self.log.sub('ignoring "%s", link exists' % dst)
                return []
            if self.dry:
                self.log.dry('would remove %s and link it to %s'
                             % (dst, src))
                return []
            if self.safe and \
                    not self.log.ask('Remove "%s" for link creation?' % dst):
                self.log.warn('ignoring "%s", link was not created' % dst)
                return []
            try:
                utils.remove(dst)
            except OSError:
                self.log.err('something went wrong with %s' % src)
                return []
        if self.dry:
            self.log.dry('would link %s to %s' % (dst, src))
            return []
        os.symlink(src, dst)
        self.log.sub('linked %s to %s' % (dst, src))
        # Follows original developer's behavior
        return [(src, dst)]

    def _handle_file(self, templater, profile, src, dst):
        '''Install a file using templater for "profile"'''
        content = templater.generate(src, profile)
        if content is None:
            self.log.err('generate from template \"%s\"' % (src))
            return []
        if not os.path.exists(src):
            self.log.err('installing %s to %s' % (src, dst))
            return []
        st = os.stat(src)
        ret = self._write(dst, content, st.st_mode)
        if ret < 0:
            self.log.err('installing %s to %s' % (src, dst))
            return []
        if ret > 0:
            if not self.quiet:
                self.log.sub('ignoring \"%s\", same content' % (dst))
            return []
        if ret == 0:
            if not self.dry and not self.comparing:
                self.log.sub('copied %s to %s' % (src, dst))
            return [(src, dst)]
        return []

    def _handle_dir(self, templater, profile, src, dst):
        '''Install a folder using templater for "profile"'''
        ret = []
        for entry in os.listdir(src):
            f = os.path.join(src, entry)
            if not os.path.isdir(f):
                res = self._handle_file(
                    templater, profile, f, os.path.join(dst, entry))
                ret.extend(res)
            else:
                res = self._handle_dir(
                    templater, profile, f, os.path.join(dst, entry))
                ret.extend(res)
        return ret

    def _fake_diff(self, dst, content):
        '''Fake diff by comparing file content with "content"'''
        cur = ''
        with open(dst, 'br') as f:
            cur = f.read()
        return cur == content

    def _write(self, dst, content, rights):
        '''Write file'''
        if self.dry:
            self.log.dry('would install %s' % (dst))
            return 0
        if os.path.exists(dst):
            if self.diff and self._fake_diff(dst, content):
                return 1
            if self.safe and not self.log.ask('Overwrite \"%s\"' % (dst)):
                self.log.warn('ignoring \"%s\", already present' % (dst))
                return 1
        if self.backup and os.path.exists(dst):
            self._backup(dst)
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            self.log.err('creating directory for %s' % (dst))
            return -1
        with open(dst, 'wb') as f:
            f.write(content)
        os.chmod(dst, rights)
        return 0

    def _create_dirs(self, folder):
        '''mkdir -p "folder"'''
        if not self.create and not os.path.exists(folder):
            return False
        if os.path.exists(folder):
            return True
        os.makedirs(folder)
        return os.path.exists(folder)

    def _backup(self, path):
        '''Backup the file'''
        if self.dry:
            return
        dst = path.rstrip(os.sep) + self.BACKUP_SUFFIX
        self.log.log('backup %s to %s' % (path, dst))
        os.rename(path, dst)

    def _install_to_temp(self, templater, profile, src, dst, tmpfolder):
        '''Install a dotfile to a tempfolder for comparing'''
        sub = dst
        if dst[0] == os.sep:
            sub = dst[1:]
        tmpdst = os.path.join(tmpfolder, sub)
        return self.install(templater, profile, src, tmpdst), tmpdst

    def compare(self, templater, tmpfolder, profile, src, dst, opts=''):
        '''Compare temporary generated dotfile with local one'''
        self.comparing = True
        retval = False, ''
        drysaved = self.dry
        self.dry = False
        diffsaved = self.diff
        self.diff = False
        src = os.path.expanduser(src)
        dst = os.path.expanduser(dst)
        if not os.path.exists(dst):
            retval = False, '\"%s\" does not exist on local\n' % (dst)
        else:
            ret, tmpdst = self._install_to_temp(templater,
                                                profile,
                                                src, dst,
                                                tmpfolder)
            if ret:
                diff = utils.diff(tmpdst, dst, log=False,
                                  raw=False, opts=opts)
                if diff == '':
                    retval = True, ''
                else:
                    retval = False, diff
        self.dry = drysaved
        self.diff = diffsaved
        self.comparing = False
        return retval
Ejemplo n.º 8
0
class Installer:
    def __init__(self,
                 base='.',
                 create=True,
                 backup=True,
                 dry=False,
                 safe=False,
                 workdir='~/.config/dotdrop',
                 debug=False,
                 diff=True,
                 totemp=None,
                 showdiff=False,
                 backup_suffix='.dotdropbak',
                 diff_cmd=''):
        """
        @base: directory path where to search for templates
        @create: create directory hierarchy if missing when installing
        @backup: backup existing dotfile when installing
        @dry: just simulate
        @safe: ask for any overwrite
        @workdir: where to install template before symlinking
        @debug: enable debug
        @diff: diff when installing if True
        @totemp: deploy to this path instead of dotfile dst if not None
        @showdiff: show the diff before overwriting (or asking for)
        @backup_suffix: suffix for dotfile backup file
        @diff_cmd: diff command to use
        """
        self.create = create
        self.backup = backup
        self.dry = dry
        self.safe = safe
        workdir = os.path.expanduser(workdir)
        workdir = os.path.normpath(workdir)
        self.workdir = workdir
        base = os.path.expanduser(base)
        base = os.path.normpath(base)
        self.base = base
        self.debug = debug
        self.diff = diff
        self.totemp = totemp
        self.showdiff = showdiff
        self.backup_suffix = backup_suffix
        self.diff_cmd = diff_cmd
        self.action_executed = False
        # avoids printing file copied logs
        # when using install_to_tmp for comparing
        self.comparing = False

        self.log = Logger()

    ########################################################
    # public methods
    ########################################################

    def install(self,
                templater,
                src,
                dst,
                linktype,
                actionexec=None,
                noempty=False,
                ignore=[],
                is_template=True,
                chmod=None,
                force_chmod=False):
        """
        install src to dst

        @templater: the templater object
        @src: dotfile source path in dotpath
        @dst: dotfile destination path in the FS
        @linktype: linktypes.LinkTypes
        @actionexec: action executor callback
        @noempty: render empty template flag
        @ignore: pattern to ignore when installing
        @is_template: this dotfile is a template
        @chmod: rights to apply if any
        @force_chmod: do not ask user to chmod

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        """
        if not src or not dst:
            # fake dotfile
            if self.debug:
                self.log.dbg('fake dotfile installed')
            self._exec_pre_actions(actionexec)
            return True, None
        if self.debug:
            msg = 'installing \"{}\" to \"{}\" (link: {})'
            self.log.dbg(msg.format(src, dst, str(linktype)))
        src, dst, cont, err = self._check_paths(src, dst, chmod)
        if not cont:
            return self._log_install(cont, err)

        # check source file exists
        src = os.path.join(self.base, src)
        if not os.path.exists(src):
            err = 'source dotfile does not exist: {}'.format(src)
            return self._log_install(False, err)

        self.action_executed = False

        # install to temporary dir
        # and ignore any actions
        if self.totemp:
            r, err, _ = self.install_to_temp(templater,
                                             self.totemp,
                                             src,
                                             dst,
                                             is_template=is_template,
                                             chmod=chmod,
                                             ignore=ignore)
            return self._log_install(r, err)

        isdir = os.path.isdir(src)
        if self.debug:
            self.log.dbg('install {} to {}'.format(src, dst))
            self.log.dbg('\"{}\" is a directory: {}'.format(src, isdir))

        if linktype == LinkTypes.NOLINK:
            # normal file
            if isdir:
                r, err = self._copy_dir(templater,
                                        src,
                                        dst,
                                        actionexec=actionexec,
                                        noempty=noempty,
                                        ignore=ignore,
                                        is_template=is_template,
                                        chmod=chmod)
            else:
                r, err = self._copy_file(templater,
                                         src,
                                         dst,
                                         actionexec=actionexec,
                                         noempty=noempty,
                                         ignore=ignore,
                                         is_template=is_template,
                                         chmod=chmod)
        elif linktype == LinkTypes.LINK:
            # symlink
            r, err = self._link(templater,
                                src,
                                dst,
                                actionexec=actionexec,
                                is_template=is_template)
        elif linktype == LinkTypes.LINK_CHILDREN:
            # symlink direct children
            if not isdir:
                if self.debug:
                    msg = 'symlink children of {} to {}'
                    self.log.dbg(msg.format(src, dst))
                err = 'source dotfile is not a directory: {}'.format(src)
                r = False
            else:
                r, err = self._link_children(templater,
                                             src,
                                             dst,
                                             actionexec=actionexec,
                                             is_template=is_template,
                                             ignore=ignore)

        if self.debug:
            self.log.dbg('before chmod: {} err:{}'.format(r, err))

        if self.dry:
            return self._log_install(r, err)

        # handle chmod
        # - on success (r, not err)
        # - no change (not r, not err)
        # but not when
        # - error (not r, err)
        # - aborted (not r, err)
        if (r or (not r and not err)):
            if not chmod:
                chmod = utils.get_file_perm(src)
            dstperms = utils.get_file_perm(dst)
            if dstperms != chmod:
                # apply mode
                msg = 'chmod {} to {:o}'.format(dst, chmod)
                if not force_chmod and self.safe and not self.log.ask(msg):
                    r = False
                    err = 'aborted'
                else:
                    if not self.comparing:
                        self.log.sub('chmod {} to {:o}'.format(dst, chmod))
                    if utils.chmod(dst, chmod, debug=self.debug):
                        r = True
                    else:
                        r = False
                        err = 'chmod failed'

        return self._log_install(r, err)

    def install_to_temp(self,
                        templater,
                        tmpdir,
                        src,
                        dst,
                        is_template=True,
                        chmod=None,
                        ignore=[]):
        """
        install a dotfile to a tempdir

        @templater: the templater object
        @tmpdir: where to install
        @src: dotfile source path in dotpath
        @dst: dotfile destination path in the FS
        @is_template: this dotfile is a template
        @chmod: rights to apply if any
        @ignore: patterns to ignore

        return
        - success, error-if-any, dotfile-installed-path
        """
        if self.debug:
            self.log.dbg('tmp install {} (defined dst: {})'.format(src, dst))
        src, dst, cont, err = self._check_paths(src, dst, chmod)
        if not cont:
            return self._log_install(cont, err)

        ret = False
        tmpdst = ''

        # save flags
        self.comparing = True
        drysaved = self.dry
        self.dry = False
        diffsaved = self.diff
        self.diff = False
        createsaved = self.create
        self.create = True
        totemp = self.totemp
        self.totemp = None

        # install the dotfile to a temp directory
        tmpdst = self._pivot_path(dst, tmpdir)
        ret, err = self.install(templater,
                                src,
                                tmpdst,
                                LinkTypes.NOLINK,
                                is_template=is_template,
                                chmod=chmod,
                                ignore=ignore)
        if self.debug:
            if ret:
                self.log.dbg('tmp installed in {}'.format(tmpdst))

        # restore flags
        self.dry = drysaved
        self.diff = diffsaved
        self.create = createsaved
        self.comparing = False
        self.totemp = totemp

        return ret, err, tmpdst

    ########################################################
    # low level accessors for public methods
    ########################################################

    def _link(self, templater, src, dst, actionexec=None, is_template=True):
        """
        install link:link

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        - False, 'aborted'    : user aborted
        """
        if is_template:
            if self.debug:
                self.log.dbg('is a template')
                self.log.dbg('install to {}'.format(self.workdir))
            tmp = self._pivot_path(dst, self.workdir, striphome=True)
            r, err = self.install(templater,
                                  src,
                                  tmp,
                                  LinkTypes.NOLINK,
                                  actionexec=actionexec,
                                  is_template=is_template)
            if not r and not os.path.exists(tmp):
                return r, err
            src = tmp
        r, err = self._symlink(src, dst, actionexec=actionexec)
        return r, err

    def _link_children(self,
                       templater,
                       src,
                       dst,
                       actionexec=None,
                       is_template=True,
                       ignore=[]):
        """
        install link:link_children

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        - False, 'aborted'    : user aborted
        """
        parent = os.path.join(self.base, src)
        if not os.path.lexists(dst):
            if self.dry:
                self.log.dry('would create directory "{}"'.format(dst))
            else:
                if not self.comparing:
                    self.log.sub('creating directory "{}"'.format(dst))
                self._create_dirs(dst)

        if os.path.isfile(dst):
            msg = ''.join([
                'Remove regular file {} and ',
                'replace with empty directory?',
            ]).format(dst)

            if self.safe and not self.log.ask(msg):
                return False, 'aborted'
            os.unlink(dst)
            self._create_dirs(dst)

        children = os.listdir(parent)
        srcs = [
            os.path.normpath(os.path.join(parent, child)) for child in children
        ]
        dsts = [
            os.path.normpath(os.path.join(dst, child)) for child in children
        ]

        installed = 0
        for i in range(len(children)):
            subsrc = srcs[i]
            subdst = dsts[i]

            if utils.must_ignore([subsrc, subdst], ignore, debug=self.debug):
                if self.debug:
                    self.log.dbg(
                        'ignoring install of {} to {}'.format(src, dst), )
                continue

            if self.debug:
                self.log.dbg('symlink child {} to {}'.format(subsrc, subdst))

            if is_template:
                if self.debug:
                    self.log.dbg('child is a template')
                    self.log.dbg('install to {} and symlink'.format(
                        self.workdir))
                tmp = self._pivot_path(subdst, self.workdir, striphome=True)
                r, e = self.install(templater,
                                    subsrc,
                                    tmp,
                                    LinkTypes.NOLINK,
                                    actionexec=actionexec,
                                    is_template=is_template)
                if not r and e and not os.path.exists(tmp):
                    continue
                subsrc = tmp

            ret, err = self._symlink(subsrc, subdst, actionexec=actionexec)
            if ret:
                installed += 1
                # void actionexec if dotfile installed
                # to prevent from running actions multiple times
                actionexec = None
            else:
                if err:
                    return ret, err

        return installed > 0, None

    ########################################################
    # file operations
    ########################################################

    def _symlink(self, src, dst, actionexec=None):
        """
        set src as a link target of dst

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        - False, 'aborted'    : user aborted
        """
        overwrite = not self.safe
        if os.path.lexists(dst):
            if os.path.realpath(dst) == os.path.realpath(src):
                msg = 'ignoring "{}", link already exists'.format(dst)
                if self.debug:
                    self.log.dbg(msg)
                return False, None
            if self.dry:
                self.log.dry('would remove {} and link to {}'.format(dst, src))
                return True, None
            if self.showdiff:
                self._show_diff_before_write(src, dst)
            msg = 'Remove "{}" for link creation?'.format(dst)
            if self.safe and not self.log.ask(msg):
                return False, 'aborted'
            overwrite = True
            try:
                utils.removepath(dst)
            except OSError as e:
                err = 'something went wrong with {}: {}'.format(src, e)
                return False, err
        if self.dry:
            self.log.dry('would link {} to {}'.format(dst, src))
            return True, None
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            err = 'error creating directory for {}'.format(dst)
            return False, err
        r, e = self._exec_pre_actions(actionexec)
        if not r:
            return False, e
        # re-check in case action created the file
        if os.path.lexists(dst):
            msg = 'Remove "{}" for link creation?'.format(dst)
            if self.safe and not overwrite and not self.log.ask(msg):
                return False, 'aborted'
            try:
                utils.removepath(dst)
            except OSError as e:
                err = 'something went wrong with {}: {}'.format(src, e)
                return False, err
        os.symlink(src, dst)
        if not self.comparing:
            self.log.sub('linked {} to {}'.format(dst, src))
        return True, None

    def _copy_file(self,
                   templater,
                   src,
                   dst,
                   actionexec=None,
                   noempty=False,
                   ignore=[],
                   is_template=True,
                   chmod=None):
        """
        install src to dst when is a file

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        - False, 'aborted'    : user aborted
        """
        if self.debug:
            self.log.dbg('deploy file: {}'.format(src))
            self.log.dbg('ignore empty: {}'.format(noempty))
            self.log.dbg('ignore pattern: {}'.format(ignore))
            self.log.dbg('is_template: {}'.format(is_template))
            self.log.dbg('no empty: {}'.format(noempty))

        # check no loop
        if utils.samefile(src, dst):
            err = 'dotfile points to itself: {}'.format(dst)
            return False, err

        if utils.must_ignore([src, dst], ignore, debug=self.debug):
            if self.debug:
                self.log.dbg('ignoring install of {} to {}'.format(src, dst))
            return False, None

        if utils.samefile(src, dst):
            # loop
            err = 'dotfile points to itself: {}'.format(dst)
            return False, err

        if not os.path.exists(src):
            err = 'source dotfile does not exist: {}'.format(src)
            return False, err

        # handle the file
        content = None
        if is_template:
            # template the file
            saved = templater.add_tmp_vars(self._get_tmp_file_vars(src, dst))
            try:
                content = templater.generate(src)
            except UndefinedException as e:
                return False, str(e)
            finally:
                templater.restore_vars(saved)
            # test is empty
            if noempty and utils.content_empty(content):
                if self.debug:
                    self.log.dbg('ignoring empty template: {}'.format(src))
                return False, None
            if content is None:
                err = 'empty template {}'.format(src)
                return False, err

        # write the file
        ret, err = self._write(src,
                               dst,
                               content=content,
                               actionexec=actionexec,
                               chmod=chmod)
        if ret and not err:
            if not self.dry and not self.comparing:
                self.log.sub('install {} to {}'.format(src, dst))
        return ret, err

    def _copy_dir(self,
                  templater,
                  src,
                  dst,
                  actionexec=None,
                  noempty=False,
                  ignore=[],
                  is_template=True,
                  chmod=None):
        """
        install src to dst when is a directory

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        - False, 'aborted'    : user aborted
        """
        if self.debug:
            self.log.dbg('deploy dir {}'.format(src))
        # default to nothing installed and no error
        ret = False, None

        # create the directory anyway
        if not self._create_dirs(dst):
            err = 'creating directory for {}'.format(dst)
            return False, err

        # handle all files in dir
        for entry in os.listdir(src):
            f = os.path.join(src, entry)
            if self.debug:
                self.log.dbg('deploy sub from {}: {}'.format(dst, entry))
            if not os.path.isdir(f):
                # is file
                res, err = self._copy_file(templater,
                                           f,
                                           os.path.join(dst, entry),
                                           actionexec=actionexec,
                                           noempty=noempty,
                                           ignore=ignore,
                                           is_template=is_template,
                                           chmod=None)
                if not res and err:
                    # error occured
                    ret = res, err
                    break
                elif res:
                    # something got installed
                    ret = True, None
            else:
                # is directory
                res, err = self._copy_dir(templater,
                                          f,
                                          os.path.join(dst, entry),
                                          actionexec=actionexec,
                                          noempty=noempty,
                                          ignore=ignore,
                                          is_template=is_template,
                                          chmod=None)
                if not res and err:
                    # error occured
                    ret = res, err
                    break
                elif res:
                    # something got installed
                    ret = True, None
        return ret

    def _write(self, src, dst, content=None, actionexec=None, chmod=None):
        """
        copy dotfile / write content to file

        return
        - True, None        : success
        - False, error_msg  : error
        - False, None       : ignored
        - False, 'aborted'    : user aborted
        """
        overwrite = not self.safe
        if self.dry:
            self.log.dry('would install {}'.format(dst))
            return True, None

        if os.path.lexists(dst):
            try:
                os.stat(dst)
            except OSError as e:
                if e.errno == errno.ENOENT:
                    # broken symlink
                    err = 'broken symlink {}'.format(dst)
                    return False, err

            if self.diff:
                if not self._is_different(src, dst, content=content):
                    if self.debug:
                        self.log.dbg('{} is the same'.format(dst))
                    return False, None
            if self.safe:
                if self.debug:
                    self.log.dbg('change detected for {}'.format(dst))
                if self.showdiff:
                    # get diff
                    self._show_diff_before_write(src, dst, content=content)
                if not self.log.ask('Overwrite \"{}\"'.format(dst)):
                    return False, 'aborted'
                overwrite = True
        if self.backup and os.path.lexists(dst):
            self._backup(dst)
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            err = 'creating directory for {}'.format(dst)
            return False, err
        r, e = self._exec_pre_actions(actionexec)
        if not r:
            return False, e
        if self.debug:
            self.log.dbg('install file to \"{}\"'.format(dst))
        # re-check in case action created the file
        if self.safe and not overwrite and os.path.lexists(dst):
            if not self.log.ask('Overwrite \"{}\"'.format(dst)):
                self.log.warn('ignoring {}'.format(dst))
                return False, 'aborted'

        if content:
            # write content the file
            try:
                with open(dst, 'wb') as f:
                    f.write(content)
                shutil.copymode(src, dst)
            except NotADirectoryError as e:
                err = 'opening dest file: {}'.format(e)
                return False, err
            except Exception as e:
                return False, str(e)
        else:
            # copy file
            try:
                shutil.copyfile(src, dst)
                shutil.copymode(src, dst)
            except Exception as e:
                return False, str(e)
        return True, None

    ########################################################
    # helpers
    ########################################################

    def _get_tmp_file_vars(self, src, dst):
        tmp = {}
        tmp['_dotfile_sub_abs_src'] = src
        tmp['_dotfile_sub_abs_dst'] = dst
        return tmp

    def _is_different(self, src, dst, content=None):
        """
        returns True if file is different and
        needs to be installed
        """
        # check file content
        if content:
            tmp = utils.write_to_tmpfile(content)
            src = tmp
        r = utils.fastdiff(src, dst)
        if r:
            if self.debug:
                self.log.dbg('content differ')
        return r

    def _show_diff_before_write(self, src, dst, content=None):
        """
        diff before writing
        using a temp file if content is not None
        returns diff string ('' if same)
        """
        tmp = None
        if content:
            tmp = utils.write_to_tmpfile(content)
            src = tmp
        diff = utils.diff(modified=src, original=dst, diff_cmd=self.diff_cmd)
        if tmp:
            utils.removepath(tmp, logger=self.log)

        if diff:
            self._print_diff(src, dst, diff)
        return diff

    def _print_diff(self, src, dst, diff):
        """show diff to user"""
        self.log.log('diff \"{}\" VS \"{}\"'.format(dst, src))
        self.log.emph(diff)

    def _create_dirs(self, directory):
        """mkdir -p <directory>"""
        if not self.create and not os.path.exists(directory):
            if self.debug:
                self.log.dbg('no mkdir as \"create\" set to false in config')
            return False
        if os.path.exists(directory):
            return True
        if self.dry:
            self.log.dry('would mkdir -p {}'.format(directory))
            return True
        if self.debug:
            self.log.dbg('mkdir -p {}'.format(directory))
        if not self.comparing:
            self.log.sub('create directory {}'.format(directory))

        os.makedirs(directory, exist_ok=True)
        return os.path.exists(directory)

    def _backup(self, path):
        """backup file pointed by path"""
        if self.dry:
            return
        dst = path.rstrip(os.sep) + self.backup_suffix
        self.log.log('backup {} to {}'.format(path, dst))
        os.rename(path, dst)

    def _pivot_path(self, path, newdir, striphome=False):
        """change path to be under newdir"""
        if self.debug:
            self.log.dbg('pivot new dir: \"{}\"'.format(newdir))
            self.log.dbg('strip home: {}'.format(striphome))
        if striphome:
            path = utils.strip_home(path)
        sub = path.lstrip(os.sep)
        new = os.path.join(newdir, sub)
        if self.debug:
            self.log.dbg('pivot \"{}\" to \"{}\"'.format(path, new))
        return new

    def _exec_pre_actions(self, actionexec):
        """execute action executor"""
        if self.action_executed:
            return True, None
        if not actionexec:
            return True, None
        ret, err = actionexec()
        self.action_executed = True
        return ret, err

    def _log_install(self, boolean, err):
        """log installation process"""
        if not self.debug:
            return boolean, err
        if boolean:
            self.log.dbg('install: SUCCESS')
        else:
            if err:
                self.log.dbg('install: ERROR: {}'.format(err))
            else:
                self.log.dbg('install: IGNORED')
        return boolean, err

    def _check_paths(self, src, dst, chmod):
        """
        check and normalize param
        returns <src>, <dst>, <continue>, <error>
        """
        # check both path are valid
        if not dst or not src:
            err = 'empty dst or src for {}'.format(src)
            if self.debug:
                self.log.dbg(err)
            return None, None, False, err

        # normalize src and dst
        src = os.path.expanduser(src)
        src = os.path.normpath(src)

        dst = os.path.expanduser(dst)
        dst = os.path.normpath(dst)

        return src, dst, True, None
Ejemplo n.º 9
0
class Installer:

    BACKUP_SUFFIX = '.dotdropbak'

    def __init__(self,
                 base='.',
                 create=True,
                 backup=True,
                 dry=False,
                 safe=False,
                 workdir='~/.config/dotdrop',
                 debug=False,
                 diff=True,
                 totemp=None,
                 showdiff=False):
        self.create = create
        self.backup = backup
        self.dry = dry
        self.safe = safe
        self.workdir = os.path.expanduser(workdir)
        self.base = base
        self.debug = debug
        self.diff = diff
        self.totemp = totemp
        self.showdiff = showdiff
        self.comparing = False
        self.action_executed = False
        self.log = Logger()

    def install(self, templater, src, dst, actions=[], noempty=False):
        """install the src to dst using a template"""
        self.action_executed = False
        src = os.path.join(self.base, os.path.expanduser(src))
        if not os.path.exists(src):
            self.log.err('source dotfile does not exist: {}'.format(src))
            return []
        dst = os.path.expanduser(dst)
        if self.totemp:
            dst = self._pivot_path(dst, self.totemp)
        if utils.samefile(src, dst):
            # symlink loop
            self.log.err('dotfile points to itself: {}'.format(dst))
            return []
        if self.debug:
            self.log.dbg('install {} to {}'.format(src, dst))
        if os.path.isdir(src):
            return self._handle_dir(templater,
                                    src,
                                    dst,
                                    actions=actions,
                                    noempty=noempty)
        return self._handle_file(templater,
                                 src,
                                 dst,
                                 actions=actions,
                                 noempty=noempty)

    def link(self, templater, src, dst, actions=[]):
        """set src as the link target of dst"""
        self.action_executed = False
        src = os.path.join(self.base, os.path.expanduser(src))
        if not os.path.exists(src):
            self.log.err('source dotfile does not exist: {}'.format(src))
            return []
        dst = os.path.expanduser(dst)
        if self.totemp:
            # ignore actions
            return self.install(templater, src, dst, actions=[])

        if Templategen.is_template(src):
            if self.debug:
                self.log.dbg('dotfile is a template')
                self.log.dbg('install to {} and symlink'.format(self.workdir))
            tmp = self._pivot_path(dst, self.workdir, striphome=True)
            i = self.install(templater, src, tmp, actions=actions)
            if not i and not os.path.exists(tmp):
                return []
            src = tmp
        return self._link(src, dst, actions=actions)

    def _link(self, src, dst, actions=[]):
        """set src as a link target of dst"""
        if os.path.lexists(dst):
            if os.path.realpath(dst) == os.path.realpath(src):
                if self.debug:
                    self.log.dbg('ignoring "{}", link exists'.format(dst))
                return []
            if self.dry:
                self.log.dry('would remove {} and link to {}'.format(dst, src))
                return []
            msg = 'Remove "{}" for link creation?'.format(dst)
            if self.safe and not self.log.ask(msg):
                msg = 'ignoring "{}", link was not created'
                self.log.warn(msg.format(dst))
                return []
            try:
                utils.remove(dst)
            except OSError as e:
                self.log.err('something went wrong with {}: {}'.format(src, e))
                return []
        if self.dry:
            self.log.dry('would link {} to {}'.format(dst, src))
            return []
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            self.log.err('creating directory for {}'.format(dst))
            return []
        self._exec_pre_actions(actions)
        os.symlink(src, dst)
        self.log.sub('linked {} to {}'.format(dst, src))
        return [(src, dst)]

    def _handle_file(self, templater, src, dst, actions=[], noempty=False):
        """install src to dst when is a file"""
        if self.debug:
            self.log.dbg('generate template for {}'.format(src))
            self.log.dbg('ignore empty: {}'.format(noempty))
        if utils.samefile(src, dst):
            # symlink loop
            self.log.err('dotfile points to itself: {}'.format(dst))
            return []
        content = templater.generate(src)
        if noempty and utils.content_empty(content):
            self.log.warn('ignoring empty template: {}'.format(src))
            return []
        if content is None:
            self.log.err('generate from template {}'.format(src))
            return []
        if not os.path.exists(src):
            self.log.err('source dotfile does not exist: {}'.format(src))
            return []
        st = os.stat(src)
        ret = self._write(src, dst, content, st.st_mode, actions=actions)
        if ret < 0:
            self.log.err('installing {} to {}'.format(src, dst))
            return []
        if ret > 0:
            if self.debug:
                self.log.dbg('ignoring {}'.format(dst))
            return []
        if ret == 0:
            if not self.dry and not self.comparing:
                self.log.sub('copied {} to {}'.format(src, dst))
            return [(src, dst)]
        return []

    def _handle_dir(self, templater, src, dst, actions=[], noempty=False):
        """install src to dst when is a directory"""
        if self.debug:
            self.log.dbg('install dir {}'.format(src))
            self.log.dbg('ignore empty: {}'.format(noempty))
        ret = []
        if not self._create_dirs(dst):
            return []
        # handle all files in dir
        for entry in os.listdir(src):
            f = os.path.join(src, entry)
            if not os.path.isdir(f):
                res = self._handle_file(templater,
                                        f,
                                        os.path.join(dst, entry),
                                        actions=actions,
                                        noempty=noempty)
                ret.extend(res)
            else:
                res = self._handle_dir(templater,
                                       f,
                                       os.path.join(dst, entry),
                                       actions=actions,
                                       noempty=noempty)
                ret.extend(res)
        return ret

    def _fake_diff(self, dst, content):
        """fake diff by comparing file content with content"""
        cur = ''
        with open(dst, 'br') as f:
            cur = f.read()
        return cur == content

    def _write(self, src, dst, content, rights, actions=[]):
        """write content to file
        return  0 for success,
                1 when already exists
               -1 when error"""
        if self.dry:
            self.log.dry('would install {}'.format(dst))
            return 0
        if os.path.lexists(dst):
            samerights = os.stat(dst).st_mode == rights
            if self.diff and self._fake_diff(dst, content) and samerights:
                if self.debug:
                    self.log.dbg('{} is the same'.format(dst))
                return 1
            if self.safe:
                if self.debug:
                    self.log.dbg('change detected for {}'.format(dst))
                if self.showdiff:
                    self._diff_before_write(src, dst, content)
                if not self.log.ask('Overwrite \"{}\"'.format(dst)):
                    self.log.warn('ignoring {}'.format(dst))
                    return 1
        if self.backup and os.path.lexists(dst):
            self._backup(dst)
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            self.log.err('creating directory for {}'.format(dst))
            return -1
        if self.debug:
            self.log.dbg('write content to {}'.format(dst))
        self._exec_pre_actions(actions)
        try:
            with open(dst, 'wb') as f:
                f.write(content)
        except NotADirectoryError as e:
            self.log.err('opening dest file: {}'.format(e))
            return -1
        os.chmod(dst, rights)
        return 0

    def _diff_before_write(self, src, dst, src_content):
        """diff before writing when using --showdiff - not efficient"""
        # create tmp to diff for templates
        tmpfile = utils.get_tmpfile()
        with open(tmpfile, 'wb') as f:
            f.write(src_content)
        comp = Comparator(debug=self.debug)
        diff = comp.compare(tmpfile, dst)
        # fake the output for readability
        self.log.log('diff \"{}\" VS \"{}\"'.format(src, dst))
        self.log.emph(diff)
        if tmpfile:
            utils.remove(tmpfile)

    def _create_dirs(self, directory):
        """mkdir -p <directory>"""
        if not self.create and not os.path.exists(directory):
            return False
        if os.path.exists(directory):
            return True
        if self.dry:
            self.log.dry('would mkdir -p {}'.format(directory))
            return True
        if self.debug:
            self.log.dbg('mkdir -p {}'.format(directory))
        os.makedirs(directory)
        return os.path.exists(directory)

    def _backup(self, path):
        """backup file pointed by path"""
        if self.dry:
            return
        dst = path.rstrip(os.sep) + self.BACKUP_SUFFIX
        self.log.log('backup {} to {}'.format(path, dst))
        os.rename(path, dst)

    def _pivot_path(self, path, newdir, striphome=False):
        """change path to be under newdir"""
        if striphome:
            home = os.path.expanduser('~')
            path = path.lstrip(home)
        sub = path.lstrip(os.sep)
        return os.path.join(newdir, sub)

    def _exec_pre_actions(self, actions):
        """execute pre-actions if any"""
        if self.action_executed:
            return
        for action in actions:
            if self.dry:
                self.log.dry('would execute action: {}'.format(action))
            else:
                if self.debug:
                    self.log.dbg('executing pre action {}'.format(action))
                action.execute()
        self.action_executed = True

    def _install_to_temp(self, templater, src, dst, tmpdir):
        """install a dotfile to a tempdir"""
        tmpdst = self._pivot_path(dst, tmpdir)
        return self.install(templater, src, tmpdst), tmpdst

    def install_to_temp(self, templater, tmpdir, src, dst):
        """install a dotfile to a tempdir"""
        ret = False
        tmpdst = ''
        # save some flags while comparing
        self.comparing = True
        drysaved = self.dry
        self.dry = False
        diffsaved = self.diff
        self.diff = False
        createsaved = self.create
        self.create = True
        # normalize src and dst
        src = os.path.expanduser(src)
        dst = os.path.expanduser(dst)
        if self.debug:
            self.log.dbg('tmp install {} to {}'.format(src, dst))
        # install the dotfile to a temp directory for comparing
        ret, tmpdst = self._install_to_temp(templater, src, dst, tmpdir)
        if self.debug:
            self.log.dbg('tmp installed in {}'.format(tmpdst))
        # reset flags
        self.dry = drysaved
        self.diff = diffsaved
        self.comparing = False
        self.create = createsaved
        return ret, tmpdst
Ejemplo n.º 10
0
class Updater:
    def __init__(self,
                 dotpath,
                 variables,
                 dotfile_key_getter,
                 dotfile_dst_getter,
                 dotfile_path_normalizer,
                 dry=False,
                 safe=True,
                 debug=False,
                 ignore=[],
                 showpatch=False):
        """constructor
        @dotpath: path where dotfiles are stored
        @variables: dictionary of variables for the templates
        @dotfile_key_getter: func to get a dotfile by key
        @dotfile_dst_getter: func to get a dotfile by dst
        @dotfile_path_normalizer: func to normalize dotfile dst
        @dry: simulate
        @safe: ask for overwrite if True
        @debug: enable debug
        @ignore: pattern to ignore when updating
        @showpatch: show patch if dotfile to update is a template
        """
        self.dotpath = dotpath
        self.variables = variables
        self.dotfile_key_getter = dotfile_key_getter
        self.dotfile_dst_getter = dotfile_dst_getter
        self.dotfile_path_normalizer = dotfile_path_normalizer
        self.dry = dry
        self.safe = safe
        self.debug = debug
        self.ignore = ignore
        self.showpatch = showpatch
        self.templater = Templategen(variables=self.variables,
                                     base=self.dotpath,
                                     debug=self.debug)
        # save template vars
        self.tvars = self.templater.add_tmp_vars()
        self.log = Logger()

    def update_path(self, path):
        """update the dotfile installed on path"""
        path = os.path.expanduser(path)
        if not os.path.lexists(path):
            self.log.err('\"{}\" does not exist!'.format(path))
            return False
        dotfile = self.dotfile_dst_getter(path)
        if not dotfile:
            return False
        if self.debug:
            self.log.dbg('updating {} from path \"{}\"'.format(dotfile, path))
        return self._update(path, dotfile)

    def update_key(self, key):
        """update the dotfile referenced by key"""
        dotfile = self.dotfile_key_getter(key)
        if not dotfile:
            return False
        if self.debug:
            self.log.dbg('updating {} from key \"{}\"'.format(dotfile, key))
        path = self.dotfile_path_normalizer(dotfile.dst)
        return self._update(path, dotfile)

    def _update(self, path, dotfile):
        """update dotfile from file pointed by path"""
        ret = False
        new_path = None
        ignores = list(set(self.ignore + dotfile.upignore))
        self.ignores = patch_ignores(ignores, dotfile.dst, debug=self.debug)
        if self.debug:
            self.log.dbg('ignore pattern(s): {}'.format(self.ignores))

        path = os.path.expanduser(path)
        dtpath = os.path.join(self.dotpath, dotfile.src)
        dtpath = os.path.expanduser(dtpath)

        if self._ignore([path, dtpath]):
            self.log.sub('\"{}\" ignored'.format(dotfile.key))
            return True
        # apply write transformation if any
        new_path = self._apply_trans_w(path, dotfile)
        if not new_path:
            return False
        if os.path.isdir(new_path):
            ret = self._handle_dir(new_path, dtpath)
        else:
            ret = self._handle_file(new_path, dtpath)
        # clean temporary files
        if new_path != path and os.path.exists(new_path):
            remove(new_path)
        return ret

    def _apply_trans_w(self, path, dotfile):
        """apply write transformation to dotfile"""
        trans = dotfile.get_trans_w()
        if not trans:
            return path
        if self.debug:
            self.log.dbg('executing write transformation {}'.format(trans))
        tmp = get_unique_tmp_name()
        self.templater.restore_vars(self.tvars)
        newvars = dotfile.get_dotfile_variables()
        self.templater.add_tmp_vars(newvars=newvars)
        if not trans.transform(
                path, tmp, templater=self.templater, debug=self.debug):
            msg = 'transformation \"{}\" failed for {}'
            self.log.err(msg.format(trans.key, dotfile.key))
            if os.path.exists(tmp):
                remove(tmp)
            return None
        return tmp

    def _is_template(self, path):
        if not Templategen.is_template(path):
            if self.debug:
                self.log.dbg('{} is NO template'.format(path))
            return False
        self.log.warn('{} uses template, update manually'.format(path))
        return True

    def _show_patch(self, fpath, tpath):
        """provide a way to manually patch the template"""
        content = self._resolve_template(tpath)
        tmp = write_to_tmpfile(content)
        cmds = ['diff', '-u', tmp, fpath, '|', 'patch', tpath]
        self.log.warn('try patching with: \"{}\"'.format(' '.join(cmds)))
        return False

    def _resolve_template(self, tpath):
        """resolve the template to a temporary file"""
        self.templater.restore_vars(self.tvars)
        return self.templater.generate(tpath)

    def _handle_file(self, path, dtpath, compare=True):
        """sync path (deployed file) and dtpath (dotdrop dotfile path)"""
        if self._ignore([path, dtpath]):
            self.log.sub('\"{}\" ignored'.format(dtpath))
            return True
        if self.debug:
            self.log.dbg('update for file {} and {}'.format(path, dtpath))
        if self._is_template(dtpath):
            # dotfile is a template
            if self.debug:
                self.log.dbg('{} is a template'.format(dtpath))
            if self.showpatch:
                self._show_patch(path, dtpath)
            return False
        if compare and filecmp.cmp(path, dtpath, shallow=True):
            # no difference
            if self.debug:
                self.log.dbg('identical files: {} and {}'.format(path, dtpath))
            return True
        if not self._overwrite(path, dtpath):
            return False
        try:
            if self.dry:
                self.log.dry('would cp {} {}'.format(path, dtpath))
            else:
                if self.debug:
                    self.log.dbg('cp {} {}'.format(path, dtpath))
                shutil.copyfile(path, dtpath)
                self.log.sub('\"{}\" updated'.format(dtpath))
        except IOError as e:
            self.log.warn('{} update failed, do manually: {}'.format(path, e))
            return False
        return True

    def _handle_dir(self, path, dtpath):
        """sync path (deployed dir) and dtpath (dotdrop dir path)"""
        if self.debug:
            self.log.dbg('handle update for dir {} to {}'.format(path, dtpath))
        # paths must be absolute (no tildes)
        path = os.path.expanduser(path)
        dtpath = os.path.expanduser(dtpath)
        if self._ignore([path, dtpath]):
            self.log.sub('\"{}\" ignored'.format(dtpath))
            return True
        # find the differences
        diff = filecmp.dircmp(path, dtpath, ignore=None)
        # handle directories diff
        return self._merge_dirs(diff)

    def _merge_dirs(self, diff):
        """Synchronize directories recursively."""
        left, right = diff.left, diff.right
        if self.debug:
            self.log.dbg('sync dir {} to {}'.format(left, right))
        if self._ignore([left, right]):
            return True

        # create dirs that don't exist in dotdrop
        for toadd in diff.left_only:
            exist = os.path.join(left, toadd)
            if not os.path.isdir(exist):
                # ignore files for now
                continue
            # match to dotdrop dotpath
            new = os.path.join(right, toadd)
            if self._ignore([exist, new]):
                self.log.sub('\"{}\" ignored'.format(exist))
                continue
            if self.dry:
                self.log.dry('would cp -r {} {}'.format(exist, new))
                continue
            if self.debug:
                self.log.dbg('cp -r {} {}'.format(exist, new))
            # Newly created directory should be copied as is (for efficiency).
            shutil.copytree(exist, new)
            self.log.sub('\"{}\" dir added'.format(new))

        # remove dirs that don't exist in deployed version
        for toremove in diff.right_only:
            old = os.path.join(right, toremove)
            if not os.path.isdir(old):
                # ignore files for now
                continue
            if self._ignore([old]):
                continue
            if self.dry:
                self.log.dry('would rm -r {}'.format(old))
                continue
            if self.debug:
                self.log.dbg('rm -r {}'.format(old))
            if not self._confirm_rm_r(old):
                continue
            remove(old)
            self.log.sub('\"{}\" dir removed'.format(old))

        # handle files diff
        # sync files that exist in both but are different
        fdiff = diff.diff_files
        fdiff.extend(diff.funny_files)
        fdiff.extend(diff.common_funny)
        for f in fdiff:
            fleft = os.path.join(left, f)
            fright = os.path.join(right, f)
            if self._ignore([fleft, fright]):
                continue
            if self.dry:
                self.log.dry('would cp {} {}'.format(fleft, fright))
                continue
            if self.debug:
                self.log.dbg('cp {} {}'.format(fleft, fright))
            self._handle_file(fleft, fright, compare=False)

        # copy files that don't exist in dotdrop
        for toadd in diff.left_only:
            exist = os.path.join(left, toadd)
            if os.path.isdir(exist):
                # ignore dirs, done above
                continue
            new = os.path.join(right, toadd)
            if self._ignore([exist, new]):
                continue
            if self.dry:
                self.log.dry('would cp {} {}'.format(exist, new))
                continue
            if self.debug:
                self.log.dbg('cp {} {}'.format(exist, new))
            shutil.copyfile(exist, new)
            self.log.sub('\"{}\" added'.format(new))

        # remove files that don't exist in deployed version
        for toremove in diff.right_only:
            new = os.path.join(right, toremove)
            if not os.path.exists(new):
                continue
            if os.path.isdir(new):
                # ignore dirs, done above
                continue
            if self._ignore([new]):
                continue
            if self.dry:
                self.log.dry('would rm {}'.format(new))
                continue
            if self.debug:
                self.log.dbg('rm {}'.format(new))
            remove(new)
            self.log.sub('\"{}\" removed'.format(new))

        # Recursively decent into common subdirectories.
        for subdir in diff.subdirs.values():
            self._merge_dirs(subdir)

        # Nothing more to do here.
        return True

    def _overwrite(self, src, dst):
        """ask for overwritting"""
        msg = 'Overwrite \"{}\" with \"{}\"?'.format(dst, src)
        if self.safe and not self.log.ask(msg):
            return False
        return True

    def _confirm_rm_r(self, directory):
        """ask for rm -r directory"""
        msg = 'Recursively remove \"{}\"?'.format(directory)
        if self.safe and not self.log.ask(msg):
            return False
        return True

    def _ignore(self, paths):
        if must_ignore(paths, self.ignores, debug=self.debug):
            if self.debug:
                self.log.dbg('ignoring update for {}'.format(paths))
            return True
        return False
Ejemplo n.º 11
0
class Importer:
    def __init__(self,
                 profile,
                 conf,
                 dotpath,
                 diff_cmd,
                 dry=False,
                 safe=True,
                 debug=False,
                 keepdot=True,
                 ignore=[]):
        """constructor
        @profile: the selected profile
        @conf: configuration manager
        @dotpath: dotfiles dotpath
        @diff_cmd: diff command to use
        @dry: simulate
        @safe: ask for overwrite if True
        @debug: enable debug
        @keepdot: keep dot prefix
        @ignore: patterns to ignore when importing
        """
        self.profile = profile
        self.conf = conf
        self.dotpath = dotpath
        self.diff_cmd = diff_cmd
        self.dry = dry
        self.safe = safe
        self.debug = debug
        self.keepdot = keepdot
        self.ignore = ignore

        self.umask = get_umask()
        self.log = Logger()

    def import_path(self,
                    path,
                    import_as=None,
                    import_link=LinkTypes.NOLINK,
                    import_mode=False):
        """
        import a dotfile pointed by path
        returns:
            1: 1 dotfile imported
            0: ignored
            -1: error
        """
        if self.debug:
            self.log.dbg('import {}'.format(path))
        if not os.path.exists(path):
            self.log.err('\"{}\" does not exist, ignored!'.format(path))
            return -1

        return self._import(path,
                            import_as=import_as,
                            import_link=import_link,
                            import_mode=import_mode)

    def _import(self,
                path,
                import_as=None,
                import_link=LinkTypes.NOLINK,
                import_mode=False):
        """
        import path
        returns:
            1: 1 dotfile imported
            0: ignored
            -1: error
        """

        # normalize path
        dst = path.rstrip(os.sep)
        dst = os.path.abspath(dst)

        # test if must be ignored
        if self._ignore(dst):
            return 0

        # ask confirmation for symlinks
        if self.safe:
            realdst = os.path.realpath(dst)
            if dst != realdst:
                msg = '\"{}\" is a symlink, dereference it and continue?'
                if not self.log.ask(msg.format(dst)):
                    return 0

        # create src path
        src = strip_home(dst)
        if import_as:
            # handle import as
            src = os.path.expanduser(import_as)
            src = src.rstrip(os.sep)
            src = os.path.abspath(src)
            src = strip_home(src)
            if self.debug:
                self.log.dbg('import src for {} as {}'.format(dst, src))
        # with or without dot prefix
        strip = '.' + os.sep
        if self.keepdot:
            strip = os.sep
        src = src.lstrip(strip)

        # get the permission
        perm = get_file_perm(dst)

        # get the link attribute
        linktype = import_link
        if linktype == LinkTypes.LINK_CHILDREN and \
                not os.path.isdir(path):
            self.log.err('importing \"{}\" failed!'.format(path))
            return -1

        if self._already_exists(src, dst):
            return -1

        if self.debug:
            self.log.dbg('import dotfile: src:{} dst:{}'.format(src, dst))

        if not self._prepare_hierarchy(src, dst):
            return -1

        # handle file mode
        chmod = None
        dflperm = get_default_file_perms(dst, self.umask)
        if self.debug:
            self.log.dbg('import mode: {}'.format(import_mode))
        if import_mode or perm != dflperm:
            if self.debug:
                msg = 'adopt mode {:o} (umask {:o})'
                self.log.dbg(msg.format(perm, dflperm))
            chmod = perm

        # add file to config file
        retconf = self.conf.new_dotfile(src, dst, linktype, chmod=chmod)
        if not retconf:
            self.log.warn('\"{}\" ignored during import'.format(path))
            return 0

        self.log.sub('\"{}\" imported'.format(path))
        return 1

    def _prepare_hierarchy(self, src, dst):
        """prepare hierarchy for dotfile"""
        srcf = os.path.join(self.dotpath, src)
        if self._ignore(srcf):
            return False

        srcfd = os.path.dirname(srcf)
        if self._ignore(srcfd):
            return False

        # a dotfile in dotpath already exists at that spot
        if os.path.exists(srcf):
            if self.safe:
                c = Comparator(debug=self.debug, diff_cmd=self.diff_cmd)
                diff = c.compare(srcf, dst)
                if diff != '':
                    # files are different, dunno what to do
                    self.log.log('diff \"{}\" VS \"{}\"'.format(dst, srcf))
                    self.log.emph(diff)
                    # ask user
                    msg = 'Dotfile \"{}\" already exists, overwrite?'
                    if not self.log.ask(msg.format(srcf)):
                        return False
                    if self.debug:
                        self.log.dbg('will overwrite existing file')

        # create directory hierarchy
        if self.dry:
            cmd = 'mkdir -p {}'.format(srcfd)
            self.log.dry('would run: {}'.format(cmd))
        else:
            try:
                os.makedirs(srcfd, exist_ok=True)
            except Exception:
                self.log.err('importing \"{}\" failed!'.format(dst))
                return False

        if self.dry:
            self.log.dry('would copy {} to {}'.format(dst, srcf))
        else:
            # copy the file to the dotpath
            try:
                if os.path.isdir(dst):
                    if os.path.exists(srcf):
                        shutil.rmtree(srcf)
                    ig = shutil.ignore_patterns(*self.ignore)
                    shutil.copytree(dst,
                                    srcf,
                                    copy_function=self._cp,
                                    ignore=ig)
                else:
                    shutil.copy2(dst, srcf)
            except shutil.Error as e:
                src = e.args[0][0][0]
                why = e.args[0][0][2]
                self.log.err('importing \"{}\" failed: {}'.format(src, why))

        return True

    def _cp(self, src, dst):
        """the copy function for copytree"""
        # test if must be ignored
        if self._ignore(src):
            return
        shutil.copy2(src, dst)

    def _already_exists(self, src, dst):
        """
        test no other dotfile exists with same
        dst for this profile but different src
        """
        dfs = self.conf.get_dotfile_by_dst(dst)
        if not dfs:
            return False
        for df in dfs:
            profiles = self.conf.get_profiles_by_dotfile_key(df.key)
            profiles = [x.key for x in profiles]
            if self.profile in profiles and \
                    not self.conf.get_dotfile_by_src_dst(src, dst):
                # same profile
                # different src
                self.log.err('duplicate dotfile for this profile')
                return True
        return False

    def _ignore(self, path):
        if must_ignore([path], self.ignore, debug=self.debug):
            if self.debug:
                self.log.dbg('ignoring import of {}'.format(path))
            self.log.warn('{} ignored'.format(path))
            return True
        return False
Ejemplo n.º 12
0
class Updater:
    def __init__(self,
                 dotpath,
                 variables,
                 conf,
                 dry=False,
                 safe=True,
                 debug=False,
                 ignore=[],
                 showpatch=False,
                 ignore_missing_in_dotdrop=False):
        """constructor
        @dotpath: path where dotfiles are stored
        @variables: dictionary of variables for the templates
        @conf: configuration manager
        @dry: simulate
        @safe: ask for overwrite if True
        @debug: enable debug
        @ignore: pattern to ignore when updating
        @showpatch: show patch if dotfile to update is a template
        """
        self.dotpath = dotpath
        self.variables = variables
        self.conf = conf
        self.dry = dry
        self.safe = safe
        self.debug = debug
        self.ignore = ignore
        self.ignores = None
        self.showpatch = showpatch
        self.ignore_missing_in_dotdrop = ignore_missing_in_dotdrop
        self.templater = Templategen(variables=self.variables,
                                     base=self.dotpath,
                                     debug=self.debug)
        # save template vars
        self.tvars = self.templater.add_tmp_vars()
        self.log = Logger()

    def update_path(self, path):
        """update the dotfile installed on path"""
        path = os.path.expanduser(path)
        if not os.path.lexists(path):
            self.log.err('\"{}\" does not exist!'.format(path))
            return False
        dotfiles = self.conf.get_dotfile_by_dst(path)
        if not dotfiles:
            return False
        for dotfile in dotfiles:
            if not dotfile:
                msg = 'invalid dotfile for update: {}'
                self.log.err(msg.format(dotfile.key))
                return False

            if self.debug:
                msg = 'updating {} from path \"{}\"'
                self.log.dbg(msg.format(dotfile, path))
            if not self._update(path, dotfile):
                return False
        return True

    def update_key(self, key):
        """update the dotfile referenced by key"""
        dotfile = self.conf.get_dotfile(key)
        if not dotfile:
            return False
        if self.debug:
            self.log.dbg('updating {} from key \"{}\"'.format(dotfile, key))
        path = self.conf.path_to_dotfile_dst(dotfile.dst)
        return self._update(path, dotfile)

    def _update(self, path, dotfile):
        """update dotfile from file pointed by path"""
        ret = False
        new_path = None
        ignores = list(set(self.ignore + dotfile.upignore))
        self.ignores = patch_ignores(ignores, dotfile.dst, debug=self.debug)
        if self.debug:
            self.log.dbg('ignore pattern(s): {}'.format(self.ignores))

        deployed_path = os.path.expanduser(path)
        local_path = os.path.join(self.dotpath, dotfile.src)
        local_path = os.path.expanduser(local_path)

        if not os.path.exists(deployed_path):
            msg = '\"{}\" does not exist'
            self.log.err(msg.format(deployed_path))
            return False

        if not os.path.exists(local_path):
            msg = '\"{}\" does not exist, import it first'
            self.log.err(msg.format(local_path))
            return False

        ignore_missing_in_dotdrop = self.ignore_missing_in_dotdrop or \
            dotfile.ignore_missing_in_dotdrop
        if (ignore_missing_in_dotdrop and not os.path.exists(local_path)) or \
                self._ignore([deployed_path, local_path]):
            self.log.sub('\"{}\" ignored'.format(dotfile.key))
            return True
        # apply write transformation if any
        new_path = self._apply_trans_w(deployed_path, dotfile)
        if not new_path:
            return False

        # save current rights
        deployed_mode = get_file_perm(deployed_path)
        local_mode = get_file_perm(local_path)

        # handle the pointed file
        if os.path.isdir(new_path):
            ret = self._handle_dir(new_path, local_path, dotfile)
        else:
            ret = self._handle_file(new_path, local_path, dotfile)

        if deployed_mode != local_mode:
            # mirror rights
            if self.debug:
                m = 'adopt mode {:o} for {}'
                self.log.dbg(m.format(deployed_mode, dotfile.key))
            r = self.conf.update_dotfile(dotfile.key, deployed_mode)
            if r:
                ret = True

        # clean temporary files
        if new_path != deployed_path and os.path.exists(new_path):
            removepath(new_path, logger=self.log)
        return ret

    def _apply_trans_w(self, path, dotfile):
        """apply write transformation to dotfile"""
        trans = dotfile.get_trans_w()
        if not trans:
            return path
        if self.debug:
            self.log.dbg('executing write transformation {}'.format(trans))
        tmp = get_unique_tmp_name()
        self.templater.restore_vars(self.tvars)
        newvars = dotfile.get_dotfile_variables()
        self.templater.add_tmp_vars(newvars=newvars)
        if not trans.transform(
                path, tmp, templater=self.templater, debug=self.debug):
            msg = 'transformation \"{}\" failed for {}'
            self.log.err(msg.format(trans.key, dotfile.key))
            if os.path.exists(tmp):
                removepath(tmp, logger=self.log)
            return None
        return tmp

    def _is_template(self, path):
        if not Templategen.is_template(path, ignore=self.ignores):
            if self.debug:
                self.log.dbg('{} is NO template'.format(path))
            return False
        self.log.warn('{} uses template, update manually'.format(path))
        return True

    def _show_patch(self, fpath, tpath):
        """provide a way to manually patch the template"""
        content = self._resolve_template(tpath)
        tmp = write_to_tmpfile(content)
        mirror_file_rights(tpath, tmp)
        cmds = ['diff', '-u', tmp, fpath, '|', 'patch', tpath]
        self.log.warn('try patching with: \"{}\"'.format(' '.join(cmds)))
        return False

    def _resolve_template(self, tpath):
        """resolve the template to a temporary file"""
        self.templater.restore_vars(self.tvars)
        return self.templater.generate(tpath)

    def _same_rights(self, left, right):
        """return True if files have the same modes"""
        try:
            lefts = get_file_perm(left)
            rights = get_file_perm(right)
            return lefts == rights
        except OSError as e:
            self.log.err(e)
            return False

    def _mirror_rights(self, src, dst):
        srcr = get_file_perm(src)
        dstr = get_file_perm(dst)
        if srcr == dstr:
            return
        if self.debug:
            msg = 'copy rights from {} ({:o}) to {} ({:o})'
            self.log.dbg(msg.format(src, srcr, dst, dstr))
        try:
            mirror_file_rights(src, dst)
        except OSError as e:
            self.log.err(e)

    def _handle_file(self, deployed_path, local_path, dotfile, compare=True):
        """sync path (deployed file) and local_path (dotdrop dotfile path)"""
        if self._ignore([deployed_path, local_path]):
            self.log.sub('\"{}\" ignored'.format(local_path))
            return True
        if self.debug:
            self.log.dbg('update for file {} and {}'.format(
                deployed_path,
                local_path,
            ))
        if self._is_template(local_path):
            # dotfile is a template
            if self.debug:
                self.log.dbg('{} is a template'.format(local_path))
            if self.showpatch:
                try:
                    self._show_patch(deployed_path, local_path)
                except UndefinedException as e:
                    msg = 'unable to show patch for {}: {}'.format(
                        deployed_path,
                        e,
                    )
                    self.log.warn(msg)
            return False
        if compare and \
                filecmp.cmp(deployed_path, local_path, shallow=False) and \
                self._same_rights(deployed_path, local_path):
            # no difference
            if self.debug:
                self.log.dbg('identical files: {} and {}'.format(
                    deployed_path,
                    local_path,
                ))
            return True
        if not self._overwrite(deployed_path, local_path):
            return False
        try:
            if self.dry:
                self.log.dry('would cp {} {}'.format(
                    deployed_path,
                    local_path,
                ))
            else:
                if self.debug:
                    self.log.dbg('cp {} {}'.format(deployed_path, local_path))
                shutil.copyfile(deployed_path, local_path)
                self._mirror_rights(deployed_path, local_path)
                self.log.sub('\"{}\" updated'.format(local_path))
        except IOError as e:
            self.log.warn('{} update failed, do manually: {}'.format(
                deployed_path, e))
            return False
        return True

    def _handle_dir(self, deployed_path, local_path, dotfile):
        """sync path (local dir) and local_path (dotdrop dir path)"""
        if self.debug:
            self.log.dbg('handle update for dir {} to {}'.format(
                deployed_path,
                local_path,
            ))
        # paths must be absolute (no tildes)
        deployed_path = os.path.expanduser(deployed_path)
        local_path = os.path.expanduser(local_path)

        if self._ignore([deployed_path, local_path]):
            self.log.sub('\"{}\" ignored'.format(local_path))
            return True
        # find the differences
        diff = filecmp.dircmp(deployed_path, local_path, ignore=None)
        # handle directories diff
        ret = self._merge_dirs(diff, dotfile)
        self._mirror_rights(deployed_path, local_path)
        return ret

    def _merge_dirs(self, diff, dotfile):
        """Synchronize directories recursively."""
        left, right = diff.left, diff.right
        if self.debug:
            self.log.dbg('sync dir {} to {}'.format(left, right))
        if self._ignore([left, right]):
            return True

        ignore_missing_in_dotdrop = self.ignore_missing_in_dotdrop or \
            dotfile.ignore_missing_in_dotdrop

        # create dirs that don't exist in dotdrop
        for toadd in diff.left_only:
            exist = os.path.join(left, toadd)
            if not os.path.isdir(exist):
                # ignore files for now
                continue
            # match to dotdrop dotpath
            new = os.path.join(right, toadd)
            if (ignore_missing_in_dotdrop and not os.path.exists(new)) or \
                    self._ignore([exist, new]):
                self.log.sub('\"{}\" ignored'.format(exist))
                continue
            if self.dry:
                self.log.dry('would cp -r {} {}'.format(exist, new))
                continue
            if self.debug:
                self.log.dbg('cp -r {} {}'.format(exist, new))

            # Newly created directory should be copied as is (for efficiency).
            def ig(src, names):
                whitelist, blacklist = set(), set()
                for ignore in self.ignores:
                    for name in names:
                        path = os.path.join(src, name)
                        if ignore.startswith('!') and \
                                fnmatch.fnmatch(path, ignore[1:]):
                            # add to whitelist
                            whitelist.add(name)
                        elif fnmatch.fnmatch(path, ignore):
                            # add to blacklist
                            blacklist.add(name)
                return blacklist - whitelist

            shutil.copytree(exist, new, ignore=ig)
            self.log.sub('\"{}\" dir added'.format(new))

        # remove dirs that don't exist in deployed version
        for toremove in diff.right_only:
            old = os.path.join(right, toremove)
            if not os.path.isdir(old):
                # ignore files for now
                continue
            if self._ignore([old]):
                continue
            if self.dry:
                self.log.dry('would rm -r {}'.format(old))
                continue
            if self.debug:
                self.log.dbg('rm -r {}'.format(old))
            if not self._confirm_rm_r(old):
                continue
            removepath(old, logger=self.log)
            self.log.sub('\"{}\" dir removed'.format(old))

        # handle files diff
        # sync files that exist in both but are different
        fdiff = diff.diff_files
        fdiff.extend(diff.funny_files)
        fdiff.extend(diff.common_funny)
        for f in fdiff:
            fleft = os.path.join(left, f)
            fright = os.path.join(right, f)
            if (ignore_missing_in_dotdrop and not os.path.exists(fright)) or \
                    self._ignore([fleft, fright]):
                continue
            if self.dry:
                self.log.dry('would cp {} {}'.format(fleft, fright))
                continue
            if self.debug:
                self.log.dbg('cp {} {}'.format(fleft, fright))
            self._handle_file(fleft, fright, dotfile, compare=False)

        # copy files that don't exist in dotdrop
        for toadd in diff.left_only:
            exist = os.path.join(left, toadd)
            if os.path.isdir(exist):
                # ignore dirs, done above
                continue
            new = os.path.join(right, toadd)
            if (ignore_missing_in_dotdrop and not os.path.exists(new)) or \
                    self._ignore([exist, new]):
                continue
            if self.dry:
                self.log.dry('would cp {} {}'.format(exist, new))
                continue
            if self.debug:
                self.log.dbg('cp {} {}'.format(exist, new))
            shutil.copyfile(exist, new)
            self._mirror_rights(exist, new)
            self.log.sub('\"{}\" added'.format(new))

        # remove files that don't exist in deployed version
        for toremove in diff.right_only:
            new = os.path.join(right, toremove)
            if not os.path.exists(new):
                continue
            if os.path.isdir(new):
                # ignore dirs, done above
                continue
            if self._ignore([new]):
                continue
            if self.dry:
                self.log.dry('would rm {}'.format(new))
                continue
            if self.debug:
                self.log.dbg('rm {}'.format(new))
            removepath(new, logger=self.log)
            self.log.sub('\"{}\" removed'.format(new))

        # compare rights
        for common in diff.common_files:
            leftf = os.path.join(left, common)
            rightf = os.path.join(right, common)
            if not self._same_rights(leftf, rightf):
                self._mirror_rights(leftf, rightf)

        # Recursively decent into common subdirectories.
        for subdir in diff.subdirs.values():
            self._merge_dirs(subdir, dotfile)

        # Nothing more to do here.
        return True

    def _overwrite(self, src, dst):
        """ask for overwritting"""
        msg = 'Overwrite \"{}\" with \"{}\"?'.format(dst, src)
        if self.safe and not self.log.ask(msg):
            return False
        return True

    def _confirm_rm_r(self, directory):
        """ask for rm -r directory"""
        msg = 'Recursively remove \"{}\"?'.format(directory)
        if self.safe and not self.log.ask(msg):
            return False
        return True

    def _ignore(self, paths):
        if must_ignore(paths, self.ignores, debug=self.debug):
            if self.debug:
                self.log.dbg('ignoring update for {}'.format(paths))
            return True
        return False
Ejemplo n.º 13
0
class Installer:
    def __init__(self,
                 base='.',
                 create=True,
                 backup=True,
                 dry=False,
                 safe=False,
                 workdir='~/.config/dotdrop',
                 debug=False,
                 diff=True,
                 totemp=None,
                 showdiff=False,
                 backup_suffix='.dotdropbak'):
        """constructor
        @base: directory path where to search for templates
        @create: create directory hierarchy if missing when installing
        @backup: backup existing dotfile when installing
        @dry: just simulate
        @safe: ask for any overwrite
        @workdir: where to install template before symlinking
        @debug: enable debug
        @diff: diff when installing if True
        @totemp: deploy to this path instead of dotfile dst if not None
        @showdiff: show the diff before overwriting (or asking for)
        @backup_suffix: suffix for dotfile backup file
        """
        self.create = create
        self.backup = backup
        self.dry = dry
        self.safe = safe
        self.workdir = os.path.expanduser(workdir)
        self.base = base
        self.debug = debug
        self.diff = diff
        self.totemp = totemp
        self.showdiff = showdiff
        self.backup_suffix = backup_suffix
        self.comparing = False
        self.action_executed = False
        self.log = Logger()

    def install(self, templater, src, dst, actions=[], noempty=False):
        """install the src to dst using a template"""
        if self.debug:
            self.log.dbg('install {} to {}'.format(src, dst))
        self.action_executed = False
        src = os.path.join(self.base, os.path.expanduser(src))
        if not os.path.exists(src):
            self.log.err('source dotfile does not exist: {}'.format(src))
            return []
        dst = os.path.expanduser(dst)
        if self.totemp:
            dst = self._pivot_path(dst, self.totemp)
        if utils.samefile(src, dst):
            # symlink loop
            self.log.err('dotfile points to itself: {}'.format(dst))
            return []
        isdir = os.path.isdir(src)
        if self.debug:
            self.log.dbg('install {} to {}'.format(src, dst))
            self.log.dbg('is \"{}\" a directory: {}'.format(src, isdir))
        if isdir:
            return self._handle_dir(templater,
                                    src,
                                    dst,
                                    actions=actions,
                                    noempty=noempty)
        return self._handle_file(templater,
                                 src,
                                 dst,
                                 actions=actions,
                                 noempty=noempty)

    def link(self, templater, src, dst, actions=[]):
        """set src as the link target of dst"""
        if self.debug:
            self.log.dbg('link {} to {}'.format(src, dst))
        self.action_executed = False
        src = os.path.normpath(os.path.join(self.base,
                                            os.path.expanduser(src)))
        if not os.path.exists(src):
            self.log.err('source dotfile does not exist: {}'.format(src))
            return []
        dst = os.path.normpath(os.path.expanduser(dst))
        if self.totemp:
            # ignore actions
            return self.install(templater, src, dst, actions=[])

        if Templategen.is_template(src):
            if self.debug:
                self.log.dbg('dotfile is a template')
                self.log.dbg('install to {} and symlink'.format(self.workdir))
            tmp = self._pivot_path(dst, self.workdir, striphome=True)
            i = self.install(templater, src, tmp, actions=actions)
            if not i and not os.path.exists(tmp):
                return []
            src = tmp
        return self._link(src, dst, actions=actions)

    def link_children(self, templater, src, dst, actions=[]):
        """link all dotfiles in a given directory"""
        if self.debug:
            self.log.dbg('link_children {} to {}'.format(src, dst))
        self.action_executed = False
        parent = os.path.join(self.base, os.path.expanduser(src))

        # Fail if source doesn't exist
        if not os.path.exists(parent):
            self.log.err('source dotfile does not exist: {}'.format(parent))
            return []

        # Fail if source not a directory
        if not os.path.isdir(parent):
            if self.debug:
                self.log.dbg('symlink children of {} to {}'.format(src, dst))

            self.log.err(
                'source dotfile is not a directory: {}'.format(parent))
            return []

        dst = os.path.normpath(os.path.expanduser(dst))
        if not os.path.lexists(dst):
            self.log.sub('creating directory "{}"'.format(dst))
            os.makedirs(dst)

        if os.path.isfile(dst):
            msg = ''.join([
                'Remove regular file {} and ',
                'replace with empty directory?',
            ]).format(dst)

            if self.safe and not self.log.ask(msg):
                msg = 'ignoring "{}", nothing installed'
                self.log.warn(msg.format(dst))
                return []
            os.unlink(dst)
            os.mkdir(dst)

        children = os.listdir(parent)
        srcs = [
            os.path.normpath(os.path.join(parent, child)) for child in children
        ]
        dsts = [
            os.path.normpath(os.path.join(dst, child)) for child in children
        ]

        for i in range(len(children)):
            src = srcs[i]
            dst = dsts[i]

            if self.debug:
                self.log.dbg('symlink child {} to {}'.format(src, dst))

            if Templategen.is_template(src):
                if self.debug:
                    self.log.dbg('dotfile is a template')
                    self.log.dbg('install to {} and symlink'.format(
                        self.workdir))
                tmp = self._pivot_path(dst, self.workdir, striphome=True)
                i = self.install(templater, src, tmp, actions=actions)
                if not i and not os.path.exists(tmp):
                    continue
                src = tmp

            result = self._link(src, dst, actions)

            # Empty actions if dotfile installed
            # This prevents from running actions multiple times
            if len(result):
                actions = []

        return (src, dst)

    def _link(self, src, dst, actions=[]):
        """set src as a link target of dst"""
        overwrite = not self.safe
        if os.path.lexists(dst):
            if os.path.realpath(dst) == os.path.realpath(src):
                if self.debug:
                    self.log.dbg('ignoring "{}", link exists'.format(dst))
                return []
            if self.dry:
                self.log.dry('would remove {} and link to {}'.format(dst, src))
                return []
            msg = 'Remove "{}" for link creation?'.format(dst)
            if self.safe and not self.log.ask(msg):
                msg = 'ignoring "{}", link was not created'
                self.log.warn(msg.format(dst))
                return []
            overwrite = True
            try:
                utils.remove(dst)
            except OSError as e:
                self.log.err('something went wrong with {}: {}'.format(src, e))
                return []
        if self.dry:
            self.log.dry('would link {} to {}'.format(dst, src))
            return []
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            self.log.err('creating directory for {}'.format(dst))
            return []
        self._exec_pre_actions(actions)
        # re-check in case action created the file
        if os.path.lexists(dst):
            msg = 'Remove "{}" for link creation?'.format(dst)
            if self.safe and not overwrite and not self.log.ask(msg):
                msg = 'ignoring "{}", link was not created'
                self.log.warn(msg.format(dst))
                return []
            try:
                utils.remove(dst)
            except OSError as e:
                self.log.err('something went wrong with {}: {}'.format(src, e))
                return []
        os.symlink(src, dst)
        self.log.sub('linked {} to {}'.format(dst, src))
        return [(src, dst)]

    def _handle_file(self, templater, src, dst, actions=[], noempty=False):
        """install src to dst when is a file"""
        if self.debug:
            self.log.dbg('generate template for {}'.format(src))
            self.log.dbg('ignore empty: {}'.format(noempty))
        if utils.samefile(src, dst):
            # symlink loop
            self.log.err('dotfile points to itself: {}'.format(dst))
            return []
        content = templater.generate(src)
        if noempty and utils.content_empty(content):
            self.log.warn('ignoring empty template: {}'.format(src))
            return []
        if content is None:
            self.log.err('generate from template {}'.format(src))
            return []
        if not os.path.exists(src):
            self.log.err('source dotfile does not exist: {}'.format(src))
            return []
        st = os.stat(src)
        ret = self._write(src, dst, content, st.st_mode, actions=actions)
        if ret < 0:
            self.log.err('installing {} to {}'.format(src, dst))
            return []
        if ret > 0:
            if self.debug:
                self.log.dbg('ignoring {}'.format(dst))
            return []
        if ret == 0:
            if not self.dry and not self.comparing:
                self.log.sub('copied {} to {}'.format(src, dst))
            return [(src, dst)]
        return []

    def _handle_dir(self, templater, src, dst, actions=[], noempty=False):
        """install src to dst when is a directory"""
        if self.debug:
            self.log.dbg('install dir {}'.format(src))
            self.log.dbg('ignore empty: {}'.format(noempty))
        ret = []
        if not self._create_dirs(dst):
            return []
        # handle all files in dir
        for entry in os.listdir(src):
            f = os.path.join(src, entry)
            if not os.path.isdir(f):
                res = self._handle_file(templater,
                                        f,
                                        os.path.join(dst, entry),
                                        actions=actions,
                                        noempty=noempty)
                ret.extend(res)
            else:
                res = self._handle_dir(templater,
                                       f,
                                       os.path.join(dst, entry),
                                       actions=actions,
                                       noempty=noempty)
                ret.extend(res)
        return ret

    def _fake_diff(self, dst, content):
        """fake diff by comparing file content with content"""
        cur = ''
        with open(dst, 'br') as f:
            cur = f.read()
        return cur == content

    def _write(self, src, dst, content, rights, actions=[]):
        """write content to file
        return  0 for success,
                1 when already exists
               -1 when error"""
        overwrite = not self.safe
        if self.dry:
            self.log.dry('would install {}'.format(dst))
            return 0
        if os.path.lexists(dst):
            samerights = False
            try:
                samerights = os.stat(dst).st_mode == rights
            except OSError as e:
                if e.errno == errno.ENOENT:
                    # broken symlink
                    self.log.err('broken symlink {}'.format(dst))
                    return -1
            if self.diff and self._fake_diff(dst, content) and samerights:
                if self.debug:
                    self.log.dbg('{} is the same'.format(dst))
                return 1
            if self.safe:
                if self.debug:
                    self.log.dbg('change detected for {}'.format(dst))
                if self.showdiff:
                    self._diff_before_write(src, dst, content)
                if not self.log.ask('Overwrite \"{}\"'.format(dst)):
                    self.log.warn('ignoring {}'.format(dst))
                    return 1
                overwrite = True
        if self.backup and os.path.lexists(dst):
            self._backup(dst)
        base = os.path.dirname(dst)
        if not self._create_dirs(base):
            self.log.err('creating directory for {}'.format(dst))
            return -1
        if self.debug:
            self.log.dbg('write content to {}'.format(dst))
        self._exec_pre_actions(actions)
        # re-check in case action created the file
        if self.safe and not overwrite and os.path.lexists(dst):
            if not self.log.ask('Overwrite \"{}\"'.format(dst)):
                self.log.warn('ignoring {}'.format(dst))
                return 1
        # write the file
        try:
            with open(dst, 'wb') as f:
                f.write(content)
        except NotADirectoryError as e:
            self.log.err('opening dest file: {}'.format(e))
            return -1
        os.chmod(dst, rights)
        return 0

    def _diff_before_write(self, src, dst, src_content):
        """diff before writing when using --showdiff - not efficient"""
        # create tmp to diff for templates
        tmpfile = utils.get_tmpfile()
        with open(tmpfile, 'wb') as f:
            f.write(src_content)
        comp = Comparator(debug=self.debug)
        diff = comp.compare(tmpfile, dst)
        # fake the output for readability
        self.log.log('diff \"{}\" VS \"{}\"'.format(src, dst))
        self.log.emph(diff)
        if tmpfile:
            utils.remove(tmpfile)

    def _create_dirs(self, directory):
        """mkdir -p <directory>"""
        if not self.create and not os.path.exists(directory):
            return False
        if os.path.exists(directory):
            return True
        if self.dry:
            self.log.dry('would mkdir -p {}'.format(directory))
            return True
        if self.debug:
            self.log.dbg('mkdir -p {}'.format(directory))
        os.makedirs(directory)
        return os.path.exists(directory)

    def _backup(self, path):
        """backup file pointed by path"""
        if self.dry:
            return
        dst = path.rstrip(os.sep) + self.backup_suffix
        self.log.log('backup {} to {}'.format(path, dst))
        os.rename(path, dst)

    def _pivot_path(self, path, newdir, striphome=False):
        """change path to be under newdir"""
        if self.debug:
            self.log.dbg('pivot new dir: \"{}\"'.format(newdir))
            self.log.dbg('strip home: {}'.format(striphome))
        if striphome:
            path = utils.strip_home(path)
        sub = path.lstrip(os.sep)
        new = os.path.join(newdir, sub)
        if self.debug:
            self.log.dbg('pivot \"{}\" to \"{}\"'.format(path, new))
        return new

    def _exec_pre_actions(self, actions):
        """execute pre-actions if any"""
        if self.action_executed:
            return
        for action in actions:
            if self.dry:
                self.log.dry('would execute action: {}'.format(action))
            else:
                if self.debug:
                    self.log.dbg('executing pre action {}'.format(action))
                action.execute()
        self.action_executed = True

    def _install_to_temp(self, templater, src, dst, tmpdir):
        """install a dotfile to a tempdir"""
        tmpdst = self._pivot_path(dst, tmpdir)
        return self.install(templater, src, tmpdst), tmpdst

    def install_to_temp(self, templater, tmpdir, src, dst):
        """install a dotfile to a tempdir"""
        ret = False
        tmpdst = ''
        # save some flags while comparing
        self.comparing = True
        drysaved = self.dry
        self.dry = False
        diffsaved = self.diff
        self.diff = False
        createsaved = self.create
        self.create = True
        # normalize src and dst
        src = os.path.expanduser(src)
        dst = os.path.expanduser(dst)
        if self.debug:
            self.log.dbg('tmp install {} to {}'.format(src, dst))
        # install the dotfile to a temp directory for comparing
        ret, tmpdst = self._install_to_temp(templater, src, dst, tmpdir)
        if self.debug:
            self.log.dbg('tmp installed in {}'.format(tmpdst))
        # reset flags
        self.dry = drysaved
        self.diff = diffsaved
        self.comparing = False
        self.create = createsaved
        return ret, tmpdst