1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-07 20:54:22 +00:00

first commit

This commit is contained in:
deadc0de6
2017-03-07 19:19:00 +01:00
commit a356979555
13 changed files with 1327 additions and 0 deletions

0
dotdrop/__init__.py Normal file
View File

133
dotdrop/config.py Normal file
View File

@@ -0,0 +1,133 @@
"""
author: deadc0de6 (https://github.com/deadc0de6)
config file manager
"""
import yaml
import os
from dotfile import Dotfile
from logger import Logger
class Cfg:
key_all = 'ALL'
key_config = 'config'
key_profiles = 'profiles'
key_dotfiles = 'dotfiles'
key_dotfiles_src = 'src'
key_dotfiles_dst = 'dst'
def __init__(self, cfgpath, dotpath):
if not os.path.exists(cfgpath):
raise ValueError('config file does not exist')
self.cfgpath = cfgpath
self.log = Logger()
relconf = dotpath
if not relconf.startswith(os.sep):
relconf = os.path.join(os.path.dirname(cfgpath), dotpath)
self.configs = {'dotpath': relconf}
self.dotfiles = {}
self.profiles = {}
self.prodots = {}
if not self._load_file():
raise ValueError('config is not valid')
def _load_file(self):
with open(self.cfgpath, 'r') as f:
self.content = yaml.load(f)
if not self._is_valid():
return False
return self._parse()
def _is_valid(self):
if self.key_profiles not in self.content:
self.log.err('missing \"%s\" in config' % (self.key_profiles))
return False
if self.key_config not in self.content:
self.log.err('missing \"%s\" in config' % (self.key_config))
return False
if self.key_dotfiles not in self.content:
self.log.err('missing \"%s\" in config' % (self.key_dotfiles))
return False
return True
def _parse(self):
""" parse config file """
self.profiles = self.content[self.key_profiles]
if self.profiles is None:
self.profiles = {}
self.configs = self.content[self.key_config]
# contains all defined dotfiles
if self.content[self.key_dotfiles] is not None:
for k, v in self.content[self.key_dotfiles].items():
src = v[self.key_dotfiles_src]
dst = v[self.key_dotfiles_dst]
self.dotfiles[k] = Dotfile(k, dst, src)
# contains a list of dotfiles defined for each profile
for k, v in self.profiles.items():
self.prodots[k] = []
if v is None:
continue
if len(v) == 1 and v == [self.key_all]:
self.prodots[k] = self.dotfiles.values()
else:
self.prodots[k].extend([self.dotfiles[dot] for dot in v])
# make sure we have a correct dotpath
if not self.configs['dotpath'].startswith(os.sep):
relconf = os.path.join(os.path.dirname(
self.cfgpath), self.configs['dotpath'])
self.configs['dotpath'] = relconf
return True
def new(self, dotfile, profile):
""" import new dotfile """
dots = self.content[self.key_dotfiles]
if dots is None:
self.content[self.key_dotfiles] = {}
dots = self.content[self.key_dotfiles]
if self.content[self.key_dotfiles] and dotfile.key in dots:
self.log.err('\"%s\" entry already exists in dotfiles' %
(dotfile.key))
return False
home = os.path.expanduser('~')
dotfile.dst = dotfile.dst.replace(home, '~')
dots[dotfile.key] = {
self.key_dotfiles_dst: dotfile.dst,
self.key_dotfiles_src: dotfile.src
}
profiles = self.profiles
if profile in profiles and profiles[profile] != [self.key_all]:
if self.content[self.key_profiles][profile] is None:
self.content[self.key_profiles][profile] = []
self.content[self.key_profiles][profile].append(dotfile.key)
elif profile not in profiles:
if self.content[self.key_profiles] is None:
self.content[self.key_profiles] = {}
self.content[self.key_profiles][profile] = [dotfile.key]
self.profiles = self.content[self.key_profiles]
def get_dotfiles(self, profile):
""" returns a list of dotfiles for a specific profile """
if profile not in self.prodots:
return []
tmp = sorted(self.prodots[profile], key=lambda x: x.key, reverse=True)
return tmp
def get_profiles(self):
""" returns all defined profiles """
return self.profiles.keys()
def get_configs(self):
""" returns all defined configs """
return self.configs.copy()
def dump(self):
""" dump config file """
return yaml.dump(self.content, default_flow_style=False, indent=2)
def save(self):
""" save config file to path """
with open(self.cfgpath, 'w') as f:
ret = yaml.dump(self.content, f,
default_flow_style=False, indent=2)
return ret

178
dotdrop/dotdrop.py Executable file
View File

