From 50a29632d183c54940a65ab032e03cef753558c5 Mon Sep 17 00:00:00 2001 From: Moritz Schillinger Date: Sun, 7 Oct 2018 11:02:30 +0200 Subject: [PATCH] Add -configs mode. Backup software configurations and terminal plist file and add -reinstall_configs mode. --- shallow_backup.py | 253 +++++++++++++++++++++++++++++++++------------- 1 file changed, 182 insertions(+), 71 deletions(-) diff --git a/shallow_backup.py b/shallow_backup.py index 8dc6e16a..abae4f60 100644 --- a/shallow_backup.py +++ b/shallow_backup.py @@ -4,7 +4,6 @@ import os from os.path import expanduser import sys -import glob import shutil import configparser import subprocess as sp @@ -20,9 +19,9 @@ def print_version_info(): version = "{} v{} by {} -> (Github: {})".format(Constants.PROJECT_NAME, - Constants.VERSION, - Constants.AUTHOR_FULL_NAME, - Constants.AUTHOR_GITHUB) + Constants.VERSION, + Constants.AUTHOR_FULL_NAME, + Constants.AUTHOR_GITHUB) line = "-" * (len(version)) print(Fore.RED + Style.BRIGHT + line) print(version) @@ -34,14 +33,14 @@ def splash_screen(): Display splash graphic, and then version info """ print(Fore.YELLOW + Style.BRIGHT + "\n" + - " dP dP dP dP dP \n" + - " 88 88 88 88 88 \n" + - " ,d8888' 88d888b. .d8888b. 88 88 .d8888b. dP dP dP 88d888b. .d8888b. .d8888b. 88 .dP dP dP 88d888b. \n" + - " Y8ooooo, 88' `88 88' `88 88 88 88' `88 88 88 88 88' `88 88' `88 88' `\"\" 88888\" 88 88 88' `88 \n" + - " 88 88 88 88. .88 88 88 88. .88 88.88b.88' 88. .88 88. .88 88. ... 88 `8b. 88. .88 88. .88 \n" + - " `88888P' dP dP `88888P8 dP dP `88888P' 8888P Y8P 88Y8888' `88888P8 `88888P' dP `YP `88888P' 88Y888P' \n" + - " 88 \n" + - " dP \n" + Style.RESET_ALL) + " dP dP dP dP dP \n" + + " 88 88 88 88 88 \n" + + " ,d8888' 88d888b. .d8888b. 88 88 .d8888b. dP dP dP 88d888b. .d8888b. .d8888b. 88 .dP dP dP 88d888b. \n" + + " Y8ooooo, 88' `88 88' `88 88 88 88' `88 88 88 88 88' `88 88' `88 88' `\"\" 88888\" 88 88 88' `88 \n" + + " 88 88 88 88. .88 88 88 88. .88 88.88b.88' 88. .88 88. .88 88. ... 88 `8b. 88. .88 88. .88 \n" + + " `88888P' dP dP `88888P8 dP dP `88888P' 8888P Y8P 88Y8888' `88888P8 `88888P' dP `YP `88888P' 88Y888P' \n" + + " 88 \n" + + " dP \n" + Style.RESET_ALL) print_version_info() @@ -53,10 +52,10 @@ def prompt_yes_no(message, color): Otherwise, it's the second line you want printed. """ questions = [inquirer.List('choice', - message=color + Style.BRIGHT + message + Fore.BLUE, - choices=[' Yes', ' No'], - ), - ] + message=color + Style.BRIGHT + message + Fore.BLUE, + choices=[' Yes', ' No'], + ), + ] answers = inquirer.prompt(questions) return answers.get('choice').strip().lower() == 'yes' @@ -85,28 +84,31 @@ def make_dir_warn_overwrite(path): """ Make destination dir if path doesn't exist, confirm before overwriting if it does. """ - if os.path.exists(path) and path.split("/")[-1] in ["dotfiles", "packages", "fonts"]: - print(Fore.RED + Style.BRIGHT + "Directory {} already exists".format(path) + "\n" + Style.RESET_ALL) + if os.path.exists(path) and path.split("/")[-1] in ["dotfiles", "packages", "fonts", "configs"]: + print(Fore.RED + Style.BRIGHT + + "Directory {} already exists".format(path) + "\n" + Style.RESET_ALL) if prompt_yes_no("Erase directory and make new back up?", Fore.RED): shutil.rmtree(path) os.makedirs(path) else: - print(Fore.RED + "Exiting to prevent accidental deletion of user data." + Style.RESET_ALL) + print( + Fore.RED + "Exiting to prevent accidental deletion of user data." + Style.RESET_ALL) sys.exit() elif not os.path.exists(path): os.makedirs(path) - print(Fore.RED + Style.BRIGHT + "CREATED DIR: " + Style.NORMAL + path + Style.RESET_ALL) + print(Fore.RED + Style.BRIGHT + "CREATED DIR: " + + Style.NORMAL + path + Style.RESET_ALL) return def backup_prompt(): """Use pick library to prompt user with choice of what to backup.""" questions = [inquirer.List('choice', - message=Fore.GREEN + Style.BRIGHT + "What would you like to do?" + Fore.BLUE, - choices=[' Back up dotfiles', ' Back up packages', ' Back up fonts', - ' Back up everything', ' Reinstall packages'], - ), - ] + message=Fore.GREEN + Style.BRIGHT + "What would you like to do?" + Fore.BLUE, + choices=[' Back up dotfiles', ' Back up configs', ' Back up packages', ' Back up fonts', + ' Back up everything', ' Reinstall packages', ' Reinstall configs'], + ), + ] answers = inquirer.prompt(questions) return answers.get('choice').strip().lower() @@ -127,9 +129,11 @@ def copy_dotfolder(dotfolder, backup_path): command = "cp -aRp " + dotfolder + " " + backup_path + "/macOS_Preferences" print(Fore.BLUE + command) elif "Application\ Support" not in dotfolder: - command = "cp -aRp " + dotfolder + " " + backup_path + "/" + dotfolder.split("/")[-2] + command = "cp -aRp " + dotfolder + " " + \ + backup_path + "/" + dotfolder.split("/")[-2] elif "Sublime" in dotfolder: - command = "cp -aRp " + dotfolder + " " + backup_path + "/" + dotfolder.split("/")[-3] + command = "cp -aRp " + dotfolder + " " + \ + backup_path + "/" + dotfolder.split("/")[-3] sp.run(command, shell=True, stdout=sp.PIPE) @@ -156,18 +160,22 @@ def backup_dotfiles(backup_path): # get dotfolders and dotfiles - dotfiles_for_backup = [".bashrc", ".bash_profile", ".gitconfig", ".pypirc", ".shallow-backup", ".zshrc"] + dotfiles_for_backup = [".bashrc", ".bash_profile", + ".gitconfig", ".pypirc", ".shallow-backup", ".zshrc"] dotfolders_for_backup = [".ssh/", ".vim/"] # Add dotfile/folder for backup if it exists on the machine - dotfiles = [file for file in dotfiles_for_backup if os.path.isfile(os.path.join(home_path, file))] - dotfolders = [folder for folder in dotfolders_for_backup if os.path.exists(os.path.join(home_path, folder))] + dotfiles = [file for file in dotfiles_for_backup if os.path.isfile( + os.path.join(home_path, file))] + dotfolders = [folder for folder in dotfolders_for_backup if os.path.exists( + os.path.join(home_path, folder))] # dotfiles/dotfolders multiprocessing in list format: [(full_dotfile_path, full_dest_path), ...] dotfolders_mp_in = [] for dotfolder in dotfolders: - dotfolders_mp_in.append((os.path.join(home_path, dotfolder), backup_path)) + dotfolders_mp_in.append( + (os.path.join(home_path, dotfolder), backup_path)) dotfiles_mp_in = [] for dotfile in dotfiles: @@ -179,10 +187,12 @@ def backup_dotfiles(backup_path): # Sublime Text Configs if os.path.isdir("/Users/alichtman/Library/Application Support/Sublime\ Text\ 2"): - dotfolders_mp_in.append((os.path.join(home_path, "Library/Application\ Support/Sublime\ Text\ 2/Packages/User"), backup_path)) + dotfolders_mp_in.append( + (os.path.join(home_path, "Library/Application\ Support/Sublime\ Text\ 2/Packages/User"), backup_path)) if os.path.isdir("/Users/alichtman/Library/Application Support/Sublime\ Text\ 3"): - dotfolders_mp_in.append((os.path.join(home_path, "Library/Application\ Support/Sublime\ Text\ 3/Packages/User"), backup_path)) + dotfolders_mp_in.append( + (os.path.join(home_path, "Library/Application\ Support/Sublime\ Text\ 3/Packages/User"), backup_path)) # pprint(dotfiles_mp_in) # pprint(dotfolders_mp_in) @@ -190,18 +200,65 @@ def backup_dotfiles(backup_path): # Multiprocessing with mp.Pool(mp.cpu_count()): - print(Fore.BLUE + Style.BRIGHT + "Backing up dotfolders..." + Style.RESET_ALL) + print(Fore.BLUE + Style.BRIGHT + + "Backing up dotfolders..." + Style.RESET_ALL) for x in dotfolders_mp_in: x = list(x) mp.Process(target=copy_dotfolder, args=(x[0], x[1],)).start() with mp.Pool(mp.cpu_count()): - print(Fore.BLUE + Style.BRIGHT + "Backing up dotfiles..." + Style.RESET_ALL) + print(Fore.BLUE + Style.BRIGHT + + "Backing up dotfiles..." + Style.RESET_ALL) for x in dotfiles_mp_in: x = list(x) mp.Process(target=copy_dotfile, args=(x[0], x[1],)).start() +def backup_configs(backup_path): + make_dir_warn_overwrite(backup_path) + + def home_prefix(path): + return os.path.join(os.path.expanduser('~'), path) + + configs_dir_mapping = {"Library/Application Support/Sublime Text 2/Packages/User/": "sublime_2", + "Library/Application Support/Sublime Text 3/Packages/User/": "sublime_3", } + plist_files = ["Library/Preferences/com.apple.Terminal.plist"] + + # backup config dirs in backup_path/configs// + for config, target in configs_dir_mapping.items(): + if os.path.isdir(home_prefix(config)): + configs_backup_path = os.path.join(backup_path, target) + _mkdir_or_pass(configs_backup_path) + _copy_dir_content(home_prefix(config), configs_backup_path) + + # backup plist files in backup_path/configs/plist/ + plist_backup_path = os.path.join(backup_path, "plist") + _mkdir_or_pass(plist_backup_path) + for plist in plist_files: + if os.path.exists(home_prefix(plist)): + _copy_dir_content(home_prefix(plist), plist_backup_path) + + +def _copy_dir_content(source, target): + """Copies the contents of a dir to a specified target path.""" + cmd = "cp -a '" + source + "' '" + target + "/'" + # print(cmd) + sp.run(cmd, shell=True, stdout=sp.PIPE) + + +def _copy_file(source, target): + """Copies a file to a specified target path.""" + cmd = "cp -a '" + source + "' '" + target + "'" + # print(cmd) + sp.run(cmd, shell=True, stdout=sp.PIPE) + + +def _mkdir_or_pass(dir): + if not os.path.isdir(dir): + os.makedirs(dir) + pass + + def backup_packages(backup_path): """Creates `packages` directory and places install list text files there.""" @@ -218,25 +275,30 @@ def backup_packages(backup_path): for mgr in std_backup_package_managers: # deal with package managers that have spaces in them. print(Fore.BLUE + "Backing up {} package list...".format(mgr) + Style.RESET_ALL) - command = "{0} list > {1}/{2}_list.txt".format(mgr, backup_path, mgr.replace(" ", "-")) + command = "{0} list > {1}/{2}_list.txt".format( + mgr, backup_path, mgr.replace(" ", "-")) sp.run(command, shell=True, stdout=sp.PIPE) # cargo print(Fore.BLUE + "Backing up cargo package list..." + Style.RESET_ALL) - sp.run("ls {0}/.cargo/bin/ > {1}/cargo_list.txt".format(os.path.expanduser('~'), backup_path), shell=True, stdout=sp.PIPE) + sp.run("ls {0}/.cargo/bin/ > {1}/cargo_list.txt".format( + os.path.expanduser('~'), backup_path), shell=True, stdout=sp.PIPE) # pip print(Fore.BLUE + "Backing up pip package list..." + Style.RESET_ALL) - sp.run("pip list --format=freeze > {}/pip_list.txt".format(backup_path), shell=True, stdout=sp.PIPE) + sp.run("pip list --format=freeze > {}/pip_list.txt".format(backup_path), + shell=True, stdout=sp.PIPE) # npm print(Fore.BLUE + "Backing up npm package list..." + Style.RESET_ALL) - sp.run("npm ls --parseable=true > {}/npm_temp_list.txt".format(backup_path), shell=True, stdout=sp.PIPE) + sp.run("npm ls --parseable=true > {}/npm_temp_list.txt".format(backup_path), + shell=True, stdout=sp.PIPE) # Parse npm output with open("{0}/npm_temp_list.txt".format(backup_path), mode="r+") as f: # Skip first line of file skip = True - sp.run("touch {0}/npm_list.txt".format(backup_path), shell=True, stdout=sp.PIPE) + sp.run("touch {0}/npm_list.txt".format(backup_path), + shell=True, stdout=sp.PIPE) with open("{0}/npm_list.txt".format(backup_path), mode="r+") as dest: for line in f: if not skip: @@ -244,30 +306,35 @@ def backup_packages(backup_path): skip = False # remove temp file - sp.run("rm {}/npm_temp_list.txt".format(backup_path), shell=True, stdout=sp.PIPE) + sp.run("rm {}/npm_temp_list.txt".format(backup_path), + shell=True, stdout=sp.PIPE) # atom package manager print(Fore.BLUE + "Backing up Atom package list..." + Style.RESET_ALL) - sp.run("apm list --installed --bare > {}/apm_list.txt".format(backup_path), shell=True, stdout=sp.PIPE) + sp.run("apm list --installed --bare > {}/apm_list.txt".format(backup_path), + shell=True, stdout=sp.PIPE) # sublime text packages if os.path.isdir("/Users/alichtman/Library/Application Support/Sublime Text 2"): print(Fore.BLUE + "Backing up Sublime Text package list..." + Style.RESET_ALL) sp.run("ls /Users/alichtman/Library/Application\ Support/Sublime\ Text\ 2/Packages/ > {}/sublime2_list.txt" - .format(backup_path), shell=True, stdout=sp.PIPE) + .format(backup_path), shell=True, stdout=sp.PIPE) if os.path.isdir("/Users/alichtman/Library/Application Support/Sublime Text 3"): print(Fore.BLUE + "Backing up Sublime Text package list..." + Style.RESET_ALL) - sp.run("ls /Users/alichtman/Library/Application\ Support/Sublime\ Text\ 3/Installed\ Packages/ > {}/sublime3_list.txt" - .format(backup_path), shell=True, stdout=sp.PIPE) + sp.run( + "ls /Users/alichtman/Library/Application\ Support/Sublime\ Text\ 3/Installed\ Packages/ > {}/sublime3_list.txt" + .format(backup_path), shell=True, stdout=sp.PIPE) # macports print(Fore.BLUE + "Backing up macports package list..." + Style.RESET_ALL) - sp.run("port installed requested > {}/macports_list.txt".format(backup_path), shell=True, stdout=sp.PIPE) + sp.run("port installed requested > {}/macports_list.txt".format(backup_path), + shell=True, stdout=sp.PIPE) # system installs print(Fore.BLUE + "Backing up system application list..." + Style.RESET_ALL) - sp.run("ls /Applications/ > {}/installed_apps_list.txt".format(backup_path), shell=True, stdout=sp.PIPE) + sp.run("ls /Applications/ > {}/installed_apps_list.txt".format(backup_path), + shell=True, stdout=sp.PIPE) # Clean up empty package list files print(Fore.BLUE + "Cleaning up empty package lists..." + Style.RESET_ALL) @@ -305,6 +372,29 @@ def backup_all(dotfiles_path, packages_path, fonts_path): backup_fonts(fonts_path) +def reinstall_config_files(configs_path): + def home_prefix(path): + return os.path.join(os.path.expanduser('~'), path) + + def backup_prefix(path): + return os.path.join(configs_path, path) + + configs_dir_mapping = {"Library/Application Support/Sublime Text 2/Packages/User/": "sublime_2", + "Library/Application Support/Sublime Text 3/Packages/User/": "sublime_3", } + plist_files = { + "Library/Preferences/com.apple.Terminal.plist": "plist/com.apple.Terminal.plist"} + + for target, backup in configs_dir_mapping.items(): + if os.path.isdir(backup_prefix(backup)): + _copy_dir_content(backup_prefix(backup), home_prefix(target)) + + for target, backup in plist_files.items(): + if os.path.exists(backup_prefix(backup)): + _copy_file(backup_prefix(backup), home_prefix(target)) + + pass + + def reinstall_packages(packages_path): """ Reinstall all packages from the files in backup/installs. @@ -324,11 +414,13 @@ def reinstall_packages(packages_path): # construct commands for pm in package_mgrs: if pm in ["brew", "brew-cask"]: - cmd = "xargs {0} install < {1}/{2}_list.txt".format(pm.replace("-", " "), packages_path, pm) + cmd = "xargs {0} install < {1}/{2}_list.txt".format( + pm.replace("-", " "), packages_path, pm) print(cmd) sp.call(cmd, shell=True, stdout=sp.PIPE) elif pm == "npm": - cmd = "cat {0}/npm_list.txt | xargs npm install -g".format(packages_path) + cmd = "cat {0}/npm_list.txt | xargs npm install -g".format( + packages_path) print(cmd) sp.call(cmd, shell=True, stdout=sp.PIPE) elif pm == "pip": @@ -336,13 +428,16 @@ def reinstall_packages(packages_path): print(cmd) sp.call(cmd, shell=True, stdout=sp.PIPE) elif pm == "apm": - cmd = "apm install --packages-file {0}/apm_list.txt".format(packages_path) + cmd = "apm install --packages-file {0}/apm_list.txt".format( + packages_path) print(cmd) sp.call(cmd, shell=True, stdout=sp.PIPE) elif pm == "macports": - print(Fore.RED + "WARNING: Macports reinstallation is not supported." + Style.RESET_ALL) + print( + Fore.RED + "WARNING: Macports reinstallation is not supported." + Style.RESET_ALL) elif pm == "gem": - print(Fore.RED + "WARNING: Gem reinstallation is not supported." + Style.RESET_ALL) + print( + Fore.RED + "WARNING: Gem reinstallation is not supported." + Style.RESET_ALL) elif pm == "cargo": print(Fore.RED + "WARNING: Cargo reinstallation is not possible at the moment. \ \n -> https://github.com/rust-lang/cargo/issues/5593" + Style.RESET_ALL) @@ -367,7 +462,8 @@ def prompt_for_path_update(config_path, config): config['USER']['backup_path']) + Style.RESET_ALL) if prompt_yes_no("Would you like to update this?", Fore.GREEN): - print(Fore.GREEN + Style.BRIGHT + "Enter relative path:" + Style.RESET_ALL) + print(Fore.GREEN + Style.BRIGHT + + "Enter relative path:" + Style.RESET_ALL) abs_path = os.path.abspath(input()) @@ -394,19 +490,22 @@ def read_config(config_path, config): @click.command(context_settings=dict(help_option_names=['-h', '-help'])) @click.option('-complete', is_flag=True, default=False, help="Back up everything.") @click.option('-dotfiles', is_flag=True, default=False, help="Back up dotfiles.") +@click.option('-configs', is_flag=True, default=False, help="Back up app config files.") @click.option('-fonts', is_flag=True, default=False, help="Back up installed fonts.") @click.option('-packages', is_flag=True, default=False, help="Back up package libraries and installed applications.") @click.option('-old_path', is_flag=True, default=False, help="Skip setting new back up directory path.") @click.option('--new_path', default="DEFAULT", help="Input a new back up directory path.") @click.option('-reinstall', is_flag=True, default=False, help="Reinstall packages from package lists.") +@click.option('-reinstall_configs', is_flag=True, default=False, help="Reinstall configs from configs backup.") @click.option('-delete_config', is_flag=True, default=False, help="Remove config file.") @click.option('-v', is_flag=True, default=False, help='Display version and author information and exit.') -def cli(complete, dotfiles, packages, fonts, old_path, new_path, reinstall, delete_config, v): +def cli(complete, dotfiles, configs, packages, fonts, old_path, new_path, reinstall, reinstall_configs, delete_config, + v): """ Easily back up installed packages, dotfiles, and more. """ - config_path = os.path.join(expanduser("~"), ".shallow-backup") + _config_path = os.path.join(expanduser("~"), ".shallow-backup") # Print version information if v: @@ -414,21 +513,24 @@ def cli(complete, dotfiles, packages, fonts, old_path, new_path, reinstall, dele sys.exit() elif delete_config: - command = "rm {}".format(config_path) + command = "rm {}".format(_config_path) sp.run(command, shell=True, stdout=sp.PIPE) - print(Fore.RED + Style.BRIGHT + "Removed config file..." + Style.RESET_ALL) + print(Fore.RED + Style.BRIGHT + + "Removed config file..." + Style.RESET_ALL) sys.exit() splash_screen() - config = configparser.ConfigParser() + # TODO: find proper naming for _config and configs. + _config = configparser.ConfigParser() # if config file doesn't exist, create it. - if not os.path.exists(config_path): - print(Fore.BLUE + Style.BRIGHT + "Creating config file at {}".format(config_path)) - config['USER'] = {'backup_path': 'DEFAULT'} - with open(config_path, 'w') as f: - config.write(f) + if not os.path.exists(_config_path): + print(Fore.BLUE + Style.BRIGHT + + "Creating config file at {}".format(_config_path)) + _config['USER'] = {'backup_path': 'DEFAULT'} + with open(_config_path, 'w') as f: + _config.write(f) # Decide to update path from CLI args, prompt user, or skip updating @@ -439,12 +541,12 @@ def cli(complete, dotfiles, packages, fonts, old_path, new_path, reinstall, dele print(Fore.BLUE + "\nUpdating shallow-backup path to -> " + Style.BRIGHT + "{}".format( abs_path) + Style.RESET_ALL) - config.read(config_path) - config['USER']['backup_path'] = abs_path + _config.read(_config_path) + _config['USER']['backup_path'] = abs_path # Write to config file - with open(config_path, 'w') as f: - config.write(f) + with open(_config_path, 'w') as f: + _config.write(f) # User didn't enter the same_path flag but entered a backup option, so no path update prompt elif old_path or complete or dotfiles or packages or fonts: @@ -452,24 +554,29 @@ def cli(complete, dotfiles, packages, fonts, old_path, new_path, reinstall, dele # User didn't enter a new path, didn't use the same_path flag or any backup options, so prompt else: - prompt_for_path_update(config_path, config) + prompt_for_path_update(_config_path, _config) - backup_home_path = read_config(config_path, config) + backup_home_path = read_config(_config_path, _config) dotfiles_path = os.path.join(backup_home_path, "dotfiles") + configs_path = os.path.join(backup_home_path, "configs") packages_path = os.path.join(backup_home_path, "packages") fonts_path = os.path.join(backup_home_path, "fonts") - print(packages_path) + # print(packages_path) # Command line options - if complete or dotfiles or packages or fonts or reinstall: + if complete or dotfiles or configs or packages or fonts or reinstall or reinstall_configs: if reinstall: reinstall_packages(packages_path) + elif reinstall_configs: + reinstall_config_files(configs_path) elif complete: backup_all(dotfiles_path, packages_path, fonts_path) elif dotfiles: backup_dotfiles(dotfiles_path) + elif configs: + backup_configs(configs_path) elif packages: backup_packages(packages_path) elif fonts: @@ -485,12 +592,16 @@ def cli(complete, dotfiles, packages, fonts, old_path, new_path, reinstall, dele backup_all(dotfiles_path, packages_path, fonts_path) elif selection == "back up dotfiles": backup_dotfiles(dotfiles_path) + elif selection == "back up configs": + backup_configs(configs_path) elif selection == "back up packages": backup_packages(packages_path) elif selection == "back up fonts": backup_fonts(fonts_path) elif selection == "reinstall packages": reinstall_packages(packages_path) + elif selection == "reinstall configs": + reinstall_config_files(configs_path) return