381 lines
13 KiB
Python
Executable File
381 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import logging
|
|
import os, os.path
|
|
import shutil
|
|
|
|
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=[] )
|
|
|
|
# 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 apps to backup', 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"]))
|
|
|
|
########################################
|