@@ -0,0 +1,178 @@
"""
author: deadc0de6 (https://github.com/deadc0de6)
entry point
"""
import os
import sys
import subprocess
import utils
from docopt import docopt
from logger import Logger
from templategen import Templategen
from installer import Installer
from dotfile import Dotfile
from config import Cfg
VERSION = '0.1'
DEF_DOTFILES = 'dotfiles'
CUR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOG = Logger()
HOSTNAME = os.uname()[1]
BANNER = """ _ _ _
__| | ___ | |_ __| |_ __ ___ _ __
/ _` |/ _ \| __/ _` | '__/ _ \| '_ |
\__,_|\___/ \__\__,_|_| \___/| .__/ v%s
|_|""" % (VERSION)
USAGE = """
%s
Usage:
dotdrop.py install [--profile=<profile>] [--cfg=<path>]
[(-f | --force)] [--nodiff] [--dry]
dotdrop.py compare [--profile=<profile>] [--cfg=<path>]
dotdrop.py list [--cfg=<path>]
dotdrop.py import [--cfg=<path>] [--profile=<profile>] [--dry] <paths>...
dotdrop.py (-h | --help)
dotdrop.py (-v | --version)
Options:
--profile=<profiles> Specify the profile to use [default: %s].
--cfg=<path> Path to the config [default: %s/config.yaml].
--dry Dry run.
--nodiff Do not diff when installing [default: False].
-f --force Do not warn if exists [default: False].
-v --version Show version.
-h --help Show this screen.
""" % (BANNER, HOSTNAME, CUR)
###########################################################
# entry point
###########################################################
def install(opts, conf):
dotfiles = conf.get_dotfiles(opts['profile'])
if dotfiles == []:
LOG.err('no dotfiles defined for this profile (\"%s\")' %
(str(opts['profile'])))
return False
t = Templategen(base=opts['dotpath'])
inst = Installer(create=opts['create'], backup=opts['backup'],
dry=opts['dry'], safe=opts['safe'], base=opts['dotpath'],
diff=opts['installdiff'])
installed = []
for dotfile in dotfiles:
r = inst.install(t, opts['profile'], dotfile.src, dotfile.dst)
installed.extend(r)
LOG.log('\n%u dotfile(s) installed.' % (len(installed)))
return True
def compare(opts, conf, tmp):
dotfiles = conf.get_dotfiles(opts['profile'])
if dotfiles == []:
LOG.err('no dotfiles defined for this profile (\"%s\")' %
(str(opts['profile'])))
return False
t = Templategen(base=opts['dotpath'])
inst = Installer(create=opts['create'], backup=opts['backup'],
dry=opts['dry'], base=opts['dotpath'], quiet=True)
for dotfile in dotfiles:
LOG.log('diffing \"%s\" VS \"%s\"' % (dotfile.key, dotfile.dst))
inst.compare(t, tmp, opts['profile'], dotfile.src, dotfile.dst)
return len(dotfiles) > 0
def importer(opts, conf, paths):
home = os.path.expanduser('~')
cnt = 0
for path in paths:
dst = path.rstrip(os.sep)
key = dst.split(os.sep)[-1]
if key == 'config':
key = '_'.join(dst.split(os.sep)[-2:])
key = key.lstrip('.')
key = key.lower()
if os.path.isdir(dst):
key = 'd_%s' % (key)
else:
key = 'f_%s' % (key)
src = dst.lstrip(home).lstrip('.')
dotfile = Dotfile(key, dst, src)
srcf = os.path.join(CUR, opts['dotpath'], src)
if os.path.exists(srcf):
LOG.err('\"%s\" already exists !' % (srcf))
continue
conf.new(dotfile, opts['profile'])
cmd = ['mkdir', '-p', '%s' % (os.path.dirname(srcf))]
if opts['dry']:
LOG.dry('would run: %s' % (' '.join(cmd)))
else:
utils.run(cmd, raw=False, log=False)
cmd = ['cp', '-r', '%s' % (dst), '%s' % (srcf)]
if opts['dry']:
LOG.dry('would run: %s' % (' '.join(cmd)))
else:
utils.run(cmd, raw=False, log=False)
LOG.sub('\"%s\" imported' % (path))
cnt += 1
if opts['dry']:
LOG.dry('new config file would be:')
LOG.raw(conf.dump())
else:
conf.save()
LOG.log('\n%u file(s) imported.' % (cnt))
def list_profiles(conf):
LOG.log('Available profile(s):')
for p in conf.get_profiles():
LOG.sub(p)
LOG.log('')
def header():
LOG.log(BANNER)
LOG.log("")
if __name__ == '__main__':
ret = True
args = docopt(USAGE, version=VERSION)
conf = Cfg(args['--cfg'], DEF_DOTFILES)
opts = conf.get_configs()
opts['dry'] = args['--dry']
opts['profile'] = args['--profile']
opts['safe'] = not args['--force']
opts['installdiff'] = not args['--nodiff']
header()
try:
if args['list']:
list_profiles(conf)
elif args['install']:
ret = install(opts, conf)
elif args['compare']:
tmp = utils.get_tmpdir()
if compare(opts, conf, tmp):
LOG.log('generated temporary files available under %s' % (tmp))
else:
os.rmdir(tmp)
elif args['import']:
importer(opts, conf, args['<paths>'])
except KeyboardInterrupt:
LOG.err('interrupted')
if ret:
sys.exit(0)
sys.exit(1)

