Compare commits

...

10 Commits

Author SHA1 Message Date
Lertsenem 86fad9b83f Adding diff functionnality 2017-01-24 12:38:43 +01:00
Lertsenem d54b818a2e Include persoconf own dir in packages 2017-01-02 23:42:43 +01:00
Lertsenem 755b079ab6 Aesthetics
What on earth was I thinking...
2017-01-02 22:22:09 +01:00
Lertsenem 671bd90024 Adding TODO 2016-06-03 03:42:29 +02:00
Lertsenem 41226d0489 Do not add extension to confname when user-defined
If the user chose the confname, we should generate an error rather than
silently change it when we find it's already taken.
2016-06-03 03:38:45 +02:00
Lertsenem e6c4dcbdbc Change confname when already taken
If the confname of the file we want to add already exists (for a
different abspath of course), we add a numeric extension to it. Same
thing if the confname is the metafile name.
2016-06-03 03:34:54 +02:00
Lertsenem 20566a3c89 Never gonna tell a lie 2016-06-02 02:16:40 +02:00
Lertsenem c6687e7b9e Lying about README being markdown 2016-06-02 02:13:53 +02:00
Lertsenem b402cba1d9 Moving one dir up 2016-06-02 02:07:58 +02:00
Lertsenem fbc72c0c12 Adding README 2016-06-02 02:06:16 +02:00
13 changed files with 406 additions and 228 deletions

90
README.adoc Normal file
View File

@ -0,0 +1,90 @@
persoconf
=========
*persoconf* is a python script intended to keep track of all your personal
configuration files (also known as dotfiles).
Usage
-----
If you want to skip this and get to the point: there's a 'help' command
available, and +--help/-h+ options for every other command.
Add an app to your persoconf repo:
----
$> persoconf add myapp
----
Add dotfiles and config directories to this app:
----
$> persoconf add myapp ~/.myapp.config
$> persoconf add myapp ~/.config/myapp/
----
List the files and dirs saved in persoconf for an app:
----
$> persoconf list -f
tmux
tmux.conf
awesome
rc.lua
vim
vimrc
myapp
myapp.config
myapp/
----
Check if modifications occured on your config files:
----
$> echo "some change in my conf" >> ~/.myapp.config
$> persoconf check
persoconf:WARNING: File ~/.myapp.config was modified
----
Update persoconf saved files with the latest configuration available:
----
$> persoconf update
----
Package your dotfiles to move them on another machine:
----
$> persoconf package --pkgtype tgz
$> ls
persoconf.20160602.tgz
$> tar tzf persoconf.20160602.tgz
.config/awesome/rc.lua
.config/myapp/somefile
.config/myapp/someotherfile
.myapp.config
.tmux.conf
.vimrc
----
Internals
---------
All persoconf data is by default saved in a '.config/persoconf' directory. You
can override this location with the global +--rootdir+ option. If the rootdir
location does not exist when you first try to use persoconf, persoconf will ask
if you want to create it.
Basically, data is saved as is (which means textfiles and dirs, mostly), and
metadata is saved as json files.
// vim: set ft=asciidoc :

View File

