diff --git a/docs/config/config-dotfiles.md b/docs/config/config-dotfiles.md index 8cebb1b..82c801b 100644 --- a/docs/config/config-dotfiles.md +++ b/docs/config/config-dotfiles.md @@ -8,7 +8,7 @@ Entry | Description `src` | Dotfile path within the `dotpath` (dotfiles with empty `src` are ignored and considered installed, can use `variables`, make sure to quote) `link` | Defines how this dotfile is installed. Possible values: *nolink*, *absolute*, *relative*, *link_children* (See [Symlinking dotfiles](config-file.md#symlinking-dotfiles)) (defaults to value of `link_dotfile_default`) `actions` | List of action keys that need to be defined in the **actions** entry below (See [actions](config-actions.md)) -`chmod` | Defines the file permissions in octal notation to apply during installation (See [permissions](config-file.md#permissions)) +`chmod` | Defines the file permissions in octal notation to apply during installation or the special keyword `preserve` (See [permissions](config-file.md#permissions)) `cmpignore` | List of patterns to ignore when comparing (enclose in quotes when using wildcards; see [ignore patterns](config-file.md#ignore-patterns)) `ignore_missing_in_dotdrop` | Ignore missing files in dotdrop when comparing and importing (see [Ignore missing](config-file.md#ignore-missing)) `ignoreempty` | If true, an empty template will not be deployed (defaults to the value of `ignoreempty`) diff --git a/docs/config/config-file.md b/docs/config/config-file.md index b87a3fd..099c804 100644 --- a/docs/config/config-file.md +++ b/docs/config/config-file.md @@ -90,8 +90,20 @@ dotfiles: src: dir dst: ~/dir chmod: 744 + f_preserve: + src: preserve + dst: ~/preserve + chmod: preserve ``` +The `chmod` value defines the file permissions in octal notation to apply on dotfiles. If undefined +new files will get the system default permissions (see `umask`, `777-` for directories and +`666-` for files). + +The special keyword `preserve` allows to ensure that if the dotfiles already exists +on the filesystem, it is not altered during `install` and the `chmod` value won't +be changed during `update`. + On `import`, the following rules are applied: * If the `-m`/`--preserve-mode` switch is provided or the config option @@ -107,12 +119,13 @@ On `install`, the following rules are applied: * Otherwise, the permissions of the dotfile in the `dotpath` are applied. * If the global setting `force_chmod` is set to true, dotdrop will not ask for confirmation to apply permissions. +* If `chmod` is `preserve` and the destination exists with a different permission set + than system default, then it is not altered On `update`, the following rule is applied: * If the permissions of the file in the filesystem differ from the dotfile in the `dotpath`, - then the dotfile entry `chmod` is added/updated accordingly. - + then the dotfile entry `chmod` is added/updated accordingly (unless `chmod` value is `preserve`) ## Symlinking dotfiles diff --git a/dotdrop/cfg_yaml.py b/dotdrop/cfg_yaml.py index 8eed556..5e07547 100644 --- a/dotdrop/cfg_yaml.py +++ b/dotdrop/cfg_yaml.py @@ -67,6 +67,9 @@ class CfgYaml: key_dotfile_template = 'template' key_dotfile_chmod = 'chmod' + # chmod value + chmod_ignore = 'preserve' + # profile key_profile_dotfiles = 'dotfiles' key_profile_include = 'include' @@ -378,14 +381,17 @@ class CfgYaml: """return all existing dotfile keys""" return self.dotfiles.keys() - def update_dotfile(self, key, chmod): - """update an existing dotfile""" - if key not in self.dotfiles.keys(): - return False - dotfile = self._yaml_dict[self.key_dotfiles][key] + def _update_dotfile_chmod(self, key, dotfile, chmod): old = None if self.key_dotfile_chmod in dotfile: old = dotfile[self.key_dotfile_chmod] + if old == self.chmod_ignore: + msg = ( + 'ignore chmod change since ' + f'{self.chmod_ignore}' + ) + self._dbg(msg) + return False if old == chmod: return False if self._debug: @@ -397,6 +403,18 @@ class CfgYaml: del dotfile[self.key_dotfile_chmod] else: dotfile[self.key_dotfile_chmod] = str(format(chmod, 'o')) + return True + + def update_dotfile(self, key, chmod): + """ + update an existing dotfile + return true if updated + """ + if key not in self.dotfiles.keys(): + return False + dotfile = self._yaml_dict[self.key_dotfiles][key] + if not self._update_dotfile_chmod(key, dotfile, chmod): + return False self._dirty = True return True @@ -743,62 +761,77 @@ class CfgYaml: new[k] = val return new + def _norm_dotfile_chmod(self, entry): + value = str(entry[self.key_dotfile_chmod]) + if value == self.chmod_ignore: + # is preserve + return + if len(value) < 3: + # bad format + err = f'bad format for chmod: {value}' + self._log.err(err) + raise YamlException(f'config content error: {err}') + + # check is valid value + try: + int(value) + except Exception as exc: + err = f'bad format for chmod: {value}' + self._log.err(err) + err = f'config content error: {err}' + raise YamlException(err) from exc + + # normalize chmod value + for chmodv in list(value): + chmodint = int(chmodv) + if chmodint < 0 or chmodint > 7: + err = f'bad format for chmod: {value}' + self._log.err(err) + raise YamlException( + f'config content error: {err}' + ) + # octal + entry[self.key_dotfile_chmod] = int(value, 8) + def _norm_dotfiles(self, dotfiles): """normalize and check dotfiles entries""" if not dotfiles: return dotfiles new = {} for k, val in dotfiles.items(): - # add 'src' as key' if not present if self.key_dotfile_src not in val: + # add 'src' as key' if not present val[self.key_dotfile_src] = k new[k] = val else: new[k] = val - # fix deprecated trans key + if self.old_key_trans_r in val: - msg = '\"trans\" is deprecated, please use \"trans_read\"' + # fix deprecated trans key + msg = f'{k} \"trans\" is deprecated, please use \"trans_read\"' self._log.warn(msg) val[self.key_trans_r] = val[self.old_key_trans_r] del val[self.old_key_trans_r] new[k] = val + if self.key_dotfile_link not in val: # apply link value if undefined value = self.settings[self.key_settings_link_dotfile_default] val[self.key_dotfile_link] = value - # apply noempty if undefined + if self.key_dotfile_noempty not in val: + # apply noempty if undefined value = self.settings.get(self.key_settings_noempty, False) val[self.key_dotfile_noempty] = value - # apply template if undefined + if self.key_dotfile_template not in val: + # apply template if undefined value = self.settings.get(self.key_settings_template, True) val[self.key_dotfile_template] = value - # validate value of chmod if defined - if self.key_dotfile_chmod in val: - value = str(val[self.key_dotfile_chmod]) - if len(value) < 3: - err = f'bad format for chmod: {value}' - self._log.err(err) - raise YamlException(f'config content error: {err}') - try: - int(value) - except Exception as exc: - err = f'bad format for chmod: {value}' - self._log.err(err) - err = f'config content error: {err}' - raise YamlException(err) from exc - # normalize chmod value - for chmodv in list(value): - chmodint = int(chmodv) - if chmodint < 0 or chmodint > 7: - err = f'bad format for chmod: {value}' - self._log.err(err) - raise YamlException( - f'config content error: {err}' - ) - val[self.key_dotfile_chmod] = int(value, 8) + if self.key_dotfile_chmod in val: + # validate value of chmod if defined + self._norm_dotfile_chmod(val) return new def _add_variables(self, new, shell=False, template=True, prio=False): diff --git a/dotdrop/dotfile.py b/dotdrop/dotfile.py index 88fdff2..0b09f5b 100644 --- a/dotdrop/dotfile.py +++ b/dotdrop/dotfile.py @@ -117,7 +117,10 @@ class Dotfile(DictParser): msg += f', link:\"{self.link}\"' msg += f', template:{self.template}' if self.chmod: - msg += f', chmod:{self.chmod:o}' + if isinstance(self.chmod, int) or len(self.chmod) == 3: + msg += f', chmod:{self.chmod:o}' + else: + msg += f', chmod:\"{self.chmod}\"' return msg def prt(self): @@ -129,7 +132,10 @@ class Dotfile(DictParser): out += f'\n{indent}link: \"{self.link}\"' out += f'\n{indent}template: \"{self.template}\"' if self.chmod: - out += f'\n{indent}chmod: \"{self.chmod:o}\"' + if isinstance(self.chmod, int) or len(self.chmod) == 3: + out += f'\n{indent}chmod: \"{self.chmod:o}\"' + else: + out += f'\n{indent}chmod: \"{self.chmod}\"' out += f'\n{indent}pre-action:' some = self.get_pre_actions() diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 0fb7e6a..ad561ae 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -14,6 +14,7 @@ from dotdrop.logger import Logger from dotdrop.linktypes import LinkTypes from dotdrop import utils from dotdrop.exceptions import UndefinedException +from dotdrop.cfg_yaml import CfgYaml class Installer: @@ -138,13 +139,15 @@ class Installer: ret, err = self._link_absolute(templater, src, dst, actionexec=actionexec, is_template=is_template, - ignore=ignore) + ignore=ignore, + chmod=chmod) elif linktype == LinkTypes.RELATIVE: # symlink ret, err = self._link_relative(templater, src, dst, actionexec=actionexec, is_template=is_template, - ignore=ignore) + ignore=ignore, + chmod=chmod) elif linktype == LinkTypes.LINK_CHILDREN: # symlink direct children if not isdir: @@ -158,7 +161,16 @@ class Installer: is_template=is_template, ignore=ignore) - self.log.dbg(f'before chmod: {ret} err:{err}') + if self.log.debug and chmod: + cur = utils.get_file_perm(dst) + if chmod == CfgYaml.chmod_ignore: + chmodstr = CfgYaml.chmod_ignore + else: + chmodstr = f'{chmod:o}' + self.log.dbg( + f'before chmod (cur:{cur:o}, new:{chmodstr}): ' + f'installed:{ret} err:{err}' + ) if self.dry: return self._log_install(ret, err) @@ -169,9 +181,15 @@ class Installer: # but not when # - error (not r, err) # - aborted (not r, err) - if os.path.exists(dst) and (ret or (not ret and not err)): + # - special keyword "preserve" + apply_chmod = linktype in [LinkTypes.NOLINK, LinkTypes.LINK_CHILDREN] + apply_chmod = apply_chmod and os.path.exists(dst) + apply_chmod = apply_chmod and (ret or (not ret and not err)) + apply_chmod = apply_chmod and chmod != CfgYaml.chmod_ignore + if apply_chmod: if not chmod: chmod = utils.get_file_perm(src) + self.log.dbg(f'applying chmod {chmod:o} to {dst}') dstperms = utils.get_file_perm(dst) if dstperms != chmod: # apply mode @@ -187,6 +205,8 @@ class Installer: else: ret = False err = 'chmod failed' + else: + self.log.dbg('no chmod applied') return self._log_install(ret, err) @@ -255,7 +275,8 @@ class Installer: def _link_absolute(self, templater, src, dst, actionexec=None, is_template=True, - ignore=None): + ignore=None, + chmod=None): """ install link:absolute|link @@ -269,12 +290,14 @@ class Installer: actionexec=actionexec, is_template=is_template, ignore=ignore, - absolute=True) + absolute=True, + chmod=chmod) def _link_relative(self, templater, src, dst, actionexec=None, is_template=True, - ignore=None): + ignore=None, + chmod=None): """ install link:relative @@ -288,13 +311,18 @@ class Installer: actionexec=actionexec, is_template=is_template, ignore=ignore, - absolute=False) + absolute=False, + chmod=chmod) def _link_dotfile(self, templater, src, dst, actionexec=None, - is_template=True, ignore=None, absolute=True): + is_template=True, ignore=None, absolute=True, + chmod=None): """ symlink + chmod is only used if the dotfile is a template + and needs to be installed to the workdir first + return - True, None : success - False, error_msg : error @@ -302,15 +330,15 @@ class Installer: - False, 'aborted' : user aborted """ if is_template: - self.log.dbg('is a template') - self.log.dbg(f'install to {self.workdir}') + self.log.dbg(f'is a template, installing to {self.workdir}') tmp = utils.pivot_path(dst, self.workdir, striphome=True, logger=self.log) ret, err = self.install(templater, src, tmp, LinkTypes.NOLINK, actionexec=actionexec, is_template=is_template, - ignore=ignore) + ignore=ignore, + chmod=chmod) if not ret and not os.path.exists(tmp): return ret, err src = tmp @@ -467,6 +495,10 @@ class Installer: dstrel = os.path.dirname(dstrel) lnk_src = os.path.relpath(src, dstrel) os.symlink(lnk_src, dst) + self.log.dbg( + f'symlink {dst} to {lnk_src} ' + f'(mode:{utils.get_file_perm(dst):o})' + ) if not self.comparing: self.log.sub(f'linked {dst} to {lnk_src}') return True, None @@ -527,7 +559,10 @@ class Installer: ret, err = self._write(src, dst, content=content, actionexec=actionexec) + if ret and not err: + rights = f'{utils.get_file_perm(src):o}' + self.log.dbg(f'installed file {src} to {dst} ({rights})') if not self.dry and not self.comparing: self.log.sub(f'install {src} to {dst}') return ret, err @@ -587,13 +622,12 @@ class Installer: @classmethod def _write_content_to_file(cls, content, src, dst): """write content to file""" - if content: # write content the file try: with open(dst, 'wb') as file: file.write(content) - shutil.copymode(src, dst) + # shutil.copymode(src, dst) except NotADirectoryError as exc: err = f'opening dest file: {exc}' return False, err @@ -605,7 +639,7 @@ class Installer: # copy file try: shutil.copyfile(src, dst) - shutil.copymode(src, dst) + # shutil.copymode(src, dst) except OSError as exc: return False, str(exc) return True, None @@ -665,7 +699,7 @@ class Installer: if not ret: return False, err - self.log.dbg(f'install file to \"{dst}\"') + self.log.dbg(f'installing file to \"{dst}\"') # re-check in case action created the file if self.safe and not overwrite and \ os.path.lexists(dst) and \ @@ -674,7 +708,10 @@ class Installer: return False, 'aborted' # writing to file - return self._write_content_to_file(content, src, dst) + self.log.dbg(f'before writing to {dst} ({utils.get_file_perm(src):o})') + ret = self._write_content_to_file(content, src, dst) + self.log.dbg(f'written to {dst} ({utils.get_file_perm(src):o})') + return ret ######################################################## # helpers @@ -749,7 +786,13 @@ class Installer: return dst = path.rstrip(os.sep) + self.backup_suffix self.log.log(f'backup {path} to {dst}') - os.rename(path, dst) + # os.rename(path, dst) + # copy to preserve mode on chmod=preserve + # since we expect dotfiles this shouldn't have + # such a big impact but who knows. + shutil.copy2(path, dst) + stat = os.stat(path) + os.chown(dst, stat.st_uid, stat.st_gid) def _exec_pre_actions(self, actionexec): """execute action executor""" diff --git a/dotdrop/utils.py b/dotdrop/utils.py index 0c28b89..6b5bf00 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -439,7 +439,9 @@ def get_default_file_perms(path, umask): def get_file_perm(path): """return file permission""" - return os.stat(path).st_mode & 0o777 + if not os.path.exists(path): + return 0o777 + return os.stat(path, follow_symlinks=True).st_mode & 0o777 def chmod(path, mode, debug=False): diff --git a/tests-ng/chmod-install.sh b/tests-ng/chmod-install.sh index ed95794..e635dc5 100755 --- a/tests-ng/chmod-install.sh +++ b/tests-ng/chmod-install.sh @@ -90,6 +90,7 @@ cfg="${tmps}/config.yaml" echo 'f777' > ${tmps}/dotfiles/f777 chmod 700 ${tmps}/dotfiles/f777 echo 'link' > ${tmps}/dotfiles/link +chmod 777 ${tmps}/dotfiles/link mkdir -p ${tmps}/dotfiles/dir echo "f1" > ${tmps}/dotfiles/dir/f1 @@ -99,6 +100,7 @@ echo "exists" > ${tmpd}/exists chmod 644 ${tmpd}/exists echo "existslink" > ${tmps}/dotfiles/existslink +chmod 777 ${tmps}/dotfiles/existslink chmod 644 ${tmpd}/exists mkdir -p ${tmps}/dotfiles/direxists @@ -187,7 +189,6 @@ profiles: p2: dotfiles: - f_exists - - f_existslink - d_linkchildren - f_symlinktemplate - f_nomode @@ -196,8 +197,8 @@ _EOF # install echo "first install round" -#cd ${ddpath} | ${bin} install -c ${cfg} -f -p p1 -V -cd ${ddpath} | ${bin} install -c ${cfg} -p p1 -V +cd ${ddpath} | ${bin} install -c ${cfg} -f -p p1 -V +echo "first install round" has_rights "${tmpd}/f777" "777" has_rights "${tmpd}/link" "777" @@ -223,17 +224,15 @@ chmod 600 ${tmps}/dotfiles/exists echo "exists" > ${tmpd}/exists chmod 600 ${tmpd}/exists -chmod 600 ${tmpd}/existslink - chmod 700 ${tmpd}/linkchildren chmod 600 ${tmpd}/symlinktemplate echo "second install round" cd ${ddpath} | ${bin} install -c ${cfg} -p p2 -f -V +echo "second install round" has_rights "${tmpd}/exists" "777" -has_rights "${tmpd}/existslink" "777" has_rights "${tmpd}/linkchildren/f1" "644" has_rights "${tmpd}/linkchildren/d1" "755" has_rights "${tmpd}/linkchildren/d1/f2" "644" diff --git a/tests-ng/chmod-preserve-install.sh b/tests-ng/chmod-preserve-install.sh new file mode 100755 index 0000000..3ff2330 --- /dev/null +++ b/tests-ng/chmod-preserve-install.sh @@ -0,0 +1,300 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2022, deadc0de6 +# +# test chmod preserve on install +# + +# exit on first error +set -e + +# all this crap to get current path +rl="readlink -f" +if ! ${rl} "${0}" >/dev/null 2>&1; then + rl="realpath" + + if ! hash ${rl}; then + echo "\"${rl}\" not found !" && exit 1 + fi +fi +cur=$(dirname "$(${rl} "${0}")") + +#hash dotdrop >/dev/null 2>&1 +#[ "$?" != "0" ] && echo "install dotdrop to run tests" && exit 1 + +#echo "called with ${1}" + +# dotdrop path can be pass as argument +ddpath="${cur}/../" +[ "${1}" != "" ] && ddpath="${1}" +[ ! -d ${ddpath} ] && echo "ddpath \"${ddpath}\" is not a directory" && exit 1 + +export PYTHONPATH="${ddpath}:${PYTHONPATH}" +bin="python3 -m dotdrop.dotdrop" +hash coverage 2>/dev/null && bin="coverage run -a --source=dotdrop -m dotdrop.dotdrop" || true + +echo "dotdrop path: ${ddpath}" +echo "pythonpath: ${PYTHONPATH}" + +# get the helpers +source ${cur}/helpers + +echo -e "$(tput setaf 6)==> RUNNING $(basename $BASH_SOURCE) <==$(tput sgr0)" + +################################################################ +# this is the test +################################################################ + +# $1 path +# $2 rights +has_rights() +{ + echo "testing ${1} is ${2}" + [ ! -e "$1" ] && echo "`basename $1` does not exist" && exit 1 + local mode=`stat -L -c '%a' "$1"` + [ "${mode}" != "$2" ] && echo "bad mode for `basename $1` (${mode} VS expected ${2})" && exit 1 + true +} + +# test $1 path has same right than $2 +is_same_as() +{ + echo "testing ${1} has same rights than ${2}" + [ ! -e "$1" ] && echo "`basename $1` does not exist" && exit 1 + [ ! -e "$2" ] && echo "`basename $2` does not exist" && exit 1 + + local mode1=`stat -L -c '%a' "$1"` + echo "$1: ${mode1}" + local mode2=`stat -L -c '%a' "$2"` + echo "$2: ${mode2}" + + [ "${mode1}" != "${mode2}" ] && echo "`basename $1` (${mode1}) does not have same mode as `basename $2` (${mode2})" && exit 1 + true +} + +get_default_file_mode() +{ + u=`umask` + u=`echo ${u} | sed 's/^0*//'` + v=$((666 - u)) + echo "${v}" +} + +get_default_dir_mode() +{ + u=`umask` + u=`echo ${u} | sed 's/^0*//'` + v=$((777 - u)) + echo "${v}" +} + +# the dotfile source +tmps=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` +mkdir -p ${tmps}/dotfiles +# the dotfile destination +tmpd=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` +#echo "dotfile destination: ${tmpd}" + +clear_on_exit "${tmps}" +clear_on_exit "${tmpd}" + +# create the config file +cfg="${tmps}/config.yaml" + +## +# non existing files +## + +# file +echo 'f777' > ${tmps}/dotfiles/f777 +chmod 700 ${tmps}/dotfiles/f777 + +# link +echo 'link' > ${tmps}/dotfiles/link +chmod 700 ${tmps}/dotfiles/link + +# directory +mkdir -p ${tmps}/dotfiles/dir +echo "f1" > ${tmps}/dotfiles/dir/f1 +chmod 700 ${tmps}/dotfiles/dir +chmod 700 ${tmps}/dotfiles/dir/f1 + +# template +echo '{{@@ profile @@}}' > ${tmps}/dotfiles/template +chmod 700 ${tmps}/dotfiles/template + +# link template +echo '{{@@ profile @@}}' > ${tmps}/dotfiles/link-template +chmod 700 ${tmps}/dotfiles/link-template + +## +# existing files +## + +# file +echo "exists-original" > ${tmps}/dotfiles/exists +chmod 644 ${tmps}/dotfiles/exists +echo "exists" > ${tmpd}/exists +chmod 700 ${tmpd}/exists + +# link +echo "existslink" > ${tmps}/dotfiles/existslink +chmod 700 ${tmps}/dotfiles/existslink +ln -s ${tmps}/dotfiles/existslink ${tmpd}/existslink + +# directory +mkdir -p ${tmps}/dotfiles/direxists +echo "f1-original" > ${tmps}/dotfiles/direxists/f1 +mkdir -p ${tmpd}/direxists +echo "f1" > ${tmpd}/direxists/f1 +chmod 700 ${tmpd}/direxists/f1 +chmod 700 ${tmpd}/direxists + +# link children +mkdir -p ${tmps}/dotfiles/linkchildren +echo "f1-original" > ${tmps}/dotfiles/linkchildren/f1 +chmod 700 ${tmps}/dotfiles/linkchildren/f1 +mkdir -p ${tmps}/dotfiles/linkchildren/d1 +chmod 700 ${tmps}/dotfiles/linkchildren/d1 +echo "f2-original" > ${tmps}/dotfiles/linkchildren/d1/f2 +chmod 700 ${tmps}/dotfiles/linkchildren/d1/f2 + +mkdir -p ${tmpd}/linkchildren +chmod 700 ${tmpd}/linkchildren +echo "f1" > ${tmpd}/linkchildren/f1 +mkdir -p ${tmpd}/linkchildren/d1 +echo "f2" > ${tmpd}/linkchildren/d1/f2 + +# no mode +echo 'nomode-original' > ${tmps}/dotfiles/nomode +echo 'nomode' > ${tmpd}/nomode + +cat > ${cfg} << _EOF +config: + backup: true + create: true + dotpath: dotfiles + force_chmod: true +dotfiles: + f_f777: + src: f777 + dst: ${tmpd}/f777 + chmod: preserve + f_link: + src: link + dst: ${tmpd}/link + chmod: preserve + link: absolute + d_dir: + src: dir + dst: ${tmpd}/dir + chmod: preserve + f_template: + src: template + dst: ${tmpd}/template + chmod: preserve + f_link_template: + src: link-template + dst: ${tmpd}/link-template + chmod: preserve + f_exists: + src: exists + dst: ${tmpd}/exists + chmod: preserve + f_existslink: + src: existslink + dst: ${tmpd}/existslink + chmod: preserve + link: absolute + d_direxists: + src: direxists + dst: ${tmpd}/direxists + chmod: preserve + d_linkchildren: + src: linkchildren + dst: ${tmpd}/linkchildren + chmod: preserve + link: link_children + f_nomode: + src: nomode + dst: ${tmpd}/nomode + chmod: preserve +profiles: + p1: + dotfiles: + - f_f777 + - f_link + - d_dir + - f_template + - f_link_template + - f_exists + - f_existslink + - d_direxists + - d_linkchildren + - f_nomode +_EOF +#cat ${cfg} + +exists_before=`stat -L -c '%a' "${tmpd}/exists"` +direxists_before=`stat -L -c '%a' "${tmpd}/direxists"` +direxists_f1_before=`stat -L -c '%a' "${tmpd}/direxists/f1"` + +# install +echo "first round" +cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V +echo "first round" + +# non-existing but will create with "default" rights on preserve +# 644 for file +# 755 for directory +# link will get the rights of the file it points to +has_rights "${tmpd}/f777" "`get_default_file_mode`" +has_rights "${tmpd}/link" "700" +has_rights "${tmpd}/dir" "`get_default_dir_mode`" +has_rights "${tmpd}/template" "`get_default_file_mode`" +# first install to workdir (def rights) and then symlink +has_rights "${tmpd}/link-template" "644" +[ -L "${tmpd}/link-template" ] && echo "link-template is not a symlink" && exit 1 + +# existing +has_rights "${tmpd}/exists" "700" +has_rights "${tmpd}/exists" "${exists_before}" + +has_rights "${tmpd}/existslink" "700" # points back to dotpath +is_same_as "${tmpd}/existslink" "${tmps}/dotfiles/existslink" + +has_rights "${tmpd}/direxists" "700" +has_rights "${tmpd}/direxists" "${direxists_before}" + +has_rights "${tmpd}/direxists/f1" "700" +has_rights "${tmpd}/direxists/f1" "${direxists_f1_before}" + +has_rights "${tmpd}/linkchildren" "700" # default for new directory +has_rights "${tmpd}/linkchildren/f1" "700" # points back to dotpath +has_rights "${tmpd}/linkchildren/d1" "700" # points back to dotpath +has_rights "${tmpd}/linkchildren/d1/f2" "700" + +# modify +echo 'f777-2' >> ${tmps}/dotfiles/f777 +chmod 701 ${tmps}/dotfiles/f777 +echo 'link-2' >> ${tmps}/dotfiles/link +chmod 701 ${tmps}/dotfiles/link +echo "f1-2" >> ${tmps}/dotfiles/dir/f1 +chmod 701 ${tmps}/dotfiles/dir +chmod 701 ${tmps}/dotfiles/dir/f1 + +f777_before=`stat -L -c '%a' "${tmpd}/f777"` +link_before=`stat -L -c '%a' "${tmpd}/link"` +dir_before=`stat -L -c '%a' "${tmpd}/dir"` + +echo "second round" +cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V +echo "second round" + +# existing +has_rights "${tmpd}/f777" "${f777_before}" +has_rights "${tmpd}/link" "${link_before}" +has_rights "${tmpd}/dir" "${dir_before}" + +echo "OK" +exit 0 diff --git a/tests-ng/chmod-preserve-update.sh b/tests-ng/chmod-preserve-update.sh new file mode 100755 index 0000000..ab9c5d7 --- /dev/null +++ b/tests-ng/chmod-preserve-update.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2022, deadc0de6 +# +# test chmod preserve on update +# + +# exit on first error +set -e + +# all this crap to get current path +rl="readlink -f" +if ! ${rl} "${0}" >/dev/null 2>&1; then + rl="realpath" + + if ! hash ${rl}; then + echo "\"${rl}\" not found !" && exit 1 + fi +fi +cur=$(dirname "$(${rl} "${0}")") + +#hash dotdrop >/dev/null 2>&1 +#[ "$?" != "0" ] && echo "install dotdrop to run tests" && exit 1 + +#echo "called with ${1}" + +# dotdrop path can be pass as argument +ddpath="${cur}/../" +[ "${1}" != "" ] && ddpath="${1}" +[ ! -d ${ddpath} ] && echo "ddpath \"${ddpath}\" is not a directory" && exit 1 + +export PYTHONPATH="${ddpath}:${PYTHONPATH}" +bin="python3 -m dotdrop.dotdrop" +hash coverage 2>/dev/null && bin="coverage run -a --source=dotdrop -m dotdrop.dotdrop" || true + +echo "dotdrop path: ${ddpath}" +echo "pythonpath: ${PYTHONPATH}" + +# get the helpers +source ${cur}/helpers + +echo -e "$(tput setaf 6)==> RUNNING $(basename $BASH_SOURCE) <==$(tput sgr0)" + +################################################################ +# this is the test +################################################################ + +# the dotfile source +tmps=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` +mkdir -p ${tmps}/dotfiles +# the dotfile destination +tmpd=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` +#echo "dotfile destination: ${tmpd}" + +clear_on_exit "${tmps}" +clear_on_exit "${tmpd}" + +## +# existing files +## + +# file +echo "exists-original" > ${tmps}/dotfiles/exists +chmod 644 ${tmps}/dotfiles/exists +echo "exists" > ${tmpd}/exists +chmod 700 ${tmpd}/exists + +# link +echo "existslink" > ${tmps}/dotfiles/existslink +chmod 700 ${tmps}/dotfiles/existslink +ln -s ${tmps}/dotfiles/existslink ${tmpd}/existslink + +# directory +mkdir -p ${tmps}/dotfiles/direxists +echo "f1-original" > ${tmps}/dotfiles/direxists/f1 +mkdir -p ${tmpd}/direxists +echo "f1" > ${tmpd}/direxists/f1 +chmod 700 ${tmpd}/direxists/f1 +chmod 700 ${tmpd}/direxists + +# link children +mkdir -p ${tmps}/dotfiles/linkchildren +echo "f1-original" > ${tmps}/dotfiles/linkchildren/f1 +chmod 700 ${tmps}/dotfiles/linkchildren/f1 +mkdir -p ${tmps}/dotfiles/linkchildren/d1 +chmod 700 ${tmps}/dotfiles/linkchildren/d1 +echo "f2-original" > ${tmps}/dotfiles/linkchildren/d1/f2 +chmod 700 ${tmps}/dotfiles/linkchildren/d1/f2 + +mkdir -p ${tmpd}/linkchildren +chmod 700 ${tmpd}/linkchildren +echo "f1" > ${tmpd}/linkchildren/f1 +mkdir -p ${tmpd}/linkchildren/d1 +echo "f2" > ${tmpd}/linkchildren/d1/f2 + +# no mode +echo 'nomode-original' > ${tmps}/dotfiles/nomode +echo 'nomode' > ${tmpd}/nomode + +# create the config file +cfg="${tmps}/config.yaml" +cat > ${cfg} << _EOF +config: + backup: true + create: true + dotpath: dotfiles + force_chmod: true +dotfiles: + f_exists: + src: exists + dst: ${tmpd}/exists + chmod: preserve + f_existslink: + src: existslink + dst: ${tmpd}/existslink + chmod: preserve + link: absolute + d_direxists: + src: direxists + dst: ${tmpd}/direxists + chmod: preserve + d_linkchildren: + src: linkchildren + dst: ${tmpd}/linkchildren + chmod: preserve + link: link_children + f_nomode: + src: nomode + dst: ${tmpd}/nomode + chmod: preserve +profiles: + p1: + dotfiles: + - f_exists + - f_existslink + - d_direxists + - d_linkchildren + - f_nomode +_EOF +#cat ${cfg} + +echo "update" +cd ${ddpath} | ${bin} update -f -c ${cfg} -p p1 -V ${tmpd}/exists +cd ${ddpath} | ${bin} update -f -c ${cfg} -p p1 -V ${tmpd}/existslink +cd ${ddpath} | ${bin} update -f -c ${cfg} -p p1 -V ${tmpd}/direxists +cd ${ddpath} | ${bin} update -f -c ${cfg} -p p1 -V ${tmpd}/linkchildren +cd ${ddpath} | ${bin} update -f -c ${cfg} -p p1 -V ${tmpd}/nomode + +count=$(cat ${cfg} | grep chmod | grep -v 'chmod: preserve\|force_chmod' | wc -l) +echo "${count}" +[ "${count}" != "0" ] && echo "chmod altered" && exit 1 + +echo "OK" +exit 0 diff --git a/tests-ng/helpers b/tests-ng/helpers index f51d832..2876dfd 100644 --- a/tests-ng/helpers +++ b/tests-ng/helpers @@ -101,3 +101,12 @@ if [[ $OSTYPE == 'darwin'* ]]; then export -f readlink export -f realpath fi + +# workdir tricks +# when tests are called without using the +# top level tests.sh script which sets the workdir +if [ -z "${DOTDROP_WORKDIR}" ]; then + _workdir="/tmp/dotdrop-test-workdir" + export DOTDROP_WORKDIR="${_workdir}" + clear_on_exit "${_workdir}" +fi \ No newline at end of file