diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index 9738855..833304f 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -9,7 +9,6 @@ import os import sys import time from concurrent import futures -import shutil # local imports from dotdrop.options import Options @@ -18,9 +17,9 @@ from dotdrop.templategen import Templategen from dotdrop.installer import Installer from dotdrop.updater import Updater from dotdrop.comparator import Comparator -from dotdrop.utils import get_tmpdir, removepath, strip_home, \ - uniq_list, patch_ignores, dependencies_met, get_file_perm, \ - get_default_file_perms +from dotdrop.importer import Importer +from dotdrop.utils import get_tmpdir, removepath, \ + uniq_list, patch_ignores, dependencies_met from dotdrop.linktypes import LinkTypes from dotdrop.exceptions import YamlException, UndefinedException @@ -430,135 +429,26 @@ def cmd_importer(o): ret = True cnt = 0 paths = o.import_path + importer = Importer(o.profile, o.conf, o.dotpath, o.diff_command, + dry=o.dry, safe=o.safe, debug=o.debug, + keepdot=o.keepdot) + for path in paths: - if o.debug: - LOG.dbg('trying to import {}'.format(path)) - if not os.path.exists(path): - LOG.err('\"{}\" does not exist, ignored!'.format(path)) + r = importer.import_path(path, import_as=o.import_as, + import_link=o.import_link, + import_mode=o.import_mode) + if r < 0: ret = False - continue - dst = path.rstrip(os.sep) - dst = os.path.abspath(dst) - - if o.safe: - # ask for symlinks - realdst = os.path.realpath(dst) - if dst != realdst: - msg = '\"{}\" is a symlink, dereference it and continue?' - if not LOG.ask(msg.format(dst)): - continue - - src = strip_home(dst) - if o.import_as: - # handle import as - src = os.path.expanduser(o.import_as) - src = src.rstrip(os.sep) - src = os.path.abspath(src) - src = strip_home(src) - if o.debug: - LOG.dbg('import src for {} as {}'.format(dst, src)) - - strip = '.' + os.sep - if o.keepdot: - strip = os.sep - src = src.lstrip(strip) - - # get the permission - perm = get_file_perm(dst) - - # set the link attribute - linktype = o.import_link - if linktype == LinkTypes.LINK_CHILDREN and \ - not os.path.isdir(path): - LOG.err('importing \"{}\" failed!'.format(path)) - ret = False - continue - - if o.debug: - LOG.dbg('import dotfile: src:{} dst:{}'.format(src, dst)) - - # test no other dotfile exists with same - # dst for this profile but different src - dfs = o.conf.get_dotfile_by_dst(dst) - if dfs: - invalid = False - for df in dfs: - profiles = o.conf.get_profiles_by_dotfile_key(df.key) - profiles = [x.key for x in profiles] - if o.profile in profiles and \ - not o.conf.get_dotfile_by_src_dst(src, dst): - # same profile - # different src - LOG.err('duplicate dotfile for this profile') - ret = False - invalid = True - break - if invalid: - continue - - # prepare hierarchy for dotfile - srcf = os.path.join(o.dotpath, src) - overwrite = not os.path.exists(srcf) - if os.path.exists(srcf): - overwrite = True - if o.safe: - c = Comparator(debug=o.debug, diff_cmd=o.diff_command) - diff = c.compare(srcf, dst) - if diff != '': - # files are different, dunno what to do - LOG.log('diff \"{}\" VS \"{}\"'.format(dst, srcf)) - LOG.emph(diff) - # ask user - msg = 'Dotfile \"{}\" already exists, overwrite?' - overwrite = LOG.ask(msg.format(srcf)) - - if o.debug: - LOG.dbg('will overwrite: {}'.format(overwrite)) - if overwrite: - cmd = 'mkdir -p {}'.format(os.path.dirname(srcf)) - if o.dry: - LOG.dry('would run: {}'.format(cmd)) - else: - try: - os.makedirs(os.path.dirname(srcf), exist_ok=True) - except Exception: - LOG.err('importing \"{}\" failed!'.format(path)) - ret = False - continue - - if o.dry: - LOG.dry('would copy {} to {}'.format(dst, srcf)) - else: - # copy the file to the dotpath - if os.path.isdir(dst): - if os.path.exists(srcf): - shutil.rmtree(srcf) - shutil.copytree(dst, srcf) - else: - shutil.copy2(dst, srcf) - - chmod = None - dflperm = get_default_file_perms(dst, o.umask) - - if o.debug: - LOG.dbg('import mode: {}'.format(o.import_mode)) - if o.import_mode or perm != dflperm: - if o.debug: - LOG.dbg('adopt mode {:o} (umask {:o})'.format(perm, dflperm)) - # insert chmod - chmod = perm - retconf = o.conf.new_dotfile(src, dst, linktype, chmod=chmod) - if retconf: - LOG.sub('\"{}\" imported'.format(path)) + elif r > 0: cnt += 1 - else: - LOG.warn('\"{}\" ignored'.format(path)) + if o.dry: LOG.dry('new config file would be:') LOG.raw(o.conf.dump()) else: o.conf.save() LOG.log('\n{} file(s) imported.'.format(cnt)) + return ret diff --git a/dotdrop/importer.py b/dotdrop/importer.py new file mode 100644 index 0000000..5abaf3d --- /dev/null +++ b/dotdrop/importer.py @@ -0,0 +1,203 @@ +""" +author: deadc0de6 (https://github.com/deadc0de6) +Copyright (c) 2020, deadc0de6 + +handle import of dotfiles +""" + +import os +import shutil + +# local imports +from dotdrop.logger import Logger +from dotdrop.utils import strip_home, get_default_file_perms, \ + get_file_perm, get_umask +from dotdrop.linktypes import LinkTypes +from dotdrop.comparator import Comparator + + +class Importer: + + def __init__(self, profile, conf, dotpath, diff_cmd, + dry=False, safe=True, debug=False, + keepdot=True): + """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 + """ + 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.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) + + # 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'.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) + + # 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 + cmd = 'mkdir -p {}'.format(os.path.dirname(srcf)) + if self.dry: + self.log.dry('would run: {}'.format(cmd)) + else: + try: + os.makedirs(os.path.dirname(srcf), 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 + if os.path.isdir(dst): + if os.path.exists(srcf): + shutil.rmtree(srcf) + shutil.copytree(dst, srcf) + else: + shutil.copy2(dst, srcf) + + return True + + 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 diff --git a/dotdrop/options.py b/dotdrop/options.py index ad7e6ef..ffdf708 100644 --- a/dotdrop/options.py +++ b/dotdrop/options.py @@ -16,7 +16,7 @@ from dotdrop.linktypes import LinkTypes from dotdrop.logger import Logger from dotdrop.cfg_aggregator import CfgAggregator as Cfg from dotdrop.action import Action -from dotdrop.utils import uniq_list, get_umask +from dotdrop.utils import uniq_list from dotdrop.exceptions import YamlException ENV_PROFILE = 'DOTDROP_PROFILE' @@ -123,7 +123,6 @@ class Options(AttrMonitor): self.log = Logger() self.debug = self.args['--verbose'] or ENV_DEBUG in os.environ self.dry = self.args['--dry'] - self.umask = get_umask() if ENV_NODEBUG in os.environ: # force disabling debugs self.debug = False