1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-04 22:04:44 +00:00
Files
dotdrop/dotdrop/installer.py
2020-10-09 23:05:08 +02:00

633 lines
23 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.templategen import Templategen
import dotdrop.utils as utils
from dotdrop.exceptions import UndefinedException
class 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=''):
"""constructor
@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
self.workdir = os.path.expanduser(workdir)
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.comparing = False
self.action_executed = False
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):
"""
install src to dst using a template
@templater: the templater object
@src: dotfile source path in dotpath
@dst: dotfile destination path in the FS
@actionexec: action executor callback
@noempty: render empty template flag
@ignore: pattern to ignore when installing
@template: template this dotfile
return
- True, None : success
- False, error_msg : error
- False, None : ignored
"""
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)
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)
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)
isdir = os.path.isdir(src)
if self.debug:
self.log.dbg('install {} to {}'.format(src, dst))
self.log.dbg('is a directory \"{}\": {}'.format(src, isdir))
if isdir:
b, e = self._install_dir(templater, src, dst,
actionexec=actionexec,
noempty=noempty, ignore=ignore,
template=template)
return self._log_install(b, e)
b, e = self._install_file(templater, src, dst,
actionexec=actionexec,
noempty=noempty, ignore=ignore,
template=template)
return self._log_install(b, e)
def link(self, templater, src, dst, actionexec=None, template=True):
"""
set src as the link target of dst
@templater: the templater
@src: dotfile source path in dotpath
@dst: dotfile destination path in the FS
@actionexec: action executor callback
@template: template this dotfile
return
- True, None : success
- False, error_msg : error
- False, None : ignored
"""
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)
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)
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)
if template and Templategen.is_template(src):
if self.debug:
self.log.dbg('dotfile is a template')
self.log.dbg('install to {} and symlink'.format(self.workdir))
tmp = self._pivot_path(dst, self.workdir, striphome=True)
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)
src = tmp
b, e = self._link(src, dst, actionexec=actionexec)
return self._log_install(b, e)
def link_children(self, templater, src, dst, actionexec=None,
template=True):
"""
link all files under a given directory
@templater: the templater
@src: dotfile source path in dotpath
@dst: dotfile destination path in the FS
@actionexec: action executor callback
@template: template this dotfile
return
- True, None: success
- False, error_msg: error
- False, None, ignored
"""
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)
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)
# Fail if source not a directory
if not os.path.isdir(parent):
if self.debug:
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)
dst = os.path.normpath(os.path.expanduser(dst))
if not os.path.lexists(dst):
self.log.sub('creating directory "{}"'.format(dst))
os.makedirs(dst)
if os.path.isfile(dst):
msg = ''.join([
'Remove regular file {} and ',
'replace with empty directory?',
]).format(dst)
if self.safe and not self.log.ask(msg):
err = 'ignoring "{}", nothing installed'.format(dst)
return self._log_install(False, err)
os.unlink(dst)
os.mkdir(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)):
src = srcs[i]
dst = dsts[i]
if self.debug:
self.log.dbg('symlink child {} to {}'.format(src, dst))
if template and Templategen.is_template(src):
if self.debug:
self.log.dbg('dotfile is a template')
self.log.dbg('install to {} and symlink'
.format(self.workdir))
tmp = self._pivot_path(dst, self.workdir, striphome=True)
r, e = self.install(templater, src, tmp, actionexec=actionexec,
template=template)
if not r and e and not os.path.exists(tmp):
continue
src = tmp
ret, err = self._link(src, dst, actionexec=actionexec)
if ret:
installed += 1
# void actionexec if dotfile installed
# to prevent from running actions multiple times
actionexec = None
else:
if err:
return self._log_install(ret, err)
return self._log_install(installed > 0, None)
def _link(self, src, dst, actionexec=None):
"""
set src as a link target of dst
return
- True, None: success
- False, error_msg: error
- False, None, ignored
"""
overwrite = not self.safe
if os.path.lexists(dst):
if os.path.realpath(dst) == os.path.realpath(src):
msg = 'ignoring "{}", link already exists'.format(dst)
if self.debug:
self.log.dbg(msg)
return False, None
if self.dry:
self.log.dry('would remove {} and link to {}'.format(dst, src))
return True, None
if self.showdiff:
self._diff_before_write(src, dst, quiet=False)
msg = 'Remove "{}" for link creation?'.format(dst)
if self.safe and not self.log.ask(msg):
err = 'ignoring "{}", link was not created'.format(dst)
return False, err
overwrite = True
try:
utils.remove(dst)
except OSError as e:
err = 'something went wrong with {}: {}'.format(src, e)
return False, err
if self.dry:
self.log.dry('would link {} to {}'.format(dst, src))
return True, None
base = os.path.dirname(dst)
if not self._create_dirs(base):
err = 'error creating directory for {}'.format(dst)
return False, err
r, e = self._exec_pre_actions(actionexec)
if not r:
return False, e
# re-check in case action created the file
if os.path.lexists(dst):
msg = 'Remove "{}" for link creation?'.format(dst)
if self.safe and not overwrite and not self.log.ask(msg):
err = 'ignoring "{}", link was not created'.format(dst)
return False, err
try:
utils.remove(dst)
except OSError as e:
err = 'something went wrong with {}: {}'.format(src, e)
return False, err
os.symlink(src, dst)
self.log.sub('linked {} to {}'.format(dst, src))
return True, None
def _get_tmp_file_vars(self, src, dst):
tmp = {}
tmp['_dotfile_sub_abs_src'] = src
tmp['_dotfile_sub_abs_dst'] = dst
return tmp
def _install_file(self, templater, src, dst,
actionexec=None, noempty=False,
ignore=[], template=True):
"""install src to dst when is a file"""
if self.debug:
self.log.dbg('deploy file: {}'.format(src))
self.log.dbg('ignore empty: {}'.format(noempty))
self.log.dbg('ignore pattern: {}'.format(ignore))
self.log.dbg('template: {}'.format(template))
self.log.dbg('no empty: {}'.format(noempty))
if utils.must_ignore([src, dst], ignore, debug=self.debug):
if self.debug:
self.log.dbg('ignoring install of {} to {}'.format(src, dst))
return False, None
if utils.samefile(src, dst):
# symlink loop
err = 'dotfile points to itself: {}'.format(dst)
return False, err
if not os.path.exists(src):
err = 'source dotfile does not exist: {}'.format(src)
return False, err
# handle the file
content = None
if template:
# template the file
saved = templater.add_tmp_vars(self._get_tmp_file_vars(src, dst))
try:
content = templater.generate(src)
except UndefinedException as e:
return False, str(e)
finally:
templater.restore_vars(saved)
if noempty and utils.content_empty(content):
if self.debug:
self.log.dbg('ignoring empty template: {}'.format(src))
return False, None
if content is None:
err = 'empty template {}'.format(src)
return False, err
ret, err = self._write(src, dst,
content=content,
actionexec=actionexec,
template=template)
# build return values
if ret < 0:
# error
return False, err
if ret > 0:
# already exists
if self.debug:
self.log.dbg('ignoring {}'.format(dst))
return False, None
if ret == 0:
# success
if not self.dry and not self.comparing:
self.log.sub('copied {} to {}'.format(src, dst))
return True, None
# error
err = 'installing {} to {}'.format(src, dst)
return False, err
def _install_dir(self, templater, src, dst,
actionexec=None, noempty=False,
ignore=[], template=True):
"""install src to dst when is a directory"""
if self.debug:
self.log.dbg('install dir {}'.format(src))
self.log.dbg('ignore empty: {}'.format(noempty))
# default to nothing installed and no error
ret = False, None
if not self._create_dirs(dst):
err = 'creating directory for {}'.format(dst)
return False, err
# handle all files in dir
for entry in os.listdir(src):
f = os.path.join(src, entry)
if not os.path.isdir(f):
# is file
res, err = self._install_file(templater, f,
os.path.join(dst, entry),
actionexec=actionexec,
noempty=noempty,
ignore=ignore,
template=template)
if not res and err:
# error occured
ret = res, err
break
elif res:
# something got installed
ret = True, None
else:
# is directory
res, err = self._install_dir(templater, f,
os.path.join(dst, entry),
actionexec=actionexec,
noempty=noempty,
ignore=ignore,
template=template)
if not res and err:
# error occured
ret = res, err
break
elif res:
# something got installed
ret = True, None
return ret
def _fake_diff(self, dst, content):
"""
fake diff by comparing file content with content
returns True if same
"""
cur = ''
with open(dst, 'br') as f:
cur = f.read()
return cur == content
def _write(self, src, dst, content=None,
actionexec=None, template=True):
"""
copy dotfile / write content to file
return 0, None: for success,
1, None: when already exists
-1, err: when error
content is always empty if template is False
and is to be ignored
"""
overwrite = not self.safe
if self.dry:
self.log.dry('would install {}'.format(dst))
return 0, None
if os.path.lexists(dst):
rights = os.stat(src).st_mode
samerights = False
try:
samerights = os.stat(dst).st_mode == rights
except OSError as e:
if e.errno == errno.ENOENT:
# broken symlink
err = 'broken symlink {}'.format(dst)
return -1, err
diff = None
if self.diff:
diff = self._diff_before_write(src, dst,
content=content,
quiet=True)
if not diff and samerights:
if self.debug:
self.log.dbg('{} is the same'.format(dst))
return 1, None
if self.safe:
if self.debug:
self.log.dbg('change detected for {}'.format(dst))
if self.showdiff:
if diff is None:
# get diff
diff = self._diff_before_write(src, dst,
content=content,
quiet=True)
if diff:
self._print_diff(src, dst, diff)
if not self.log.ask('Overwrite \"{}\"'.format(dst)):
self.log.warn('ignoring {}'.format(dst))
return 1, None
overwrite = True
if self.backup and os.path.lexists(dst):
self._backup(dst)
base = os.path.dirname(dst)
if not self._create_dirs(base):
err = 'creating directory for {}'.format(dst)
return -1, err
r, e = self._exec_pre_actions(actionexec)
if not r:
return -1, e
if self.debug:
self.log.dbg('install dotfile to \"{}\"'.format(dst))
# re-check in case action created the file
if self.safe and not overwrite and os.path.lexists(dst):
if not self.log.ask('Overwrite \"{}\"'.format(dst)):
self.log.warn('ignoring {}'.format(dst))
return 1, None
if template:
# write content the file
try:
with open(dst, 'wb') as f:
f.write(content)
shutil.copymode(src, dst)
except NotADirectoryError as e:
err = 'opening dest file: {}'.format(e)
return -1, err
except Exception as e:
return -1, str(e)
else:
# copy file
try:
shutil.copyfile(src, dst)
shutil.copymode(src, dst)
except Exception as e:
return -1, str(e)
return 0, None
def _diff_before_write(self, src, dst, content=None, quiet=False):
"""
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, raw=False,
diff_cmd=self.diff_cmd)
if tmp:
utils.remove(tmp, quiet=True)
if not quiet and diff:
self._print_diff(src, dst, diff)
return diff
def _print_diff(self, src, dst, diff):
"""show diff to user"""
self.log.log('diff \"{}\" VS \"{}\"'.format(dst, src))
self.log.emph(diff)
def _create_dirs(self, directory):
"""mkdir -p <directory>"""
if not self.create and not os.path.exists(directory):
if self.debug:
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('would mkdir -p {}'.format(directory))
return True
if self.debug:
self.log.dbg('mkdir -p {}'.format(directory))
os.makedirs(directory)
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('backup {} to {}'.format(path, dst))
os.rename(path, dst)
def _pivot_path(self, path, newdir, striphome=False):
"""change path to be under newdir"""
if self.debug:
self.log.dbg('pivot new dir: \"{}\"'.format(newdir))
self.log.dbg('strip home: {}'.format(striphome))
if striphome:
path = utils.strip_home(path)
sub = path.lstrip(os.sep)
new = os.path.join(newdir, sub)
if self.debug:
self.log.dbg('pivot \"{}\" to \"{}\"'.format(path, new))
return new
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 _install_to_temp(self, templater, src, dst, tmpdir, template=True):
"""install a dotfile to a tempdir"""
tmpdst = self._pivot_path(dst, tmpdir)
r = self.install(templater, src, tmpdst, template=template)
return r, tmpdst
def install_to_temp(self, templater, tmpdir, src, dst, template=True):
"""install a dotfile to a tempdir"""
ret = False
tmpdst = ''
# save some flags while comparing
self.comparing = True
drysaved = self.dry
self.dry = False
diffsaved = self.diff
self.diff = False
createsaved = self.create
self.create = True
# normalize src and dst
src = os.path.expanduser(src)
dst = os.path.expanduser(dst)
if self.debug:
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)
ret, err = r
if self.debug:
self.log.dbg('tmp installed in {}'.format(tmpdst))
# reset flags
self.dry = drysaved
self.diff = diffsaved
self.comparing = False
self.create = createsaved
return ret, err, tmpdst