17
dotdrop/dotfile.py Normal file
View File

@@ -0,0 +1,17 @@
"""
author: deadc0de6 (https://github.com/deadc0de6)
represents a dotfile in dotdrop
"""
class Dotfile:
def __init__(self, key, dst, src):
self.key = key
self.dst = dst
self.src = src
def __str__(self):
string = 'key:%s, src: %s, dst: %s' % (self.key,
self.src, self.dst)
return string

139
dotdrop/installer.py Normal file
View File

@@ -0,0 +1,139 @@
"""
author: deadc0de6 (https://github.com/deadc0de6)
handle the installation of dotfiles
"""
import os
import utils
from logger import Logger
class Installer:
BACKUP_SUFFIX = '.dotdropbak'
def __init__(self, base='.', create=True, backup=True,
dry=False, safe=False, quiet=False, diff=True):
self.create = create
self.backup = backup
self.dry = dry
self.safe = safe
self.base = base
self.quiet = quiet
self.diff = diff
self.log = Logger()
def install(self, templater, profile, src, dst):
src = os.path.join(self.base, os.path.expanduser(src))
dst = os.path.join(self.base, os.path.expanduser(dst))
if os.path.isdir(src):
return self._handle_dir(templater, profile, src, dst)
return self._handle_file(templater, profile, src, dst)
def _preparesub(self):
if not os.path.exists(self.sub):
os.makedirs(self.sub)
def _handle_file(self, templater, profile, src, dst):
content = templater.generate(src, profile)
if content is None:
self.log.err('generate from template \"%s\"' % (src))
return []
st = os.stat(src)
ret = self._write(dst, content, st.st_mode)
if ret < 0:
self.log.err('installing %s to %s' % (src, dst))
return []
if ret > 0:
self.log.sub('ignoring \"%s\", same content' % (dst))
return []
if ret == 0:
if not self.quiet:
self.log.sub('copied %s to %s' % (src, dst))
return [(src, dst)]
return []
def _handle_dir(self, templater, profile, src, dst):
ret = []
for entry in os.listdir(src):
f = os.path.join(src, entry)
if not os.path.isdir(f):
res = self._handle_file(
templater, profile, f, os.path.join(dst, entry))
ret.extend(res)
else:
res = self._handle_dir(
templater, profile, f, os.path.join(dst, entry))
ret.extend(res)
return ret
def _fake_diff(self, dst, content):
cur = ''
with open(dst, 'br') as f:
cur = f.read()
return cur == content
def _write(self, dst, content, rights):
""" write file """
if self.dry:
self.log.dry('would install %s' % (dst))
return 0
if os.path.exists(dst) and self.safe:
if self.diff and self._fake_diff(dst, content):
return 1
if not self.log.ask('Overwrite \"%s\"' % (dst)):
self.log.warn('ignoring \"%s\", already present' % (dst))
return 1
if self.backup and os.path.exists(dst):
self._backup(dst)
base = os.path.dirname(dst)
if not self._create_dirs(base):
self.log.err('creating directory for %s' % (dst))
return -1
with open(dst, 'wb') as f:
f.write(content)
os.chmod(dst, rights)
return 0
def _create_dirs(self, folder):
if not self.create and not os.path.exists(folder):
return False
if os.path.exists(folder):
return True
os.makedirs(folder)
return os.path.exists(folder)
def _backup(self, path):
if self.dry:
return
dst = path.rstrip(os.sep) + self.BACKUP_SUFFIX
self.log.log('backup %s to %s' % (path, dst))
os.rename(path, dst)
def _install_to_temp(self, templater, profile, src, dst, tmpfolder):
sub = dst
if dst[0] == os.sep:
sub = dst[1:]
tmpdst = os.path.join(tmpfolder, sub)
return self.install(templater, profile, src, tmpdst), tmpdst
def compare(self, templater, tmpfolder, profile, src, dst):
drysaved = self.dry
self.dry = False
diffsaved = self.diff
self.diff = False
src = os.path.expanduser(src)
dst = os.path.expanduser(dst)
if not os.path.exists(dst):
self.log.warn('\"%s\" does not exist on local' % (dst))
else:
ret, tmpdst = self._install_to_temp(
templater, profile, src, dst, tmpfolder)
if ret:
diff = utils.diff(tmpdst, dst, log=False, raw=False)
if diff == '':
self.log.raw('same file')
else:
self.log.emph(diff)
self.dry = drysaved
self.diff = diffsaved