@ -6,26 +6,26 @@ import os.path
import utils
from metafile import Metafile, MalformedMetafileError, NoPathDefinedError
# --------------------------------------
# -----------------------------------------------------------------------------
def init(parser):
parser.add_argument( "app" ,
type=str ,
parser.add_argument( "app",
type=str,
help="The app to add, or the app to add a conf " \
"file to" )
parser.add_argument( "conf" ,
nargs="?" ,
type=str ,
parser.add_argument( "conf",
nargs="?",
type=str,
help="The conf file or directory to add to the app" )
parser.add_argument( "--confname" ,
type=str ,
parser.add_argument( "--confname",
type=str,
help="A special name to save the conf " \
"file/directory in the persoconf directory" )
# --------------------------------------
# -----------------------------------------------------------------------------
def run(args, persoconf, logger):
# Check (and create) the app directory
@ -51,37 +51,17 @@ def run(args, persoconf, logger):
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] == os.path.sep :
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 = os.path.join( appdir ,
appmeta = Metafile( json_path = os.path.join( appdir,
persoconf.metafile ) )
except FileNotFoundError:
logger.info( "Metafile %s does not exist for app %s"
% (persoconf.metafile, args.app) )
appmeta = Metafile( name=args.app )
appmeta.json_path = os.path.join( persoconf.path ,
args.app ,
appmeta.json_path = os.path.join( persoconf.path,
args.app,
persoconf.metafile )
except MalformedMetafileError:
@ -107,9 +87,54 @@ def run(args, persoconf, logger):
% (absconf, dstdic[absconf], args.app) )
exit(1)
# 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] == os.path.sep :
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
# Check the conf file name
if ( confname == persoconf.metafile
or confname in appmeta.files ):
# If the confanme was user-defined, it's a serious error.
if args.confname:
logger.error( "Confname '%s' is already taken: try another one"
% confname )
exit(1)
# Otherwise it's cool, we just have to change the confname by ourselves
logger.info( "Confname '%s' is already taken: trying another one"
% confname )
ending = 1
while ( confname + "." + str(ending) == persoconf.metafile
or confname + "." + str(ending) in appmeta.files ):
ending += 1
confname = confname + "." + str(ending)
logger.info( "New confname is '%s'" % confname )
# Deduce the path from the conf file name
confpath = appdir + "/" + confname
# Copy the file (or directory)
if utils.copy_file_or_directory( logger ,
path_dst=confpath ,
if utils.copy_file_or_directory( logger,
path_dst=confpath,
path_src=args.conf,
overwrite=False ):
logger.info( "New conf file or directory '%s' associated with app '%s'"
@ -132,3 +157,5 @@ def run(args, persoconf, logger):
"defined"
% appmeta.name )
exit(1)
# TODO Add or update the app METAFILE in a 'persoconf' special app.

View File

@ -1,38 +1,44 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os.path
import os, os.path
import filecmp
import subprocess
from metafile import Metafile, MalformedMetafileError
# --------------------------------------
# -----------------------------------------------------------------------------
def init(parser):
"""Initialize check subcommand"""
parser.add_argument( "app" ,
type=str ,
nargs="?" ,
parser.add_argument( "app",
type=str,
nargs="?",
help="The app to check" )
parser.add_argument( "files" ,
type=str ,
nargs="*" ,
default=[] ,
parser.add_argument( "files",
type=str,
nargs="*",
default=[],
help="The files to check ; default to all " \
"added files" )
parser.add_argument( "-d", "--diff" ,
type=str ,
parser.add_argument( "-d", "--diff",
action="store_true",
help="Print the diff between the saved and the new " \
"file/directory" )
"files" )
# --------------------------------------
# -----------------------------------------------------------------------------
def run(args, persoconf, logger):
logger.debug("Starting 'check' command")
difftool = None
if args.diff:
difftool = os.environ.get('DIFFTOOL', 'vimdiff')
logger.info("Selecting %s as diff tool" % difftool)
# app == None => check all apps
apps_to_check = {}
if args.app is None:
@ -84,7 +90,8 @@ def run(args, persoconf, logger):
_compare_and_log( original_file_path,
persoconf_backup_path,
logger )
logger,
difftool )
# 2) If not, it must be a real filename
else:
@ -101,7 +108,8 @@ def run(args, persoconf, logger):
_compare_and_log( original_file_path,
persoconf_backup_path,
logger )
logger,
difftool )
# 3) Otherwise, no idea what it is
@ -125,11 +133,15 @@ def run(args, persoconf, logger):
_compare_and_log( original_file_path,
persoconf_backup_path,
logger )
logger,
difftool )
# --------------------------------------
def _compare_and_log(original_file_path, persoconf_backup_path, logger):
# -----------------------------------------------------------------------------
def _compare_and_log( original_file_path,
persoconf_backup_path,
logger,
difftool = None ):
"""Compare a file with its persoconf backup and log the result (at info
level if the files are matching, warning level if they are not)."""
@ -158,6 +170,8 @@ def _compare_and_log(original_file_path, persoconf_backup_path, logger):
else:
logger.warning( "File %s was modified"
% original_file_path )
if difftool:
subprocess.call([ difftool, resolved_pbp, resolved_ofp ])
except FileNotFoundError as err:
if err.filename == resolved_pbp:

View File

@ -9,32 +9,32 @@ import utils
from metafile import Metafile, MalformedMetafileError
# --------------------------------------
# -----------------------------------------------------------------------------
def init(parser):
parser.add_argument( "app" ,
type=str ,
parser.add_argument( "app",
type=str,
help="The app to delete, or the app to delete a " \
"conf file from" )
parser.add_argument( "files" ,
nargs="*" ,
type=str ,
parser.add_argument( "files",
nargs="*",
type=str,
help="The conf files or directories to delete from " \
"the app" )
parser.add_argument( "--force-yes", "-y" ,
action="store_true" ,
parser.add_argument( "--force-yes", "-y",
action="store_true",
help="Don't ask silly questions" )
# --------------------------------------
# -----------------------------------------------------------------------------
def run(args, persoconf, logger):
# Does this app really exists ?
try:
appmeta = Metafile( json_path = os.path.join( persoconf.path ,
args.app ,
appmeta = Metafile( json_path = os.path.join( persoconf.path,
args.app,
persoconf.metafile ) )
except FileNotFoundError:
logger.error( "App %s does not seem to exist" % args.app )
@ -80,9 +80,9 @@ def run(args, persoconf, logger):
# 1) It's probably a confname
if f in appmeta.files:
if utils.delete_file_or_dir(
logger = logger ,
logger = logger,
path = os.path.join(persoconf.path,
appmeta.name ,
appmeta.name,
f )
):
# File deleted, let's delete the entry in Metafile
@ -104,7 +104,7 @@ def run(args, persoconf, logger):
if delete_file_or_dir( logger = logger,
path = os.path.join(
persoconf.path,
appmeta.name ,
appmeta.name,
dstdir[absf]
)
):

View File

@ -3,24 +3,24 @@
import os.path
# --------------------------------------
# -----------------------------------------------------------------------------
def init(parser):
parser.add_argument( "apps" ,
type=str ,
nargs="*" ,
default=[] ,
parser.add_argument( "apps",
type=str,
nargs="*",
default=[],
help="The apps to list ; defaults to all " \
"apps" )
parser.add_argument( "--show-files", "-f" ,
action="store_true" ,
parser.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." )
# --------------------------------------
# -----------------------------------------------------------------------------
def run(args, persoconf, logger):
app_list = persoconf.list_apps(logger=logger)

145
commands/package.py Normal file
View File

@ -0,0 +1,145 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os.path
import time
import tarfile
import utils
from metafile import Metafile, MalformedMetafileError
# -----------------------------------------------------------------------------
def init(parser):
parser.add_argument( "apps",
type=str,
nargs="*",
default=[],
help="The apps to package ; defaults to all " \
"apps" )
parser.add_argument( "--pkgtype", "-t",
type=str,
nargs="?",
default="tgz",
choices = [ "tgz" ],
help="The type of package to use" )
parser.add_argument( "--pkgname", "-p",
type=str,
nargs="?",
default="persoconf." + (time.strftime("%Y%m%d")),
help="The name of the package to create" )
parser.add_argument( "--nopersoconf", "-n",
action="store_true",
help="Do not include persoconf own config " \
"directory in the resulting package" )
# -----------------------------------------------------------------------------
def run(args, persoconf, logger):
pkgname = args.pkgname
# If no app is given: package all apps
if args.apps == []:
logger.info("Packaging all apps")
args.apps = persoconf.list_apps(logger=logger)
# 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 = os.path.join(
persoconf.path,
app,
persoconf.metafile
)
)
)
except FileNotFoundError:
logger.warning( "Metafile %s does not exist for app %s; ignoring"
% (persoconf.metafile, app) )
continue
except MalformedMetafileError:
logger.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 += ("." + appmetas[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:
# Add META file to the archive
if not args.nopersoconf:
filename_meta = os.path.join( persoconf.path,
appmeta.name,
persoconf.metafile )
filedest_meta = utils.contractuser( filename_meta )
if filedest_meta.startswith("~/"):
filedest_meta = filedest_meta[2:]
try:
pkg.add(filename_meta, arcname=filedest_meta)
logger.info(
"Adding app %s to package".format(appmeta.name)
)
except Exception as e:
logger.warning(
"Failed to add app {app} to tar package {pkg}: {err}" \
.format( app = appmeta.name,
pkg = pkgname,
err = str(e) )
)
for f in appmeta.files:
filename = os.path.join( persoconf.path,
appmeta.name,
f )
filedest = appmeta.files[f]["dest"]
filedest_pc = utils.contractuser( filename )
# Remove possible 'home'. The final targz should be extracted
# at $HOME.
if filedest.startswith("~/"):
filedest = filedest[2:]
if filedest_pc.startswith("~/"):
filedest_pc = filedest_pc[2:]
# TODO save and restore owners and permissions
try:
pkg.add(filename, arcname=filedest)
if not args.nopersoconf:
pkg.add(filename, arcname=filedest_pc)
logger.info("Adding %s to package" % f)
except Exception as e:
logger.warning( "Failed to add %s to tar package %s: %s"
% (f, pkgname, str(e)) )
pkg.close()

View File

@ -6,22 +6,22 @@ import os.path
import utils
from metafile import Metafile, MalformedMetafileError
# --------------------------------------
# -----------------------------------------------------------------------------
def init(parser):
parser.add_argument( "app" ,
type=str ,
nargs="?" ,
parser.add_argument( "app",
type=str,
nargs="?",
help="The app to update" )
parser.add_argument( "files" ,
type=str ,
nargs="*" ,
default=[] ,
parser.add_argument( "files",
type=str,
nargs="*",
default=[],
help="The files to update ; default to all " \
"added files" )
# --------------------------------------
# -----------------------------------------------------------------------------
def run(args, persoconf, logger):
# app == None => update all apps
@ -32,8 +32,8 @@ def run(args, persoconf, logger):
else:
try:
appmeta = Metafile(
json_path = os.path.join( persoconf.path ,
args.app ,
json_path = os.path.join( persoconf.path,
args.app,
persoconf.metafile )
)
@ -66,10 +66,10 @@ def run(args, persoconf, logger):
if f in apps_to_update[app].files:
if utils.copy_file_or_directory(
logger ,
logger,
path_dst = os.path.join( persoconf.path,
app ,
f ) ,
app,
f ),
path_src = apps_to_update[app].files[f]["dest"]
):
@ -93,10 +93,10 @@ def run(args, persoconf, logger):
if absf in dstdic:
if utils.copy_file_or_directory(
logger ,
logger,
path_dst = os.path.join( persoconf.path,
app ,
dstdic[absf] ) ,
app,
dstdic[absf] ),
path_src = absf
):
logger.info( "Updated %s from %s"
@ -121,10 +121,10 @@ def run(args, persoconf, logger):
for f in apps_to_update[app].files:
if utils.copy_file_or_directory(
logger ,
logger,
path_dst = os.path.join( persoconf.path,
app ,
f ) ,
app,
f ),
path_src = apps_to_update[app].files[f]["dest"]
):
logger.info( "Updated %s from %s"
@ -132,7 +132,7 @@ def run(args, persoconf, logger):
else:
logger.warning( "Failed to update %s from %s ; ignoring"
% ( f ,
% ( f,
apps_to_update[app].files[f]["dest"] )
)

View File

@ -21,77 +21,86 @@ import commands.list, commands.package, commands.update, commands.add, \
# ARGPARSE
########################################
##############################################################################
# Argument parsing using argparse
# =============================================================================
parser = argparse.ArgumentParser()
parser.add_argument( "--rootdir" ,
type=str ,
default="~/.config/persoconf/" ,
parser.add_argument( "--rootdir",
type=str,
default="~/.config/persoconf/",
help="The persoconf directory to use" )
parser.add_argument( "--metafile" ,
type=str ,
default="META" ,
parser.add_argument( "--metafile",
type=str,
default="META",
help="The name of the metadata files" )
parser.add_argument( "--verbose", "-v" ,
action="store_true" ,
parser.add_argument( "--verbose", "-v",
action="store_true",
help="Spout out more logs" )
subparsers = parser.add_subparsers( dest="command" ,
subparsers = parser.add_subparsers( dest="command",
title="commands" )
# Help
parser_help = subparsers.add_parser( "help" ,
# -----------------------------------------------------------------------------
parser_help = subparsers.add_parser( "help",
help="Print this help" )
# List
parser_list = subparsers.add_parser( "list" ,
# -----------------------------------------------------------------------------
parser_list = subparsers.add_parser( "list",
help="List backuped apps" )
commands.list.init(parser_list)
# Package
parser_package = subparsers.add_parser( "package" ,
# -----------------------------------------------------------------------------
parser_package = subparsers.add_parser( "package",
help="Package a persoconf repository" )
commands.package.init(parser_package)
# Update
parser_update = subparsers.add_parser( "update" ,
# -----------------------------------------------------------------------------
parser_update = subparsers.add_parser( "update",
help="Backup an app configuration in " \
"the persoconf directory with " \
"the configuration in use now" )
commands.update.init(parser_update)
# Add
parser_add = subparsers.add_parser( "add" ,
# -----------------------------------------------------------------------------
parser_add = subparsers.add_parser( "add",
help="Add an app to the persoconf " \
"directory, or add a config file " \
"to an existing app" )
commands.add.init(parser_add)
# Check
parser_check = subparsers.add_parser( "check" ,
# -----------------------------------------------------------------------------
parser_check = subparsers.add_parser( "check",
help="Checks if the backuped and the " \
"original file are different." )
commands.check.init(parser_check)
# Delete
parser_del = subparsers.add_parser( "delete" ,
# -----------------------------------------------------------------------------
parser_del = subparsers.add_parser( "delete",
help="Delete an app from the persoconf " \
"directory, or a config file from " \
"an existing app" )
commands.delete.init(parser_del)
# Parse arguments
# =============================================================================
args = parser.parse_args()
# Transform paths
args.rootdir = os.path.expanduser(args.rootdir)
# LOGGING
########################################
##############################################################################
log = logging.getLogger("persoconf")
log.setLevel(logging.DEBUG)
@ -110,13 +119,14 @@ 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)
@ -151,38 +161,31 @@ except PersoconfRootDoesNotExistError:
exit()
# LIST COMMAND
########################################
# =============================================================================
if args.command == "list":
commands.list.run(args, persoconf, log)
# DELETE COMMAND
########################################
# =============================================================================
if args.command == "delete":
commands.delete.run(args, persoconf, log)
# ADD COMMAND
########################################
# =============================================================================
if args.command == "add":
commands.add.run(args, persoconf, log)
# CHECK COMMAND
########################################
# =============================================================================
if args.command == "check":
commands.check.run(args, persoconf, log)
# UPDATE COMMAND
########################################
# =============================================================================
if args.command == "update":
commands.update.run(args, persoconf, log)
# PACKAGE COMMAND
########################################
# =============================================================================
if args.command == "package":
commands.package.run(args, persoconf, log)
########################################

View File

@ -1,101 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os.path
import time
import tarfile
from metafile import Metafile, MalformedMetafileError
# --------------------------------------
def init(parser):
parser.add_argument( "apps" ,
type=str ,
nargs="*" ,
default=[] ,
help="The apps to package ; defaults to all " \
"apps" )
parser.add_argument( "--pkgtype", "-t" ,
type=str ,
nargs="?" ,
default="tgz" ,
choices = [ "tgz" ] ,
help="The type of package to use" )
parser.add_argument( "--pkgname", "-p" ,
type=str ,
nargs="?" ,
default="persoconf." + (time.strftime("%Y%m%d")) ,
help="The name of the package to create" )
# --------------------------------------
def run(args, persoconf, logger):
pkgname = args.pkgname
# If no app is given: package all apps
if args.apps == []:
logger.info("Packaging all apps")
args.apps = persoconf.list_apps(logger=logger)
# 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 = os.path.join(
persoconf.path ,
app ,
persoconf.metafile)
)
)
except FileNotFoundError:
logger.warning( "Metafile %s does not exist for app %s; ignoring"
% (persoconf.metafile, app) )
continue
except MalformedMetafileError:
logger.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 += ("." + appmetas[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)
logger.info("Adding %s to package" % f)
except Exception as e:
logger.warning( "Failed to add %s to tar package %s: %s"
% (f, pkgname, str(e)) )
pkg.close()