class Options(AttrMonitor): def __init__(self, args=None): """constructor @args: argument dictionary (if None use sys) """ self.args = args if not args: self.args = docopt(USAGE, version=VERSION) self.log = Logger() self.debug = self.args['--verbose'] if not self.debug and ENV_DEBUG in os.environ: self.debug = True if ENV_NODEBUG in os.environ: self.debug = False self.profile = self.args['--profile'] self.confpath = os.path.expanduser(self.args['--cfg']) if self.debug: self.log.dbg('config file: {}'.format(self.confpath)) self._read_config(self.profile) self._apply_args() self._fill_attr() if ENV_NOBANNER not in os.environ \ and self.banner \ and not self.args['--no-banner']: self._header() self._print_attr() # start monitoring for bad attribute self._set_attr_err = True def _header(self): """print the header""" self.log.log(BANNER) self.log.log('') def _read_config(self, profile=None): """read the config file""" self.conf = Cfg(self.confpath, profile=profile, debug=self.debug) # transform the configs in attribute for k, v in self.conf.get_settings().items(): setattr(self, k, v) def _apply_args(self): """apply cli args as attribute""" # the commands self.cmd_list = self.args['list'] self.cmd_listfiles = self.args['listfiles'] self.cmd_install = self.args['install'] self.cmd_compare = self.args['compare'] self.cmd_import = self.args['import'] self.cmd_update = self.args['update'] self.cmd_detail = self.args['detail'] # adapt attributes based on arguments self.dry = self.args['--dry'] self.safe = not self.args['--force'] self.link = LinkTypes.NOLINK if self.link_by_default: self.link = LinkTypes.PARENTS if self.args['--inv-link']: # Only invert link type from NOLINK to PARENTS and vice-versa if self.link == LinkTypes.NOLINK: self.link = LinkTypes.PARENTS elif self.link == LinkTypes.PARENTS: self.link = LinkTypes.NOLINK # "listfiles" specifics self.listfiles_templateonly = self.args['--template'] # "install" specifics self.install_temporary = self.args['--temp'] self.install_keys = self.args['<key>'] self.install_diff = not self.args['--nodiff'] self.install_showdiff = self.showdiff or self.args['--showdiff'] self.install_backup_suffix = BACKUP_SUFFIX # "compare" specifics self.compare_dopts = self.args['--dopts'] self.compare_focus = self.args['--file'] self.compare_ignore = self.args['--ignore'] self.compare_ignore.append('*{}'.format(self.install_backup_suffix)) # "import" specifics self.import_path = self.args['<path>'] # "update" specifics self.update_path = self.args['<path>'] self.update_iskey = self.args['--key'] self.update_ignore = self.args['--ignore'] self.update_ignore.append('*{}'.format(self.install_backup_suffix)) self.update_showpatch = self.args['--show-patch'] # "detail" specifics self.detail_keys = self.args['<key>'] def _fill_attr(self): """create attributes from conf""" # variables self.variables = self.conf.get_variables(self.profile, debug=self.debug).copy() # the dotfiles self.dotfiles = self.conf.eval_dotfiles(self.profile, self.variables, debug=self.debug).copy() # the profiles self.profiles = self.conf.get_profiles() def _print_attr(self): """print all of this class attributes""" if not self.debug: return self.log.dbg('options:') for att in dir(self): if att.startswith('_'): continue val = getattr(self, att) if callable(val): continue self.log.dbg('- {}: \"{}\"'.format(att, val)) def _attr_set(self, attr): """error when some inexistent attr is set""" raise Exception('bad option: {}'.format(attr))
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 Options(AttrMonitor): def __init__(self, args=None): """constructor @args: argument dictionary (if None use sys) """ self.args = {} if not args: self.args = docopt(USAGE, version=VERSION) if args: self.args = args.copy() self.log = Logger() self.debug = self.args['--verbose'] or ENV_DEBUG in os.environ self.dry = self.args['--dry'] if ENV_NODEBUG in os.environ: # force disabling debugs self.debug = False self.profile = self.args['--profile'] self.confpath = self._get_config_path() if self.debug: self.log.dbg('version: {}'.format(VERSION)) self.log.dbg('command: {}'.format(' '.join(sys.argv))) self.log.dbg('config file: {}'.format(self.confpath)) self._read_config() self._apply_args() self._fill_attr() if ENV_NOBANNER not in os.environ \ and self.banner \ and not self.args['--no-banner']: self._header() self._debug_attr() # start monitoring for bad attribute self._set_attr_err = True def _get_config_path(self): """get the config path""" # cli provided if self.args['--cfg']: return os.path.expanduser(self.args['--cfg']) # environment variable provided if ENV_CONFIG in os.environ: return os.path.expanduser(os.environ[ENV_CONFIG]) # look in current directory if os.path.exists(CONFIG): return CONFIG # look in XDG_CONFIG_HOME if ENV_XDG in os.environ: cfg = os.path.expanduser(os.environ[ENV_XDG]) path = os.path.join(cfg, NAME, CONFIG) if os.path.exists(path): return path # look in ~/.config/dotdrop cfg = os.path.expanduser(HOMECFG) path = os.path.join(cfg, CONFIG) if os.path.exists(path): return path # look in /etc/xdg/dotdrop path = os.path.join(ETCXDGCFG, CONFIG) if os.path.exists(path): return path # look in /etc/dotdrop path = os.path.join(ETCCFG, CONFIG) if os.path.exists(path): return path return '' def _header(self): """display the header""" self.log.log(BANNER) self.log.log('') def _read_config(self): """read the config file""" self.conf = Cfg(self.confpath, self.profile, debug=self.debug, dry=self.dry) # transform the config settings to self attribute self._debug_dict('effective settings', self.conf.get_settings()) for k, v in self.conf.get_settings().items(): setattr(self, k, v) def _apply_args(self): """apply cli args as attribute""" # the commands self.cmd_profiles = self.args['profiles'] self.cmd_files = self.args['files'] self.cmd_install = self.args['install'] self.cmd_compare = self.args['compare'] self.cmd_import = self.args['import'] self.cmd_update = self.args['update'] self.cmd_detail = self.args['detail'] self.cmd_remove = self.args['remove'] # adapt attributes based on arguments self.safe = not self.args['--force'] # import link default value self.import_link = self.link_on_import if self.args['--link']: # overwrite default import link with cli switch link = self.args['--link'] if link not in OPT_LINK.keys(): self.log.err('bad option for --link: {}'.format(link)) sys.exit(USAGE) self.import_link = OPT_LINK[link] # "files" specifics self.files_templateonly = self.args['--template'] self.files_grepable = self.args['--grepable'] # "profiles" specifics self.profiles_grepable = self.args['--grepable'] # "install" specifics self.install_force_action = self.args['--force-actions'] self.install_temporary = self.args['--temp'] self.install_keys = self.args['<key>'] self.install_diff = not self.args['--nodiff'] self.install_showdiff = self.showdiff or self.args['--showdiff'] self.install_backup_suffix = BACKUP_SUFFIX self.install_default_actions_pre = [ a for a in self.default_actions if a.kind == Action.pre ] self.install_default_actions_post = [ a for a in self.default_actions if a.kind == Action.post ] self.install_ignore = self.instignore # "compare" specifics self.compare_focus = self.args['--file'] self.compare_ignore = self.args['--ignore'] self.compare_ignore.extend(self.cmpignore) self.compare_ignore.append('*{}'.format(self.install_backup_suffix)) self.compare_ignore = uniq_list(self.compare_ignore) # "import" specifics self.import_path = self.args['<path>'] self.import_as = self.args['--as'] # "update" specifics self.update_path = self.args['<path>'] self.update_iskey = self.args['--key'] self.update_ignore = self.args['--ignore'] self.update_ignore.extend(self.upignore) self.update_ignore.append('*{}'.format(self.install_backup_suffix)) self.update_ignore = uniq_list(self.update_ignore) self.update_showpatch = self.args['--show-patch'] # "detail" specifics self.detail_keys = self.args['<key>'] # "remove" specifics self.remove_path = self.args['<path>'] self.remove_iskey = self.args['--key'] def _fill_attr(self): """create attributes from conf""" # variables self.variables = self.conf.get_variables() # the dotfiles self.dotfiles = self.conf.get_dotfiles() # the profiles self.profiles = self.conf.get_profiles() def _debug_attr(self): """debug display all of this class attributes""" if not self.debug: return self.log.dbg('effective options:') for att in dir(self): if att.startswith('_'): continue val = getattr(self, att) if callable(val): continue if type(val) is list: self._debug_list('-> {}'.format(att), val) elif type(val) is dict: self._debug_dict('-> {}'.format(att), val) else: self.log.dbg('-> {}: {}'.format(att, val)) def _attr_set(self, attr): """error when some inexistent attr is set""" raise Exception('bad option: {}'.format(attr)) def _debug_list(self, title, elems): """pretty print list""" if not self.debug: return self.log.dbg('{}:'.format(title)) for e in elems: self.log.dbg('\t- {}'.format(e)) def _debug_dict(self, title, elems): """pretty print dict""" if not self.debug: return self.log.dbg('{}:'.format(title)) for k, v in elems.items(): if type(v) is list: self.log.dbg('\t- \"{}\":'.format(k)) for i in v: self.log.dbg('\t\t- {}'.format(i)) else: self.log.dbg('\t- \"{}\": {}'.format(k, v))
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, 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: 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: 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 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 Options(AttrMonitor): """dotdrop options manager""" def __init__(self, args=None): """constructor @args: argument dictionary (if None use sys) """ # attributes gotten from self.conf.get_settings() self.banner = None self.showdiff = None self.default_actions = [] self.instignore = None self.force_chmod = None self.cmpignore = None self.impignore = None self.upignore = None self.link_on_import = None self.chmod_on_import = None self.check_version = None self.clear_workdir = None self.key_prefix = None self.key_separator = None # args parsing self.args = {} if not args: self.args = docopt(USAGE, version=VERSION) if args: self.args = args.copy() self.debug = self.args['--verbose'] or ENV_DEBUG in os.environ self.log = Logger(debug=self.debug) self.dry = self.args['--dry'] if ENV_NODEBUG in os.environ: # force disabling debugs self.debug = False self.profile = self.args['--profile'] self.confpath = self._get_config_path() if not self.confpath: raise YamlException('no config file found') self.log.dbg('#################################################') self.log.dbg('#################### DOTDROP ####################') self.log.dbg('#################################################') self.log.dbg('version: {}'.format(VERSION)) self.log.dbg('command: {}'.format(' '.join(sys.argv))) self.log.dbg('config file: {}'.format(self.confpath)) self._read_config() self._apply_args() self._fill_attr() if ENV_NOBANNER not in os.environ \ and self.banner \ and not self.args['--no-banner']: self._header() self._debug_attr() # start monitoring for bad attribute self._set_attr_err = True @classmethod def _get_config_from_fs(cls): """get config from filesystem""" # look in ~/.config/dotdrop cfg = os.path.expanduser(HOMECFG) path = os.path.join(cfg, CONFIG) if os.path.exists(path): return path # look in /etc/xdg/dotdrop path = os.path.join(ETCXDGCFG, CONFIG) if os.path.exists(path): return path # look in /etc/dotdrop path = os.path.join(ETCCFG, CONFIG) if os.path.exists(path): return path return '' def _get_config_path(self): """get the config path""" # cli provided if self.args['--cfg']: return os.path.expanduser(self.args['--cfg']) # environment variable provided if ENV_CONFIG in os.environ: return os.path.expanduser(os.environ[ENV_CONFIG]) # look in current directory if os.path.exists(CONFIG): return CONFIG # look in XDG_CONFIG_HOME if ENV_XDG in os.environ: cfg = os.path.expanduser(os.environ[ENV_XDG]) path = os.path.join(cfg, NAME, CONFIG) if os.path.exists(path): return path return self._get_config_from_fs() def _header(self): """display the header""" self.log.log(BANNER) self.log.log('') def _read_config(self): """read the config file""" self.conf = Cfg(self.confpath, self.profile, debug=self.debug, dry=self.dry) # transform the config settings to self attribute settings = self.conf.get_settings() debug_dict('effective settings', settings, self.debug) for k, val in settings.items(): setattr(self, k, val) def _apply_args_files(self): """files specifics""" self.files_templateonly = self.args['--template'] self.files_grepable = self.args['--grepable'] def _apply_args_install(self): """install specifics""" self.install_force_action = self.args['--force-actions'] self.install_temporary = self.args['--temp'] self.install_keys = self.args['<key>'] self.install_diff = not self.args['--nodiff'] self.install_showdiff = self.showdiff or self.args['--showdiff'] self.install_backup_suffix = BACKUP_SUFFIX self.install_default_actions_pre = [a for a in self.default_actions if a.kind == Action.pre] self.install_default_actions_post = [a for a in self.default_actions if a.kind == Action.post] self.install_ignore = self.instignore self.install_force_chmod = self.force_chmod self.install_clear_workdir = self.args['--workdir-clear'] or \ self.clear_workdir def _apply_args_compare(self): """compare specifics""" self.compare_focus = self.args['--file'] self.compare_ignore = self.args['--ignore'] self.compare_ignore.extend(self.cmpignore) self.compare_ignore.append('*{}'.format(self.install_backup_suffix)) self.compare_ignore = uniq_list(self.compare_ignore) self.compare_fileonly = self.args['--file-only'] self.ignore_missing_in_dotdrop = self.ignore_missing_in_dotdrop or \ self.args['--ignore-missing'] def _apply_args_import(self): """import specifics""" self.import_path = self.args['<path>'] self.import_as = self.args['--as'] self.import_mode = self.args['--preserve-mode'] or self.chmod_on_import self.import_ignore = self.args['--ignore'] self.import_ignore.extend(self.impignore) self.import_ignore.append('*{}'.format(self.install_backup_suffix)) self.import_ignore = uniq_list(self.import_ignore) def _apply_args_update(self): """update specifics""" self.update_path = self.args['<path>'] self.update_iskey = self.args['--key'] self.update_ignore = self.args['--ignore'] self.update_ignore.extend(self.upignore) self.update_ignore.append('*{}'.format(self.install_backup_suffix)) self.update_ignore = uniq_list(self.update_ignore) self.update_showpatch = self.args['--show-patch'] def _apply_args_profiles(self): """profiles specifics""" self.profiles_grepable = self.args['--grepable'] def _apply_args_remove(self): """remove specifics""" self.remove_path = self.args['<path>'] self.remove_iskey = self.args['--key'] def _apply_args_detail(self): """detail specifics""" self.detail_keys = self.args['<key>'] def _apply_args(self): """apply cli args as attribute""" # the commands self.cmd_profiles = self.args['profiles'] self.cmd_files = self.args['files'] self.cmd_install = self.args['install'] self.cmd_compare = self.args['compare'] self.cmd_import = self.args['import'] self.cmd_update = self.args['update'] self.cmd_detail = self.args['detail'] self.cmd_remove = self.args['remove'] # adapt attributes based on arguments self.safe = not self.args['--force'] try: if ENV_WORKERS in os.environ: workers = int(os.environ[ENV_WORKERS]) else: workers = int(self.args['--workers']) self.workers = workers except ValueError: self.log.err('bad option for --workers') sys.exit(USAGE) # import link default value self.import_link = self.link_on_import if self.args['--link']: # overwrite default import link with cli switch link = self.args['--link'] if link not in OPT_LINK: self.log.err('bad option for --link: {}'.format(link)) sys.exit(USAGE) self.import_link = OPT_LINK[link] # "files" specifics self._apply_args_files() # "install" specifics self._apply_args_install() # "compare" specifics self._apply_args_compare() # "import" specifics self._apply_args_import() # "update" specifics self._apply_args_update() # "profiles" specifics self._apply_args_profiles() # "detail" specifics self._apply_args_detail() # "remove" specifics self._apply_args_remove() def _fill_attr(self): """create attributes from conf""" # variables self.variables = self.conf.get_variables() # the dotfiles self.dotfiles = self.conf.get_dotfiles() # the profiles self.profiles = self.conf.get_profiles() def _debug_attr(self): """debug display all of this class attributes""" if not self.debug: return self.log.dbg('effective options:') for att in dir(self): if att.startswith('_'): continue val = getattr(self, att) if callable(val): continue if isinstance(val, list): debug_list('-> {}'.format(att), val, self.debug) elif isinstance(val, dict): debug_dict('-> {}'.format(att), val, self.debug) else: self.log.dbg('-> {}: {}'.format(att, val)) def _attr_set(self, attr): """error when some inexistent attr is set""" raise Exception('bad option: {}'.format(attr))
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