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)
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)
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
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
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
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
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
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
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
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
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
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