persoconf/persoconf/main.py

612 lines
23 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import logging
import os, os.path
import shutil
import time
import tarfile
from utils import yes_no_question, contractuser, \
copy_file_or_directory, delete_file_or_dir
from metafile import Metafile, MalformedMetafileError, NoPathDefinedError
from persoconf import Persoconf, FileInsteadOfDirError, \
PersoconfRootDoesNotExistError
# ARGPARSE
########################################
# Argument parsing using argparse
parser = argparse.ArgumentParser()
parser.add_argument( "--rootdir" ,
type=str ,
default="~/.config/persoconf/" ,
help="The persoconf directory to use" )
parser.add_argument( "--metafile" ,
type=str ,
default="META" ,
help="The name of the metadata files" )
parser.add_argument( "--verbose", "-v" ,
action="store_true" ,
help="Spout out more logs" )
subparsers = parser.add_subparsers( dest="command" ,
title="commands" )
# Help
parser_help = subparsers.add_parser( "help" ,
help="Print this help" )
# List
parser_list = subparsers.add_parser( "list" ,
help="List backuped apps" )
parser_list.add_argument( "apps" ,
type=str ,
nargs="*" ,
default=[] ,
help="The apps to list ; defaults to all " \
"apps" )
parser_list.add_argument( "--show-files", "-f" ,
action="store_true" ,
help="Show the files for each app ; it is " \
"automatically set if you specify the apps " \
"to list." )
# Package
parser_package = subparsers.add_parser( "package" ,
help="Package a persoconf repository" )
parser_package.add_argument( "apps" ,
type=str ,
nargs="*" ,
default=[] ,
help="The apps to package ; defaults to all " \
"apps" )
parser_package.add_argument( "--pkgtype", "-t" ,
type=str ,
nargs="?" ,
default="tgz" ,
choices = [ "tgz" ] ,
help="The type of package to use" )
parser_package.add_argument( "--pkgname", "-p" ,
type=str ,
nargs="?" ,
default="persoconf." + (time.strftime("%Y%m%d")) ,
help="The name of the package to create" )
# 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" ,
type=str ,
nargs="?" ,
help="The app to update" )
parser_update.add_argument( "files" ,
type=str ,
nargs="*" ,
default=[] ,
help="The files to update ; default to all " \
"added files" )
# 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" ,
type=str ,
help="The app to add, or the app to add a conf " \
"file to" )
parser_add.add_argument( "conf" ,
nargs="?" ,
type=str ,
help="The conf file or directory to add to the app" )
parser_add.add_argument( "--confname" ,
type=str ,
help="A special name to save the conf " \
"file/directory in the persoconf directory" )
# Delete
parser_del = subparsers.add_parser( "delete" ,
help="Delete an app from the persoconf " \
"directory, or a config file from " \
"an existing app" )
parser_del.add_argument( "app" ,
type=str ,
help="The app to delete, or the app to delete a " \
"conf file from" )
parser_del.add_argument( "files" ,
nargs="*" ,
type=str ,
help="The conf files or directories to delete from " \
"the app" )
parser_del.add_argument( "--force-yes", "-y" ,
action="store_true" ,
help="Don't ask silly questions" )
# Parse arguments
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()
# Try to create the Persoconf
try:
persoconf = Persoconf(path=args.rootdir, metafile=args.metafile)
# If the place is already taken, there's nothing we can do
except FileInsteadOfDirError:
log.error("'%s' exists but is a file" % str(args.rootdir))
# Otherwise, maybe we can create the directory ?
except PersoconfRootDoesNotExistError:
log.warning("'%s' does not exist" % str(args.rootdir))
# Ask before creating the directory
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)
log.info("New persoconf directory created at '%s'" % args.rootdir)
# Finally retry to create the Persoconf
try:
persoconf = Persoconf(args.rootdir, metafile=args.metafile)
except Error as err:
log.error("Something very unexpected occured, I'm sorry")
log.error("Error dump follows")
log.error(str(err))
exit()
# LIST COMMAND
########################################
if args.command == "list":
app_list = persoconf.list_apps(logger=log)
# If no app is given: list all apps
if args.apps == []:
log.info("Listing all apps")
for app in app_list:
# Print the app name
print(app)
# Print the files too if asked so
if args.show_files:
for f in app_list[app].files:
if os.path.isdir(persoconf.path + "/" + app + "/" + f):
print("\t%s/" % f)
else:
print("\t%s" % f)
# Some apps were specified, so we list them with their files
else:
for app in args.apps:
if app not in app_list:
log.warning("No such app: %s" % app)
continue
# Print the app name
print(app)
# Print the app files
for f in app_list[app].files:
if os.path.isdir(persoconf.path + "/" + app + "/" + f):
print("\t%s/" % f)
else:
print("\t%s" % f)
# DELETE COMMAND
########################################
if args.command == "delete":
# Does this app really exists ?
try:
appmeta = Metafile( json_path = persoconf.path
+"/"+ args.app
+"/"+ persoconf.metafile )
except FileNotFoundError:
log.error( "App %s does not seem to exist" % args.app )
exit(1)
except MalformedMetafileError:
log.error( "Failed to load metafile from app %s" % args.app )
exit(1)
# Does the user want to remove files or the entire app ?
if args.files == []:
# Delete the entire app
if ( args.force_yes
or yes_no_question("Really delete app '%s'?" % appmeta.name )
):
shutil.rmtree( persoconf.path +"/"+ appmeta.name )
log.warning("App %s was deleted" % appmeta.name)
else:
log.warning("Nothing was deleted")
exit(0)
else:
# Delete all required files (or directories)
if ( not args.force_yes
and not yes_no_question( "Delete all files from following list: " \
"%s from app %s?"
% (str(args.files),appmeta.name))
):
log.warning("Nothing was deleted")
exit(0)
# 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.
dstdic = appmeta.get_files_dst2src() # Get that here for optimization
for f in args.files:
# 1) It's probably a confname
if f in appmeta.files:
if delete_file_or_dir( logger = log ,
path = persoconf.path
+"/"+ appmeta.name
+"/"+ f ):
# File deleted, let's delete the entry in Metafile
appmeta.del_file(f)
appmeta.save()
else:
log.warning( "Something wrong occured while deleting " \
"file %s"
% f )
continue
# 2) If not, it must be a real filename
else:
absf = contractuser(os.path.abspath(os.path.expanduser(f)))
if absf in dstdic:
if delete_file_or_dir( logger = log ,
path = persoconf.path
+"/"+ appmeta.name
+"/"+ dstdir[absf] ):
# File deleted, let's delete the entry in Metafile
appmeta.del_file(dstdir[absf])
appmeta.save()
else:
log.warning( "Something wrong occured while " \
"deleting file %s"
% f )
continue
# 3) Otherwise, no idea what it is
else:
log.warning( "Cannot find file %s in app %s data; " \
"ignoring it"
% (f, appmeta.name) )
continue
# ADD COMMAND
########################################
if args.command == "add":
# Check (and create) the app directory
appdir = persoconf.path + "/" + 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 = args.conf
# We need to remove trailing '/' because 'os.path.basename("toto/")'
# returns ''. And that's not cool.
while confname[-1] == "/":
confname = confname[:-1] # Remove trailing slashes
confname = os.path.basename(confname)
# Remove leading dots, so the name under which the conffile is saved is
# not hidden.
while confname[0] == ".":
confname = confname[1:] # Remove leading dots
confpath = appdir + "/" + confname
# Load app META file
try:
appmeta = Metafile(json_path = appdir + "/" + persoconf.metafile)
except FileNotFoundError:
log.info("Metafile %s does not exist for app %s" % (persoconf.metafile, args.app))
appmeta = Metafile( name=args.app )
appmeta.json_path = "/".join( [persoconf.path, args.app, persoconf.metafile] )
except MalformedMetafileError:
log.error("Malformed metafile %s in app %s" % (persoconf.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" % (persoconf.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":
# app == None => update all apps
apps_to_update = {}
if args.app is None:
apps_to_update = persoconf.list_apps(logger=log)
else:
try:
appmeta = Metafile( json_path = persoconf.path +"/"+
args.app +"/"+
persoconf.metafile )
apps_to_update[appmeta.name] = appmeta
except FileNotFoundError:
log.error( "Metafile %s does not exist for app %s"
% (persoconf.metafile, args.app) )
exit(1)
except MalformedMetafileError:
log.error( "Malformed metafile %s in app %s"
% (persoconf.metafile, args.app) )
exit(1)
for app in apps_to_update:
if args.files != []:
dstdic = apps_to_update[app].get_files_dst2src() # Get that here
# for optimization
# 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.
for f in args.files:
# 1) It's probably a confname
if f in apps_to_update[app].files:
if copy_file_or_directory(
log ,
path_dst = persoconf.path
+"/"+ app
+"/"+ f ,
path_src = apps_to_update[app].files[f]["dest"]
):
log.info( "Updated %s from %s"
% (f, apps_to_update[app].files[f]["dest"]) )
else:
log.warning(
"Failed to update %s from %s ; ignoring"
% ( f, apps_to_update[app].files[f]["dest"])
)
continue
# 2) 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 = persoconf.path
+"/"+ 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) )
continue
# 3) Otherwise, no idea what it is
else:
log.warning( "Cannot find file %s in app data; " \
"ignoring it"
% f )
continue
# If they were no 'file' args, it means we need to update all files in
# the app
else:
for f in apps_to_update[app].files:
if copy_file_or_directory(
log ,
path_dst = persoconf.path
+"/"+ app
+"/"+ f ,
path_src = apps_to_update[app].files[f]["dest"]
):
log.info( "Updated %s from %s"
% (f, apps_to_update[app].files[f]["dest"]) )
else:
log.warning( "Failed to update %s from %s ; ignoring"
% (f, apps_to_update[app].files[f]["dest"]) )
# PACKAGE COMMAND
########################################
if args.command == "package":
pkgname = args.pkgname
# If no app is given: package all apps
if args.apps == []:
log.info("Packaging all apps")
args.apps = persoconf.list_apps(logger=log)
# Some apps were specified, so we list them with their files
appmetas = []
for app in args.apps :
# Load app META file
try:
appmetas.append( Metafile(json_path = "/".join([persoconf.path, app, persoconf.metafile])) )
except FileNotFoundError:
log.warning("Metafile %s does not exist for app %s; ignoring" % (persoconf.metafile, app))
continue
except MalformedMetafileError:
log.warning("Malformed metafile %s in app %s; ignoring" % (persoconf.metafile, app) )
continue
# TODO check that the app exists
# Adds the app name if there is only one packaged
if len(appmetas) == 1:
pkgname += ("." + appmeta[0].name)
# Switch according to the pkgtype
if args.pkgtype == "tgz":
pkgname += ".tgz"
# Create a tar containing the files in the right place
pkg = tarfile.open(pkgname, "w:gz")
for appmeta in appmetas:
for f in appmeta.files:
filename = persoconf.path +"/"+ appmeta.name +"/"+ f
filedest = appmeta.files[f]["dest"]
# Remove possible 'home'. The final targz should be extracted
# at 'home.
if filedest.startswith("~/"):
filedest = filedest[2:]
# TODO save and restore owners and permissions
try:
pkg.add(filename, arcname=filedest)
log.info("Adding %s to package" % f)
except:
log.warning("Failed to add %s to tar package %s" % (f, pkgname))
pkg.close()
########################################