diff --git a/dotdrop/cfg_aggregator.py b/dotdrop/cfg_aggregator.py index fdce64d..efa45e8 100644 --- a/dotdrop/cfg_aggregator.py +++ b/dotdrop/cfg_aggregator.py @@ -286,7 +286,7 @@ class CfgAggregator: islist=False) self._patch_keys_to_objs(self.dotfiles, "trans_w", - self._get_trans_w_args(self._get_trans_w), + self._get_trans_w_args(self.get_trans_w), islist=False) def _patch_keys_to_objs(self, containers, keys, get_by_key, islist=True): @@ -460,7 +460,7 @@ class CfgAggregator: except StopIteration: return None - def _get_trans_w(self, key): + def get_trans_w(self, key): """return the trans_w with this key""" try: return next(x for x in self.trans_w if x.key == key) diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index a53c034..88c4207 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -537,7 +537,8 @@ def cmd_importer(opts): for path in paths: tmpret = importer.import_path(path, import_as=opts.import_as, import_link=opts.import_link, - import_mode=opts.import_mode) + import_mode=opts.import_mode, + import_transw=opts.import_transw) if tmpret < 0: ret = False elif tmpret > 0: diff --git a/dotdrop/importer.py b/dotdrop/importer.py index 4115d3e..c0d3cf3 100644 --- a/dotdrop/importer.py +++ b/dotdrop/importer.py @@ -11,7 +11,8 @@ import shutil # local imports from dotdrop.logger import Logger from dotdrop.utils import strip_home, get_default_file_perms, \ - get_file_perm, get_umask, must_ignore + get_file_perm, get_umask, must_ignore, \ + get_unique_tmp_name, removepath from dotdrop.linktypes import LinkTypes from dotdrop.comparator import Comparator @@ -47,7 +48,9 @@ class Importer: self.log = Logger(debug=self.debug) def import_path(self, path, import_as=None, - import_link=LinkTypes.NOLINK, import_mode=False): + import_link=LinkTypes.NOLINK, + import_mode=False, + import_transw=""): """ import a dotfile pointed by path returns: @@ -61,11 +64,20 @@ class Importer: self.log.err('\"{}\" does not exist, ignored!'.format(path)) return -1 + # check transw if any + trans_write = None + if import_transw: + trans_write = self.conf.get_trans_w(import_transw) + return self._import(path, import_as=import_as, - import_link=import_link, import_mode=import_mode) + import_link=import_link, + import_mode=import_mode, + trans_write=trans_write) def _import(self, path, import_as=None, - import_link=LinkTypes.NOLINK, import_mode=False): + import_link=LinkTypes.NOLINK, + import_mode=False, + trans_write=None): """ import path returns: @@ -120,12 +132,16 @@ class Importer: self.log.dbg('import dotfile: src:{} dst:{}'.format(src, dst)) - if not self._prepare_hierarchy(src, dst): + if not self._import_file(src, dst, trans_write=trans_write): return -1 - return self._import_it(path, src, dst, perm, linktype, import_mode) + # TODO add trans_write + # TODO add trans_read too + return self._import_in_config(path, src, dst, perm, linktype, + import_mode) - def _import_it(self, path, src, dst, perm, linktype, import_mode): + def _import_in_config(self, path, src, dst, perm, + linktype, import_mode): """ import path returns: @@ -151,34 +167,45 @@ class Importer: self.log.sub('\"{}\" imported'.format(path)) return 1 - def _prepare_hier_when_exists(self, srcf, dst): - """a dotfile in dotpath already exists at that spot""" - if not os.path.exists(srcf): + def _check_existing_dotfile(self, src, dst): + """ + check if a dotfile in the dotpath + already exists for this src + """ + if not os.path.exists(src): return True if not self.safe: return True cmp = Comparator(debug=self.debug, diff_cmd=self.diff_cmd) - diff = cmp.compare(srcf, dst) + diff = cmp.compare(src, dst) if diff != '': # files are different, dunno what to do - self.log.log('diff \"{}\" VS \"{}\"'.format(dst, srcf)) + self.log.log('diff \"{}\" VS \"{}\"'.format(dst, src)) self.log.emph(diff) # ask user msg = 'Dotfile \"{}\" already exists, overwrite?' - if not self.log.ask(msg.format(srcf)): + if not self.log.ask(msg.format(src)): return False self.log.dbg('will overwrite existing file') return True - def _prepare_hierarchy(self, src, dst): - """prepare hierarchy for dotfile""" + def _import_file(self, src, dst, trans_write=None): + """ + prepare hierarchy for dotfile in dotpath + and copy file + src is file in dotpath + dst is file on filesystem + """ srcf = os.path.join(self.dotpath, src) srcfd = os.path.dirname(srcf) + + # check if must be ignored if self._ignore(srcf) or self._ignore(srcfd): return False - if not self._prepare_hier_when_exists(srcf, dst): + # check we are not overwritting + if not self._check_existing_dotfile(srcf, dst): return False # create directory hierarchy @@ -192,9 +219,15 @@ class Importer: self.log.err('importing \"{}\" failed!'.format(dst)) return False + # import the file if self.dry: self.log.dry('would copy {} to {}'.format(dst, srcf)) else: + # apply trans_w + dst = self._apply_trans_w(dst, trans_write) + if not dst: + # transformation failed + return False # copy the file to the dotpath try: if os.path.isdir(dst): @@ -246,3 +279,23 @@ class Importer: self.log.warn('{} ignored'.format(path)) return True return False + + def _apply_trans_w(self, path, trans): + """ + apply transformation to path on filesystem) + returns + - the new path (tmp file) if trans + - original path if no trans + - None/empty string if error + """ + if not trans: + return path + self.log.dbg('executing write transformation {}'.format(trans)) + tmp = get_unique_tmp_name() + if not trans.transform(path, tmp, debug=self.debug): + msg = 'transformation \"{}\" failed for {}' + self.log.err(msg.format(trans.key, path)) + if os.path.exists(tmp): + removepath(tmp, logger=self.log) + return None + return tmp diff --git a/dotdrop/options.py b/dotdrop/options.py index 9f1fa38..9a4646b 100644 --- a/dotdrop/options.py +++ b/dotdrop/options.py @@ -59,8 +59,8 @@ USAGE = """ Usage: dotdrop install [-VbtfndDaW] [-c ] [-p ] [-w ] [...] - dotdrop import [-Vbdfm] [-c ] [-p ] [-s ] - [-l ] [-i ...] ... + dotdrop import [-Vbdfm] [-c ] [-p ] [-i ...] + [-l ] [-S ] [-s ] ... dotdrop compare [-LVbz] [-c ] [-p ] [-w ] [-C ...] [-i ...] dotdrop update [-VbfdkPz] [-c ] [-p ] @@ -90,6 +90,7 @@ Options: -p --profile= Specify the profile to use [default: {}]. -P --show-patch Provide a one-liner to manually patch template. -s --as= Import as a different path from actual path. + -S --transw= Apply trans_write key on import. -t --temp Install to a temporary directory for review. -T --template Only template dotfiles. -V --verbose Be verbose. @@ -273,6 +274,7 @@ class Options(AttrMonitor): self.import_ignore.extend(self.impignore) self.import_ignore.append('*{}'.format(self.install_backup_suffix)) self.import_ignore = uniq_list(self.import_ignore) + self.import_transw = self.args['--transw'] def _apply_args_update(self): """update specifics""" diff --git a/tests-ng/import-with-trans.sh b/tests-ng/import-with-trans.sh new file mode 100755 index 0000000..11ccd59 --- /dev/null +++ b/tests-ng/import-with-trans.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2022, deadc0de6 +# +# test transformations for import +# returns 1 in case of error +# + +# exit on first error +set -e +#set -v + +# 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 +echo "dotfiles source (dotpath): ${tmps}" +# the dotfile destination +tmpd=`mktemp -d --suffix='-dotdrop-tests' || mktemp -d` +echo "dotfiles destination: ${tmpd}" + +clear_on_exit "${tmps}" +clear_on_exit "${tmpd}" + +# create the config file +cfg="${tmps}/config.yaml" + +cat > ${cfg} << _EOF +trans_read: + base64: cat {0} | base64 -d > {1} + uncompress: mkdir -p {1} && tar -xf {0} -C {1} +trans_write: + base64: cat {0} | base64 > {1} + compress: tar -cf {1} -C {0} . +config: + backup: true + create: true + dotpath: dotfiles +dotfiles: +profiles: +_EOF +#cat ${cfg} + +# tokens +token="test-base64" +tokend="compressed archive" + +# create the dotfiles +echo ${token} > ${tmpd}/abc +mkdir -p ${tmpd}/def/a +echo ${tokend} > ${tmpd}/def/a/file + +########################### +# test import +########################### + +echo "[+] run import" +# import file +cd ${ddpath} | ${bin} import -f -c ${cfg} -p p1 -b -V -S base64 ${tmpd}/abc +# import directory +cd ${ddpath} | ${bin} import -f -c ${cfg} -p p1 -b -V -S compress ${tmpd}/def + +# check file imported in dotpath +[ ! -e ${tmps}/dotfiles/${tmpd}/abc ] && echo "abc does not exist" && exit 1 +[ ! -e ${tmps}/dotfiles/${tmpd}/def ] && echo "def does not exist" && exit 1 + +# check content in dotpath +echo "checking content" +file ${tmps}/dotfiles/${tmpd}/abc | grep -i 'text' +cat ${tmpd}/abc | base64 > ${tmps}/test-abc +diff ${tmps}/dotfiles/${tmpd}/abc ${tmps}/test-abc + +file ${tmps}/dotfiles/${tmpd}/def | grep -i 'tar' +tar -cf ${tmps}/test-def -C ${tmpd}/def . +diff ${tmps}/dotfiles/${tmpd}/def ${tmps}/test-def + +# check is imported in config +echo "checking imported in config" +cd ${ddpath} | ${bin} -p p1 -c ${cfg} files +cd ${ddpath} | ${bin} -p p1 -c ${cfg} files | grep '^f_abc' +cd ${ddpath} | ${bin} -p p1 -c ${cfg} files | grep '^d_def' + +# check has trans_write in config +echo "checking trans_write is set in config" +cat ${cfg} +cat ${cfg} | grep -m 1 -A 3 'f_abc' | grep 'trans_write: base64' +cat ${cfg} | grep -m 1 -A 3 'd_def' | grep 'trans_write: compress' + +echo "OK" +exit 0