diff --git a/bak/__init__.py b/bak/__init__.py index 94076a6..9d8483f 100644 --- a/bak/__init__.py +++ b/bak/__init__.py @@ -1 +1 @@ -BAK_VERSION = "0.2.2a5" +BAK_VERSION = "0.2.2a6" diff --git a/bak/__main__.py b/bak/__main__.py index fdaa98b..d6f3360 100644 --- a/bak/__main__.py +++ b/bak/__main__.py @@ -1,247 +1,20 @@ -import functools -from pathlib import Path -from typing import Union -import click -from click_default_group import DefaultGroup +from os import geteuid +from sys import exit as exitapp -from bak import commands -from bak import BAK_VERSION as bak_version -from bak.configuration import bak_cfg as cfg +from click import confirm +def run_bak(): + if geteuid() == 0: + if not confirm("WARNING: You are running bak as root! " + "This will create separate config and bakfiles for root, " + "and is probably not what you're trying to do.\n\n" + "If bak needs superuser privileges to copy or overwrite a file, " + "it will invoke sudo cp by itself.\n\n" + "Are you sure you want to continue as root?"): + exitapp() -def __print_help(): - with click.get_current_context() as ctx: - click.echo(bak.get_help(ctx)) - - -def normalize_path(args_key: str = 'filename'): - def on_decorator(func): - @functools.wraps(func) - def on_call(*args, **kwargs): - try: - # expand path - arg = Path(kwargs[args_key]).expanduser().resolve() - if arg.is_dir(): - click.echo( - f"Error: bak cannot operate on directories ({arg})") - return - else: - kwargs[args_key] = arg - # Account for optional params and params that default to None or False - except (IndexError, KeyError, TypeError): - pass - return func(*args, **kwargs) - return on_call - return on_decorator - - -BASIC_HELP_TEXT = "bak FILENAME (creates a bakfile)\n\nalias: bak create\n\n" +\ - "See also: bak COMMAND --help" - - -@click.group(cls=DefaultGroup, default='\0', no_args_is_help=True, help=BASIC_HELP_TEXT, - invoke_without_command=True) -# default command behavior is duplicated here because it's cleaner from a Click perspective, -# which is to say that it gets the desired behavior across the board. ugly but it works! -@click.option("--version", required=False, is_flag=True, help="Print current version and exit.") -def bak(version:bool=False): - if version: - create_bak_cmd(None, version) - - -@bak.command("\0", hidden=True) -@normalize_path() -@click.option("--version", required=False, is_flag=True, help="Print current version and exit.") -@click.argument("filename", required=False, type=click.Path(exists=True)) -def _create(filename, version): - create_bak_cmd(filename, version) - - -@bak.command("create", hidden=True) -@normalize_path() -@click.option("--version", required=False, is_flag=True) -@click.argument("filename", required=False, type=click.Path(exists=True)) -def create(filename, version): - create_bak_cmd(filename, version) - - -def create_bak_cmd(filename, version): - if version: - click.echo(f"bak version {bak_version}") - elif not filename: - # Ensures that 'bak --help' is printed if it doesn't get a filename - __print_help() - else: - filename = Path(filename).expanduser().resolve() - commands.create_bakfile(filename) - - -@bak.command("up", help="Replace a .bakfile with a fresh copy of the parent file") -@normalize_path() -@click.argument("filename", required=True, type=click.Path(exists=True)) -@click.argument("bakfile_number", metavar="[#]", required=False, type=int) -def bak_up(filename, bakfile_number): - if not filename: - click.echo("A filename or operation is required.\n" - "\tbak --help") - filename = Path(filename).expanduser().resolve() - if not commands.bak_up_cmd(filename, bakfile_number): - # TODO descriptive failures - click.echo("An error occurred.") - - -@bak.command("down", help="Restore from a .bakfile (.bakfiles deleted without '--keep')") -@click.option("--keep", "-k", - is_flag=False, - default=0, - multiple=True, - type=str, - help="Keep .bakfiles (optionally accepts a bakfile #; can be used multiple times.\n" +\ - "Also accepts 'all'") -@click.option("--quietly", "-q", - is_flag=True, - default=False, - help="No confirmation prompt") -@click.option('-d', '-o', '--destination', default=None, type=str) -@click.argument("filename", required=True) -@click.argument("bakfile_number", metavar="[#]", required=False, type=int) -def bak_down(filename: str, keep: bool, quietly: bool, destination: str, bakfile_number: int=0): - if not filename: - click.echo("A filename or operation is required.\n" - "\tbak --help") - filename = Path(filename).expanduser().resolve() - if destination: - destination = Path(destination).expanduser().resolve() - if not isinstance(keep, tuple): - if keep == -1 or keep == 'all': - keep = True - elif keep == 0: - keep = False - else: - keep = list(keep) - commands.bak_down_cmd(filename, destination, keep, quietly, bakfile_number) - - -@bak.command("off", help="Use when finished to delete .bakfiles") -@click.option("--quietly", "-q", - is_flag=True, - default=False, - help="Delete all related .bakfiles without confirming") -@click.argument("filename", required=True) -def bak_off(filename, quietly): - filename = Path(filename).expanduser().resolve() - if not commands.bak_off_cmd(filename, quietly): - # TODO better output here - click.echo("Operation cancelled or failed.") - -@bak.command("del", help="Delete a single .bakfile by number (see `bak list FILENAME`)" - "\n\n\talias: `bak rm`") -@click.option("--quietly", "-q", - is_flag=True, - default=False, - help="Delete .bakfile without confirming") -@click.argument("filename", required=True, type=click.Path(exists=False)) -@click.argument("number", metavar="#", required=False, type=int) -def bak_del(filename, number, quietly): - filename = Path(filename).expanduser().resolve() - if not commands.bak_del_cmd(filename, number, quietly): - # TODO this is just a copy of `bak off`, so... - click.echo("Operation cancelled or failed.") - -@bak.command("rm", hidden=True, help="Delete a single .bakfile by number (see `bak list FILENAME`)" - "\n\n\talias of `bak del`") -@click.option("--quietly", "-q", - is_flag=True, - default=False, - help="Delete .bakfile without confirming") -@click.argument("filename", required=True, type=click.Path(exists=False)) -@click.argument("number", metavar="#", required=False, type=int) -def _bak_rm(filename, number, quietly): - bak_del(filename, number, quietly) - -@bak.command("open", help="View or edit a .bakfile in an external program") -@click.option("--using", "--in", "--with", - help="Program to open (default: $PAGER or less)", - required=False, hidden=True) -@normalize_path() -@click.argument("filename", required=True, type=click.Path(exists=True)) -@click.argument("bakfile_number", metavar="[#]", required=False, type=int) -def bak_print(filename, using, bakfile_number): - filename = Path(filename).expanduser().resolve() - commands.bak_print_cmd(filename, using, bakfile_number) - - -@bak.command("where", - help="Outputs the real path of a .bakfile. " - "Useful for piping, and not much else.", - short_help="Output the real path of a .bakfile") -@click.argument("filename", - required=True, - type=click.Path()) -@click.argument("bakfile_number", metavar="[#]", required=False, type=int) -@normalize_path() -def bak_get(filename, bakfile_number=0): - to_where_you_once_belonged = Path( - filename).expanduser().resolve() - commands.bak_getfile_cmd(to_where_you_once_belonged, bakfile_number) - - -@bak.command("diff", - help="diff a file against its .bakfile") -@click.option("--using", "--with", - help="Program to use instead of system diff", - required=False) -@normalize_path() -@click.argument("filename", required=True, type=click.Path(exists=True)) -@click.argument("bakfile_number", metavar="[#]", required=False, type=int) -def bak_diff(filename, using, bakfile_number=0): - filename = Path(filename).expanduser().resolve() - commands.bak_diff_cmd(filename, command=using, bakfile_number=bakfile_number or 0) - - -@bak.command("list", - help="List all .bakfiles, or a particular file's") -@click.option("--colors/--nocolors", "-c/-C", - help="Colorize output", - is_flag=True, - default=cfg['bak_list_colors'] and not cfg['fast_mode']) -@click.option("--relpaths", "--rel", "-r", - help="Display relative paths instead of abspaths", - required=False, - is_flag=True, - default=commands.BAK_LIST_RELPATHS) -@click.option("--compare", "--diff", "-d", - help="Compare .bakfiles with current file, identify exact copies", - required=False, - is_flag=True, - default=False) -@click.argument("filename", - required=False, - type=click.Path(exists=True)) -@normalize_path() -def bak_list(colors, relpaths, compare, filename): - if filename: - filename = Path(filename).expanduser().resolve() - commands.show_bak_list(filename=filename or None, - relative_paths=relpaths, colors=colors, compare=compare) - - -TAB = '\t' -CFG_HELP_TEXT = '\b\nGet/set config values. Valid settings include:\n\n\t' + \ - f'\b\n{(TAB + cfg.newline).join(cfg.SETTABLE_VALUES)}' + \ - '\b\n\nNOTE: diff-exec\'s value should be enclosed in quotes, and' \ - '\nformatted like:\b\n\n\t\'diff %old %new\' \b\n\n(%old and %new will be substituted ' \ - 'with the bakfile and the original file, respectively)' - - -@bak.command("config", - short_help="get/set config options", help=CFG_HELP_TEXT) -@click.option("--get/--set", default=True) -@click.argument("setting", required=True) -@click.argument("value", required=False, nargs=-1, type=str) -def bak_config(get, setting, value): - commands.bak_config_command(get, setting, value) - + from bak.cli import bak as _bak + _bak() if __name__ == "__main__": - bak() + run_bak() diff --git a/bak/cli.py b/bak/cli.py new file mode 100644 index 0000000..cfcb4a3 --- /dev/null +++ b/bak/cli.py @@ -0,0 +1,244 @@ +import functools +from pathlib import Path +from typing import Union + + +import click +from click_default_group import DefaultGroup + +from bak import commands +from bak import BAK_VERSION as bak_version +from bak.configuration import bak_cfg as cfg + +def __print_help(): + with click.get_current_context() as ctx: + click.echo(bak.get_help(ctx)) + + +def normalize_path(args_key: str = 'filename'): + def on_decorator(func): + @functools.wraps(func) + def on_call(*args, **kwargs): + try: + # expand path + arg = Path(kwargs[args_key]).expanduser().resolve() + if arg.is_dir(): + click.echo( + f"Error: bak cannot operate on directories ({arg})") + return + else: + kwargs[args_key] = arg + # Account for optional params and params that default to None or False + except (IndexError, KeyError, TypeError): + pass + return func(*args, **kwargs) + return on_call + return on_decorator + + +BASIC_HELP_TEXT = "bak FILENAME (creates a bakfile)\n\nalias: bak create\n\n" +\ + "See also: bak COMMAND --help" + + +@click.group(cls=DefaultGroup, default='\0', no_args_is_help=True, help=BASIC_HELP_TEXT, + invoke_without_command=True) +# default command behavior is duplicated here because it's cleaner from a Click perspective, +# which is to say that it gets the desired behavior across the board. ugly but it works! +@click.option("--version", required=False, is_flag=True, help="Print current version and exit.") +def bak(version:bool=False): + if version: + create_bak_cmd(None, version) + + +@bak.command("\0", hidden=True) +@normalize_path() +@click.option("--version", required=False, is_flag=True, help="Print current version and exit.") +@click.argument("filename", required=False, type=click.Path(exists=True)) +def _create(filename, version): + create_bak_cmd(filename, version) + + +@bak.command("create", hidden=True) +@normalize_path() +@click.option("--version", required=False, is_flag=True) +@click.argument("filename", required=False, type=click.Path(exists=True)) +def create(filename, version): + create_bak_cmd(filename, version) + + +def create_bak_cmd(filename, version): + if version: + click.echo(f"bak version {bak_version}") + elif not filename: + # Ensures that 'bak --help' is printed if it doesn't get a filename + __print_help() + else: + filename = Path(filename).expanduser().resolve() + commands.create_bakfile(filename) + + +@bak.command("up", help="Replace a .bakfile with a fresh copy of the parent file") +@normalize_path() +@click.argument("filename", required=True, type=click.Path(exists=True)) +@click.argument("bakfile_number", metavar="[#]", required=False, type=int) +def bak_up(filename, bakfile_number): + if not filename: + click.echo("A filename or operation is required.\n" + "\tbak --help") + filename = Path(filename).expanduser().resolve() + if not commands.bak_up_cmd(filename, bakfile_number): + # TODO descriptive failures + click.echo("An error occurred.") + + +@bak.command("down", help="Restore from a .bakfile (.bakfiles deleted without '--keep')") +@click.option("--keep", "-k", + is_flag=False, + default=0, + multiple=True, + type=str, + help="Keep .bakfiles (optionally accepts a bakfile #; can be used multiple times.\n" +\ + "Also accepts 'all'") +@click.option("--quietly", "-q", + is_flag=True, + default=False, + help="No confirmation prompt") +@click.option('-d', '-o', '--destination', default=None, type=str) +@click.argument("filename", required=True) +@click.argument("bakfile_number", metavar="[#]", required=False, type=int) +def bak_down(filename: str, keep: bool, quietly: bool, destination: str, bakfile_number: int=0): + if not filename: + click.echo("A filename or operation is required.\n" + "\tbak --help") + filename = Path(filename).expanduser().resolve() + if destination: + destination = Path(destination).expanduser().resolve() + if not isinstance(keep, tuple): + if keep == -1 or keep == 'all': + keep = True + elif keep == 0: + keep = False + else: + keep = list(keep) + commands.bak_down_cmd(filename, destination, keep, quietly, bakfile_number) + + +@bak.command("off", help="Use when finished to delete .bakfiles") +@click.option("--quietly", "-q", + is_flag=True, + default=False, + help="Delete all related .bakfiles without confirming") +@click.argument("filename", required=True) +def bak_off(filename, quietly): + filename = Path(filename).expanduser().resolve() + if not commands.bak_off_cmd(filename, quietly): + # TODO better output here + click.echo("Operation cancelled or failed.") + +@bak.command("del", help="Delete a single .bakfile by number (see `bak list FILENAME`)" + "\n\n\talias: `bak rm`") +@click.option("--quietly", "-q", + is_flag=True, + default=False, + help="Delete .bakfile without confirming") +@click.argument("filename", required=True, type=click.Path(exists=False)) +@click.argument("number", metavar="#", required=False, type=int) +def bak_del(filename, number, quietly): + filename = Path(filename).expanduser().resolve() + if not commands.bak_del_cmd(filename, number, quietly): + # TODO this is just a copy of `bak off`, so... + click.echo("Operation cancelled or failed.") + +@bak.command("rm", hidden=True, help="Delete a single .bakfile by number (see `bak list FILENAME`)" + "\n\n\talias of `bak del`") +@click.option("--quietly", "-q", + is_flag=True, + default=False, + help="Delete .bakfile without confirming") +@click.argument("filename", required=True, type=click.Path(exists=False)) +@click.argument("number", metavar="#", required=False, type=int) +def _bak_rm(filename, number, quietly): + bak_del(filename, number, quietly) + +@bak.command("open", help="View or edit a .bakfile in an external program") +@click.option("--using", "--in", "--with", + help="Program to open (default: $PAGER or less)", + required=False, hidden=True) +@normalize_path() +@click.argument("filename", required=True, type=click.Path(exists=True)) +@click.argument("bakfile_number", metavar="[#]", required=False, type=int) +def bak_print(filename, using, bakfile_number): + filename = Path(filename).expanduser().resolve() + commands.bak_print_cmd(filename, using, bakfile_number) + + +@bak.command("where", + help="Outputs the real path of a .bakfile. " + "Useful for piping, and not much else.", + short_help="Output the real path of a .bakfile") +@click.argument("filename", + required=True, + type=click.Path()) +@click.argument("bakfile_number", metavar="[#]", required=False, type=int) +@normalize_path() +def bak_get(filename, bakfile_number=0): + to_where_you_once_belonged = Path( + filename).expanduser().resolve() + commands.bak_getfile_cmd(to_where_you_once_belonged, bakfile_number) + + +@bak.command("diff", + help="diff a file against its .bakfile") +@click.option("--using", "--with", + help="Program to use instead of system diff", + required=False) +@normalize_path() +@click.argument("filename", required=True, type=click.Path(exists=True)) +@click.argument("bakfile_number", metavar="[#]", required=False, type=int) +def bak_diff(filename, using, bakfile_number=0): + filename = Path(filename).expanduser().resolve() + commands.bak_diff_cmd(filename, command=using, bakfile_number=bakfile_number or 0) + + +@bak.command("list", + help="List all .bakfiles, or a particular file's") +@click.option("--colors/--nocolors", "-c/-C", + help="Colorize output", + is_flag=True, + default=cfg['bak_list_colors'] and not cfg['fast_mode']) +@click.option("--relpaths", "--rel", "-r", + help="Display relative paths instead of abspaths", + required=False, + is_flag=True, + default=commands.BAK_LIST_RELPATHS) +@click.option("--compare", "--diff", "-d", + help="Compare .bakfiles with current file, identify exact copies", + required=False, + is_flag=True, + default=False) +@click.argument("filename", + required=False, + type=click.Path(exists=True)) +@normalize_path() +def bak_list(colors, relpaths, compare, filename): + if filename: + filename = Path(filename).expanduser().resolve() + commands.show_bak_list(filename=filename or None, + relative_paths=relpaths, colors=colors, compare=compare) + + +TAB = '\t' +CFG_HELP_TEXT = '\b\nGet/set config values. Valid settings include:\n\n\t' + \ + f'\b\n{(TAB + cfg.newline).join(cfg.SETTABLE_VALUES)}' + \ + '\b\n\nNOTE: diff-exec\'s value should be enclosed in quotes, and' \ + '\nformatted like:\b\n\n\t\'diff %old %new\' \b\n\n(%old and %new will be substituted ' \ + 'with the bakfile and the original file, respectively)' + + +@bak.command("config", + short_help="get/set config options", help=CFG_HELP_TEXT) +@click.option("--get/--set", default=True) +@click.argument("setting", required=True) +@click.argument("value", required=False, nargs=-1, type=str) +def bak_config(get, setting, value): + commands.bak_config_command(get, setting, value) \ No newline at end of file diff --git a/bak/configuration/__init__.py b/bak/configuration/__init__.py index cd8aeb2..0d94423 100644 --- a/bak/configuration/__init__.py +++ b/bak/configuration/__init__.py @@ -4,7 +4,9 @@ from pathlib import Path from re import sub from shutil import copy2 +from sys import exit as _exit +from click import echo from config import Config, KeyNotFoundError @@ -52,11 +54,20 @@ def __init__(self): self.config_file = self.config_dir / 'bak.cfg' if not self.config_file.exists(): - copy2(self.config_dir / 'bak.cfg.default', self.config_file) + try: + copy2(Path('/etc/xdg/bak.cfg.default'), self.config_file) + except FileNotFoundError: + try: + copy2(self.config_dir / 'bak.cfg.default', self.config_file) + except FileNotFoundError: + echo("Error: current user can't find bak's default config file! " + "Try copying \n\t~/.config/bak.cfg.default\nfrom your default user's ~" + " into this user's, or installing bak another way.") + _exit() _cfg = Config(str(self.config_file)) reload = False - for cfg_value in self.DEFAULT_VALUES: + for cfg_value in self.DEFAULT_VALUES.keys(): if cfg_value not in _cfg.as_dict(): with open(self.config_file, 'a') as _file: _file.writelines( diff --git a/setup.py b/setup.py index f65d100..7685366 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ 'rich==9.1.0'] setup(name='bak', - version='0.2.2a5', + version='0.2.2a6', description='the .bak manager', author='ChanceNCounter', author_email='ChanceNCounter@icloud.com', @@ -17,7 +17,7 @@ install_requires=require, entry_points=''' [console_scripts] - bak=bak.__main__:bak''', + bak=bak.__main__:run_bak''', license='MIT License', url='https://github.com/bakfile/bak') @@ -29,10 +29,14 @@ config_file = os.path.join(config_dir, 'bak.cfg') default_config = os.path.join(config_dir, 'bak.cfg.default') +system_default_config = os.path.join('/etc/xdg', 'bak.cfg.default') if not os.path.exists(config_file): copy2('bak/default.cfg', config_file) if not os.path.exists(default_config): copy2('bak/default.cfg', default_config) - + try: + copy2('bak/default.cfg', system_default_config) + except PermissionError: + pass