From af62d68c248e6caa6155c7a8c495120484264078 Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 05:17:49 -0500 Subject: [PATCH 1/9] Split main file into subfiles Progress on #136 --- constants.py | 16 +- setup.py | 16 +- shallow_backup.py | 920 ------------------------------- shallow_backup/backup.py | 201 +++++++ shallow_backup/config.py | 152 +++++ shallow_backup/constants.py | 20 + shallow_backup/git_wrapper.py | 87 +++ shallow_backup/printing.py | 57 ++ shallow_backup/prompts.py | 60 ++ shallow_backup/reinstall.py | 86 +++ shallow_backup/shallow_backup.py | 193 +++++++ shallow_backup/utils.py | 100 ++++ 12 files changed, 968 insertions(+), 940 deletions(-) delete mode 100644 shallow_backup.py create mode 100644 shallow_backup/backup.py create mode 100644 shallow_backup/config.py create mode 100644 shallow_backup/constants.py create mode 100644 shallow_backup/git_wrapper.py create mode 100644 shallow_backup/printing.py create mode 100644 shallow_backup/prompts.py create mode 100644 shallow_backup/reinstall.py create mode 100644 shallow_backup/shallow_backup.py create mode 100644 shallow_backup/utils.py diff --git a/constants.py b/constants.py index a4308d41..db17583b 100644 --- a/constants.py +++ b/constants.py @@ -1,12 +1,11 @@ class Constants: - PROJECT_NAME='shallow-backup' - VERSION='1.3' - AUTHOR_GITHUB='alichtman' - AUTHOR_FULL_NAME='Aaron Lichtman' - DESCRIPTION="Easily create lightweight documentation of installed packages, dotfiles, and more." - URL='https://github.com/alichtman/shallow-backup' - AUTHOR_EMAIL='aaronlichtman@gmail.com' - CONFIG_PATH='.shallow-backup' + PROJECT_NAME = 'shallow-backup' + VERSION = '1.3' + AUTHOR_GITHUB = 'alichtman' + AUTHOR_FULL_NAME = 'Aaron Lichtman' + DESCRIPTION = "Easily create lightweight backups of installed packages, dotfiles, and more." + URL = 'https://github.com/alichtman/shallow-backup' + CONFIG_PATH = '.shallow-backup' INVALID_DIRS = [".Trash", ".npm", ".cache", ".rvm"] PACKAGE_MANAGERS = ["gem", "brew-cask", "cargo", "npm", "pip", "brew", "apm"] LOGO = """ @@ -18,4 +17,3 @@ class Constants: `88888P' dP dP `88888P8 dP dP `88888P' 8888P Y8P 88Y8888' `88888P8 `88888P' dP `YP `88888P' 88Y888P' 88 dP """ - diff --git a/setup.py b/setup.py index 2008851e..369c1936 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup from codecs import open from os import path -from constants import Constants +from shallow_backup.constants import Constants here = path.abspath(path.dirname(__file__)) @@ -9,23 +9,17 @@ with open(path.join(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() -# Arguments marked as "Required" below must be included for upload to PyPI. -# Fields marked as "Optional" may be commented out. - setup( - name=Constants.PROJECT_NAME, # Required - version=Constants.VERSION, # Required - description=Constants.DESCRIPTION, # Required + name=Constants.PROJECT_NAME, + version=Constants.VERSION, + description=Constants.DESCRIPTION, long_description_content_type="text/markdown", long_description=long_description, url=Constants.URL, author=Constants.AUTHOR_GITHUB, - # author_email=Constants.AUTHOR_EMAIL, author_email="aaronlichtman@gmail.com", # Classifiers help users find your project by categorizing it. - # - # For a list of valid classifiers, see # https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # Optional 'Development Status :: 4 - Beta', @@ -89,7 +83,7 @@ # For example, the following would provide a command called `sample` which # executes the function `main` from this package when invoked: entry_points={ - 'console_scripts':'shallow-backup=shallow_backup:cli' + 'console_scripts': 'shallow-backup=shallow_backup/shallow_backup:cli' }, # List additional URLs that are relevant to your project as a dict. diff --git a/shallow_backup.py b/shallow_backup.py deleted file mode 100644 index 13081847..00000000 --- a/shallow_backup.py +++ /dev/null @@ -1,920 +0,0 @@ -import os -import git -import sys -import json -import click -import inquirer -import subprocess as sp -import multiprocessing as mp -from constants import Constants -from colorama import Fore, Style -from shutil import copyfile, copytree, rmtree, move - -######## -# Globals -######## - -COMMIT_MSG = { - "fonts" : "Back up fonts.", - "packages": "Back up packages.", - "configs" : "Back up configs.", - "complete": "Back up everything.", - "dotfiles": "Back up dotfiles.," -} - - -######### -# Display -######### - -def print_version_info(cli=True): - """ - Formats version differently for CLI and splash screen. - """ - version = "v{} by {} (@{})".format(Constants.VERSION, - Constants.AUTHOR_FULL_NAME, - Constants.AUTHOR_GITHUB) - if not cli: - print(Fore.RED + Style.BRIGHT + "\t{}\n".format(version) + Style.RESET_ALL) - else: - print(version) - - -def splash_screen(): - """ - Display splash graphic, and then stylized version - """ - print(Fore.YELLOW + Style.BRIGHT + "\n" + Constants.LOGO + Style.RESET_ALL) - print_version_info(False) - - -def print_section_header(title, COLOR): - """ - Prints variable sized section header - """ - block = "#" * (len(title) + 2) - print("\n" + COLOR + Style.BRIGHT + block) - print("#", title) - print(block + "\n" + Style.RESET_ALL) - - -def print_pkg_mgr_backup(mgr): - print("{}Backing up {}{}{}{}{} packages list...{}".format(Fore.BLUE, Style.BRIGHT, Fore.YELLOW, mgr, Fore.BLUE, Style.NORMAL, Style.RESET_ALL)) - - -# TODO: Integrate this in the reinstallation section -def print_pkg_mgr_reinstall(mgr): - print("{}Reinstalling {}{}{}{}{} packages...{}".format(Fore.BLUE, Style.BRIGHT, Fore.YELLOW, mgr, Fore.BLUE, Style.NORMAL, Style.RESET_ALL)) - - -def prompt_yes_no(message, color): - """ - Print question and return True or False depending on user selection from list. - """ - questions = [inquirer.List('choice', - message=color + Style.BRIGHT + message + Fore.BLUE, - choices=[' Yes', ' No'], - ) - ] - - answers = inquirer.prompt(questions) - return answers.get('choice').strip().lower() == 'yes' - - -########### -# Utilities -########### - - -def run_shell_cmd(command): - """ - Wrapper on subprocess.run that handles both lists and strings as commands. - """ - try: - if not isinstance(command, list): - process = sp.run(command.split(), stdout=sp.PIPE) - return process - else: - process = sp.run(command, stdout=sp.PIPE) - return process - except FileNotFoundError: # If package manager is missing - return None - - -def run_shell_cmd_write_stdout_to_file(command, filepath): - """ - Runs a command and then writes its stdout to a file - :param: command String representing command to run and write output of to file - """ - process = run_shell_cmd(command) - if process: - with open(filepath, "w+") as f: - f.write(process.stdout.decode('utf-8')) - - -def make_dir_warn_overwrite(path): - """ - Make destination dir if path doesn't exist, confirm before overwriting if it does. - """ - subdirs = ["dotfiles", "packages", "fonts", "configs"] - if os.path.exists(path) and path.split("/")[-1] in subdirs: - 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): - rmtree(path) - os.makedirs(path) - else: - 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) - - -def get_subfiles(directory): - """ - Returns list of absolute paths of immediate subfiles of a directory - """ - file_paths = [] - for path, subdirs, files in os.walk(directory): - for name in files: - file_paths.append(os.path.join(path, name)) - return file_paths - - -def _copy_dir(source_dir, backup_path): - """ - Copy dotfolder from $HOME. - """ - invalid = set(Constants.INVALID_DIRS) - if len(invalid.intersection(set(source_dir.split("/")))) != 0: - return - - if "Application Support" not in source_dir: - copytree(source_dir, os.path.join(backup_path, source_dir.split("/")[-2]), symlinks=True) - elif "Sublime" in source_dir: - copytree(source_dir, os.path.join(backup_path, source_dir.split("/")[-3]), symlinks=True) - else: - copytree(source_dir, backup_path, symlinks=True) - - -def _mkdir_or_pass(dir): - if not os.path.isdir(dir): - os.makedirs(dir) - pass - - -def _home_prefix(path): - return os.path.join(os.path.expanduser('~'), path) - - -################ -# BACKUP METHODS -################ - -def get_configs_path_mapping(): - """ - Gets a dictionary mapping directories to back up to their destination path. - """ - return { - "Library/Application Support/Sublime Text 2/Packages/User/": "sublime_2", - "Library/Application Support/Sublime Text 3/Packages/User/": "sublime_3", - "Library/Preferences/IntelliJIdea2018.2/" : "intellijidea_2018.2", - "Library/Preferences/PyCharm2018.2/" : "pycharm_2018.2", - "Library/Preferences/CLion2018.2/" : "clion_2018.2", - "Library/Preferences/PhpStorm2018.2" : "phpstorm_2018.2", - ".atom/" : "atom", - } - - -def get_plist_mapping(): - """ - Gets a dictionary mapping plist files to back up to their destination path. - """ - return { - "Library/Preferences/com.apple.Terminal.plist": "plist/com.apple.Terminal.plist", - } - - -def backup_dotfiles(backup_path): - """ - Create `dotfiles` dir and makes copies of dotfiles and dotfolders. - """ - print_section_header("DOTFILES", Fore.BLUE) - make_dir_warn_overwrite(backup_path) - - # assumes dotfiles are stored in home directory - home_path = os.path.expanduser('~') - - # get dotfolders and dotfiles - config = get_config() - dotfiles_for_backup = config["dotfiles"] - dotfolders_for_backup = config["dotfolders"] - - # 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/folders multiprocessing 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)) - - dotfiles_mp_in = [] - for dotfile in dotfiles: - dotfiles_mp_in.append((os.path.join(home_path, dotfile), os.path.join(backup_path, dotfile))) - - # Multiprocessing - with mp.Pool(mp.cpu_count()): - print(Fore.BLUE + Style.BRIGHT + "Backing up dotfolders..." + Style.RESET_ALL) - for x in dotfolders_mp_in: - x = list(x) - mp.Process(target=_copy_dir, args=(x[0], x[1],)).start() - - with mp.Pool(mp.cpu_count()): - print(Fore.BLUE + Style.BRIGHT + "Backing up dotfiles..." + Style.RESET_ALL) - for x in dotfiles_mp_in: - x = list(x) - mp.Process(target=copyfile, args=(x[0], x[1],)).start() - - -def backup_configs(backup_path): - """ - Creates `configs` directory and places config backups there. - Configs are application settings, generally. .plist files count. - """ - print_section_header("CONFIGS", Fore.BLUE) - make_dir_warn_overwrite(backup_path) - - configs_dir_mapping = get_configs_path_mapping() - plist_files = get_plist_mapping() - - print(Fore.BLUE + Style.BRIGHT + "Backing up configs..." + Style.RESET_ALL) - - # backup config dirs in backup_path// - for config, target in configs_dir_mapping.items(): - src_dir = _home_prefix(config) - configs_backup_path = os.path.join(backup_path, target) - if os.path.isdir(src_dir): - # TODO: Exclude Sublime/Atom/VS Code Packages here to speed things up - copytree(src_dir, configs_backup_path, symlinks=True) - - # backup plist files in backup_path/configs/plist/ - print(Fore.BLUE + Style.BRIGHT + "Backing up plist files..." + Style.RESET_ALL) - plist_backup_path = os.path.join(backup_path, "plist") - _mkdir_or_pass(plist_backup_path) - for plist, dest in plist_files.items(): - plist_path = _home_prefix(plist) - if os.path.exists(plist_path): - copyfile(plist_path, os.path.join(backup_path, dest)) - - -def backup_packages(backup_path): - """ - Creates `packages` directory and places install list text files there. - """ - print_section_header("PACKAGES", Fore.BLUE) - make_dir_warn_overwrite(backup_path) - - std_package_managers = [ - "brew", - "brew cask", - "gem" - ] - - for mgr in std_package_managers: - # deal with package managers that have spaces in them. - print_pkg_mgr_backup(mgr) - command = "{} list".format(mgr) - dest = "{}/{}_list.txt".format(backup_path, mgr.replace(" ", "-")) - run_shell_cmd_write_stdout_to_file(command, dest) - - # cargo - print_pkg_mgr_backup("cargo") - command = "ls {}".format(_home_prefix(".cargo/bin/")) - dest = "{}/cargo_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) - - # pip - print_pkg_mgr_backup("pip") - command = "pip list --format=freeze".format(backup_path) - dest = "{}/pip_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) - - # npm - print_pkg_mgr_backup("npm") - command = "npm ls --global --parseable=true --depth=0" - temp_file_path = "{}/npm_temp_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, temp_file_path) - npm_dest_file = "{0}/npm_list.txt".format(backup_path) - # Parse npm output - with open(temp_file_path, mode="r+") as temp_file: - # Skip first line of file - temp_file.seek(1) - with open(npm_dest_file, mode="w+") as dest: - for line in temp_file: - dest.write(line.split("/")[-1]) - - os.remove(temp_file_path) - - # atom package manager - print_pkg_mgr_backup("Atom") - command = "apm list --installed --bare" - dest = "{}/apm_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) - - # sublime text 2 packages - sublime_2_path = _home_prefix("Library/Application Support/Sublime Text 2/Packages/") - if os.path.isdir(sublime_2_path): - print_pkg_mgr_backup("Sublime Text 2") - command = ["ls", sublime_2_path] - dest = "{}/sublime2_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) - - # sublime text 3 packages - sublime_3_path = _home_prefix("Library/Application Support/Sublime Text 3/Installed Packages/") - if os.path.isdir(sublime_3_path): - print_pkg_mgr_backup("Sublime Text 3") - command = ["ls", sublime_3_path] - dest = "{}/sublime3_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) - else: - print(sublime_3_path, "IS NOT DIR") - - # macports - print_pkg_mgr_backup("macports") - command = "port installed requested" - dest = "{}/macports_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) - - # system installs - print_pkg_mgr_backup("macOS Applications") - command = "ls /Applications/" - dest = "{}/system_apps_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) - - # Clean up empty package list files - print(Fore.BLUE + "Cleaning up empty package lists..." + Style.RESET_ALL) - for file in get_subfiles(backup_path): - if os.path.getsize(file) == 0: - os.remove(file) - - -def backup_fonts(path): - """ - Creates list of all .ttf and .otf files in ~/Library/Fonts/ - """ - print_section_header("FONTS", Fore.BLUE) - make_dir_warn_overwrite(path) - print(Fore.BLUE + "Copying '.otf' and '.ttf' fonts..." + Style.RESET_ALL) - fonts_path = _home_prefix("Library/Fonts/") - fonts = [os.path.join(fonts_path, font) for font in os.listdir(fonts_path) if - font.endswith(".otf") or font.endswith(".ttf")] - - for font in fonts: - if os.path.exists(font): - copyfile(font, os.path.join(path, font.split("/")[-1])) - - -def backup_all(dotfiles_path, packages_path, fonts_path, configs_path): - """ - Complete backup procedure. - """ - backup_dotfiles(dotfiles_path) - backup_packages(packages_path) - backup_fonts(fonts_path) - backup_configs(configs_path) - - -################ -# Reinstallation -################ - - -def reinstall_config_files(configs_path): - """ - Reinstall all configs from the backup. - """ - print_section_header("REINSTALLING CONFIG FILES", Fore.BLUE) - - def backup_prefix(path): - return os.path.join(configs_path, path) - - configs_dir_mapping = get_configs_path_mapping() - plist_files = get_plist_mapping() - - for target, backup in configs_dir_mapping.items(): - if os.path.isdir(backup_prefix(backup)): - copytree(backup_prefix(backup), _home_prefix(target)) - - for target, backup in plist_files.items(): - if os.path.exists(backup_prefix(backup)): - copyfile(backup_prefix(backup), _home_prefix(target)) - - print_section_header("SUCCESSFUL CONFIG REINSTALLATION", Fore.BLUE) - sys.exit() - - -def reinstall_packages_from_lists(packages_path): - """ - Reinstall all packages from the files in backup/installs. - """ - print_section_header("REINSTALLING PACKAGES", Fore.BLUE) - - # Figure out which install lists they have saved - package_mgrs = set() - for file in os.listdir(packages_path): - # print(file) - manager = file.split("_")[0].replace("-", " ") - if manager in Constants.PACKAGE_MANAGERS: - package_mgrs.add(file.split("_")[0]) - - # TODO: USE print_pkg_mgr_reinstall() - # TODO: Restylize this printing - print(Fore.BLUE + Style.BRIGHT + "Package Managers detected:" + Style.RESET_ALL) - for mgr in package_mgrs: - print(Fore.BLUE + Style.BRIGHT + "\t" + mgr) - print(Style.RESET_ALL) - - # TODO: Multithreading for reinstallation. - # construct commands - for pm in package_mgrs: - if pm in ["brew", "brew-cask"]: - pm_formatted = pm.replace("-", " ") - print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm_formatted) + Style.RESET_ALL) - cmd = "xargs {0} install < {1}/{2}_list.txt".format(pm.replace("-", " "), packages_path, pm_formatted) - run_shell_cmd(cmd) - elif pm == "npm": - print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) - cmd = "cat {0}/npm_list.txt | xargs npm install -g".format(packages_path) - run_shell_cmd(cmd) - elif pm == "pip": - print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) - cmd = "pip install -r {0}/pip_list.txt".format(packages_path) - run_shell_cmd(cmd) - elif pm == "apm": - print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) - cmd = "apm install --packages-file {0}/apm_list.txt".format(packages_path) - run_shell_cmd(cmd) - elif pm == "macports": - 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) - 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) - - print_section_header("SUCCESSFUL PACKAGE REINSTALLATION", Fore.BLUE) - sys.exit() - - -##### -# Git -##### - - -def git_set_remote(repo, remote_url): - """ - Sets git repo upstream URL and fast-forwards history. - """ - print(Fore.YELLOW + Style.BRIGHT + "Setting remote URL to -> " + Style.NORMAL + "{}...".format( - remote_url) + Style.RESET_ALL) - - try: - origin = repo.create_remote('origin', remote_url) - origin.fetch() - except git.CommandError: - print(Fore.YELLOW + Style.BRIGHT + "Updating existing remote URL..." + Style.RESET_ALL) - repo.delete_remote(repo.remotes.origin) - origin = repo.create_remote('origin', remote_url) - origin.fetch() - - -def create_gitignore_if_needed(dir_path): - """ - Creates a .gitignore file that ignores all files listed in config. - """ - gitignore_path = os.path.join(dir_path, ".gitignore") - if os.path.exists(gitignore_path): - print(Fore.YELLOW + Style.BRIGHT + ".gitignore detected." + Style.RESET_ALL) - pass - else: - print(Fore.YELLOW + Style.BRIGHT + "Creating default .gitignore..." + Style.RESET_ALL) - files_to_ignore = get_config()["gitignore"] - with open(gitignore_path, "w+") as f: - for ignore in files_to_ignore: - f.write("{}\n".format(ignore)) - - -def git_init_if_needed(dir_path): - """ - If there is no git repo inside the dir_path, intialize one. - Returns tuple of (git.Repo, bool new_git_repo_created) - """ - if not os.path.isdir(os.path.join(dir_path, ".git")): - print(Fore.YELLOW + Style.BRIGHT + "Initializing new git repo..." + Style.RESET_ALL) - repo = git.Repo.init(dir_path) - return repo, True - else: - print(Fore.YELLOW + Style.BRIGHT + "Detected git repo." + Style.RESET_ALL) - repo = git.Repo(dir_path) - return repo, False - - -def git_add_all_commit_push(repo, message): - """ - Stages all changed files in dir_path and its children folders for commit, - commits them and pushes to a remote if it's configured. - """ - if repo.index.diff(None) or repo.untracked_files: - print(Fore.YELLOW + Style.BRIGHT + "Making new commit..." + Style.RESET_ALL) - repo.git.add(A=True) - repo.git.commit(m=message) - - if "origin" in [remote.name for remote in repo.remotes]: - print(Fore.YELLOW + Style.BRIGHT + "Pushing to master: " + Style.NORMAL + "{}...".format( - repo.remotes.origin.url) + Style.RESET_ALL) - repo.git.fetch() - repo.git.push("--set-upstream", "origin", "master") - else: - print(Fore.YELLOW + Style.BRIGHT + "No changes to commit..." + Style.RESET_ALL) - - -######## -# Config -######## - - -def get_config_path(): - return _home_prefix(Constants.CONFIG_PATH) - - -def get_config(): - """ - Returns the config. - :return: dictionary for config - """ - with open(get_config_path()) as f: - config = json.load(f) - return config - - -def write_config(config): - """ - Write to config file - """ - with open(get_config_path(), 'w') as f: - json.dump(config, f, indent=4) - - -def get_default_config(): - """ - Returns a default configuration. - """ - return { - "backup_path": "~/shallow-backup", - "dotfiles" : [ - ".bashrc", - ".bash_profile", - ".gitconfig", - ".profile", - ".pypirc", - ".shallow-backup", - ".vimrc", - ".zshrc" - ], - "dotfolders" : [ - ".ssh", - ".vim" - ], - "gitignore" : [ - "dotfiles/.ssh", - "packages/", - "dotfiles/.pypirc", - ] - } - - -def create_config_file_if_needed(): - """ - Creates config file if it doesn't exist already. - """ - backup_config_path = get_config_path() - if not os.path.exists(backup_config_path): - print(Fore.BLUE + Style.BRIGHT + "Creating config file at {}".format(backup_config_path) + Style.RESET_ALL) - backup_config = get_default_config() - write_config(backup_config) - - -def add_path_to_config(section, path): - """ - Adds the path under the correct section in the config file. - FIRST ARG: [dot, config, other] - SECOND ARG: path, relative to home directory for dotfiles, absolute for configs - """ - full_path = _home_prefix(path) - if not os.path.exists(full_path): - print(Fore.RED + Style.BRIGHT + "ERR: {} doesn't exist.".format(full_path) + Style.RESET_ALL) - sys.exit(1) - - if section == "dot": - # Make sure dotfile starts with a period - if path[0] != ".": - print(Fore.RED + Style.BRIGHT + "ERR: Not a dotfile." + Style.RESET_ALL) - sys.exit(1) - - if not os.path.isdir(full_path): - section = "dotfiles" - print(Fore.BLUE + Style.BRIGHT + "Adding {} to dotfile backup.".format(full_path) + Style.RESET_ALL) - else: - section = "dotfolders" - if path[-1] != "/": - full_path += "/" - path += "/" - print(Fore.BLUE + Style.BRIGHT + "Adding {} to dotfolder backup.".format(full_path) + Style.RESET_ALL) - - # TODO: Add config section once configs backup prefs are moved to the config file - elif section == "config": - print(Fore.RED + Style.BRIGHT + "ERR: Option not currently supported." + Style.RESET_ALL) - sys.exit(1) - elif section == "other": - print(Fore.RED + Style.BRIGHT + "ERR: Option not currently supported." + Style.RESET_ALL) - sys.exit(1) - - config = get_config() - file_set = set(config[section]) - file_set.update([path]) - config[section] = list(file_set) - write_config(config) - - -def rm_path_from_config(path): - """ - Removes the path from a section in the config file. Exits if the path doesn't exist. - Path, relative to home directory for dotfiles, absolute for configs - """ - flag = False - config = get_config() - for section, items in config.items(): - if path in items: - print(Fore.BLUE + Style.BRIGHT + "Removing {} from backup...".format(path) + Style.RESET_ALL) - items.remove(path) - config[section] = items - flag = True - - if not flag: - print(Fore.RED + Style.BRIGHT + "ERR: Not currently backing that path up..." + Style.RESET_ALL) - else: - write_config(config) - - -def show_config(): - """ - Print the config. Colorize section titles and indent contents. - """ - print_section_header("SHALLOW BACKUP CONFIG", Fore.RED) - config = get_config() - for section, contents in config.items(): - # Hide gitignore config - if section == "gitignore": - continue - # Print backup path on same line - if section == "backup_path": - print(Fore.RED + Style.BRIGHT + "Backup Path:" + Style.RESET_ALL + contents) - # Print section header and then contents indented. - else: - print(Fore.RED + Style.BRIGHT + "\n{}: ".format(section.capitalize()) + Style.RESET_ALL) - for item in contents: - print(" {}".format(item)) - - print() - - -##### -# CLI -##### - -def move_git_folder_to_path(source_path, new_path): - """ - Moves git folder and .gitignore to the new backup directory. - """ - git_dir = os.path.join(source_path, '.git') - git_ignore_file = os.path.join(source_path, '.gitignore') - - try: - move(git_dir, new_path) - move(git_ignore_file, new_path) - print(Fore.BLUE + Style.BRIGHT + "Moving git repo to new destination" + Style.RESET_ALL) - except FileNotFoundError: - pass - - -def prompt_for_path_update(config): - """ - Ask user if they'd like to update the backup path or not. - If yes, update. If no... don't. - """ - current_path = config["backup_path"] - print("{}{}Current shallow-backup path: {}{}{}".format(Fore.BLUE, Style.BRIGHT, Style.NORMAL, current_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) - abs_path = os.path.abspath(input()) - print(Fore.BLUE + "\nUpdating shallow-backup path to {}".format(abs_path) + Style.RESET_ALL) - config["backup_path"] = abs_path - write_config(config) - make_dir_warn_overwrite(abs_path) - move_git_folder_to_path(current_path, abs_path) - - -def prompt_for_git_url(repo): - """ - Ask user if they'd like to add a remote URL to their git repo. - If yes, do it. - """ - if prompt_yes_no("Would you like to set a remote URL for this git repo?", Fore.GREEN): - print(Fore.GREEN + Style.BRIGHT + "Enter URL:" + Style.RESET_ALL) - remote_url = input() - git_set_remote(repo, remote_url) - - -def destroy_backup_dir(backup_path): - """ - Deletes the backup directory and its content - """ - try: - print("{} Deleting backup directory {} {}...".format(Fore.RED, backup_path, Style.BRIGHT)) - rmtree(backup_path) - except OSError as e: - print("{} Error: {} - {}. {}".format(Fore.RED, e.filename, e.strerror, Style.RESET_ALL)) - - -def actions_menu_prompt(): - """ - Prompt user for an action. - """ - # TODO: Implement `add` and `rm` path here. - questions = [inquirer.List('choice', - 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 configs', - ' Reinstall packages', - ' Show config', - ' Destroy backup' - ], - ), - ] - - answers = inquirer.prompt(questions) - return answers.get('choice').strip().lower() - - -# custom help options -@click.command(context_settings=dict(help_option_names=['-h', '-help', '--help'])) -@click.option('--add', nargs=2, default=[None, None], type=(click.Choice(['dot', 'config', 'other']), str), - help="Add path (relative to home dir) to be backed up. Arg format: [dots, configs, other] ") -@click.option('--rm', default=None, type=str, help="Remove path from config.") -@click.option('-show', is_flag=True, default=False, help="Show config file.") -@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.") -@click.option('-old_path', is_flag=True, default=False, help="Skip setting new back up directory path.") -@click.option('--new_path', default=None, help="Input a new back up directory path.") -@click.option('--remote', default=None, help="Input a URL for a git repository.") -@click.option('-reinstall_packages', 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('-destroy_backup', is_flag=True, default=False, help='Removes the backup directory and its content.') -@click.option('-v', is_flag=True, default=False, help='Display version and author information and exit.') -def cli(add, rm, show, complete, dotfiles, configs, packages, fonts, old_path, new_path, remote, reinstall_packages, - reinstall_configs, delete_config, destroy_backup, v): - """ - Easily back up installed packages, dotfiles, and more. You can edit which dotfiles are backed up in ~/.shallow-backup. - """ - backup_config_path = get_config_path() - - # No interface going to be displayed - if any([v, delete_config, destroy_backup, show, rm]) or None not in add: - if v: - print_version_info() - elif delete_config: - os.remove(backup_config_path) - print(Fore.RED + Style.BRIGHT + "Removed config file..." + Style.RESET_ALL) - elif destroy_backup: - backup_home_path = get_config()["backup_path"] - destroy_backup_dir(backup_home_path) - elif None not in add: - add_path_to_config(add[0], add[1]) - elif rm: - rm_path_from_config(rm) - elif show: - show_config() - sys.exit() - - # Start CLI - splash_screen() - create_config_file_if_needed() - backup_config = get_config() - - # User entered a new path, so update the config - if new_path: - abs_path = os.path.abspath(new_path) - print(Fore.BLUE + Style.NORMAL + "\nUpdating shallow-backup path to -> " + Style.BRIGHT + "{}".format( - abs_path) + Style.RESET_ALL) - backup_config["backup_path"] = abs_path - write_config(backup_config) - - # User didn't enter any CLI args so prompt for path update before showing menu - elif not (old_path or complete or dotfiles or packages or fonts): - prompt_for_path_update(backup_config) - - # Create backup directory and do git setup - backup_home_path = get_config()["backup_path"] - make_dir_warn_overwrite(backup_home_path) - repo, new_git_repo_created = git_init_if_needed(backup_home_path) - - # Create default gitignore if we just ran git init - if new_git_repo_created: - create_gitignore_if_needed(backup_home_path) - # Prompt user for remote URL - if not remote: - prompt_for_git_url(repo) - - # Set remote URL from CLI arg - if remote: - git_set_remote(repo, remote) - - 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") - - # Command line options - if any([complete, dotfiles, configs, packages, fonts, reinstall_packages, reinstall_configs]): - if reinstall_packages: - reinstall_packages_from_lists(packages_path) - elif reinstall_configs: - reinstall_config_files(configs_path) - elif complete: - backup_all(dotfiles_path, packages_path, fonts_path, configs_path) - git_add_all_commit_push(repo, COMMIT_MSG["everything"]) - elif dotfiles: - backup_dotfiles(dotfiles_path) - git_add_all_commit_push(repo, COMMIT_MSG["dotfiles"]) - elif configs: - backup_configs(configs_path) - git_add_all_commit_push(repo, COMMIT_MSG["configs"]) - elif packages: - backup_packages(packages_path) - git_add_all_commit_push(repo, COMMIT_MSG["packages"]) - elif fonts: - backup_fonts(fonts_path) - git_add_all_commit_push(repo, COMMIT_MSG["fonts"]) - # No CL options, prompt for selection - else: - selection = actions_menu_prompt().lower().strip() - if selection == "back up everything": - backup_all(dotfiles_path, packages_path, fonts_path, configs_path) - git_add_all_commit_push(repo, COMMIT_MSG["everything"]) - elif selection == "back up dotfiles": - backup_dotfiles(dotfiles_path) - git_add_all_commit_push(repo, COMMIT_MSG["dotfiles"]) - elif selection == "back up configs": - backup_configs(configs_path) - git_add_all_commit_push(repo, COMMIT_MSG["configs"]) - elif selection == "back up packages": - backup_packages(packages_path) - git_add_all_commit_push(repo, COMMIT_MSG["packages"]) - elif selection == "back up fonts": - backup_fonts(fonts_path) - git_add_all_commit_push(repo, COMMIT_MSG["fonts"]) - elif selection == "reinstall packages": - reinstall_packages_from_lists(packages_path) - elif selection == "reinstall configs": - reinstall_config_files(configs_path) - elif selection == "show config": - show_config() - elif selection == "destroy backup": - if prompt_yes_no("Erase backup directory: {}?".format(backup_home_path), Fore.RED): - destroy_backup_dir(backup_home_path) - else: - print("{} Exiting to prevent accidental deletion of backup directory... {}".format( - Fore.RED, Style.RESET_ALL)) - - sys.exit() - - -if __name__ == '__main__': - """ - I'm just here so I don't get fined. - """ - cli() diff --git a/shallow_backup/backup.py b/shallow_backup/backup.py new file mode 100644 index 00000000..8e29f19a --- /dev/null +++ b/shallow_backup/backup.py @@ -0,0 +1,201 @@ +import os +import multiprocessing as mp +from config import get_config +from colorama import Fore, Style +from shutil import copytree, copyfile +from printing import print_section_header, print_pkg_mgr_backup +from shallow_backup import get_configs_path_mapping, get_plist_mapping +from utils import _home_prefix, make_dir_warn_overwrite, run_shell_cmd_write_stdout_to_file, _copy_dir, _mkdir_or_pass, get_subfiles + + +def backup_dotfiles(backup_path): + """ + Create `dotfiles` dir and makes copies of dotfiles and dotfolders. + """ + print_section_header("DOTFILES", Fore.BLUE) + make_dir_warn_overwrite(backup_path) + + # assumes dotfiles are stored in home directory + home_path = os.path.expanduser('~') + + # get dotfolders and dotfiles + config = get_config() + dotfiles_for_backup = config["dotfiles"] + dotfolders_for_backup = config["dotfolders"] + + # 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/folders multiprocessing 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)) + + dotfiles_mp_in = [] + for dotfile in dotfiles: + dotfiles_mp_in.append((os.path.join(home_path, dotfile), os.path.join(backup_path, dotfile))) + + # Multiprocessing + with mp.Pool(mp.cpu_count()): + print(Fore.BLUE + Style.BRIGHT + "Backing up dotfolders..." + Style.RESET_ALL) + for x in dotfolders_mp_in: + x = list(x) + mp.Process(target=_copy_dir, args=(x[0], x[1],)).start() + + with mp.Pool(mp.cpu_count()): + print(Fore.BLUE + Style.BRIGHT + "Backing up dotfiles..." + Style.RESET_ALL) + for x in dotfiles_mp_in: + x = list(x) + mp.Process(target=copyfile, args=(x[0], x[1],)).start() + + +def backup_configs(backup_path): + """ + Creates `configs` directory and places config backups there. + Configs are application settings, generally. .plist files count. + """ + print_section_header("CONFIGS", Fore.BLUE) + make_dir_warn_overwrite(backup_path) + + configs_dir_mapping = get_configs_path_mapping() + plist_files = get_plist_mapping() + + print(Fore.BLUE + Style.BRIGHT + "Backing up configs..." + Style.RESET_ALL) + + # backup config dirs in backup_path// + for config, target in configs_dir_mapping.items(): + src_dir = _home_prefix(config) + configs_backup_path = os.path.join(backup_path, target) + if os.path.isdir(src_dir): + # TODO: Exclude Sublime/Atom/VS Code Packages here to speed things up + copytree(src_dir, configs_backup_path, symlinks=True) + + # backup plist files in backup_path/configs/plist/ + print(Fore.BLUE + Style.BRIGHT + "Backing up plist files..." + Style.RESET_ALL) + plist_backup_path = os.path.join(backup_path, "plist") + _mkdir_or_pass(plist_backup_path) + for plist, dest in plist_files.items(): + plist_path = _home_prefix(plist) + if os.path.exists(plist_path): + copyfile(plist_path, os.path.join(backup_path, dest)) + + +def backup_packages(backup_path): + """ + Creates `packages` directory and places install list text files there. + """ + print_section_header("PACKAGES", Fore.BLUE) + make_dir_warn_overwrite(backup_path) + + std_package_managers = [ + "brew", + "brew cask", + "gem" + ] + + for mgr in std_package_managers: + # deal with package managers that have spaces in them. + print_pkg_mgr_backup(mgr) + command = "{} list".format(mgr) + dest = "{}/{}_list.txt".format(backup_path, mgr.replace(" ", "-")) + run_shell_cmd_write_stdout_to_file(command, dest) + + # cargo + print_pkg_mgr_backup("cargo") + command = "ls {}".format(_home_prefix(".cargo/bin/")) + dest = "{}/cargo_list.txt".format(backup_path) + run_shell_cmd_write_stdout_to_file(command, dest) + + # pip + print_pkg_mgr_backup("pip") + command = "pip list --format=freeze".format(backup_path) + dest = "{}/pip_list.txt".format(backup_path) + run_shell_cmd_write_stdout_to_file(command, dest) + + # npm + print_pkg_mgr_backup("npm") + command = "npm ls --global --parseable=true --depth=0" + temp_file_path = "{}/npm_temp_list.txt".format(backup_path) + run_shell_cmd_write_stdout_to_file(command, temp_file_path) + npm_dest_file = "{0}/npm_list.txt".format(backup_path) + # Parse npm output + with open(temp_file_path, mode="r+") as temp_file: + # Skip first line of file + temp_file.seek(1) + with open(npm_dest_file, mode="w+") as dest: + for line in temp_file: + dest.write(line.split("/")[-1]) + + os.remove(temp_file_path) + + # atom package manager + print_pkg_mgr_backup("Atom") + command = "apm list --installed --bare" + dest = "{}/apm_list.txt".format(backup_path) + run_shell_cmd_write_stdout_to_file(command, dest) + + # sublime text 2 packages + sublime_2_path = _home_prefix("Library/Application Support/Sublime Text 2/Packages/") + if os.path.isdir(sublime_2_path): + print_pkg_mgr_backup("Sublime Text 2") + command = ["ls", sublime_2_path] + dest = "{}/sublime2_list.txt".format(backup_path) + run_shell_cmd_write_stdout_to_file(command, dest) + + # sublime text 3 packages + sublime_3_path = _home_prefix("Library/Application Support/Sublime Text 3/Installed Packages/") + if os.path.isdir(sublime_3_path): + print_pkg_mgr_backup("Sublime Text 3") + command = ["ls", sublime_3_path] + dest = "{}/sublime3_list.txt".format(backup_path) + run_shell_cmd_write_stdout_to_file(command, dest) + else: + print(sublime_3_path, "IS NOT DIR") + + # macports + print_pkg_mgr_backup("macports") + command = "port installed requested" + dest = "{}/macports_list.txt".format(backup_path) + run_shell_cmd_write_stdout_to_file(command, dest) + + # system installs + print_pkg_mgr_backup("macOS Applications") + command = "ls /Applications/" + dest = "{}/system_apps_list.txt".format(backup_path) + run_shell_cmd_write_stdout_to_file(command, dest) + + # Clean up empty package list files + print(Fore.BLUE + "Cleaning up empty package lists..." + Style.RESET_ALL) + for file in get_subfiles(backup_path): + if os.path.getsize(file) == 0: + os.remove(file) + + +def backup_fonts(path): + """ + Creates list of all .ttf and .otf files in ~/Library/Fonts/ + """ + print_section_header("FONTS", Fore.BLUE) + make_dir_warn_overwrite(path) + print(Fore.BLUE + "Copying '.otf' and '.ttf' fonts..." + Style.RESET_ALL) + fonts_path = _home_prefix("Library/Fonts/") + fonts = [os.path.join(fonts_path, font) for font in os.listdir(fonts_path) if + font.endswith(".otf") or font.endswith(".ttf")] + + for font in fonts: + if os.path.exists(font): + copyfile(font, os.path.join(path, font.split("/")[-1])) + + +def backup_all(dotfiles_path, packages_path, fonts_path, configs_path): + """ + Complete backup procedure. + """ + backup_dotfiles(dotfiles_path) + backup_packages(packages_path) + backup_fonts(fonts_path) + backup_configs(configs_path) diff --git a/shallow_backup/config.py b/shallow_backup/config.py new file mode 100644 index 00000000..a0ba3804 --- /dev/null +++ b/shallow_backup/config.py @@ -0,0 +1,152 @@ +import os +import sys +import json +from colorama import Fore, Style +import constants as Constants +from printing import print_section_header +from utils import _home_prefix + + +def get_config_path(): + return _home_prefix(Constants.CONFIG_PATH) + + +def get_config(): + """ + Returns the config. + :return: dictionary for config + """ + with open(get_config_path()) as f: + config = json.load(f) + return config + + +def write_config(config): + """ + Write to config file + """ + with open(get_config_path(), 'w') as f: + json.dump(config, f, indent=4) + + +def get_default_config(): + """ + Returns a default configuration. + """ + return { + "backup_path": "~/shallow-backup", + "dotfiles": [ + ".bashrc", + ".bash_profile", + ".gitconfig", + ".profile", + ".pypirc", + ".shallow-backup", + ".vimrc", + ".zshrc" + ], + "dotfolders": [ + ".ssh", + ".vim" + ], + "gitignore": [ + "dotfiles/.ssh", + "packages/", + "dotfiles/.pypirc", + ] + } + + +def create_config_file_if_needed(): + """ + Creates config file if it doesn't exist already. + """ + backup_config_path = get_config_path() + if not os.path.exists(backup_config_path): + print(Fore.BLUE + Style.BRIGHT + "Creating config file at {}".format(backup_config_path) + Style.RESET_ALL) + backup_config = get_default_config() + write_config(backup_config) + + +def add_path_to_config(section, path): + """ + Adds the path under the correct section in the config file. + FIRST ARG: [dot, config, other] + SECOND ARG: path, relative to home directory for dotfiles, absolute for configs + """ + full_path = _home_prefix(path) + if not os.path.exists(full_path): + print(Fore.RED + Style.BRIGHT + "ERR: {} doesn't exist.".format(full_path) + Style.RESET_ALL) + sys.exit(1) + + if section == "dot": + # Make sure dotfile starts with a period + if path[0] != ".": + print(Fore.RED + Style.BRIGHT + "ERR: Not a dotfile." + Style.RESET_ALL) + sys.exit(1) + + if not os.path.isdir(full_path): + section = "dotfiles" + print(Fore.BLUE + Style.BRIGHT + "Adding {} to dotfile backup.".format(full_path) + Style.RESET_ALL) + else: + section = "dotfolders" + if path[-1] != "/": + full_path += "/" + path += "/" + print(Fore.BLUE + Style.BRIGHT + "Adding {} to dotfolder backup.".format(full_path) + Style.RESET_ALL) + + # TODO: Add config section once configs backup prefs are moved to the config file + elif section == "config": + print(Fore.RED + Style.BRIGHT + "ERR: Option not currently supported." + Style.RESET_ALL) + sys.exit(1) + elif section == "other": + print(Fore.RED + Style.BRIGHT + "ERR: Option not currently supported." + Style.RESET_ALL) + sys.exit(1) + + config = get_config() + file_set = set(config[section]) + file_set.update([path]) + config[section] = list(file_set) + write_config(config) + + +def rm_path_from_config(path): + """ + Removes the path from a section in the config file. Exits if the path doesn't exist. + Path, relative to home directory for dotfiles, absolute for configs + """ + flag = False + config = get_config() + for section, items in config.items(): + if path in items: + print(Fore.BLUE + Style.BRIGHT + "Removing {} from backup...".format(path) + Style.RESET_ALL) + items.remove(path) + config[section] = items + flag = True + + if not flag: + print(Fore.RED + Style.BRIGHT + "ERR: Not currently backing that path up..." + Style.RESET_ALL) + else: + write_config(config) + + +def show_config(): + """ + Print the config. Colorize section titles and indent contents. + """ + print_section_header("SHALLOW BACKUP CONFIG", Fore.RED) + config = get_config() + for section, contents in config.items(): + # Hide gitignore config + if section == "gitignore": + continue + # Print backup path on same line + if section == "backup_path": + print(Fore.RED + Style.BRIGHT + "Backup Path:" + Style.RESET_ALL + contents) + # Print section header and then contents indented. + else: + print(Fore.RED + Style.BRIGHT + "\n{}: ".format(section.capitalize()) + Style.RESET_ALL) + for item in contents: + print(" {}".format(item)) + + print() diff --git a/shallow_backup/constants.py b/shallow_backup/constants.py new file mode 100644 index 00000000..6a1de1d5 --- /dev/null +++ b/shallow_backup/constants.py @@ -0,0 +1,20 @@ +class Constants: + PROJECT_NAME = 'shallow-backup' + VERSION = '1.3' + AUTHOR_GITHUB = 'alichtman' + AUTHOR_FULL_NAME = 'Aaron Lichtman' + DESCRIPTION = "Easily create lightweight backups of installed packages, dotfiles, and more." + URL = 'https://github.com/alichtman/shallow-backup' + AUTHOR_EMAIL = 'aaronlichtman@gmail.com' + CONFIG_PATH = '.shallow-backup' + INVALID_DIRS = [".Trash", ".npm", ".cache", ".rvm"] + PACKAGE_MANAGERS = ["gem", "brew-cask", "cargo", "npm", "pip", "brew", "apm"] + LOGO = """ + dP dP dP dP dP + 88 88 88 88 88 + ,d8888' 88d888b. .d8888b. 88 88 .d8888b. dP dP dP 88d888b. .d8888b. .d8888b. 88 .dP dP dP 88d888b. + Y8ooooo, 88' `88 88' `88 88 88 88' `88 88 88 88 88' `88 88' `88 88' `\"\" 88888\" 88 88 88' `88 + 88 88 88 88. .88 88 88 88. .88 88.88b.88' 88. .88 88. .88 88. ... 88 `8b. 88. .88 88. .88 + `88888P' dP dP `88888P8 dP dP `88888P' 8888P Y8P 88Y8888' `88888P8 `88888P' dP `YP `88888P' 88Y888P' + 88 + dP """ diff --git a/shallow_backup/git_wrapper.py b/shallow_backup/git_wrapper.py new file mode 100644 index 00000000..fb45a0d6 --- /dev/null +++ b/shallow_backup/git_wrapper.py @@ -0,0 +1,87 @@ +import git +import os +from colorama import Fore, Style +from config import get_config +from shutil import move + + +def git_set_remote(repo, remote_url): + """ + Sets git repo upstream URL and fast-forwards history. + """ + print(Fore.YELLOW + Style.BRIGHT + "Setting remote URL to -> " + Style.NORMAL + "{}...".format( + remote_url) + Style.RESET_ALL) + + try: + origin = repo.create_remote('origin', remote_url) + origin.fetch() + except git.CommandError: + print(Fore.YELLOW + Style.BRIGHT + "Updating existing remote URL..." + Style.RESET_ALL) + repo.delete_remote(repo.remotes.origin) + origin = repo.create_remote('origin', remote_url) + origin.fetch() + + +def create_gitignore_if_needed(dir_path): + """ + Creates a .gitignore file that ignores all files listed in config. + """ + gitignore_path = os.path.join(dir_path, ".gitignore") + if os.path.exists(gitignore_path): + print(Fore.YELLOW + Style.BRIGHT + ".gitignore detected." + Style.RESET_ALL) + pass + else: + print(Fore.YELLOW + Style.BRIGHT + "Creating default .gitignore..." + Style.RESET_ALL) + files_to_ignore = get_config()["gitignore"] + with open(gitignore_path, "w+") as f: + for ignore in files_to_ignore: + f.write("{}\n".format(ignore)) + + +def git_init_if_needed(dir_path): + """ + If there is no git repo inside the dir_path, intialize one. + Returns tuple of (git.Repo, bool new_git_repo_created) + """ + if not os.path.isdir(os.path.join(dir_path, ".git")): + print(Fore.YELLOW + Style.BRIGHT + "Initializing new git repo..." + Style.RESET_ALL) + repo = git.Repo.init(dir_path) + return repo, True + else: + print(Fore.YELLOW + Style.BRIGHT + "Detected git repo." + Style.RESET_ALL) + repo = git.Repo(dir_path) + return repo, False + + +def git_add_all_commit_push(repo, message): + """ + Stages all changed files in dir_path and its children folders for commit, + commits them and pushes to a remote if it's configured. + """ + if repo.index.diff(None) or repo.untracked_files: + print(Fore.YELLOW + Style.BRIGHT + "Making new commit..." + Style.RESET_ALL) + repo.git.add(A=True) + repo.git.commit(m=message) + + if "origin" in [remote.name for remote in repo.remotes]: + print(Fore.YELLOW + Style.BRIGHT + "Pushing to master: " + Style.NORMAL + "{}...".format( + repo.remotes.origin.url) + Style.RESET_ALL) + repo.git.fetch() + repo.git.push("--set-upstream", "origin", "master") + else: + print(Fore.YELLOW + Style.BRIGHT + "No changes to commit..." + Style.RESET_ALL) + + +def move_git_folder_to_path(source_path, new_path): + """ + Moves git folder and .gitignore to the new backup directory. + """ + git_dir = os.path.join(source_path, '.git') + git_ignore_file = os.path.join(source_path, '.gitignore') + + try: + move(git_dir, new_path) + move(git_ignore_file, new_path) + print(Fore.BLUE + Style.BRIGHT + "Moving git repo to new destination" + Style.RESET_ALL) + except FileNotFoundError: + pass diff --git a/shallow_backup/printing.py b/shallow_backup/printing.py new file mode 100644 index 00000000..3dc6abdf --- /dev/null +++ b/shallow_backup/printing.py @@ -0,0 +1,57 @@ +from colorama import Fore, Style +import constants as Constants +import inquirer + + +def print_version_info(cli=True): + """ + Formats version differently for CLI and splash screen. + """ + version = "v{} by {} (@{})".format(Constants.VERSION, + Constants.AUTHOR_FULL_NAME, + Constants.AUTHOR_GITHUB) + if not cli: + print(Fore.RED + Style.BRIGHT + "\t{}\n".format(version) + Style.RESET_ALL) + else: + print(version) + + +def splash_screen(): + """ + Display splash graphic, and then stylized version + """ + print(Fore.YELLOW + Style.BRIGHT + "\n" + Constants.LOGO + Style.RESET_ALL) + print_version_info(False) + + +def print_section_header(title, COLOR): + """ + Prints variable sized section header + """ + block = "#" * (len(title) + 2) + print("\n" + COLOR + Style.BRIGHT + block) + print("#", title) + print(block + "\n" + Style.RESET_ALL) + + +def print_pkg_mgr_backup(mgr): + print("{}Backing up {}{}{}{}{} packages list...{}".format(Fore.BLUE, Style.BRIGHT, Fore.YELLOW, mgr, Fore.BLUE, Style.NORMAL, Style.RESET_ALL)) + + +# TODO: Integrate this in the reinstallation section +def print_pkg_mgr_reinstall(mgr): + print("{}Reinstalling {}{}{}{}{} packages...{}".format(Fore.BLUE, Style.BRIGHT, Fore.YELLOW, mgr, Fore.BLUE, Style.NORMAL, Style.RESET_ALL)) + + +def prompt_yes_no(message, color): + """ + Print question and return True or False depending on user selection from list. + """ + questions = [inquirer.List('choice', + message=color + Style.BRIGHT + message + Fore.BLUE, + choices=[' Yes', ' No'], + ) + ] + + answers = inquirer.prompt(questions) + return answers.get('choice').strip().lower() == 'yes' diff --git a/shallow_backup/prompts.py b/shallow_backup/prompts.py new file mode 100644 index 00000000..2d1275b0 --- /dev/null +++ b/shallow_backup/prompts.py @@ -0,0 +1,60 @@ +import os +import inquirer +from colorama import Fore, Style +from config import write_config +from utils import make_dir_warn_overwrite +from printing import prompt_yes_no +from git_wrapper import git_set_remote, move_git_folder_to_path + + +def prompt_for_path_update(config): + """ + Ask user if they'd like to update the backup path or not. + If yes, update. If no... don't. + """ + current_path = config["backup_path"] + print("{}{}Current shallow-backup path: {}{}{}".format(Fore.BLUE, Style.BRIGHT, Style.NORMAL, current_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) + abs_path = os.path.abspath(input()) + print(Fore.BLUE + "\nUpdating shallow-backup path to {}".format(abs_path) + Style.RESET_ALL) + config["backup_path"] = abs_path + write_config(config) + make_dir_warn_overwrite(abs_path) + move_git_folder_to_path(current_path, abs_path) + + +def prompt_for_git_url(repo): + """ + Ask user if they'd like to add a remote URL to their git repo. + If yes, do it. + """ + if prompt_yes_no("Would you like to set a remote URL for this git repo?", Fore.GREEN): + print(Fore.GREEN + Style.BRIGHT + "Enter URL:" + Style.RESET_ALL) + remote_url = input() + git_set_remote(repo, remote_url) + + +def actions_menu_prompt(): + """ + Prompt user for an action. + """ + # TODO: Implement `add` and `rm` path here. + questions = [inquirer.List('choice', + 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 configs', + ' Reinstall packages', + ' Show config', + ' Destroy backup' + ], + ), + ] + + answers = inquirer.prompt(questions) + return answers.get('choice').strip().lower() diff --git a/shallow_backup/reinstall.py b/shallow_backup/reinstall.py new file mode 100644 index 00000000..cd206644 --- /dev/null +++ b/shallow_backup/reinstall.py @@ -0,0 +1,86 @@ +import os +import sys +from util import run_shell_cmd +from shutil import copytree, copyfile +from colorama import Fore, Style +from printing import print_section_header +from constants import Constants +from shallow_backup import get_configs_path_mapping, get_plist_mapping +from utils import _home_prefix + + +def reinstall_config_files(configs_path): + """ + Reinstall all configs from the backup. + """ + print_section_header("REINSTALLING CONFIG FILES", Fore.BLUE) + + def backup_prefix(path): + return os.path.join(configs_path, path) + + configs_dir_mapping = get_configs_path_mapping() + plist_files = get_plist_mapping() + + for target, backup in configs_dir_mapping.items(): + if os.path.isdir(backup_prefix(backup)): + copytree(backup_prefix(backup), _home_prefix(target)) + + for target, backup in plist_files.items(): + if os.path.exists(backup_prefix(backup)): + copyfile(backup_prefix(backup), _home_prefix(target)) + + print_section_header("SUCCESSFUL CONFIG REINSTALLATION", Fore.BLUE) + sys.exit() + + +def reinstall_packages_from_lists(packages_path): + """ + Reinstall all packages from the files in backup/installs. + """ + print_section_header("REINSTALLING PACKAGES", Fore.BLUE) + + # Figure out which install lists they have saved + package_mgrs = set() + for file in os.listdir(packages_path): + # print(file) + manager = file.split("_")[0].replace("-", " ") + if manager in Constants.PACKAGE_MANAGERS: + package_mgrs.add(file.split("_")[0]) + + # TODO: USE print_pkg_mgr_reinstall() + # TODO: Restylize this printing + print(Fore.BLUE + Style.BRIGHT + "Package Managers detected:" + Style.RESET_ALL) + for mgr in package_mgrs: + print(Fore.BLUE + Style.BRIGHT + "\t" + mgr) + print(Style.RESET_ALL) + + # TODO: Multithreading for reinstallation. + # construct commands + for pm in package_mgrs: + if pm in ["brew", "brew-cask"]: + pm_formatted = pm.replace("-", " ") + print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm_formatted) + Style.RESET_ALL) + cmd = "xargs {0} install < {1}/{2}_list.txt".format(pm.replace("-", " "), packages_path, pm_formatted) + run_shell_cmd(cmd) + elif pm == "npm": + print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) + cmd = "cat {0}/npm_list.txt | xargs npm install -g".format(packages_path) + run_shell_cmd(cmd) + elif pm == "pip": + print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) + cmd = "pip install -r {0}/pip_list.txt".format(packages_path) + run_shell_cmd(cmd) + elif pm == "apm": + print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) + cmd = "apm install --packages-file {0}/apm_list.txt".format(packages_path) + run_shell_cmd(cmd) + elif pm == "macports": + 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) + 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) + + print_section_header("SUCCESSFUL PACKAGE REINSTALLATION", Fore.BLUE) + sys.exit() diff --git a/shallow_backup/shallow_backup.py b/shallow_backup/shallow_backup.py new file mode 100644 index 00000000..19e85e20 --- /dev/null +++ b/shallow_backup/shallow_backup.py @@ -0,0 +1,193 @@ +import os +import sys +import click +from colorama import Fore, Style +from reinstall import reinstall_packages_from_lists, reinstall_config_files +from config import get_config, show_config, add_path_to_config, rm_path_from_config, write_config, create_config_file_if_needed, get_config_path +from utils import make_dir_warn_overwrite, destroy_backup_dir +from printing import print_version_info, prompt_yes_no, splash_screen +from backup import backup_all, backup_configs, backup_dotfiles, backup_fonts, backup_packages +from git_wrapper import git_init_if_needed, git_set_remote, git_add_all_commit_push, create_gitignore_if_needed +from prompts import actions_menu_prompt, prompt_for_git_url, prompt_for_path_update + +######## +# Globals +######## + +# TODO: Refactor this to the git file +COMMIT_MSG = { + "fonts": "Back up fonts.", + "packages": "Back up packages.", + "configs": "Back up configs.", + "complete": "Back up everything.", + "dotfiles": "Back up dotfiles.," +} + + +# TODO: Convert these two functions to store info in the actual config and just read from it. +def get_configs_path_mapping(): + """ + Gets a dictionary mapping directories to back up to their destination path. + """ + return { + "Library/Application Support/Sublime Text 2/Packages/User/": "sublime_2", + "Library/Application Support/Sublime Text 3/Packages/User/": "sublime_3", + "Library/Preferences/IntelliJIdea2018.2/": "intellijidea_2018.2", + "Library/Preferences/PyCharm2018.2/": "pycharm_2018.2", + "Library/Preferences/CLion2018.2/": "clion_2018.2", + "Library/Preferences/PhpStorm2018.2": "phpstorm_2018.2", + ".atom/": "atom", + } + + +def get_plist_mapping(): + """ + Gets a dictionary mapping plist files to back up to their destination path. + """ + return { + "Library/Preferences/com.apple.Terminal.plist": "plist/com.apple.Terminal.plist", + } + + +# custom help options +@click.command(context_settings=dict(help_option_names=['-h', '-help', '--help'])) +@click.option('--add', nargs=2, default=[None, None], type=(click.Choice(['dot', 'config', 'other']), str), + help="Add path (relative to home dir) to be backed up. Arg format: [dots, configs, other] ") +@click.option('--rm', default=None, type=str, help="Remove path from config.") +@click.option('-show', is_flag=True, default=False, help="Show config file.") +@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.") +@click.option('-old_path', is_flag=True, default=False, help="Skip setting new back up directory path.") +@click.option('--new_path', default=None, help="Input a new back up directory path.") +@click.option('--remote', default=None, help="Input a URL for a git repository.") +@click.option('-reinstall_packages', 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('-destroy_backup', is_flag=True, default=False, help='Removes the backup directory and its content.') +@click.option('-v', is_flag=True, default=False, help='Display version and author information and exit.') +def cli(add, rm, show, complete, dotfiles, configs, packages, fonts, old_path, new_path, remote, reinstall_packages, + reinstall_configs, delete_config, destroy_backup, v): + """ + Easily back up installed packages, dotfiles, and more. You can edit which dotfiles are backed up in ~/.shallow-backup. + """ + backup_config_path = get_config_path() + + # No interface going to be displayed + if any([v, delete_config, destroy_backup, show, rm]) or None not in add: + if v: + print_version_info() + elif delete_config: + os.remove(backup_config_path) + print(Fore.RED + Style.BRIGHT + "Removed config file..." + Style.RESET_ALL) + elif destroy_backup: + backup_home_path = get_config()["backup_path"] + destroy_backup_dir(backup_home_path) + elif None not in add: + add_path_to_config(add[0], add[1]) + elif rm: + rm_path_from_config(rm) + elif show: + show_config() + sys.exit() + + # Start CLI + splash_screen() + create_config_file_if_needed() + backup_config = get_config() + + # User entered a new path, so update the config + if new_path: + abs_path = os.path.abspath(new_path) + print(Fore.BLUE + Style.NORMAL + "\nUpdating shallow-backup path to -> " + Style.BRIGHT + "{}".format( + abs_path) + Style.RESET_ALL) + backup_config["backup_path"] = abs_path + write_config(backup_config) + + # User didn't enter any CLI args so prompt for path update before showing menu + elif not (old_path or complete or dotfiles or packages or fonts): + prompt_for_path_update(backup_config) + + # Create backup directory and do git setup + backup_home_path = get_config()["backup_path"] + make_dir_warn_overwrite(backup_home_path) + repo, new_git_repo_created = git_init_if_needed(backup_home_path) + + # Create default gitignore if we just ran git init + if new_git_repo_created: + create_gitignore_if_needed(backup_home_path) + # Prompt user for remote URL + if not remote: + prompt_for_git_url(repo) + + # Set remote URL from CLI arg + if remote: + git_set_remote(repo, remote) + + 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") + + # Command line options + if any([complete, dotfiles, configs, packages, fonts, reinstall_packages, reinstall_configs]): + if reinstall_packages: + reinstall_packages_from_lists(packages_path) + elif reinstall_configs: + reinstall_config_files(configs_path) + elif complete: + backup_all(dotfiles_path, packages_path, fonts_path, configs_path) + git_add_all_commit_push(repo, COMMIT_MSG["everything"]) + elif dotfiles: + backup_dotfiles(dotfiles_path) + git_add_all_commit_push(repo, COMMIT_MSG["dotfiles"]) + elif configs: + backup_configs(configs_path) + git_add_all_commit_push(repo, COMMIT_MSG["configs"]) + elif packages: + backup_packages(packages_path) + git_add_all_commit_push(repo, COMMIT_MSG["packages"]) + elif fonts: + backup_fonts(fonts_path) + git_add_all_commit_push(repo, COMMIT_MSG["fonts"]) + # No CL options, prompt for selection + else: + selection = actions_menu_prompt().lower().strip() + if selection == "back up everything": + backup_all(dotfiles_path, packages_path, fonts_path, configs_path) + git_add_all_commit_push(repo, COMMIT_MSG["everything"]) + elif selection == "back up dotfiles": + backup_dotfiles(dotfiles_path) + git_add_all_commit_push(repo, COMMIT_MSG["dotfiles"]) + elif selection == "back up configs": + backup_configs(configs_path) + git_add_all_commit_push(repo, COMMIT_MSG["configs"]) + elif selection == "back up packages": + backup_packages(packages_path) + git_add_all_commit_push(repo, COMMIT_MSG["packages"]) + elif selection == "back up fonts": + backup_fonts(fonts_path) + git_add_all_commit_push(repo, COMMIT_MSG["fonts"]) + elif selection == "reinstall packages": + reinstall_packages_from_lists(packages_path) + elif selection == "reinstall configs": + reinstall_config_files(configs_path) + elif selection == "show config": + show_config() + elif selection == "destroy backup": + if prompt_yes_no("Erase backup directory: {}?".format(backup_home_path), Fore.RED): + destroy_backup_dir(backup_home_path) + else: + print("{} Exiting to prevent accidental deletion of backup directory... {}".format( + Fore.RED, Style.RESET_ALL)) + + sys.exit() + + +if __name__ == '__main__': + """ + I'm just here so I don't get fined. + """ + cli() diff --git a/shallow_backup/utils.py b/shallow_backup/utils.py new file mode 100644 index 00000000..a94830e7 --- /dev/null +++ b/shallow_backup/utils.py @@ -0,0 +1,100 @@ +import os +import sys +import subprocess as sp +from colorama import Fore, Style +from printing import prompt_yes_no +from shutil import rmtree, copytree +from constants import Constants + + +def run_shell_cmd(command): + """ + Wrapper on subprocess.run that handles both lists and strings as commands. + """ + try: + if not isinstance(command, list): + process = sp.run(command.split(), stdout=sp.PIPE) + return process + else: + process = sp.run(command, stdout=sp.PIPE) + return process + except FileNotFoundError: # If package manager is missing + return None + + +def run_shell_cmd_write_stdout_to_file(command, filepath): + """ + Runs a command and then writes its stdout to a file + :param: command String representing command to run and write output of to file + """ + process = run_shell_cmd(command) + if process: + with open(filepath, "w+") as f: + f.write(process.stdout.decode('utf-8')) + + +def make_dir_warn_overwrite(path): + """ + Make destination dir if path doesn't exist, confirm before overwriting if it does. + """ + subdirs = ["dotfiles", "packages", "fonts", "configs"] + if os.path.exists(path) and path.split("/")[-1] in subdirs: + 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): + rmtree(path) + os.makedirs(path) + else: + 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) + + +def get_subfiles(directory): + """ + Returns list of absolute paths of immediate subfiles of a directory + """ + file_paths = [] + for path, subdirs, files in os.walk(directory): + for name in files: + file_paths.append(os.path.join(path, name)) + return file_paths + + +def _copy_dir(source_dir, backup_path): + """ + Copy dotfolder from $HOME. + """ + invalid = set(Constants.INVALID_DIRS) + if len(invalid.intersection(set(source_dir.split("/")))) != 0: + return + + if "Application Support" not in source_dir: + copytree(source_dir, os.path.join(backup_path, source_dir.split("/")[-2]), symlinks=True) + elif "Sublime" in source_dir: + copytree(source_dir, os.path.join(backup_path, source_dir.split("/")[-3]), symlinks=True) + else: + copytree(source_dir, backup_path, symlinks=True) + + +def _mkdir_or_pass(dir): + if not os.path.isdir(dir): + os.makedirs(dir) + pass + + +def _home_prefix(path): + return os.path.join(os.path.expanduser('~'), path) + + +def destroy_backup_dir(backup_path): + """ + Deletes the backup directory and its content + """ + try: + print("{} Deleting backup directory {} {}...".format(Fore.RED, backup_path, Style.BRIGHT)) + rmtree(backup_path) + except OSError as e: + print("{} Error: {} - {}. {}".format(Fore.RED, e.filename, e.strerror, Style.RESET_ALL)) From 0d36b759eb3587187b99abf2b56396a03c32e40e Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 05:47:05 -0500 Subject: [PATCH 2/9] Refactoring --- shallow_backup/backup.py | 2 +- shallow_backup/backup_paths_temp.py | 26 +++++++++++++ shallow_backup/config.py | 9 ++--- shallow_backup/constants.py | 8 ++-- shallow_backup/git_wrapper.py | 18 ++++++++- shallow_backup/printing.py | 10 ++--- shallow_backup/reinstall.py | 8 ++-- shallow_backup/shallow_backup.py | 58 +++++------------------------ shallow_backup/utils.py | 3 +- 9 files changed, 72 insertions(+), 70 deletions(-) create mode 100644 shallow_backup/backup_paths_temp.py diff --git a/shallow_backup/backup.py b/shallow_backup/backup.py index 8e29f19a..c5453d59 100644 --- a/shallow_backup/backup.py +++ b/shallow_backup/backup.py @@ -4,7 +4,7 @@ from colorama import Fore, Style from shutil import copytree, copyfile from printing import print_section_header, print_pkg_mgr_backup -from shallow_backup import get_configs_path_mapping, get_plist_mapping +from backup_paths_temp import get_configs_path_mapping, get_plist_mapping from utils import _home_prefix, make_dir_warn_overwrite, run_shell_cmd_write_stdout_to_file, _copy_dir, _mkdir_or_pass, get_subfiles diff --git a/shallow_backup/backup_paths_temp.py b/shallow_backup/backup_paths_temp.py new file mode 100644 index 00000000..df1ffeaa --- /dev/null +++ b/shallow_backup/backup_paths_temp.py @@ -0,0 +1,26 @@ +# NOTE: THIS FILE EXISTS AS PART OF A MAJOR REFACTORING. THESE FUNCTIONS SHOULD NOT BE INCLUDED IN THE NEXT RELEASE + + +# TODO: Convert these two functions to store info in the actual config and just read from it. +def get_configs_path_mapping(): + """ + Gets a dictionary mapping directories to back up to their destination path. + """ + return { + "Library/Application Support/Sublime Text 2/Packages/User/": "sublime_2", + "Library/Application Support/Sublime Text 3/Packages/User/": "sublime_3", + "Library/Preferences/IntelliJIdea2018.2/": "intellijidea_2018.2", + "Library/Preferences/PyCharm2018.2/": "pycharm_2018.2", + "Library/Preferences/CLion2018.2/": "clion_2018.2", + "Library/Preferences/PhpStorm2018.2": "phpstorm_2018.2", + ".atom/": "atom", + } + + +def get_plist_mapping(): + """ + Gets a dictionary mapping plist files to back up to their destination path. + """ + return { + "Library/Preferences/com.apple.Terminal.plist": "plist/com.apple.Terminal.plist", + } \ No newline at end of file diff --git a/shallow_backup/config.py b/shallow_backup/config.py index a0ba3804..5a90df15 100644 --- a/shallow_backup/config.py +++ b/shallow_backup/config.py @@ -2,13 +2,12 @@ import sys import json from colorama import Fore, Style -import constants as Constants from printing import print_section_header from utils import _home_prefix def get_config_path(): - return _home_prefix(Constants.CONFIG_PATH) + return _home_prefix(".shallow-backup") def get_config(): @@ -35,7 +34,7 @@ def get_default_config(): """ return { "backup_path": "~/shallow-backup", - "dotfiles": [ + "dotfiles" : [ ".bashrc", ".bash_profile", ".gitconfig", @@ -45,11 +44,11 @@ def get_default_config(): ".vimrc", ".zshrc" ], - "dotfolders": [ + "dotfolders" : [ ".ssh", ".vim" ], - "gitignore": [ + "gitignore" : [ "dotfiles/.ssh", "packages/", "dotfiles/.pypirc", diff --git a/shallow_backup/constants.py b/shallow_backup/constants.py index 6a1de1d5..c031c004 100644 --- a/shallow_backup/constants.py +++ b/shallow_backup/constants.py @@ -1,4 +1,4 @@ -class Constants: +class ProjInfo: PROJECT_NAME = 'shallow-backup' VERSION = '1.3' AUTHOR_GITHUB = 'alichtman' @@ -6,9 +6,6 @@ class Constants: DESCRIPTION = "Easily create lightweight backups of installed packages, dotfiles, and more." URL = 'https://github.com/alichtman/shallow-backup' AUTHOR_EMAIL = 'aaronlichtman@gmail.com' - CONFIG_PATH = '.shallow-backup' - INVALID_DIRS = [".Trash", ".npm", ".cache", ".rvm"] - PACKAGE_MANAGERS = ["gem", "brew-cask", "cargo", "npm", "pip", "brew", "apm"] LOGO = """ dP dP dP dP dP 88 88 88 88 88 @@ -18,3 +15,6 @@ class Constants: `88888P' dP dP `88888P8 dP dP `88888P' 8888P Y8P 88Y8888' `88888P8 `88888P' dP `YP `88888P' 88Y888P' 88 dP """ + + +ProjInfo = ProjInfo() diff --git a/shallow_backup/git_wrapper.py b/shallow_backup/git_wrapper.py index fb45a0d6..4e1d8004 100644 --- a/shallow_backup/git_wrapper.py +++ b/shallow_backup/git_wrapper.py @@ -4,6 +4,22 @@ from config import get_config from shutil import move +######### +# GLOBALS +######### + +COMMIT_MSG = { + "fonts": "Back up fonts.", + "packages": "Back up packages.", + "configs": "Back up configs.", + "everything": "Back up everything.", + "dotfiles": "Back up dotfiles.," +} + +########### +# FUNCTIONS +########### + def git_set_remote(repo, remote_url): """ @@ -61,7 +77,7 @@ def git_add_all_commit_push(repo, message): if repo.index.diff(None) or repo.untracked_files: print(Fore.YELLOW + Style.BRIGHT + "Making new commit..." + Style.RESET_ALL) repo.git.add(A=True) - repo.git.commit(m=message) + repo.git.commit(m=COMMIT_MSG[message]) if "origin" in [remote.name for remote in repo.remotes]: print(Fore.YELLOW + Style.BRIGHT + "Pushing to master: " + Style.NORMAL + "{}...".format( diff --git a/shallow_backup/printing.py b/shallow_backup/printing.py index 3dc6abdf..f15bb8d1 100644 --- a/shallow_backup/printing.py +++ b/shallow_backup/printing.py @@ -1,5 +1,5 @@ from colorama import Fore, Style -import constants as Constants +from constants import ProjInfo import inquirer @@ -7,9 +7,9 @@ def print_version_info(cli=True): """ Formats version differently for CLI and splash screen. """ - version = "v{} by {} (@{})".format(Constants.VERSION, - Constants.AUTHOR_FULL_NAME, - Constants.AUTHOR_GITHUB) + version = "v{} by {} (@{})".format(ProjInfo.VERSION, + ProjInfo.AUTHOR_FULL_NAME, + ProjInfo.AUTHOR_GITHUB) if not cli: print(Fore.RED + Style.BRIGHT + "\t{}\n".format(version) + Style.RESET_ALL) else: @@ -20,7 +20,7 @@ def splash_screen(): """ Display splash graphic, and then stylized version """ - print(Fore.YELLOW + Style.BRIGHT + "\n" + Constants.LOGO + Style.RESET_ALL) + print(Fore.YELLOW + Style.BRIGHT + "\n" + ProjInfo.LOGO + Style.RESET_ALL) print_version_info(False) diff --git a/shallow_backup/reinstall.py b/shallow_backup/reinstall.py index cd206644..9aff5571 100644 --- a/shallow_backup/reinstall.py +++ b/shallow_backup/reinstall.py @@ -1,11 +1,10 @@ import os import sys -from util import run_shell_cmd +from utils import run_shell_cmd from shutil import copytree, copyfile from colorama import Fore, Style from printing import print_section_header -from constants import Constants -from shallow_backup import get_configs_path_mapping, get_plist_mapping +from backup_paths_temp import get_configs_path_mapping, get_plist_mapping from utils import _home_prefix @@ -44,7 +43,8 @@ def reinstall_packages_from_lists(packages_path): for file in os.listdir(packages_path): # print(file) manager = file.split("_")[0].replace("-", " ") - if manager in Constants.PACKAGE_MANAGERS: + # TODO: Add macports + if manager in ["gem", "brew-cask", "cargo", "npm", "pip", "brew", "apm"]: package_mgrs.add(file.split("_")[0]) # TODO: USE print_pkg_mgr_reinstall() diff --git a/shallow_backup/shallow_backup.py b/shallow_backup/shallow_backup.py index 19e85e20..b849b3c6 100644 --- a/shallow_backup/shallow_backup.py +++ b/shallow_backup/shallow_backup.py @@ -10,44 +10,6 @@ from git_wrapper import git_init_if_needed, git_set_remote, git_add_all_commit_push, create_gitignore_if_needed from prompts import actions_menu_prompt, prompt_for_git_url, prompt_for_path_update -######## -# Globals -######## - -# TODO: Refactor this to the git file -COMMIT_MSG = { - "fonts": "Back up fonts.", - "packages": "Back up packages.", - "configs": "Back up configs.", - "complete": "Back up everything.", - "dotfiles": "Back up dotfiles.," -} - - -# TODO: Convert these two functions to store info in the actual config and just read from it. -def get_configs_path_mapping(): - """ - Gets a dictionary mapping directories to back up to their destination path. - """ - return { - "Library/Application Support/Sublime Text 2/Packages/User/": "sublime_2", - "Library/Application Support/Sublime Text 3/Packages/User/": "sublime_3", - "Library/Preferences/IntelliJIdea2018.2/": "intellijidea_2018.2", - "Library/Preferences/PyCharm2018.2/": "pycharm_2018.2", - "Library/Preferences/CLion2018.2/": "clion_2018.2", - "Library/Preferences/PhpStorm2018.2": "phpstorm_2018.2", - ".atom/": "atom", - } - - -def get_plist_mapping(): - """ - Gets a dictionary mapping plist files to back up to their destination path. - """ - return { - "Library/Preferences/com.apple.Terminal.plist": "plist/com.apple.Terminal.plist", - } - # custom help options @click.command(context_settings=dict(help_option_names=['-h', '-help', '--help'])) @@ -139,37 +101,37 @@ def cli(add, rm, show, complete, dotfiles, configs, packages, fonts, old_path, n reinstall_config_files(configs_path) elif complete: backup_all(dotfiles_path, packages_path, fonts_path, configs_path) - git_add_all_commit_push(repo, COMMIT_MSG["everything"]) + git_add_all_commit_push(repo, "everything") elif dotfiles: backup_dotfiles(dotfiles_path) - git_add_all_commit_push(repo, COMMIT_MSG["dotfiles"]) + git_add_all_commit_push(repo, "dotfiles") elif configs: backup_configs(configs_path) - git_add_all_commit_push(repo, COMMIT_MSG["configs"]) + git_add_all_commit_push(repo, "configs") elif packages: backup_packages(packages_path) - git_add_all_commit_push(repo, COMMIT_MSG["packages"]) + git_add_all_commit_push(repo, "packages") elif fonts: backup_fonts(fonts_path) - git_add_all_commit_push(repo, COMMIT_MSG["fonts"]) + git_add_all_commit_push(repo, "fonts") # No CL options, prompt for selection else: selection = actions_menu_prompt().lower().strip() if selection == "back up everything": backup_all(dotfiles_path, packages_path, fonts_path, configs_path) - git_add_all_commit_push(repo, COMMIT_MSG["everything"]) + git_add_all_commit_push(repo, "everything") elif selection == "back up dotfiles": backup_dotfiles(dotfiles_path) - git_add_all_commit_push(repo, COMMIT_MSG["dotfiles"]) + git_add_all_commit_push(repo, "dotfiles") elif selection == "back up configs": backup_configs(configs_path) - git_add_all_commit_push(repo, COMMIT_MSG["configs"]) + git_add_all_commit_push(repo, "configs") elif selection == "back up packages": backup_packages(packages_path) - git_add_all_commit_push(repo, COMMIT_MSG["packages"]) + git_add_all_commit_push(repo, "packages") elif selection == "back up fonts": backup_fonts(fonts_path) - git_add_all_commit_push(repo, COMMIT_MSG["fonts"]) + git_add_all_commit_push(repo, "fonts") elif selection == "reinstall packages": reinstall_packages_from_lists(packages_path) elif selection == "reinstall configs": diff --git a/shallow_backup/utils.py b/shallow_backup/utils.py index a94830e7..02bdcf3d 100644 --- a/shallow_backup/utils.py +++ b/shallow_backup/utils.py @@ -4,7 +4,6 @@ from colorama import Fore, Style from printing import prompt_yes_no from shutil import rmtree, copytree -from constants import Constants def run_shell_cmd(command): @@ -67,7 +66,7 @@ def _copy_dir(source_dir, backup_path): """ Copy dotfolder from $HOME. """ - invalid = set(Constants.INVALID_DIRS) + invalid = {".Trash", ".npm", ".cache", ".rvm"} if len(invalid.intersection(set(source_dir.split("/")))) != 0: return From fd7b0ac866a11a2f23863ce94a60c6fab913304e Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 05:56:46 -0500 Subject: [PATCH 3/9] More refactoring --- shallow_backup/__init__.py | 0 shallow_backup/backup.py | 20 ++++++++++---------- shallow_backup/git_wrapper.py | 10 +++++----- shallow_backup/prompts.py | 4 ++-- shallow_backup/reinstall.py | 3 --- shallow_backup/shallow_backup.py | 6 +++--- shallow_backup/utils.py | 8 ++++---- tests/test_git_folder_moving.py | 15 +++++++++------ 8 files changed, 33 insertions(+), 33 deletions(-) create mode 100644 shallow_backup/__init__.py diff --git a/shallow_backup/__init__.py b/shallow_backup/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/shallow_backup/backup.py b/shallow_backup/backup.py index c5453d59..b2a1ca26 100644 --- a/shallow_backup/backup.py +++ b/shallow_backup/backup.py @@ -5,7 +5,7 @@ from shutil import copytree, copyfile from printing import print_section_header, print_pkg_mgr_backup from backup_paths_temp import get_configs_path_mapping, get_plist_mapping -from utils import _home_prefix, make_dir_warn_overwrite, run_shell_cmd_write_stdout_to_file, _copy_dir, _mkdir_or_pass, get_subfiles +from utils import _home_prefix, make_dir_warn_overwrite, run_shell_cmd_write_stdout, _copy_dir, _mkdir_or_pass, get_subfiles def backup_dotfiles(backup_path): @@ -102,25 +102,25 @@ def backup_packages(backup_path): print_pkg_mgr_backup(mgr) command = "{} list".format(mgr) dest = "{}/{}_list.txt".format(backup_path, mgr.replace(" ", "-")) - run_shell_cmd_write_stdout_to_file(command, dest) + run_shell_cmd_write_stdout(command, dest) # cargo print_pkg_mgr_backup("cargo") command = "ls {}".format(_home_prefix(".cargo/bin/")) dest = "{}/cargo_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) + run_shell_cmd_write_stdout(command, dest) # pip print_pkg_mgr_backup("pip") command = "pip list --format=freeze".format(backup_path) dest = "{}/pip_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) + run_shell_cmd_write_stdout(command, dest) # npm print_pkg_mgr_backup("npm") command = "npm ls --global --parseable=true --depth=0" temp_file_path = "{}/npm_temp_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, temp_file_path) + run_shell_cmd_write_stdout(command, temp_file_path) npm_dest_file = "{0}/npm_list.txt".format(backup_path) # Parse npm output with open(temp_file_path, mode="r+") as temp_file: @@ -136,7 +136,7 @@ def backup_packages(backup_path): print_pkg_mgr_backup("Atom") command = "apm list --installed --bare" dest = "{}/apm_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) + run_shell_cmd_write_stdout(command, dest) # sublime text 2 packages sublime_2_path = _home_prefix("Library/Application Support/Sublime Text 2/Packages/") @@ -144,7 +144,7 @@ def backup_packages(backup_path): print_pkg_mgr_backup("Sublime Text 2") command = ["ls", sublime_2_path] dest = "{}/sublime2_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) + run_shell_cmd_write_stdout(command, dest) # sublime text 3 packages sublime_3_path = _home_prefix("Library/Application Support/Sublime Text 3/Installed Packages/") @@ -152,7 +152,7 @@ def backup_packages(backup_path): print_pkg_mgr_backup("Sublime Text 3") command = ["ls", sublime_3_path] dest = "{}/sublime3_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) + run_shell_cmd_write_stdout(command, dest) else: print(sublime_3_path, "IS NOT DIR") @@ -160,13 +160,13 @@ def backup_packages(backup_path): print_pkg_mgr_backup("macports") command = "port installed requested" dest = "{}/macports_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) + run_shell_cmd_write_stdout(command, dest) # system installs print_pkg_mgr_backup("macOS Applications") command = "ls /Applications/" dest = "{}/system_apps_list.txt".format(backup_path) - run_shell_cmd_write_stdout_to_file(command, dest) + run_shell_cmd_write_stdout(command, dest) # Clean up empty package list files print(Fore.BLUE + "Cleaning up empty package lists..." + Style.RESET_ALL) diff --git a/shallow_backup/git_wrapper.py b/shallow_backup/git_wrapper.py index 4e1d8004..0833d3df 100644 --- a/shallow_backup/git_wrapper.py +++ b/shallow_backup/git_wrapper.py @@ -1,8 +1,8 @@ import git import os -from colorama import Fore, Style -from config import get_config from shutil import move +from config import get_config +from colorama import Fore, Style ######### # GLOBALS @@ -38,7 +38,7 @@ def git_set_remote(repo, remote_url): origin.fetch() -def create_gitignore_if_needed(dir_path): +def safe_create_gitignore(dir_path): """ Creates a .gitignore file that ignores all files listed in config. """ @@ -54,7 +54,7 @@ def create_gitignore_if_needed(dir_path): f.write("{}\n".format(ignore)) -def git_init_if_needed(dir_path): +def safe_git_init(dir_path): """ If there is no git repo inside the dir_path, intialize one. Returns tuple of (git.Repo, bool new_git_repo_created) @@ -88,7 +88,7 @@ def git_add_all_commit_push(repo, message): print(Fore.YELLOW + Style.BRIGHT + "No changes to commit..." + Style.RESET_ALL) -def move_git_folder_to_path(source_path, new_path): +def move_git_dir_to_path(source_path, new_path): """ Moves git folder and .gitignore to the new backup directory. """ diff --git a/shallow_backup/prompts.py b/shallow_backup/prompts.py index 2d1275b0..ea1d9155 100644 --- a/shallow_backup/prompts.py +++ b/shallow_backup/prompts.py @@ -4,7 +4,7 @@ from config import write_config from utils import make_dir_warn_overwrite from printing import prompt_yes_no -from git_wrapper import git_set_remote, move_git_folder_to_path +from git_wrapper import git_set_remote, move_git_dir_to_path def prompt_for_path_update(config): @@ -22,7 +22,7 @@ def prompt_for_path_update(config): config["backup_path"] = abs_path write_config(config) make_dir_warn_overwrite(abs_path) - move_git_folder_to_path(current_path, abs_path) + move_git_dir_to_path(current_path, abs_path) def prompt_for_git_url(repo): diff --git a/shallow_backup/reinstall.py b/shallow_backup/reinstall.py index 9aff5571..477bd131 100644 --- a/shallow_backup/reinstall.py +++ b/shallow_backup/reinstall.py @@ -1,5 +1,4 @@ import os -import sys from utils import run_shell_cmd from shutil import copytree, copyfile from colorama import Fore, Style @@ -29,7 +28,6 @@ def backup_prefix(path): copyfile(backup_prefix(backup), _home_prefix(target)) print_section_header("SUCCESSFUL CONFIG REINSTALLATION", Fore.BLUE) - sys.exit() def reinstall_packages_from_lists(packages_path): @@ -83,4 +81,3 @@ def reinstall_packages_from_lists(packages_path): "\n -> https://github.com/rust-lang/cargo/issues/5593" + Style.RESET_ALL) print_section_header("SUCCESSFUL PACKAGE REINSTALLATION", Fore.BLUE) - sys.exit() diff --git a/shallow_backup/shallow_backup.py b/shallow_backup/shallow_backup.py index b849b3c6..547e7893 100644 --- a/shallow_backup/shallow_backup.py +++ b/shallow_backup/shallow_backup.py @@ -7,7 +7,7 @@ from utils import make_dir_warn_overwrite, destroy_backup_dir from printing import print_version_info, prompt_yes_no, splash_screen from backup import backup_all, backup_configs, backup_dotfiles, backup_fonts, backup_packages -from git_wrapper import git_init_if_needed, git_set_remote, git_add_all_commit_push, create_gitignore_if_needed +from git_wrapper import safe_git_init, git_set_remote, git_add_all_commit_push, safe_create_gitignore from prompts import actions_menu_prompt, prompt_for_git_url, prompt_for_path_update @@ -75,11 +75,11 @@ def cli(add, rm, show, complete, dotfiles, configs, packages, fonts, old_path, n # Create backup directory and do git setup backup_home_path = get_config()["backup_path"] make_dir_warn_overwrite(backup_home_path) - repo, new_git_repo_created = git_init_if_needed(backup_home_path) + repo, new_git_repo_created = safe_git_init(backup_home_path) # Create default gitignore if we just ran git init if new_git_repo_created: - create_gitignore_if_needed(backup_home_path) + safe_create_gitignore(backup_home_path) # Prompt user for remote URL if not remote: prompt_for_git_url(repo) diff --git a/shallow_backup/utils.py b/shallow_backup/utils.py index 02bdcf3d..4e4c1cda 100644 --- a/shallow_backup/utils.py +++ b/shallow_backup/utils.py @@ -21,7 +21,7 @@ def run_shell_cmd(command): return None -def run_shell_cmd_write_stdout_to_file(command, filepath): +def run_shell_cmd_write_stdout(command, filepath): """ Runs a command and then writes its stdout to a file :param: command String representing command to run and write output of to file @@ -78,9 +78,9 @@ def _copy_dir(source_dir, backup_path): copytree(source_dir, backup_path, symlinks=True) -def _mkdir_or_pass(dir): - if not os.path.isdir(dir): - os.makedirs(dir) +def _mkdir_or_pass(directory): + if not os.path.isdir(directory): + os.makedirs(directory) pass diff --git a/tests/test_git_folder_moving.py b/tests/test_git_folder_moving.py index 271e9ef2..0b52a0ad 100644 --- a/tests/test_git_folder_moving.py +++ b/tests/test_git_folder_moving.py @@ -1,6 +1,7 @@ import os import shutil -from shallow_backup import move_git_folder_to_path, git_init_if_needed, create_config_file_if_needed, create_gitignore_if_needed +from shallow_backup.git_wrapper import move_git_dir_to_path, safe_git_init, safe_create_gitignore +from shallow_backup.config import create_config_file_if_needed OLD_BACKUP_DIR = 'shallow-backup-test-git-old-backup-dir' NEW_BACKUP_DIR = 'shallow-backup-test-git-new-backup-backup-dir' @@ -12,7 +13,8 @@ class TestGitFolderCopying: Test the functionality of .git copying """ - def setup_method(self): + @staticmethod + def setup_method(): create_config_file_if_needed() for directory in DIRS: try: @@ -21,7 +23,8 @@ def setup_method(self): shutil.rmtree(directory) os.mkdir(directory) - def teardown_method(self): + @staticmethod + def teardown_method(): for directory in DIRS: shutil.rmtree(directory) @@ -29,9 +32,9 @@ def test_copy_git_folder(self): """ Test copying the .git folder and .gitignore from an old directory to a new one """ - git_init_if_needed(OLD_BACKUP_DIR) - create_gitignore_if_needed(OLD_BACKUP_DIR) - move_git_folder_to_path(OLD_BACKUP_DIR, NEW_BACKUP_DIR) + safe_git_init(OLD_BACKUP_DIR) + safe_create_gitignore(OLD_BACKUP_DIR) + move_git_dir_to_path(OLD_BACKUP_DIR, NEW_BACKUP_DIR) assert os.path.isdir(os.path.join(NEW_BACKUP_DIR, '.git/')) assert os.path.isfile(os.path.join(NEW_BACKUP_DIR, '.gitignore')) assert not os.path.isdir(os.path.join(OLD_BACKUP_DIR, '.git/')) From 0821ca846897a5cf37f1599dd5f28b12eab30277 Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 06:07:44 -0500 Subject: [PATCH 4/9] More refactoring --- constants.py | 19 ------------------- setup.py | 14 +++++++------- shallow_backup/constants.py | 1 + tests/test_copies.py | 5 ++--- 4 files changed, 10 insertions(+), 29 deletions(-) delete mode 100644 constants.py diff --git a/constants.py b/constants.py deleted file mode 100644 index db17583b..00000000 --- a/constants.py +++ /dev/null @@ -1,19 +0,0 @@ -class Constants: - PROJECT_NAME = 'shallow-backup' - VERSION = '1.3' - AUTHOR_GITHUB = 'alichtman' - AUTHOR_FULL_NAME = 'Aaron Lichtman' - DESCRIPTION = "Easily create lightweight backups of installed packages, dotfiles, and more." - URL = 'https://github.com/alichtman/shallow-backup' - CONFIG_PATH = '.shallow-backup' - INVALID_DIRS = [".Trash", ".npm", ".cache", ".rvm"] - PACKAGE_MANAGERS = ["gem", "brew-cask", "cargo", "npm", "pip", "brew", "apm"] - LOGO = """ - dP dP dP dP dP - 88 88 88 88 88 - ,d8888' 88d888b. .d8888b. 88 88 .d8888b. dP dP dP 88d888b. .d8888b. .d8888b. 88 .dP dP dP 88d888b. - Y8ooooo, 88' `88 88' `88 88 88 88' `88 88 88 88 88' `88 88' `88 88' `\"\" 88888\" 88 88 88' `88 - 88 88 88 88. .88 88 88 88. .88 88.88b.88' 88. .88 88. .88 88. ... 88 `8b. 88. .88 88. .88 - `88888P' dP dP `88888P8 dP dP `88888P' 8888P Y8P 88Y8888' `88888P8 `88888P' dP `YP `88888P' 88Y888P' - 88 - dP """ diff --git a/setup.py b/setup.py index 369c1936..66544d21 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup from codecs import open from os import path -from shallow_backup.constants import Constants +from shallow_backup.constants import ProjInfo here = path.abspath(path.dirname(__file__)) @@ -10,13 +10,13 @@ long_description = f.read() setup( - name=Constants.PROJECT_NAME, - version=Constants.VERSION, - description=Constants.DESCRIPTION, + name=ProjInfo.PROJECT_NAME, + version=ProjInfo.VERSION, + description=ProjInfo.DESCRIPTION, long_description_content_type="text/markdown", long_description=long_description, - url=Constants.URL, - author=Constants.AUTHOR_GITHUB, + url=ProjInfo.URL, + author=ProjInfo.AUTHOR_GITHUB, author_email="aaronlichtman@gmail.com", # Classifiers help users find your project by categorizing it. @@ -96,7 +96,7 @@ # maintainers, and where to support the project financially. The key is # what's used to render the link text on PyPI. project_urls={ - 'Bug Reports': 'https://github.com/alichtman/shallow-backup/issues', + 'Bug Reports': ProjInfo.BUG_REPORT_URL, 'Donations': 'https://www.patreon.com/alichtman', }, ) diff --git a/shallow_backup/constants.py b/shallow_backup/constants.py index c031c004..15154bee 100644 --- a/shallow_backup/constants.py +++ b/shallow_backup/constants.py @@ -5,6 +5,7 @@ class ProjInfo: AUTHOR_FULL_NAME = 'Aaron Lichtman' DESCRIPTION = "Easily create lightweight backups of installed packages, dotfiles, and more." URL = 'https://github.com/alichtman/shallow-backup' + BUG_REPORT_URL = 'https://github.com/alichtman/shallow-backup/issues' AUTHOR_EMAIL = 'aaronlichtman@gmail.com' LOGO = """ dP dP dP dP dP diff --git a/tests/test_copies.py b/tests/test_copies.py index 3ce02066..6a8dd506 100644 --- a/tests/test_copies.py +++ b/tests/test_copies.py @@ -1,8 +1,7 @@ import os import pytest import shutil -from shallow_backup import _copy_dir -from constants import Constants +from shallow_backup.utils import _copy_dir DIR_TO_BACKUP = 'shallow-backup-test-copy-dir' BACKUP_DIR = 'shallow-backup-test-copy-backup-dir' @@ -42,7 +41,7 @@ def test_copy_dir(self): assert os.path.isdir(test_path) assert os.path.isdir(os.path.join(BACKUP_DIR, test_dir)) - @pytest.mark.parametrize('invalid', Constants.INVALID_DIRS) + @pytest.mark.parametrize('invalid', {".Trash", ".npm", ".cache", ".rvm"}) def test_copy_dir_invalid(self, invalid): """ Test that attempting to copy an invalid directory fails From 1211b9f84fe05a0d9a205149fdf8f90f9fc56413 Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 06:45:20 -0500 Subject: [PATCH 5/9] Add config paths to config file Fix #128 --- shallow_backup/backup.py | 7 +++---- shallow_backup/backup_paths_temp.py | 26 -------------------------- shallow_backup/config.py | 19 ++++++++++++++++--- shallow_backup/git_wrapper.py | 2 +- shallow_backup/reinstall.py | 7 ++++--- 5 files changed, 24 insertions(+), 37 deletions(-) delete mode 100644 shallow_backup/backup_paths_temp.py diff --git a/shallow_backup/backup.py b/shallow_backup/backup.py index b2a1ca26..f32f5fcd 100644 --- a/shallow_backup/backup.py +++ b/shallow_backup/backup.py @@ -4,7 +4,6 @@ from colorama import Fore, Style from shutil import copytree, copyfile from printing import print_section_header, print_pkg_mgr_backup -from backup_paths_temp import get_configs_path_mapping, get_plist_mapping from utils import _home_prefix, make_dir_warn_overwrite, run_shell_cmd_write_stdout, _copy_dir, _mkdir_or_pass, get_subfiles @@ -60,9 +59,9 @@ def backup_configs(backup_path): """ print_section_header("CONFIGS", Fore.BLUE) make_dir_warn_overwrite(backup_path) - - configs_dir_mapping = get_configs_path_mapping() - plist_files = get_plist_mapping() + config = get_config() + configs_dir_mapping = config["config_path_to_dest_map"] + plist_files = config["plist_path_to_dest_map"] print(Fore.BLUE + Style.BRIGHT + "Backing up configs..." + Style.RESET_ALL) diff --git a/shallow_backup/backup_paths_temp.py b/shallow_backup/backup_paths_temp.py deleted file mode 100644 index df1ffeaa..00000000 --- a/shallow_backup/backup_paths_temp.py +++ /dev/null @@ -1,26 +0,0 @@ -# NOTE: THIS FILE EXISTS AS PART OF A MAJOR REFACTORING. THESE FUNCTIONS SHOULD NOT BE INCLUDED IN THE NEXT RELEASE - - -# TODO: Convert these two functions to store info in the actual config and just read from it. -def get_configs_path_mapping(): - """ - Gets a dictionary mapping directories to back up to their destination path. - """ - return { - "Library/Application Support/Sublime Text 2/Packages/User/": "sublime_2", - "Library/Application Support/Sublime Text 3/Packages/User/": "sublime_3", - "Library/Preferences/IntelliJIdea2018.2/": "intellijidea_2018.2", - "Library/Preferences/PyCharm2018.2/": "pycharm_2018.2", - "Library/Preferences/CLion2018.2/": "clion_2018.2", - "Library/Preferences/PhpStorm2018.2": "phpstorm_2018.2", - ".atom/": "atom", - } - - -def get_plist_mapping(): - """ - Gets a dictionary mapping plist files to back up to their destination path. - """ - return { - "Library/Preferences/com.apple.Terminal.plist": "plist/com.apple.Terminal.plist", - } \ No newline at end of file diff --git a/shallow_backup/config.py b/shallow_backup/config.py index 5a90df15..4f524905 100644 --- a/shallow_backup/config.py +++ b/shallow_backup/config.py @@ -48,11 +48,23 @@ def get_default_config(): ".ssh", ".vim" ], - "gitignore" : [ + "default-gitignore" : [ "dotfiles/.ssh", "packages/", "dotfiles/.pypirc", - ] + ], + "config_path_to_dest_map": { + "Library/Application Support/Sublime Text 2/Packages/User/": "sublime_2", + "Library/Application Support/Sublime Text 3/Packages/User/": "sublime_3", + "Library/Preferences/IntelliJIdea2018.2/" : "intellijidea_2018.2", + "Library/Preferences/PyCharm2018.2/" : "pycharm_2018.2", + "Library/Preferences/CLion2018.2/" : "clion_2018.2", + "Library/Preferences/PhpStorm2018.2" : "phpstorm_2018.2", + ".atom/" : "atom", + }, + "plist_path_to_dest_map" : { + "Library/Preferences/com.apple.Terminal.plist": "plist/com.apple.Terminal.plist", + }, } @@ -67,6 +79,7 @@ def create_config_file_if_needed(): write_config(backup_config) +# TODO: Rethink these methods. def add_path_to_config(section, path): """ Adds the path under the correct section in the config file. @@ -137,7 +150,7 @@ def show_config(): config = get_config() for section, contents in config.items(): # Hide gitignore config - if section == "gitignore": + if section == "default-gitignore": continue # Print backup path on same line if section == "backup_path": diff --git a/shallow_backup/git_wrapper.py b/shallow_backup/git_wrapper.py index 0833d3df..ec39d6d1 100644 --- a/shallow_backup/git_wrapper.py +++ b/shallow_backup/git_wrapper.py @@ -48,7 +48,7 @@ def safe_create_gitignore(dir_path): pass else: print(Fore.YELLOW + Style.BRIGHT + "Creating default .gitignore..." + Style.RESET_ALL) - files_to_ignore = get_config()["gitignore"] + files_to_ignore = get_config()["default-gitignore"] with open(gitignore_path, "w+") as f: for ignore in files_to_ignore: f.write("{}\n".format(ignore)) diff --git a/shallow_backup/reinstall.py b/shallow_backup/reinstall.py index 477bd131..f106810a 100644 --- a/shallow_backup/reinstall.py +++ b/shallow_backup/reinstall.py @@ -3,8 +3,8 @@ from shutil import copytree, copyfile from colorama import Fore, Style from printing import print_section_header -from backup_paths_temp import get_configs_path_mapping, get_plist_mapping from utils import _home_prefix +from config import get_config def reinstall_config_files(configs_path): @@ -16,8 +16,9 @@ def reinstall_config_files(configs_path): def backup_prefix(path): return os.path.join(configs_path, path) - configs_dir_mapping = get_configs_path_mapping() - plist_files = get_plist_mapping() + config = get_config() + configs_dir_mapping = config["config_path_to_dest_map"] + plist_files = config["plist_path_to_dest_map"] for target, backup in configs_dir_mapping.items(): if os.path.isdir(backup_prefix(backup)): From 078d712b6756577667c84eeb7f52704ae4fded41 Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 06:53:38 -0500 Subject: [PATCH 6/9] All tests pass --- shallow_backup/backup.py | 6 +++--- shallow_backup/config.py | 4 ++-- shallow_backup/git_wrapper.py | 2 +- shallow_backup/printing.py | 4 ++-- shallow_backup/prompts.py | 8 ++++---- shallow_backup/reinstall.py | 8 ++++---- shallow_backup/utils.py | 2 +- tests/test_deletes.py | 8 +++++--- 8 files changed, 22 insertions(+), 20 deletions(-) diff --git a/shallow_backup/backup.py b/shallow_backup/backup.py index f32f5fcd..de92ba3b 100644 --- a/shallow_backup/backup.py +++ b/shallow_backup/backup.py @@ -1,10 +1,10 @@ import os import multiprocessing as mp -from config import get_config from colorama import Fore, Style from shutil import copytree, copyfile -from printing import print_section_header, print_pkg_mgr_backup -from utils import _home_prefix, make_dir_warn_overwrite, run_shell_cmd_write_stdout, _copy_dir, _mkdir_or_pass, get_subfiles +from shallow_backup.config import get_config +from shallow_backup.printing import print_section_header, print_pkg_mgr_backup +from shallow_backup.utils import _home_prefix, make_dir_warn_overwrite, run_shell_cmd_write_stdout, _copy_dir, _mkdir_or_pass, get_subfiles def backup_dotfiles(backup_path): diff --git a/shallow_backup/config.py b/shallow_backup/config.py index 4f524905..60598d25 100644 --- a/shallow_backup/config.py +++ b/shallow_backup/config.py @@ -2,8 +2,8 @@ import sys import json from colorama import Fore, Style -from printing import print_section_header -from utils import _home_prefix +from shallow_backup.utils import _home_prefix +from shallow_backup.printing import print_section_header def get_config_path(): diff --git a/shallow_backup/git_wrapper.py b/shallow_backup/git_wrapper.py index ec39d6d1..a76c6e59 100644 --- a/shallow_backup/git_wrapper.py +++ b/shallow_backup/git_wrapper.py @@ -1,8 +1,8 @@ import git import os from shutil import move -from config import get_config from colorama import Fore, Style +from shallow_backup.config import get_config ######### # GLOBALS diff --git a/shallow_backup/printing.py b/shallow_backup/printing.py index f15bb8d1..674f9cfd 100644 --- a/shallow_backup/printing.py +++ b/shallow_backup/printing.py @@ -1,6 +1,6 @@ -from colorama import Fore, Style -from constants import ProjInfo import inquirer +from colorama import Fore, Style +from shallow_backup.constants import ProjInfo def print_version_info(cli=True): diff --git a/shallow_backup/prompts.py b/shallow_backup/prompts.py index ea1d9155..33d89f0d 100644 --- a/shallow_backup/prompts.py +++ b/shallow_backup/prompts.py @@ -1,10 +1,10 @@ import os import inquirer from colorama import Fore, Style -from config import write_config -from utils import make_dir_warn_overwrite -from printing import prompt_yes_no -from git_wrapper import git_set_remote, move_git_dir_to_path +from shallow_backup.config import write_config +from shallow_backup.utils import make_dir_warn_overwrite +from shallow_backup.printing import prompt_yes_no +from shallow_backup.git_wrapper import git_set_remote, move_git_dir_to_path def prompt_for_path_update(config): diff --git a/shallow_backup/reinstall.py b/shallow_backup/reinstall.py index f106810a..54751f9a 100644 --- a/shallow_backup/reinstall.py +++ b/shallow_backup/reinstall.py @@ -1,10 +1,10 @@ import os -from utils import run_shell_cmd from shutil import copytree, copyfile from colorama import Fore, Style -from printing import print_section_header -from utils import _home_prefix -from config import get_config +from shallow_backup.config import get_config +from shallow_backup.utils import _home_prefix +from shallow_backup.utils import run_shell_cmd +from shallow_backup.printing import print_section_header def reinstall_config_files(configs_path): diff --git a/shallow_backup/utils.py b/shallow_backup/utils.py index 4e4c1cda..d2b7c1d6 100644 --- a/shallow_backup/utils.py +++ b/shallow_backup/utils.py @@ -2,8 +2,8 @@ import sys import subprocess as sp from colorama import Fore, Style -from printing import prompt_yes_no from shutil import rmtree, copytree +from shallow_backup.printing import prompt_yes_no def run_shell_cmd(command): diff --git a/tests/test_deletes.py b/tests/test_deletes.py index c7778bee..be77cecc 100644 --- a/tests/test_deletes.py +++ b/tests/test_deletes.py @@ -1,7 +1,7 @@ import os import shutil -from shallow_backup import destroy_backup_dir +from shallow_backup.utils import destroy_backup_dir BACKUP_DIR = 'shallow-backup-test-copy-backup-dir' TEST_BACKUP_TEXT_FILE = os.path.join(BACKUP_DIR, 'test-file.txt') @@ -13,7 +13,8 @@ class TestDeleteMethods: Test the functionality of deleting """ - def setup_method(self): + @staticmethod + def setup_method(): for directory in DIRS: try: os.mkdir(directory) @@ -23,7 +24,8 @@ def setup_method(self): f = open(TEST_BACKUP_TEXT_FILE, "w+") f.close() - def teardown_method(self): + @staticmethod + def teardown_method(): for directory in DIRS: try: shutil.rmtree(directory) From 225a89efafb32c08c489fdd88dfaf08ec66da621 Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 07:18:52 -0500 Subject: [PATCH 7/9] Update setup.py --- setup.py | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/setup.py b/setup.py index 66544d21..2919a0ea 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ -from setuptools import setup -from codecs import open from os import path +from codecs import open +from setuptools import setup from shallow_backup.constants import ProjInfo here = path.abspath(path.dirname(__file__)) @@ -53,22 +53,7 @@ # # Note that this is a string of words separated by whitespace, not a list. keywords='backup documentation system dotfiles install list configuration', # Optional - - # Just want to distribute a single Python file, so using `py_modules` - # argument as follows, which will expect a file called - # `stronghold.py` to exist: - # - py_modules=[ - "shallow_backup", - "constants" - ], - - # This field lists other packages that your project depends on to run. - # Any package you put here will be installed by pip when your project is - # installed, so they must be valid existing projects. - # - # For an analysis of "install_requires" vs pip's requirements files see: - # https://packaging.python.org/en/latest/requirements.html + packages=["shallow_backup"], install_requires=[ 'inquirer>=2.2.0', 'colorama>=0.3.9', @@ -83,7 +68,7 @@ # For example, the following would provide a command called `sample` which # executes the function `main` from this package when invoked: entry_points={ - 'console_scripts': 'shallow-backup=shallow_backup/shallow_backup:cli' + 'console_scripts': 'shallow-backup=shallow_backup.shallow_backup:cli' }, # List additional URLs that are relevant to your project as a dict. From 348b6ccae6072c2295c08d5940dc23806053a97c Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 07:19:07 -0500 Subject: [PATCH 8/9] Refactoring --- shallow_backup/backup.py | 44 ++++++++++++++++---------------- shallow_backup/config.py | 6 ++--- shallow_backup/prompts.py | 4 +-- shallow_backup/reinstall.py | 16 ++++++------ shallow_backup/shallow_backup.py | 21 ++++++++------- shallow_backup/utils.py | 41 ++++++++++++++++------------- 6 files changed, 70 insertions(+), 62 deletions(-) diff --git a/shallow_backup/backup.py b/shallow_backup/backup.py index de92ba3b..4f4f0a66 100644 --- a/shallow_backup/backup.py +++ b/shallow_backup/backup.py @@ -4,7 +4,7 @@ from shutil import copytree, copyfile from shallow_backup.config import get_config from shallow_backup.printing import print_section_header, print_pkg_mgr_backup -from shallow_backup.utils import _home_prefix, make_dir_warn_overwrite, run_shell_cmd_write_stdout, _copy_dir, _mkdir_or_pass, get_subfiles +from shallow_backup.utils import home_prefix, mkdir_warn_overwrite, run_cmd_write_stdout, copy_dir, mkdir_or_pass, get_subfiles def backup_dotfiles(backup_path): @@ -12,7 +12,7 @@ def backup_dotfiles(backup_path): Create `dotfiles` dir and makes copies of dotfiles and dotfolders. """ print_section_header("DOTFILES", Fore.BLUE) - make_dir_warn_overwrite(backup_path) + mkdir_warn_overwrite(backup_path) # assumes dotfiles are stored in home directory home_path = os.path.expanduser('~') @@ -43,7 +43,7 @@ def backup_dotfiles(backup_path): print(Fore.BLUE + Style.BRIGHT + "Backing up dotfolders..." + Style.RESET_ALL) for x in dotfolders_mp_in: x = list(x) - mp.Process(target=_copy_dir, args=(x[0], x[1],)).start() + mp.Process(target=copy_dir, args=(x[0], x[1],)).start() with mp.Pool(mp.cpu_count()): print(Fore.BLUE + Style.BRIGHT + "Backing up dotfiles..." + Style.RESET_ALL) @@ -58,7 +58,7 @@ def backup_configs(backup_path): Configs are application settings, generally. .plist files count. """ print_section_header("CONFIGS", Fore.BLUE) - make_dir_warn_overwrite(backup_path) + mkdir_warn_overwrite(backup_path) config = get_config() configs_dir_mapping = config["config_path_to_dest_map"] plist_files = config["plist_path_to_dest_map"] @@ -67,7 +67,7 @@ def backup_configs(backup_path): # backup config dirs in backup_path// for config, target in configs_dir_mapping.items(): - src_dir = _home_prefix(config) + src_dir = home_prefix(config) configs_backup_path = os.path.join(backup_path, target) if os.path.isdir(src_dir): # TODO: Exclude Sublime/Atom/VS Code Packages here to speed things up @@ -76,9 +76,9 @@ def backup_configs(backup_path): # backup plist files in backup_path/configs/plist/ print(Fore.BLUE + Style.BRIGHT + "Backing up plist files..." + Style.RESET_ALL) plist_backup_path = os.path.join(backup_path, "plist") - _mkdir_or_pass(plist_backup_path) + mkdir_or_pass(plist_backup_path) for plist, dest in plist_files.items(): - plist_path = _home_prefix(plist) + plist_path = home_prefix(plist) if os.path.exists(plist_path): copyfile(plist_path, os.path.join(backup_path, dest)) @@ -88,7 +88,7 @@ def backup_packages(backup_path): Creates `packages` directory and places install list text files there. """ print_section_header("PACKAGES", Fore.BLUE) - make_dir_warn_overwrite(backup_path) + mkdir_warn_overwrite(backup_path) std_package_managers = [ "brew", @@ -101,25 +101,25 @@ def backup_packages(backup_path): print_pkg_mgr_backup(mgr) command = "{} list".format(mgr) dest = "{}/{}_list.txt".format(backup_path, mgr.replace(" ", "-")) - run_shell_cmd_write_stdout(command, dest) + run_cmd_write_stdout(command, dest) # cargo print_pkg_mgr_backup("cargo") - command = "ls {}".format(_home_prefix(".cargo/bin/")) + command = "ls {}".format(home_prefix(".cargo/bin/")) dest = "{}/cargo_list.txt".format(backup_path) - run_shell_cmd_write_stdout(command, dest) + run_cmd_write_stdout(command, dest) # pip print_pkg_mgr_backup("pip") command = "pip list --format=freeze".format(backup_path) dest = "{}/pip_list.txt".format(backup_path) - run_shell_cmd_write_stdout(command, dest) + run_cmd_write_stdout(command, dest) # npm print_pkg_mgr_backup("npm") command = "npm ls --global --parseable=true --depth=0" temp_file_path = "{}/npm_temp_list.txt".format(backup_path) - run_shell_cmd_write_stdout(command, temp_file_path) + run_cmd_write_stdout(command, temp_file_path) npm_dest_file = "{0}/npm_list.txt".format(backup_path) # Parse npm output with open(temp_file_path, mode="r+") as temp_file: @@ -135,23 +135,23 @@ def backup_packages(backup_path): print_pkg_mgr_backup("Atom") command = "apm list --installed --bare" dest = "{}/apm_list.txt".format(backup_path) - run_shell_cmd_write_stdout(command, dest) + run_cmd_write_stdout(command, dest) # sublime text 2 packages - sublime_2_path = _home_prefix("Library/Application Support/Sublime Text 2/Packages/") + sublime_2_path = home_prefix("Library/Application Support/Sublime Text 2/Packages/") if os.path.isdir(sublime_2_path): print_pkg_mgr_backup("Sublime Text 2") command = ["ls", sublime_2_path] dest = "{}/sublime2_list.txt".format(backup_path) - run_shell_cmd_write_stdout(command, dest) + run_cmd_write_stdout(command, dest) # sublime text 3 packages - sublime_3_path = _home_prefix("Library/Application Support/Sublime Text 3/Installed Packages/") + sublime_3_path = home_prefix("Library/Application Support/Sublime Text 3/Installed Packages/") if os.path.isdir(sublime_3_path): print_pkg_mgr_backup("Sublime Text 3") command = ["ls", sublime_3_path] dest = "{}/sublime3_list.txt".format(backup_path) - run_shell_cmd_write_stdout(command, dest) + run_cmd_write_stdout(command, dest) else: print(sublime_3_path, "IS NOT DIR") @@ -159,13 +159,13 @@ def backup_packages(backup_path): print_pkg_mgr_backup("macports") command = "port installed requested" dest = "{}/macports_list.txt".format(backup_path) - run_shell_cmd_write_stdout(command, dest) + run_cmd_write_stdout(command, dest) # system installs print_pkg_mgr_backup("macOS Applications") command = "ls /Applications/" dest = "{}/system_apps_list.txt".format(backup_path) - run_shell_cmd_write_stdout(command, dest) + run_cmd_write_stdout(command, dest) # Clean up empty package list files print(Fore.BLUE + "Cleaning up empty package lists..." + Style.RESET_ALL) @@ -179,9 +179,9 @@ def backup_fonts(path): Creates list of all .ttf and .otf files in ~/Library/Fonts/ """ print_section_header("FONTS", Fore.BLUE) - make_dir_warn_overwrite(path) + mkdir_warn_overwrite(path) print(Fore.BLUE + "Copying '.otf' and '.ttf' fonts..." + Style.RESET_ALL) - fonts_path = _home_prefix("Library/Fonts/") + fonts_path = home_prefix("Library/Fonts/") fonts = [os.path.join(fonts_path, font) for font in os.listdir(fonts_path) if font.endswith(".otf") or font.endswith(".ttf")] diff --git a/shallow_backup/config.py b/shallow_backup/config.py index 60598d25..1a0af699 100644 --- a/shallow_backup/config.py +++ b/shallow_backup/config.py @@ -2,12 +2,12 @@ import sys import json from colorama import Fore, Style -from shallow_backup.utils import _home_prefix +from shallow_backup.utils import home_prefix from shallow_backup.printing import print_section_header def get_config_path(): - return _home_prefix(".shallow-backup") + return home_prefix(".shallow-backup") def get_config(): @@ -86,7 +86,7 @@ def add_path_to_config(section, path): FIRST ARG: [dot, config, other] SECOND ARG: path, relative to home directory for dotfiles, absolute for configs """ - full_path = _home_prefix(path) + full_path = home_prefix(path) if not os.path.exists(full_path): print(Fore.RED + Style.BRIGHT + "ERR: {} doesn't exist.".format(full_path) + Style.RESET_ALL) sys.exit(1) diff --git a/shallow_backup/prompts.py b/shallow_backup/prompts.py index 33d89f0d..ae3eaa97 100644 --- a/shallow_backup/prompts.py +++ b/shallow_backup/prompts.py @@ -2,7 +2,7 @@ import inquirer from colorama import Fore, Style from shallow_backup.config import write_config -from shallow_backup.utils import make_dir_warn_overwrite +from shallow_backup.utils import mkdir_warn_overwrite from shallow_backup.printing import prompt_yes_no from shallow_backup.git_wrapper import git_set_remote, move_git_dir_to_path @@ -21,7 +21,7 @@ def prompt_for_path_update(config): print(Fore.BLUE + "\nUpdating shallow-backup path to {}".format(abs_path) + Style.RESET_ALL) config["backup_path"] = abs_path write_config(config) - make_dir_warn_overwrite(abs_path) + mkdir_warn_overwrite(abs_path) move_git_dir_to_path(current_path, abs_path) diff --git a/shallow_backup/reinstall.py b/shallow_backup/reinstall.py index 54751f9a..44042858 100644 --- a/shallow_backup/reinstall.py +++ b/shallow_backup/reinstall.py @@ -2,8 +2,8 @@ from shutil import copytree, copyfile from colorama import Fore, Style from shallow_backup.config import get_config -from shallow_backup.utils import _home_prefix -from shallow_backup.utils import run_shell_cmd +from shallow_backup.utils import home_prefix +from shallow_backup.utils import run_cmd from shallow_backup.printing import print_section_header @@ -22,11 +22,11 @@ def backup_prefix(path): for target, backup in configs_dir_mapping.items(): if os.path.isdir(backup_prefix(backup)): - copytree(backup_prefix(backup), _home_prefix(target)) + copytree(backup_prefix(backup), home_prefix(target)) for target, backup in plist_files.items(): if os.path.exists(backup_prefix(backup)): - copyfile(backup_prefix(backup), _home_prefix(target)) + copyfile(backup_prefix(backup), home_prefix(target)) print_section_header("SUCCESSFUL CONFIG REINSTALLATION", Fore.BLUE) @@ -60,19 +60,19 @@ def reinstall_packages_from_lists(packages_path): pm_formatted = pm.replace("-", " ") print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm_formatted) + Style.RESET_ALL) cmd = "xargs {0} install < {1}/{2}_list.txt".format(pm.replace("-", " "), packages_path, pm_formatted) - run_shell_cmd(cmd) + run_cmd(cmd) elif pm == "npm": print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) cmd = "cat {0}/npm_list.txt | xargs npm install -g".format(packages_path) - run_shell_cmd(cmd) + run_cmd(cmd) elif pm == "pip": print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) cmd = "pip install -r {0}/pip_list.txt".format(packages_path) - run_shell_cmd(cmd) + run_cmd(cmd) elif pm == "apm": print(Fore.BLUE + Style.BRIGHT + "Reinstalling {} packages...".format(pm) + Style.RESET_ALL) cmd = "apm install --packages-file {0}/apm_list.txt".format(packages_path) - run_shell_cmd(cmd) + run_cmd(cmd) elif pm == "macports": print(Fore.RED + "WARNING: Macports reinstallation is not supported." + Style.RESET_ALL) elif pm == "gem": diff --git a/shallow_backup/shallow_backup.py b/shallow_backup/shallow_backup.py index 547e7893..17b92ffb 100644 --- a/shallow_backup/shallow_backup.py +++ b/shallow_backup/shallow_backup.py @@ -2,13 +2,13 @@ import sys import click from colorama import Fore, Style -from reinstall import reinstall_packages_from_lists, reinstall_config_files -from config import get_config, show_config, add_path_to_config, rm_path_from_config, write_config, create_config_file_if_needed, get_config_path -from utils import make_dir_warn_overwrite, destroy_backup_dir -from printing import print_version_info, prompt_yes_no, splash_screen -from backup import backup_all, backup_configs, backup_dotfiles, backup_fonts, backup_packages -from git_wrapper import safe_git_init, git_set_remote, git_add_all_commit_push, safe_create_gitignore -from prompts import actions_menu_prompt, prompt_for_git_url, prompt_for_path_update +from shallow_backup.utils import mkdir_warn_overwrite, destroy_backup_dir +from shallow_backup.printing import print_version_info, prompt_yes_no, splash_screen +from shallow_backup.reinstall import reinstall_packages_from_lists, reinstall_config_files +from shallow_backup.prompts import actions_menu_prompt, prompt_for_git_url, prompt_for_path_update +from shallow_backup.backup import backup_all, backup_configs, backup_dotfiles, backup_fonts, backup_packages +from shallow_backup.git_wrapper import safe_git_init, git_set_remote, git_add_all_commit_push, safe_create_gitignore +from shallow_backup.config import get_config, show_config, add_path_to_config, rm_path_from_config, write_config, create_config_file_if_needed, get_config_path # custom help options @@ -33,7 +33,10 @@ def cli(add, rm, show, complete, dotfiles, configs, packages, fonts, old_path, new_path, remote, reinstall_packages, reinstall_configs, delete_config, destroy_backup, v): """ - Easily back up installed packages, dotfiles, and more. You can edit which dotfiles are backed up in ~/.shallow-backup. + Easily back up installed packages, dotfiles, and more. + You can edit which dotfiles are backed up in ~/.shallow-backup. + + Written by Aaron Lichtman (@alichtman). """ backup_config_path = get_config_path() @@ -74,7 +77,7 @@ def cli(add, rm, show, complete, dotfiles, configs, packages, fonts, old_path, n # Create backup directory and do git setup backup_home_path = get_config()["backup_path"] - make_dir_warn_overwrite(backup_home_path) + mkdir_warn_overwrite(backup_home_path) repo, new_git_repo_created = safe_git_init(backup_home_path) # Create default gitignore if we just ran git init diff --git a/shallow_backup/utils.py b/shallow_backup/utils.py index d2b7c1d6..9aea0930 100644 --- a/shallow_backup/utils.py +++ b/shallow_backup/utils.py @@ -6,9 +6,9 @@ from shallow_backup.printing import prompt_yes_no -def run_shell_cmd(command): +def run_cmd(command): """ - Wrapper on subprocess.run that handles both lists and strings as commands. + Wrapper on subprocess.run that handles both lists and strings as shell commands. """ try: if not isinstance(command, list): @@ -21,18 +21,18 @@ def run_shell_cmd(command): return None -def run_shell_cmd_write_stdout(command, filepath): +def run_cmd_write_stdout(command, filepath): """ Runs a command and then writes its stdout to a file :param: command String representing command to run and write output of to file """ - process = run_shell_cmd(command) + process = run_cmd(command) if process: with open(filepath, "w+") as f: f.write(process.stdout.decode('utf-8')) -def make_dir_warn_overwrite(path): +def mkdir_warn_overwrite(path): """ Make destination dir if path doesn't exist, confirm before overwriting if it does. """ @@ -51,6 +51,17 @@ def make_dir_warn_overwrite(path): print(Fore.RED + Style.BRIGHT + "CREATED DIR: " + Style.NORMAL + path + Style.RESET_ALL) +def destroy_backup_dir(backup_path): + """ + Deletes the backup directory and its content + """ + try: + print("{} Deleting backup directory {} {}...".format(Fore.RED, backup_path, Style.BRIGHT)) + rmtree(backup_path) + except OSError as e: + print("{} Error: {} - {}. {}".format(Fore.RED, e.filename, e.strerror, Style.RESET_ALL)) + + def get_subfiles(directory): """ Returns list of absolute paths of immediate subfiles of a directory @@ -62,7 +73,7 @@ def get_subfiles(directory): return file_paths -def _copy_dir(source_dir, backup_path): +def copy_dir(source_dir, backup_path): """ Copy dotfolder from $HOME. """ @@ -78,22 +89,16 @@ def _copy_dir(source_dir, backup_path): copytree(source_dir, backup_path, symlinks=True) -def _mkdir_or_pass(directory): +def mkdir_or_pass(directory): if not os.path.isdir(directory): os.makedirs(directory) pass -def _home_prefix(path): - return os.path.join(os.path.expanduser('~'), path) - - -def destroy_backup_dir(backup_path): +def home_prefix(path): """ - Deletes the backup directory and its content + Appends the path to the user's home path. + :param path: Path to be appended. + :return: (str) ~/path """ - try: - print("{} Deleting backup directory {} {}...".format(Fore.RED, backup_path, Style.BRIGHT)) - rmtree(backup_path) - except OSError as e: - print("{} Error: {} - {}. {}".format(Fore.RED, e.filename, e.strerror, Style.RESET_ALL)) + return os.path.join(os.path.expanduser('~'), path) From a595cb4dca1a4ef1c81f77094be145f3e5b9ffd1 Mon Sep 17 00:00:00 2001 From: Aaron Lichtman Date: Thu, 25 Oct 2018 07:19:17 -0500 Subject: [PATCH 9/9] Update tests --- tests/test_copies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_copies.py b/tests/test_copies.py index 6a8dd506..84cffeab 100644 --- a/tests/test_copies.py +++ b/tests/test_copies.py @@ -1,7 +1,7 @@ import os import pytest import shutil -from shallow_backup.utils import _copy_dir +from shallow_backup.utils import copy_dir DIR_TO_BACKUP = 'shallow-backup-test-copy-dir' BACKUP_DIR = 'shallow-backup-test-copy-backup-dir' @@ -37,7 +37,7 @@ def test_copy_dir(self): test_dir = 'test/' test_path = os.path.join(DIR_TO_BACKUP, test_dir) os.mkdir(test_path) - _copy_dir(test_path, BACKUP_DIR) + copy_dir(test_path, BACKUP_DIR) assert os.path.isdir(test_path) assert os.path.isdir(os.path.join(BACKUP_DIR, test_dir)) @@ -46,5 +46,5 @@ def test_copy_dir_invalid(self, invalid): """ Test that attempting to copy an invalid directory fails """ - _copy_dir(invalid, DIR_TO_BACKUP) + copy_dir(invalid, DIR_TO_BACKUP) assert not os.path.isdir(os.path.join(BACKUP_DIR, invalid))