persoconf/persoconf/main.py

432 lines
14 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import logging
import os, os.path
import shutil
import time
import tarfile
from metafile import Metafile, MalformedMetafileError, NoPathDefinedError
# FUNCTIONS
########################################
def yes_no_question(question="Yes or no?", default=False):
prompt=""
if(default is True):
prompt = " [Y/n]"
else:
prompt = " [y/N]"
answer = input(question + prompt).lower()
if(len(answer) == 0):
return default
else:
answer = answer[0]
if(default is True):
if answer in ['n','0']:
return False
else:
if answer in ['y','o','1']:
return True
return default
def contractuser(path):
home = os.path.expanduser("~")
if path.startswith(home):
return path.replace(home, "~", 1) # Replace only one instance: the first one
else:
return path
def copy_file_or_directory(logger, path_dst, path_src, overwrite=True):
# Expand '~' as user's home dir
dst = os.path.expanduser(path_dst)
src = os.path.expanduser(path_src)
if overwrite:
# --------------------------------------
def replace_backup():
try:
logger.info("Putting backup back in place")
shutil.move(dst + ".bak", dst)
except FileNotFoundError:
logger.error("No backup of %s found. Oops" % dst)
return False
except PermissionError:
logger.error("Cannot replace backup %s because of permissions" % (dst + ".bak"))
return False
except FileExistsError:
logger.error("%s has been created, you need to chose manually to keep it or to use the backup %s" % (dst, dst + ".bak"))
return False
return True
# --------------------------------------
try:
logger.info("Moving old directory %s" % dst)
shutil.move(dst, dst + ".bak")
except FileNotFoundError:
logger.info("No file %s exists yet" % dst)
except PermissionError:
logger.error("Unable to write %s, please check permissions" % (dst + ".bak"))
return False
except FileExistsError:
logger.error("%s already exists" % (dst + ".bak"))
return False
# No overwrite ? No problem.
else:
def replace_backup():
return True
# Copy the conf dir
# --------------------------------------
try:
logger.info("Copying directory %s" % src)
shutil.copytree(src, dst)
except FileNotFoundError:
logger.error("%s does not seem to exist" % src)
replace_backup()
return False
except PermissionError:
logger.error("Unable to write %s, please check permissions" % dst)
replace_backup()
return False
except FileExistsError:
logger.error("The directory '%s' already exists" % dst)
replace_backup()
return False
# It's not a dir, it's a file !
# --------------------------------------
except NotADirectoryError:
# Try not to overwrite an existing file.
# Note that if 'overwrite' was set to True, any existing file would
# have been moved at this point.
if os.path.exists(dst):
logger.error("The file %s already exists" % dst)
replace_backup()
return False
try:
logger.info("Copying file %s" % src)
shutil.copyfile(src, dst)
except PermissionError:
logger.error("Unable to write %s, please check permissions" % dst)
replace_backup()
return False
# Everything went well, time to remove the backup
# --------------------------------------
if overwrite:
# As before, try it as a dir first
try:
logger.info("Removing backup directory %s" % (dst + ".bak"))
shutil.rmtree(dst + ".bak")
except FileNotFoundError:
logger.warning("Backup file %s seems to be already gone" % (dst + ".bak"))
except PermissionError:
logger.warning("Cannot remove backup dir %s, you will have to do it manually" % (dst + ".bak"))
# If not, try it as a file
except NotADirectoryError:
try:
logger.info("Removing backup file %s" % (dst + ".bak"))
os.remove(dst + ".bak")
except PermissionError:
logger.warning("Cannot remove backup file %s, you will have to do it manually" % (dst + ".bak"))
# The End.
return True
# ARGPARSE
########################################
# Argument parsing using argparse
parser = argparse.ArgumentParser()
parser.add_argument( "--rootdir", type=str, help="The persoconf directory to use", default="~/.config/persoconf/" )
parser.add_argument( "--metafile", type=str, help="The name of the metadata files", default="META" )
parser.add_argument( '--verbose', "-v", help='Spout out more logs', action="store_true")
subparsers = parser.add_subparsers( dest="command", title="commands") #, description="Valid commands" )
# Help
parser_help = subparsers.add_parser("help", help="Print this help")
# Package
parser_package = subparsers.add_parser("package", help="Package a persoconf repository")
parser_package.add_argument( "apps", type=str, help="The apps to package ; defaults to all apps", nargs="*", default=[] )
parser_package.add_argument( "--pkgtype", "-t", type=str, help="The type of package to use", nargs="?", default="tar", choices = [ "tar" ] )
parser_package.add_argument( "--pkgname", "-p", type=str, help="The name of the package to create", nargs="?", default="persoconf." + (time.strftime("%Y%m%d")) )
# Update
parser_update = subparsers.add_parser("update", help="Backup an app configuration in the persoconf directory with the configuration in use now")
parser_update.add_argument( 'app', help='The app to update', type=str )
parser_update.add_argument( 'files', help='The files to update ; default to all added files', type=str, nargs="*", default=[])
# Add
parser_add = subparsers.add_parser("add", help="Add an app to the persoconf directory, or add a config file to an existing app")
parser_add.add_argument( 'app', help="The app to add, or the app to add a conf file to", type=str)
parser_add.add_argument( 'conf', help="The conf file or directory to add to the app", nargs="?", type=str)
parser.add_argument( '--confname', help='A special name to save the conf file/directory in the persoconf directory', type=str)
args = parser.parse_args()
# Transform paths
args.rootdir = os.path.expanduser(args.rootdir)
# LOGGING
########################################
log = logging.getLogger("persoconf")
log.setLevel(logging.DEBUG)
# Console handler
log_handler_console = logging.StreamHandler()
log_handler_console.setLevel(logging.WARNING)
# Let's adjust the console handler to the verbosity level required
if(args.verbose):
log_handler_console.setLevel(logging.DEBUG)
log_formatter_console = logging.Formatter("%(name)s:%(levelname)s: %(message)s")
log_handler_console.setFormatter(log_formatter_console)
log.addHandler(log_handler_console)
# SCRIPT
########################################
# If we were asked for help, let's give some
if args.command in [ "help", None ]:
parser.print_help()
exit()
# Let's check that the rootdir exists
if not os.path.isdir(args.rootdir):
if os.path.exists(args.rootdir):
log.error("'%s' exists but is a file" % str(args.rootdir))
log.warning("'%s' does not exist" % str(args.rootdir))
if not yes_no_question( "Do you want to create the persoconf directory '%s'?" % str(args.rootdir) ) :
log.info("The persoconf directory was not created")
exit()
# Create the directory
log.info("Creating persoconf directory...")
os.mkdir(args.rootdir)
print("New persoconf directory created at '%s'" % args.rootdir)
# ADD COMMAND
########################################
if args.command == "add":
# Check (and create) the app directory
appdir = args.rootdir + "/" + args.app
if os.path.isdir(appdir):
if args.conf is None:
log.warning("App '%s' already exists" % args.app)
exit()
elif os.path.exists(appdir):
log.error("The name '%s' is already used by a file in the persoconf directory" % args.app)
exit()
else:
log.info("Creating app '%s' in persoconf directory..." % args.app)
os.mkdir(appdir)
print("New app '%s' created in persoconf directory" % args.app)
if args.conf is None:
exit()
# Check that the conf file we are saving exists
if args.confname:
confname = args.confname
else:
confname = os.path.basename(args.conf)
while confname[0] == ".":
confname = confname[1:] # Remove leading dots
confpath = appdir + "/" + confname
# Load app META file
try:
appmeta = Metafile(json_path = appdir + "/" + args.metafile)
except FileNotFoundError:
log.info("Metafile %s does not exist for app %s" % (args.metafile, args.app))
appmeta = Metafile( name=args.app )
appmeta.json_path = "/".join( [args.rootdir, args.app, args.metafile] )
except MalformedMetafileError:
log.error("Malformed metafile %s in app %s" % (args.metafile, args.app) )
exit(1)
# Check that the file is really new
absconf = contractuser( os.path.abspath(os.path.expanduser(args.conf)) )
try:
dstdic = appmeta.get_files_dst2src()
except MalformedMetafileError:
log.error("Malformed metafile %s in app %s" % (args.metafile, args.app) )
exit(1)
if absconf in dstdic:
log.error("File %s already exists as %s in app %s" % (absconf, dstdic[absconf], args.app))
exit(1)
# Copy the file (or directory)
if copy_file_or_directory(log, path_dst=confpath, path_src=args.conf, overwrite=False):
log.info("New conf dir '%s' associated with app '%s'" % (confname, args.app))
else:
log.error("Failed to copy file or directory %s" % args.conf)
exit(1)
# Add the file to META: first update META data
log.debug("adding meta data for %s in %s" % (confname, args.app))
appmeta.add_file( confname, dest=absconf )
# Then write to the metafile
try:
log.debug("Writing to metafile")
appmeta.save()
except NoPathDefinedError:
log.error("Unable to save json Metafile for app %s : no path defined" % appmeta.name)
exit(1)
# UPDATE COMMAND
########################################
if args.command == "update":
# TODO app == None => update all apps
# Load app META file
try:
appmeta = Metafile(json_path = "/".join([args.rootdir, args.app, args.metafile]))
except FileNotFoundError:
log.info("Metafile %s does not exist for app %s" % (args.metafile, args.app))
appmeta = Metafile( name=args.app )
appmeta.json_path = "/".join( [args.rootdir, args.app, args.metafile] )
except MalformedMetafileError:
log.error("Malformed metafile %s in app %s" % (args.metafile, args.app) )
exit(1)
# TODO check that the app exists
# The filenames can be confnames or real files names. For each we'll assume
# first that it's a confname, then that it's a file name if that fails.
if args.files != []:
dstdic = appmeta.get_files_dst2src()
for f in args.files:
# It's probably a confname
if f in appmeta.files:
if copy_file_or_directory(log, path_dst='/'.join([args.rootdir, args.app, f]), path_src=appmeta.files[f]["dest"]):
log.info("Updated %s from %s" % (f, appmeta.files[f]["dest"]))
else:
log.warning("Failed to update %s from %s ; ignoring" % (f, appmeta.files[f]["dest"]))
# If not, it must be a real filename
else:
absf = contractuser( os.path.abspath(os.path.expanduser(f)) )
if absf in dstdic:
if copy_file_or_directory(log, path_dst='/'.join([args.rootdir, args.app, dstdic[absf]]), path_src=absf):
log.info("Updated %s from %s" % (dstdic[absf], absf))
else:
log.warning("Failed to update %s from %s ; ignoring" % (dstdic[absf], absf))
# Otherwise, it's nothing.
else:
log.warning("Cannot find file %s in app data; ignoring it")
# If they were no 'file' args, it means we need to update all files in the app
else:
for f in appmeta.files:
if copy_file_or_directory(log, path_dst='/'.join([args.rootdir, args.app, f]), path_src=appmeta.files[f]["dest"]):
log.info("Updated %s from %s" % (f, appmeta.files[f]["dest"]))
else:
log.warning("Failed to update %s from %s ; ignoring" % (f, appmeta.files[f]["dest"]))
# PACKAGE COMMAND
########################################
if args.command == "package":
pkgname = args.pkgname
# TODO app == None => package all apps
# Load app META file
try:
appmeta = Metafile(json_path = "/".join([args.rootdir, args.apps[0], args.metafile]))
except FileNotFoundError:
log.error("Metafile %s does not exist for app %s" % (args.metafile, args.apps[0]))
exit(1)
except MalformedMetafileError:
log.error("Malformed metafile %s in app %s" % (args.metafile, args.apps[0]) )
exit(1)
# TODO check that the app exists
pkgname += "."
pkgname += appmeta.name
if args.pkgtype == "tar":
pkgname += ".tgz"
# Create a tar containing the files in the right place
pkg = tarfile.open(pkgname, "w:gz")
for f in appmeta.files:
# TODO remove leading '~'
# TODO save and restore owners and permissions
try:
pkg.add(args.rootdir +"/"+ appmeta.name +"/"+ f, arcname=appmeta.files[f]["dest"])
log.info("Adding %s to package" % f)
except:
log.warning("Failed to add %s to tar package %s" % (f, pkgname))
pkg.close()
########################################