diff --git a/dotdrop/cfg_aggregator.py b/dotdrop/cfg_aggregator.py index c83afb6..779f281 100644 --- a/dotdrop/cfg_aggregator.py +++ b/dotdrop/cfg_aggregator.py @@ -156,7 +156,8 @@ class CfgAggregator: if self.debug: self.log.dbg('new dotfile key: {}'.format(key)) # add the dotfile - self.cfgyaml.add_dotfile(key, src, dst, link, chmod=chmod) + if not self.cfgyaml.add_dotfile(key, src, dst, link, chmod=chmod): + return None return Dotfile(key, dst, src) def new(self, src, dst, link, chmod=None): @@ -172,6 +173,9 @@ class CfgAggregator: if not dotfile: dotfile = self._create_new_dotfile(src, dst, link, chmod=chmod) + if not dotfile: + return False + key = dotfile.key ret = self.cfgyaml.add_dotfile_to_profile(key, self.profile_key) if ret and self.debug: diff --git a/dotdrop/cfg_yaml.py b/dotdrop/cfg_yaml.py index a25ad7a..35e3551 100644 --- a/dotdrop/cfg_yaml.py +++ b/dotdrop/cfg_yaml.py @@ -337,13 +337,17 @@ class CfgYaml: dfl = self.settings[self.key_settings_link_dotfile_default] if str(link) != dfl: df_dict[self.key_dotfile_link] = str(link) + # chmod if chmod: - df_dict[self.key_dotfile_chmod] = format(chmod, 'o') + lnkval = df_dict.get(self.key_dotfile_link, None) + if lnkval != self.lnk_children: + df_dict[self.key_dotfile_chmod] = format(chmod, 'o') # add to global dict self._yaml_dict[self.key_dotfiles][key] = df_dict self._dirty = True + return True def del_dotfile(self, key): """remove this dotfile from config""" @@ -603,7 +607,7 @@ class CfgYaml: return new def _norm_dotfiles(self, dotfiles): - """normalize dotfiles entries""" + """normalize and check dotfiles entries""" if not dotfiles: return dotfiles new = {} @@ -621,7 +625,14 @@ class CfgYaml: v[self.key_trans_r] = v[self.old_key_trans_r] del v[self.old_key_trans_r] new[k] = v - if self.key_dotfile_link not in v: + if self.key_dotfile_link in v: + # validate link value + val = v[self.key_dotfile_link] + if val not in self.allowed_link_val: + err = 'bad link value: {}'.format(val) + self._log.err(err) + raise YamlException('config content error: {}'.format(err)) + else: # apply link value if undefined val = self.settings[self.key_settings_link_dotfile_default] v[self.key_dotfile_link] = val @@ -639,19 +650,24 @@ class CfgYaml: if len(val) < 3: err = 'bad format for chmod: {}'.format(val) self._log.err(err) - raise YamlException(err) + raise YamlException('config content error: {}'.format(err)) try: int(val) except Exception: err = 'bad format for chmod: {}'.format(val) self._log.err(err) - raise YamlException(err) - for x in val: + raise YamlException('config content error: {}'.format(err)) + for x in list(val): y = int(x) - if y < 0 or y > 7: - err = 'bad format for chmod: {}'.format(val) - self._log.err(err) - raise YamlException(err) + if y >= 0 or y <= 7: + continue + err = 'bad format for chmod: {}'.format(val) + self._log.err(err) + raise YamlException('config content error: {}'.format(err)) + if v[self.key_dotfile_link] == self.lnk_children: + err = 'incompatible use of chmod and link_children' + self._log.err(err) + raise YamlException('config content error: {}'.format(err)) return new diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 0e22968..b956f4b 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -101,7 +101,8 @@ def _dotfile_install(o, dotfile, tmpdir=None): # link r, err = inst.link(t, dotfile.src, dotfile.dst, actionexec=pre_actions_exec, - template=dotfile.template) + template=dotfile.template, + chmod=dotfile.chmod) elif hasattr(dotfile, 'link') and \ dotfile.link == LinkTypes.LINK_CHILDREN: # link_children @@ -123,7 +124,8 @@ def _dotfile_install(o, dotfile, tmpdir=None): actionexec=pre_actions_exec, noempty=dotfile.noempty, ignore=ignores, - template=dotfile.template) + template=dotfile.template, + chmod=dotfile.chmod) if tmp: tmp = os.path.join(o.dotpath, tmp) if os.path.exists(tmp): @@ -300,7 +302,8 @@ def cmd_compare(o, tmp): # install dotfile to temporary dir and compare ret, err, insttmp = inst.install_to_temp(t, tmp, src, dotfile.dst, - template=dotfile.template) + template=dotfile.template, + chmod=dotfile.chmod) if not ret: # failed to install to tmp line = '=> compare {}: error' @@ -339,6 +342,7 @@ def cmd_compare(o, tmp): def cmd_update(o): """update the dotfile(s) from path(s) or key(s)""" + # TODO chmod ret = True paths = o.update_path iskey = o.update_iskey @@ -689,8 +693,13 @@ def _get_templater(o): def _detail(dotpath, dotfile): """display details on all files under a dotfile entry""" - LOG.log('{} (dst: \"{}\", link: {})'.format(dotfile.key, dotfile.dst, - dotfile.link.name.lower())) + entry = '{}'.format(dotfile.key) + attribs = [] + attribs.append('dst: \"{}\"'.format(dotfile.dst)) + attribs.append('link: \"{}\"'.format(dotfile.link.name.lower())) + if dotfile.chmod: + attribs.append('chmod: \"{}\"'.format(dotfile.chmod)) + LOG.log('{} ({})'.format(entry, ', '.join(attribs))) path = os.path.join(dotpath, os.path.expanduser(dotfile.src)) if not os.path.isdir(path): template = 'no' diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 663ac6f..8f670dc 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -50,23 +50,13 @@ class Installer: self.diff_cmd = diff_cmd self.comparing = False self.action_executed = False + self.umask = utils.get_umask() 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): + ignore=[], template=True, + chmod=None): """ install src to dst using a template @templater: the templater object @@ -76,30 +66,50 @@ class Installer: @noempty: render empty template flag @ignore: pattern to ignore when installing @template: template this dotfile + @chmod: rights to apply if any return - True, None : success - False, error_msg : error - False, None : ignored """ + if not self._chmod_file(dst, chmod): + err = 'ignoring "{}", nothing installed'.format(dst) + return False, err + + r, err = self._install(templater, src, dst, + actionexec=actionexec, + noempty=noempty, + ignore=ignore, + template=template) + + if chmod: + os.chmod(dst, chmod) + + return self._log_install(r, err) + + def _install(self, templater, src, dst, + actionexec=None, noempty=False, + ignore=[], template=True): + """install link:nolink""" 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) + return 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) + return 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) + return False, err isdir = os.path.isdir(src) if self.debug: self.log.dbg('install {} to {}'.format(src, dst)) @@ -109,14 +119,15 @@ class Installer: actionexec=actionexec, noempty=noempty, ignore=ignore, template=template) - return self._log_install(b, e) + return b, e b, e = self._install_file(templater, src, dst, actionexec=actionexec, noempty=noempty, ignore=ignore, template=template) - return self._log_install(b, e) + return b, e - def link(self, templater, src, dst, actionexec=None, template=True): + def link(self, templater, src, dst, actionexec=None, + template=True, chmod=None): """ set src as the link target of dst @templater: the templater @@ -124,30 +135,45 @@ class Installer: @dst: dotfile destination path in the FS @actionexec: action executor callback @template: template this dotfile + @chmod: rights to apply if any return - True, None : success - False, error_msg : error - False, None : ignored """ + if not self._chmod_file(dst, chmod): + err = 'ignoring "{}", nothing installed'.format(dst) + return False, err + + r, err = self._link(templater, src, dst, + actionexec=actionexec, + template=template) + if chmod: + os.chmod(dst, chmod) + return self._log_install(r, err) + + def _link(self, templater, src, dst, actionexec=None, + template=True): + """install link:link""" 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) + return 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) + return 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) + return b, e if template and Templategen.is_template(src): if self.debug: @@ -157,10 +183,10 @@ class Installer: 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) + return i, err src = tmp - b, e = self._link(src, dst, actionexec=actionexec) - return self._log_install(b, e) + b, e = self._symlink(src, dst, actionexec=actionexec) + return b, e def link_children(self, templater, src, dst, actionexec=None, template=True): @@ -177,19 +203,27 @@ class Installer: - False, error_msg: error - False, None, ignored """ + r, err = self._link_children(templater, src, dst, + actionexec=actionexec, + template=template) + return self._log_install(r, err) + + def _link_children(self, templater, src, dst, actionexec=None, + template=True): + """install link:link_children""" 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) + return 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) + return False, err # Fail if source not a directory if not os.path.isdir(parent): @@ -197,7 +231,7 @@ class Installer: 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) + return False, err dst = os.path.normpath(os.path.expanduser(dst)) if not os.path.lexists(dst): @@ -212,7 +246,7 @@ class Installer: if self.safe and not self.log.ask(msg): err = 'ignoring "{}", nothing installed'.format(dst) - return self._log_install(False, err) + return False, err os.unlink(dst) os.mkdir(dst) @@ -242,7 +276,7 @@ class Installer: continue src = tmp - ret, err = self._link(src, dst, actionexec=actionexec) + ret, err = self._symlink(src, dst, actionexec=actionexec) if ret: installed += 1 # void actionexec if dotfile installed @@ -250,11 +284,11 @@ class Installer: actionexec = None else: if err: - return self._log_install(ret, err) + return ret, err - return self._log_install(installed > 0, None) + return installed > 0, None - def _link(self, src, dst, actionexec=None): + def _symlink(self, src, dst, actionexec=None): """ set src as a link target of dst @@ -595,13 +629,16 @@ class Installer: self.action_executed = True return ret, err - def _install_to_temp(self, templater, src, dst, tmpdir, template=True): + def _install_to_temp(self, templater, src, dst, tmpdir, + template=True, chmod=None): """install a dotfile to a tempdir""" tmpdst = self._pivot_path(dst, tmpdir) - r = self.install(templater, src, tmpdst, template=template) + r = self.install(templater, src, tmpdst, + template=template, chmod=chmod) return r, tmpdst - def install_to_temp(self, templater, tmpdir, src, dst, template=True): + def install_to_temp(self, templater, tmpdir, src, dst, + template=True, chmod=None): """install a dotfile to a tempdir""" ret = False tmpdst = '' @@ -620,7 +657,7 @@ class Installer: 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) + template=template, chmod=chmod) ret, err = r if self.debug: self.log.dbg('tmp installed in {}'.format(tmpdst)) @@ -630,3 +667,31 @@ class Installer: self.comparing = False self.create = createsaved return ret, err, tmpdst + + 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 _chmod_file(self, path, chmod): + """chmod file if needed and return True to continue""" + if chmod: + return True + if os.path.exists(path) and self.safe: + perms = utils.get_file_perm(path) + if perms & self.umask: + # perms and umask differ + msg = 'File mode ({}) differs from umask ({})' + msg.format(perms, self.umask) + msg += ', continue' + if not self.log.ask(msg): + return False + return True diff --git a/dotdrop/utils.py b/dotdrop/utils.py index bc32990..95e1539 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -308,7 +308,8 @@ def get_umask(): """return current umask value""" cur = os.umask(0) os.umask(cur) - return 0o777 - cur + # return 0o777 - cur + return cur def get_file_perm(path):