From ea28276eec02ab66265b4bb5f2d694085179283f Mon Sep 17 00:00:00 2001 From: Julian_Chu Date: Tue, 21 Apr 2020 02:55:17 +0900 Subject: [PATCH] Implement simple copy plugin reuse `link` plugin to write `copy`. --- README.md | 28 ++++++ dotbot/plugins/__init__.py | 1 + dotbot/plugins/copy.py | 139 +++++++++++++++++++++++++++++ test/tests/copy-create.bash | 45 ++++++++++ test/tests/copy-files-and-dir.bash | 39 ++++++++ test/tests/copy-force.bash | 98 ++++++++++++++++++++ 6 files changed, 350 insertions(+) create mode 100644 dotbot/plugins/copy.py create mode 100644 test/tests/copy-create.bash create mode 100644 test/tests/copy-files-and-dir.bash create mode 100644 test/tests/copy-force.bash diff --git a/README.md b/README.md index f5e43c6..3847f5e 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,34 @@ the following config files equivalent: relink: true ``` +### Copy + +Copy command copies files or directories to destination. + +#### Format + +| Copy Option | Explanation | +| -- | -- | +| `path` | The source file to copy, the same as in the shortcut syntax (default:null, automatic (see below)) | +| `create` | When true, create parent directories to the destination as needed. (default:false) | +| `force` | Force removes the old target, file or folder, and forces a new copy (default:false) | +| `skippable` | If old target exists, skip this copy (default:true) | + +#### Example + +```yaml +- copy: + ~/.config/terminator: + create: true + path: config/terminator + ~/.bash_history: + path: bash_history + skippable: true + ~/.vimrc: + path: vimrc + force: true +``` + ### Create Create commands specify empty directories to be created. This can be useful diff --git a/dotbot/plugins/__init__.py b/dotbot/plugins/__init__.py index f75bef5..4ca50dd 100644 --- a/dotbot/plugins/__init__.py +++ b/dotbot/plugins/__init__.py @@ -2,3 +2,4 @@ from .clean import Clean from .create import Create from .link import Link from .shell import Shell +from .copy import Copy diff --git a/dotbot/plugins/copy.py b/dotbot/plugins/copy.py new file mode 100644 index 0000000..a0b78d7 --- /dev/null +++ b/dotbot/plugins/copy.py @@ -0,0 +1,139 @@ +import os +import shutil +import dotbot + +def exists(path): + ''' + Returns true if the path exists. + ''' + path = os.path.expanduser(path) + return os.path.exists(path) + +def default_source(destination, source): + if source is None: + basename = os.path.basename(destination) + if basename.startswith('.'): + return basename[1:] + else: + return basename + else: + return source + + +class Copy(dotbot.Plugin): + ''' + copy dotfiles. + ''' + + _directive = 'copy' + + def can_handle(self, directive): + return directive == self._directive + + def handle(self, directive, data): + if directive != self._directive: + raise ValueError('Copy cannot handle directive %s' % directive) + return self._iterate_copy(data) + + def _make_opts(self, conf_opt): + ''' + combine config options and default options + ''' + opts = { + 'force': False, + 'skippable': True, + 'create': False + } + + opts.update(self._context.defaults().get('copy', {})) + + if isinstance(conf_opt, dict): + opts.update(conf_opt) + return opts + + def _iterate_copy(self, copy_segment): + success = True + for key, conf_opt in copy_segment.items(): + destination = os.path.expandvars(key) + # for this case, create a config with only 'path' + # -copy: + # ~/foo: bar + if not isinstance(conf_opt, dict): + conf_opt = { + 'path': default_source(destination, conf_opt) + } + opts = self._make_opts(conf_opt) + src = self._get_source(destination, opts) + success &= self._process_copy(src, destination, opts) + if success: + self._log.info('All files have been copied') + else: + self._log.error('Some files not copied successfully') + return success + + def _process_copy(self, src, dst, opts): + if (not exists(src)): + self._log.warning('Nonexistent source %s ' % (src)) + return False + if not self._ensure_parent_dir(opts, dst): + self._log.warning('cannot create parent dir for destination %s ' % (dst)) + return False + + if exists(dst): + # if destination files exists. but we won't skip it either force rewrite it + if not opts.get('force'): + self._log.warning('Destination exists, skip: %s ' % (dst)) + # if destination files exists, and it is skippable, return True. otherwise, it failed. + return opts.get('skippable') + return self._copy(src, dst, opts) + + def _get_source(self, destination, opts): + source = opts.get('path') + source = default_source(destination, source) + source = os.path.expandvars(os.path.expanduser(source)) + return source + + def _ensure_parent_dir(self, opts, dst): + parent = os.path.abspath(os.path.join(os.path.expanduser(dst), os.pardir)) + if not exists(parent): + return opts.get('create') and self._create(dst) + else: + return True + + def _create(self, path): + success = True + parent = os.path.abspath(os.path.join(os.path.expanduser(path), os.pardir)) + if not exists(parent): + self._log.debug("Try to create parent: " + str(parent)) + try: + os.makedirs(parent) + except OSError: + self._log.warning('Failed to create directory %s' % parent) + success = False + else: + self._log.lowinfo('Creating directory %s' % parent) + return success + + def _copy(self, source, dest_name, opts): + ''' + copy from source to path. + + Returns true if successfully copied files. + ''' + success = False + destination = os.path.expanduser(dest_name) + base_directory = self._context.base_directory() + source = os.path.join(base_directory, source) + try: + if os.path.isdir(source): + self._log.warning("copytree %s -> %s" % (source, destination)) + shutil.copytree(source, destination, dirs_exist_ok=opts.get('force')) + else: + shutil.copy2(source, destination) + except OSError: + self._log.warning('Copy failed %s -> %s' % (source, destination)) + else: + self._log.lowinfo('Copying file %s -> %s' % (source, destination)) + success = True + return success + diff --git a/test/tests/copy-create.bash b/test/tests/copy-create.bash new file mode 100644 index 0000000..3d38f2d --- /dev/null +++ b/test/tests/copy-create.bash @@ -0,0 +1,45 @@ +test_description='Basic copy test' +. '../test-lib.bash' + +test_expect_success 'setup' ' +mkdir -p ${DOTFILES}/box +echo "apple" > ${DOTFILES}/apple +echo "banana" > ${DOTFILES}/box/banana +' + +test_expect_failure 'copy file should fail without create option' ' +run_dotbot < ${DOTFILES}/apple +echo "watermelon" > ${DOTFILES}/water +echo "grape" > ${DOTFILES}/grape +mkdir -p ${DOTFILES}/more/fruits +echo "guava" > ${DOTFILES}/more/fruits/guava +' + +test_expect_success 'run by conf' ' +run_dotbot < ${DOTFILES}/apple +echo "banana" > ~/apple +mkdir -p ${DOTFILES}/fruits/box/ +mkdir -p ~/fruits/box/ +echo "orange" > ~/fruits/box/lemon +echo "guava" > ~/fruits/box/guava +echo "cherry" > ${DOTFILES}/fruits/box/cherry +echo "lemon" > ${DOTFILES}/fruits/box/lemon +' + +# test single file + +## destination file already exists, do not overwrite it +test_expect_success 'do not overwrite existing file' ' +run_dotbot <