Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add -dry_run flag #275

Merged
merged 4 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ Options:
-delete_config Delete config file.
-destroy_backup Delete backup directory.
-dotfiles Back up dotfiles.
-dry_run Don't backup or reinstall any files, just give
verbose output.

-fonts Back up installed fonts.
-full_backup Full back up.
--new_path TEXT Input a new back up directory path.
Expand All @@ -86,7 +89,7 @@ Options:

-show Display config file.
-v, --version Display version and author info.
-h, -help, --help Show this message and exit.
-help, -h, --help Show this message and exit.
```

### Git Integration
Expand Down
40 changes: 23 additions & 17 deletions shallow_backup/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
@click.option('--add_dot', default=None, help="Add a dotfile or dotfolder to config by path.")
@click.option('-configs', is_flag=True, default=False, help="Back up app config files.")
@click.option('-delete_config', is_flag=True, default=False, help="Delete config file.")
@click.option('-destroy_backup', is_flag=True, default=False, help='Delete backup directory.')
@click.option('-destroy_backup', is_flag=True, default=False, help="Delete backup directory.")
@click.option('-dotfiles', is_flag=True, default=False, help="Back up dotfiles.")
@click.option('-dry_run', is_flag=True, default=False, help="Don't backup or reinstall any files, just give verbose output.")
@click.option('-fonts', is_flag=True, default=False, help="Back up installed fonts.")
@click.option('-full_backup', is_flag=True, default=False, help="Full back up.")
@click.option('--new_path', default=None, help="Input a new back up directory path.")
Expand All @@ -31,7 +32,7 @@
@click.option('-separate_dotfiles_repo', is_flag=True, default=False, help="Use if you are trying to maintain a separate dotfiles repo and running into issue #229.")
@click.option('-show', is_flag=True, default=False, help="Display config file.")
@click.option('--version', '-v', is_flag=True, default=False, help='Display version and author info.')
def cli(add_dot, full_backup, configs, delete_config, destroy_backup, dotfiles, fonts, new_path,
def cli(add_dot, configs, delete_config, destroy_backup, dotfiles, dry_run, fonts, full_backup, new_path,
no_splash, old_path, packages, reinstall_all, reinstall_configs,
reinstall_dots, reinstall_fonts, reinstall_packages, remote,
separate_dotfiles_repo, show, version):
Expand Down Expand Up @@ -115,30 +116,35 @@ def cli(add_dot, full_backup, configs, delete_config, destroy_backup, dotfiles,
# Command line options
if skip_prompt:
if reinstall_packages:
reinstall_packages_sb(packages_path)
reinstall_packages_sb(packages_path, dry_run=dry_run)
elif reinstall_configs:
reinstall_configs_sb(configs_path)
reinstall_configs_sb(configs_path, dry_run=dry_run)
elif reinstall_fonts:
reinstall_fonts_sb(fonts_path)
reinstall_fonts_sb(fonts_path, dry_run=dry_run)
elif reinstall_dots:
reinstall_dots_sb(dotfiles_path)
reinstall_dots_sb(dotfiles_path, dry_run=dry_run)
elif reinstall_all:
reinstall_all_sb(dotfiles_path, packages_path, fonts_path, configs_path)
reinstall_all_sb(dotfiles_path, packages_path, fonts_path, configs_path, dry_run=dry_run)
elif full_backup:
backup_all(dotfiles_path, packages_path, fonts_path, configs_path, skip=True)
git_add_all_commit_push(repo, "full_backup")
backup_all(dotfiles_path, packages_path, fonts_path, configs_path, dry_run=dry_run, skip=True)
if not dry_run:
git_add_all_commit_push(repo, "full_backup")
elif dotfiles:
backup_dotfiles(dotfiles_path, skip=True)
git_add_all_commit_push(repo, "dotfiles", separate_dotfiles_repo)
backup_dotfiles(dotfiles_path, dry_run=dry_run, skip=True)
if not dry_run:
git_add_all_commit_push(repo, "dotfiles", separate_dotfiles_repo)
elif configs:
backup_configs(configs_path, skip=True)
git_add_all_commit_push(repo, "configs")
backup_configs(configs_path, dry_run=dry_run, skip=True)
if not dry_run:
git_add_all_commit_push(repo, "configs")
elif packages:
backup_packages(packages_path, skip=True)
git_add_all_commit_push(repo, "packages")
backup_packages(packages_path, dry_run=dry_run, skip=True)
if not dry_run:
git_add_all_commit_push(repo, "packages")
elif fonts:
backup_fonts(fonts_path, skip=True)
git_add_all_commit_push(repo, "fonts")
backup_fonts(fonts_path, dry_run=dry_run, skip=True)
if not dry_run:
git_add_all_commit_push(repo, "fonts")
# No CL options, show action menu and process selected option.
else:
selection = main_menu_prompt().lower().strip()
Expand Down
136 changes: 82 additions & 54 deletions shallow_backup/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,26 @@
from shlex import quote
from colorama import Fore
import multiprocessing as mp
from pathlib import Path
from shutil import copyfile
from .utils import *
from .printing import *
from .compatibility import *
from .config import get_config


def backup_dotfiles(backup_dest_path, home_path=os.path.expanduser("~"), skip=False):
def backup_dotfiles(backup_dest_path, dry_run=False, home_path=os.path.expanduser("~"), skip=False):
"""
Create `dotfiles` dir and makes copies of dotfiles and dotfolders.
Assumes that dotfiles are stored in the home directory.
:param skip: Boolean flag to skip prompting for overwrite. Used for scripting.
:param backup_dest_path: Destination path for dotfiles. Like, ~/shallow-backup/dotfiles. Used in tests.
:param home_path: Path where dotfiles will be found. $HOME by default.
:param dry_run: Flag for determining if debug info should be shown or copying should occur.
"""
print_section_header("DOTFILES", Fore.BLUE)
overwrite_dir_prompt_if_needed(backup_dest_path, skip)
if not dry_run:
overwrite_dir_prompt_if_needed(backup_dest_path, skip)

# get dotfolders and dotfiles
config = get_config()["dotfiles"]
Expand Down Expand Up @@ -56,6 +59,18 @@ def backup_dotfiles(backup_dest_path, home_path=os.path.expanduser("~"), skip=Fa
else:
dotfiles_mp_in.append(path_pair)

# Print source -> dest and skip the copying step
if dry_run:
print_yellow_bold("Dotfiles:")
for source, dest in dotfiles_mp_in:
print_dry_run_copy_info(source, dest)

print_yellow_bold("\nDotfolders:")
for source, dest in dotfolders_mp_in:
print_dry_run_copy_info(source, dest)

return

# Fix https://github.com/alichtman/shallow-backup/issues/230
for dest_path in [path_pair[1] for path_pair in dotfiles_mp_in + dotfolders_mp_in]:
print(f"Creating: {os.path.split(dest_path)[0]}")
Expand All @@ -75,77 +90,88 @@ def backup_dotfiles(backup_dest_path, home_path=os.path.expanduser("~"), skip=Fa
p.join()


def backup_configs(backup_path, skip=False):
def backup_configs(backup_path, dry_run: bool = False, skip=False):
"""
Creates `configs` directory and places config backups there.
Configs are application settings, generally. .plist files count.
In the config file, the value of the configs dictionary is the dest
path relative to the configs/ directory.
"""
print_section_header("CONFIGS", Fore.BLUE)
overwrite_dir_prompt_if_needed(backup_path, skip)
# Don't clear any directories if this is a dry run
if not dry_run:
alichtman marked this conversation as resolved.
Show resolved Hide resolved
overwrite_dir_prompt_if_needed(backup_path, skip)
config = get_config()

print_blue_bold("Backing up configs...")

# backup config files + dirs in backup_path/<target>/
for path_to_backup, target in config["config_mapping"].items():
print("BACKUP:", path_to_backup)
print("TARGET:", target)
for config_path, target in config["config_mapping"].items():

alichtman marked this conversation as resolved.
Show resolved Hide resolved
dest = os.path.join(backup_path, target)
if os.path.isdir(path_to_backup):
copytree(path_to_backup, quote(dest), symlinks=True)
elif os.path.isfile(path_to_backup):
parent_dir = dest[:dest.rfind("/")]

if dry_run:
print_dry_run_copy_info(config_path, dest)
continue

quoted_dest = quote(dest)
if os.path.isdir(config_path):
copytree(config_path, quoted_dest, symlinks=True)
elif os.path.isfile(config_path):
parent_dir = Path(dest).parent
safe_mkdir(parent_dir)
copyfile(path_to_backup, quote(dest))
copyfile(config_path, quoted_dest)


def backup_packages(backup_path, skip=False):
def backup_packages(backup_path, dry_run: bool = False, skip=False):
"""
Creates `packages` directory and places install list text files there.
"""
print_section_header("PACKAGES", Fore.BLUE)
overwrite_dir_prompt_if_needed(backup_path, skip)
def run_cmd_if_no_dry_run(command, dest, dry_run) -> int:
if dry_run:
print_dry_run_copy_info(f"$ {command}", dest)
# Return -1 for any processes depending on chained successful commands (npm)
return -1
else:
return run_cmd_write_stdout(command, dest)

std_package_managers = [
"brew",
"brew cask",
"gem"
]
print_section_header("PACKAGES", Fore.BLUE)
if not dry_run:
overwrite_dir_prompt_if_needed(backup_path, skip)

for mgr in std_package_managers:
for mgr in ["brew", "brew cask", "gem"]:
# 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_cmd_write_stdout(command, dest)
command = f"{mgr} list"
dest = f"{backup_path}/{mgr.replace(' ', '-')}_list.txt"
run_cmd_if_no_dry_run(command, dest, dry_run)

# cargo
print_pkg_mgr_backup("cargo")
command = "ls {}".format(home_prefix(".cargo/bin/"))
dest = "{}/cargo_list.txt".format(backup_path)
run_cmd_write_stdout(command, dest)
dest = f"{backup_path}/cargo_list.txt"
run_cmd_if_no_dry_run(command, dest, dry_run)

# pip
print_pkg_mgr_backup("pip")
command = "pip list --format=freeze"
dest = "{}/pip_list.txt".format(backup_path)
run_cmd_write_stdout(command, dest)
dest = f"{backup_path}/pip_list.txt"
run_cmd_if_no_dry_run(command, dest, dry_run)

# pip3
print_pkg_mgr_backup("pip3")
command = "pip3 list --format=freeze"
dest = "{}/pip3_list.txt".format(backup_path)
run_cmd_write_stdout(command, dest)
dest = f"{backup_path}/pip3_list.txt"
run_cmd_if_no_dry_run(command, dest, dry_run)

# npm
print_pkg_mgr_backup("npm")
command = "npm ls --global --parseable=true --depth=0"
temp_file_path = "{}/npm_temp_list.txt".format(backup_path)
temp_file_path = f"{backup_path}/npm_temp_list.txt"
# If command is successful, go to the next parsing step.
if run_cmd_write_stdout(command, temp_file_path) == 0:
npm_dest_file = "{0}/npm_list.txt".format(backup_path)
npm_backup_cmd_success = run_cmd_if_no_dry_run(command, dest, dry_run) == 0
if npm_backup_cmd_success:
npm_dest_file = f"{backup_path}/npm_list.txt"
# Parse npm output
with open(temp_file_path, mode="r+") as temp_file:
# Skip first line of file
Expand All @@ -158,53 +184,55 @@ def backup_packages(backup_path, skip=False):
# atom package manager
print_pkg_mgr_backup("Atom")
command = "apm list --installed --bare"
dest = "{}/apm_list.txt".format(backup_path)
run_cmd_write_stdout(command, dest)
dest = f"{backup_path}/apm_list.txt"
run_cmd_if_no_dry_run(command, dest, dry_run)

# vscode extensions
print_pkg_mgr_backup("VSCode")
command = "code --list-extensions --show-versions"
dest = "{}/vscode_list.txt".format(backup_path)
run_cmd_write_stdout(command, dest)
dest = f"{backup_path}/vscode_list.txt"
run_cmd_if_no_dry_run(command, dest, dry_run)

# macports
print_pkg_mgr_backup("macports")
command = "port installed requested"
dest = "{}/macports_list.txt".format(backup_path)
run_cmd_write_stdout(command, dest)
dest = f"{backup_path}/macports_list.txt"
run_cmd_if_no_dry_run(command, dest, dry_run)

# system installs
print_pkg_mgr_backup("System Applications")
applications_path = get_applications_dir()
command = "ls {}".format(applications_path)
dest = "{}/system_apps_list.txt".format(backup_path)
run_cmd_write_stdout(command, dest)
dest = f"{backup_path}/system_apps_list.txt"
run_cmd_if_no_dry_run(command, dest, dry_run)


def backup_fonts(backup_path, skip=False):
"""
Copies all .ttf and .otf files in ~/Library/Fonts/ to backup/fonts/
def backup_fonts(backup_path: str, dry_run: bool = False, skip: bool = False):
"""Copies all .ttf and .otf files in the to backup/fonts/
"""
print_section_header("FONTS", Fore.BLUE)
overwrite_dir_prompt_if_needed(backup_path, skip)
if not dry_run:
overwrite_dir_prompt_if_needed(backup_path, skip)
print_blue("Copying '.otf' and '.ttf' fonts...")
fonts_path = get_fonts_dir()
if os.path.isdir(fonts_path):
fonts = [quote(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:
dest = os.path.join(backup_path, font.split("/")[-1])
if os.path.exists(font):
copyfile(font, os.path.join(backup_path, font.split("/")[-1]))
if dry_run:
print_dry_run_copy_info(font, dest)
else:
copyfile(font, dest)
else:
print_red('Skipping fonts backup. No fonts directory found.')


def backup_all(dotfiles_path, packages_path, fonts_path, configs_path, skip=False):
"""
Complete backup procedure.
"""
backup_dotfiles(dotfiles_path, skip=skip)
backup_packages(packages_path, skip)
backup_fonts(fonts_path, skip)
backup_configs(configs_path, skip)
def backup_all(dotfiles_path, packages_path, fonts_path, configs_path, dry_run=False, skip=False):
"""Complete backup procedure."""
backup_dotfiles(dotfiles_path, dry_run=dry_run, skip=skip)
backup_packages(packages_path, dry_run=dry_run, skip=skip)
backup_fonts(fonts_path, dry_run=dry_run, skip=skip)
backup_configs(configs_path, dry_run=dry_run, skip=skip)
35 changes: 35 additions & 0 deletions shallow_backup/printing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sys
import inquirer
from colorama import Fore, Style
Expand Down Expand Up @@ -52,6 +53,40 @@ def print_path_green(text, path):
print(Fore.GREEN + Style.BRIGHT + text, Style.NORMAL + path + Style.RESET_ALL)


def print_dry_run_copy_info(source, dest):
"""Show source -> dest copy. Replaces expanded ~ with ~ if it's at the beginning of paths.
source and dest are trimmed in the middle if needed. Removed characters will be replaced by ...
:param source: Can be of type str or Path
:param dest: Can be of type str or Path
"""
def shorten_home(path):
expanded_home = os.path.expanduser("~")
path = str(path)
if path.startswith(expanded_home):
return path.replace(expanded_home, "~")
return path

def truncate_middle(path: str, acceptable_len: int):
"""Middle truncate a string
https://www.xormedia.com/string-truncate-middle-with-ellipsis/
"""
if len(path) <= acceptable_len:
return path
# half of the size, minus the 3 .'s
n_2 = int(acceptable_len / 2 - 3)
# whatever's left
n_1 = int(acceptable_len - n_2 - 3)
return f"{path[:n_1]}...{path[-n_2:]}"

trimmed_source = shorten_home(source)
trimmed_dest = shorten_home(dest)
longest_allowed_path_len = 87
if len(trimmed_source) + len(trimmed_dest) > longest_allowed_path_len:
trimmed_source = truncate_middle(trimmed_source, longest_allowed_path_len)
trimmed_dest = truncate_middle(trimmed_dest, longest_allowed_path_len)
print(Fore.YELLOW + Style.BRIGHT + trimmed_source + Style.NORMAL, "->", Style.BRIGHT + trimmed_dest + Style.RESET_ALL)


def print_version_info(cli=True):
"""
Formats version differently for CLI and splash screen.
Expand Down
Loading