65
dotdrop/logger.py Normal file
View File

@@ -0,0 +1,65 @@
"""
author: deadc0de6 (https://github.com/deadc0de6)
handle logging to stdout/stderr
"""
import sys
class Logger:
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
RESET = '\033[0m'
EMPH = '\033[33m'
def __init__(self):
pass
def log(self, string, end='\n', pre=''):
cs = self._color(self.BLUE)
ce = self._color(self.RESET)
sys.stdout.write('%s%s%s%s%s' % (pre, cs, string, end, ce))
def sub(self, string):
cs = self._color(self.BLUE)
ce = self._color(self.RESET)
sys.stdout.write('\t%s->%s %s\n' % (cs, ce, string))
def emph(self, string):
cs = self._color(self.EMPH)
ce = self._color(self.RESET)
sys.stderr.write('%s%s%s' % (cs, string, ce))
def err(self, string, end='\n'):
cs = self._color(self.RED)
ce = self._color(self.RESET)
sys.stderr.write('%s[ERR]%s %s%s' % (cs, string, end, ce))
def warn(self, string, end='\n'):
cs = self._color(self.YELLOW)
ce = self._color(self.RESET)
sys.stderr.write('%s[WARN]%s %s%s' % (cs, string, end, ce))
def dry(self, string, end='\n'):
cs = self._color(self.GREEN)
ce = self._color(self.RESET)
sys.stdout.write('%s[DRY]%s %s%s' % (cs, string, end, ce))
def raw(self, string, end='\n'):
sys.stdout.write('%s%s' % (string, end))
def ask(self, query):
cs = self._color(self.BLUE)
ce = self._color(self.RESET)
q = '%s%s%s' % (cs, query + ' [y/N] ? ', ce)
r = input(q)
return r == 'y'
def _color(self, col):
if not sys.stdout.isatty():
return ''
return col

56
dotdrop/templategen.py Normal file
View File

@@ -0,0 +1,56 @@
"""
author: deadc0de6 (https://github.com/deadc0de6)
jinja2 template generator
"""
import os
import utils
from jinja2 import Environment, Template, FileSystemLoader
BLOCK_START = '{%@@'
BLOCK_END = '@@%}'
VAR_START = '{{@@'
VAR_END = '@@}}'
COMMENT_START = '{#@@'
COMMENT_END = '@@#}'
class Templategen:
def __init__(self, base='.'):
self.base = base
loader = FileSystemLoader(self.base)
self.env = Environment(loader=loader,
trim_blocks=True, lstrip_blocks=True,
keep_trailing_newline=True,
block_start_string=BLOCK_START,
block_end_string=BLOCK_END,
variable_start_string=VAR_START,
variable_end_string=VAR_END,
comment_start_string=COMMENT_START,
comment_end_string=COMMENT_END)
def generate(self, src, profile):
return self._handle_file(src, profile)
def _handle_file(self, src, profile):
""" generate the file content from template """
filetype = utils.run(['file', '-b', src], raw=False)
istext = 'text' in filetype
if not istext:
return self._handle_bin_file(src, profile)
return self._handle_text_file(src, profile)
def _handle_text_file(self, src, profile):
l = len(self.base) + 1
template = self.env.get_template(src[l:])
content = template.render(profile=profile)
content = content.encode('UTF-8')
return content
def _handle_bin_file(self, src, profile):
# this is dirty
if not src.startswith(self.base):
src = os.path.join(self.base, src)
with open(src, 'rb') as f:
return f.read()

29
dotdrop/utils.py Normal file
View File

@@ -0,0 +1,29 @@
"""
author: deadc0de6 (https://github.com/deadc0de6)
utilities
"""
import subprocess
import tempfile
from logger import Logger
LOG = Logger()
def run(cmd, log=False, raw=True):
""" expects a list """
if log:
LOG.log('cmd: \"%s\"' % (' '.join(cmd)))
p = subprocess.Popen(cmd, shell=False,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if raw:
return p.stdout.readlines()
return ''.join([x.decode("utf-8") for x in p.stdout.readlines()])
def diff(src, dst, log=False, raw=True):
return run(['diff', '-r', src, dst], log=log, raw=raw)
def get_tmpdir():
return tempfile.mkdtemp(prefix='dotdrop-')