mirror of
https://github.com/deadc0de6/dotdrop.git
synced 2026-02-04 20:19:46 +00:00
842 lines
29 KiB
Python
842 lines
29 KiB
Python
"""
|
|
author: deadc0de6 (https://github.com/deadc0de6)
|
|
Copyright (c) 2017, deadc0de6
|
|
|
|
handle the installation of dotfiles
|
|
"""
|
|
|
|
import os
|
|
import errno
|
|
import shutil
|
|
|
|
# local imports
|
|
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:
|
|
"""dotfile 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(debug=self.debug)
|
|
|
|
########################################################
|
|
# public methods
|
|
########################################################
|
|
|
|
def install(self, templater, src, dst, linktype,
|
|
actionexec=None, noempty=False,
|
|
ignore=None, 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
|
|
self.log.dbg('fake dotfile installed')
|
|
self._exec_pre_actions(actionexec)
|
|
return True, None
|
|
msg = f'installing \"{src}\" to \"{dst}\" (link: {linktype})'
|
|
self.log.dbg(msg)
|
|
src, dst, cont, err = self._check_paths(src, dst)
|
|
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 = f'source dotfile does not exist: {src}'
|
|
return self._log_install(False, err)
|
|
|
|
self.action_executed = False
|
|
|
|
# install to temporary dir
|
|
# and ignore any actions
|
|
if self.totemp:
|
|
ret, err, _ = self.install_to_temp(templater,
|
|
self.totemp,
|
|
src, dst,
|
|
is_template=is_template,
|
|
chmod=chmod,
|
|
ignore=ignore)
|
|
return self._log_install(ret, err)
|
|
|
|
isdir = os.path.isdir(src)
|
|
self.log.dbg(f'install {src} to {dst}')
|
|
self.log.dbg(f'\"{src}\" is a directory: {isdir}')
|
|
|
|
if linktype == LinkTypes.NOLINK:
|
|
# normal file
|
|
if isdir:
|
|
ret, err = self._copy_dir(templater, src, dst,
|
|
actionexec=actionexec,
|
|
noempty=noempty, ignore=ignore,
|
|
is_template=is_template)
|
|
else:
|
|
ret, err = self._copy_file(templater, src, dst,
|
|
actionexec=actionexec,
|
|
noempty=noempty, ignore=ignore,
|
|
is_template=is_template)
|
|
elif linktype in (LinkTypes.LINK, LinkTypes.ABSOLUTE):
|
|
# symlink
|
|
ret, err = self._link_absolute(templater, src, dst,
|
|
actionexec=actionexec,
|
|
is_template=is_template,
|
|
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,
|
|
chmod=chmod)
|
|
elif linktype == LinkTypes.LINK_CHILDREN:
|
|
# symlink direct children
|
|
if not isdir:
|
|
msg = f'symlink children of {src} to {dst}'
|
|
self.log.dbg(msg)
|
|
err = f'source dotfile is not a directory: {src}'
|
|
ret = False
|
|
else:
|
|
ret, err = self._link_children(templater, src, dst,
|
|
actionexec=actionexec,
|
|
is_template=is_template,
|
|
ignore=ignore)
|
|
|
|
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)
|
|
|
|
# handle chmod
|
|
# - on success (r, not err)
|
|
# - no change (not r, not err)
|
|
# but not when
|
|
# - error (not r, err)
|
|
# - aborted (not r, 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
|
|
msg = f'chmod {dst} to {chmod:o}'
|
|
if not force_chmod and self.safe and not self.log.ask(msg):
|
|
ret = False
|
|
err = 'aborted'
|
|
else:
|
|
if not self.comparing:
|
|
self.log.sub(f'chmod {dst} to {chmod:o}')
|
|
if utils.chmod(dst, chmod, debug=self.debug):
|
|
ret = True
|
|
else:
|
|
ret = False
|
|
err = 'chmod failed'
|
|
else:
|
|
self.log.dbg('no chmod applied')
|
|
|
|
return self._log_install(ret, err)
|
|
|
|
def install_to_temp(self, templater, tmpdir, src, dst,
|
|
is_template=True, chmod=None, ignore=None,
|
|
set_create=False):
|
|
"""
|
|
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
|
|
@set_create: force create to True
|
|
|
|
return
|
|
- success, error-if-any, dotfile-installed-path
|
|
"""
|
|
self.log.dbg(f'tmp install {src} (defined dst: {dst})')
|
|
src, dst, cont, err = self._check_paths(src, dst)
|
|
if not cont:
|
|
self._log_install(cont, err)
|
|
return cont, err, None
|
|
|
|
ret = False
|
|
tmpdst = ''
|
|
|
|
# save flags
|
|
self.comparing = True
|
|
drysaved = self.dry
|
|
self.dry = False
|
|
diffsaved = self.diff
|
|
self.diff = False
|
|
if set_create:
|
|
createsaved = self.create
|
|
self.create = True
|
|
totemp = self.totemp
|
|
self.totemp = None
|
|
|
|
# install the dotfile to a temp directory
|
|
tmpdst = utils.pivot_path(dst, tmpdir, logger=self.log)
|
|
ret, err = self.install(templater, src, tmpdst,
|
|
LinkTypes.NOLINK,
|
|
is_template=is_template,
|
|
chmod=chmod, ignore=ignore)
|
|
if ret:
|
|
self.log.dbg(f'tmp installed in {tmpdst}')
|
|
|
|
# restore flags
|
|
self.dry = drysaved
|
|
self.diff = diffsaved
|
|
if set_create:
|
|
self.create = createsaved
|
|
self.comparing = False
|
|
self.totemp = totemp
|
|
|
|
return ret, err, tmpdst
|
|
|
|
########################################################
|
|
# low level accessors for public methods
|
|
########################################################
|
|
|
|
def _link_absolute(self, templater, src, dst,
|
|
actionexec=None,
|
|
is_template=True,
|
|
ignore=None,
|
|
chmod=None):
|
|
"""
|
|
install link:absolute|link
|
|
|
|
return
|
|
- True, None : success
|
|
- False, error_msg : error
|
|
- False, None : ignored
|
|
- False, 'aborted' : user aborted
|
|
"""
|
|
return self._link_dotfile(templater, src, dst,
|
|
actionexec=actionexec,
|
|
is_template=is_template,
|
|
ignore=ignore,
|
|
absolute=True,
|
|
chmod=chmod)
|
|
|
|
def _link_relative(self, templater, src, dst,
|
|
actionexec=None,
|
|
is_template=True,
|
|
ignore=None,
|
|
chmod=None):
|
|
"""
|
|
install link:relative
|
|
|
|
return
|
|
- True, None : success
|
|
- False, error_msg : error
|
|
- False, None : ignored
|
|
- False, 'aborted' : user aborted
|
|
"""
|
|
return self._link_dotfile(templater, src, dst,
|
|
actionexec=actionexec,
|
|
is_template=is_template,
|
|
ignore=ignore,
|
|
absolute=False,
|
|
chmod=chmod)
|
|
|
|
def _link_dotfile(self, templater, src, dst, actionexec=None,
|
|
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
|
|
- False, None : ignored
|
|
- False, 'aborted' : user aborted
|
|
"""
|
|
if is_template:
|
|
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,
|
|
chmod=chmod)
|
|
if not ret and not os.path.exists(tmp):
|
|
return ret, err
|
|
src = tmp
|
|
ret, err = self._symlink(src, dst, actionexec=actionexec,
|
|
absolute=absolute)
|
|
return ret, err
|
|
|
|
def _link_children(self, templater, src, dst,
|
|
actionexec=None, is_template=True, ignore=None):
|
|
"""
|
|
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(f'would create directory "{dst}"')
|
|
else:
|
|
if not self.comparing:
|
|
self.log.sub(f'creating directory "{dst}"')
|
|
self._create_dirs(dst)
|
|
|
|
if os.path.isfile(dst):
|
|
msg = ''.join([
|
|
f'Remove regular file {dst} and ',
|
|
'replace with empty directory?',
|
|
])
|
|
|
|
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):
|
|
self.log.dbg(
|
|
f'ignoring install of {src} to {dst}',
|
|
)
|
|
continue
|
|
|
|
self.log.dbg(f'symlink child {subsrc} to {subdst}')
|
|
|
|
if is_template:
|
|
self.log.dbg('child is a template')
|
|
self.log.dbg(f'install to {self.workdir} and symlink')
|
|
tmp = utils.pivot_path(subdst, self.workdir,
|
|
striphome=True, logger=self.log)
|
|
ret2, err2 = self.install(templater, subsrc, tmp,
|
|
LinkTypes.NOLINK,
|
|
actionexec=actionexec,
|
|
is_template=is_template,
|
|
ignore=ignore)
|
|
if not ret2 and err2 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, absolute=True):
|
|
"""
|
|
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):
|
|
# symlink exists
|
|
if os.path.realpath(dst) == os.path.realpath(src):
|
|
msg = f'ignoring "{dst}", link already exists'
|
|
self.log.dbg(msg)
|
|
return False, None
|
|
if self.dry:
|
|
self.log.dry(f'would remove {dst} and link to {src}')
|
|
return True, None
|
|
if self.showdiff:
|
|
self._show_diff_before_write(src, dst)
|
|
msg = f'Remove "{dst}" for link creation?'
|
|
if self.safe and not self.log.ask(msg):
|
|
return False, 'aborted'
|
|
|
|
# remove symlink
|
|
overwrite = True
|
|
try:
|
|
utils.removepath(dst)
|
|
except OSError as exc:
|
|
err = f'something went wrong with {src}: {exc}'
|
|
return False, err
|
|
|
|
if self.dry:
|
|
self.log.dry(f'would link {dst} to {src}')
|
|
return True, None
|
|
|
|
base = os.path.dirname(dst)
|
|
if not self._create_dirs(base):
|
|
err = f'error creating directory for {dst}'
|
|
return False, err
|
|
|
|
# execute pre-actions
|
|
ret, err = self._exec_pre_actions(actionexec)
|
|
if not ret:
|
|
return False, err
|
|
|
|
# re-check in case action created the file
|
|
if os.path.lexists(dst):
|
|
msg = f'Remove "{dst}" for link creation?'
|
|
if self.safe and not overwrite and not self.log.ask(msg):
|
|
return False, 'aborted'
|
|
try:
|
|
utils.removepath(dst)
|
|
except OSError as exc:
|
|
err = f'something went wrong with {src}: {exc}'
|
|
return False, err
|
|
|
|
# create symlink
|
|
lnk_src = src
|
|
if not absolute:
|
|
# relative symlink
|
|
dstrel = dst
|
|
if not os.path.isdir(dstrel):
|
|
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
|
|
|
|
def _copy_file(self, templater, src, dst,
|
|
actionexec=None, noempty=False,
|
|
ignore=None, is_template=True):
|
|
"""
|
|
install src to dst when is a file
|
|
|
|
return
|
|
- True, None : success
|
|
- False, error_msg : error
|
|
- False, None : ignored
|
|
- False, 'aborted' : user aborted
|
|
"""
|
|
self.log.dbg(f'deploy file: {src}')
|
|
self.log.dbg(f'ignore empty: {noempty}')
|
|
self.log.dbg(f'ignore pattern: {ignore}')
|
|
self.log.dbg(f'is_template: {is_template}')
|
|
self.log.dbg(f'no empty: {noempty}')
|
|
|
|
# ignore file
|
|
if utils.must_ignore([src, dst], ignore, debug=self.debug):
|
|
self.log.dbg(f'ignoring install of {src} to {dst}')
|
|
return False, None
|
|
|
|
# check no loop
|
|
if utils.samefile(src, dst):
|
|
err = f'dotfile points to itself: {dst}'
|
|
return False, err
|
|
|
|
# check source file exists
|
|
if not os.path.exists(src):
|
|
err = f'source dotfile does not exist: {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 exc:
|
|
return False, str(exc)
|
|
finally:
|
|
templater.restore_vars(saved)
|
|
# test is empty
|
|
if noempty and utils.content_empty(content):
|
|
self.log.dbg(f'ignoring empty template: {src}')
|
|
return False, None
|
|
if content is None:
|
|
err = f'empty template {src}'
|
|
return False, err
|
|
|
|
# write the file
|
|
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
|
|
|
|
def _copy_dir(self, templater, src, dst,
|
|
actionexec=None, noempty=False,
|
|
ignore=None, is_template=True):
|
|
"""
|
|
install src to dst when is a directory
|
|
|
|
return
|
|
- True, None : success
|
|
- False, error_msg : error
|
|
- False, None : ignored
|
|
- False, 'aborted' : user aborted
|
|
"""
|
|
self.log.dbg(f'deploy dir {src}')
|
|
# default to nothing installed and no error
|
|
ret = False, None
|
|
|
|
# handle all files in dir
|
|
for entry in os.listdir(src):
|
|
fpath = os.path.join(src, entry)
|
|
self.log.dbg(f'deploy sub from {dst}: {entry}')
|
|
if not os.path.isdir(fpath):
|
|
# is file
|
|
res, err = self._copy_file(templater, fpath,
|
|
os.path.join(dst, entry),
|
|
actionexec=actionexec,
|
|
noempty=noempty,
|
|
ignore=ignore,
|
|
is_template=is_template)
|
|
if not res and err:
|
|
# error occured
|
|
return res, err
|
|
|
|
if res:
|
|
# something got installed
|
|
ret = True, None
|
|
else:
|
|
# is directory
|
|
res, err = self._copy_dir(templater, fpath,
|
|
os.path.join(dst, entry),
|
|
actionexec=actionexec,
|
|
noempty=noempty,
|
|
ignore=ignore,
|
|
is_template=is_template)
|
|
if not res and err:
|
|
# error occured
|
|
return res, err
|
|
|
|
if res:
|
|
# something got installed
|
|
ret = True, None
|
|
return ret
|
|
|
|
@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)
|
|
except NotADirectoryError as exc:
|
|
err = f'opening dest file: {exc}'
|
|
return False, err
|
|
except OSError as exc:
|
|
return False, str(exc)
|
|
except TypeError as exc:
|
|
return False, str(exc)
|
|
else:
|
|
# copy file
|
|
try:
|
|
shutil.copyfile(src, dst)
|
|
# shutil.copymode(src, dst)
|
|
except OSError as exc:
|
|
return False, str(exc)
|
|
return True, None
|
|
|
|
def _write(self, src, dst, content=None,
|
|
actionexec=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(f'would install {dst}')
|
|
return True, None
|
|
|
|
if os.path.lexists(dst):
|
|
# file/symlink exists
|
|
try:
|
|
os.stat(dst)
|
|
except OSError as exc:
|
|
if exc.errno == errno.ENOENT:
|
|
# broken symlink
|
|
err = f'broken symlink {dst}'
|
|
return False, err
|
|
|
|
if self.diff:
|
|
if not self._is_different(src, dst, content=content):
|
|
self.log.dbg(f'{dst} is the same')
|
|
return False, None
|
|
|
|
if self.safe:
|
|
self.log.dbg(f'change detected for {dst}')
|
|
if self.showdiff:
|
|
# get diff
|
|
self._show_diff_before_write(src, dst,
|
|
content=content)
|
|
if not self.log.ask(f'Overwrite \"{dst}\"'):
|
|
return False, 'aborted'
|
|
overwrite = True
|
|
|
|
if self.backup:
|
|
self._backup(dst)
|
|
|
|
# create hierarchy
|
|
base = os.path.dirname(dst)
|
|
if not self._create_dirs(base):
|
|
err = f'creating directory for {dst}'
|
|
return False, err
|
|
|
|
# execute pre actions
|
|
ret, err = self._exec_pre_actions(actionexec)
|
|
if not ret:
|
|
return False, err
|
|
|
|
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 \
|
|
not self.log.ask(f'Overwrite \"{dst}\"'):
|
|
self.log.warn(f'ignoring {dst}')
|
|
return False, 'aborted'
|
|
|
|
# writing to file
|
|
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
|
|
########################################################
|
|
|
|
@classmethod
|
|
def _get_tmp_file_vars(cls, 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
|
|
tmp = None
|
|
if content:
|
|
tmp = utils.write_to_tmpfile(content)
|
|
src = tmp
|
|
ret = utils.fastdiff(src, dst)
|
|
if ret:
|
|
self.log.dbg('content differ')
|
|
if content:
|
|
utils.removepath(tmp)
|
|
return ret
|
|
|
|
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(f'diff \"{dst}\" VS \"{src}\"')
|
|
self.log.emph(diff)
|
|
|
|
def _create_dirs(self, directory):
|
|
"""mkdir -p <directory>"""
|
|
if not self.create and not os.path.exists(directory):
|
|
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(f'would mkdir -p {directory}')
|
|
return True
|
|
self.log.dbg(f'mkdir -p {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(f'backup {path} to {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"""
|
|
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
|
|
returns success, error-if-any
|
|
"""
|
|
if not self.debug:
|
|
return boolean, err
|
|
if boolean:
|
|
self.log.dbg('install: SUCCESS')
|
|
else:
|
|
if err:
|
|
self.log.dbg(f'install: ERROR: {err}')
|
|
else:
|
|
self.log.dbg('install: IGNORED')
|
|
return boolean, err
|
|
|
|
def _check_paths(self, src, dst):
|
|
"""
|
|
check and normalize param
|
|
returns <src>, <dst>, <continue>, <error>
|
|
"""
|
|
# check both path are valid
|
|
if not dst or not src:
|
|
err = f'empty dst or src for {src}'
|
|
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
|