Implement simple copy plugin
reuse `link` plugin to write `copy`.
This commit is contained in:
parent
7ffaa65482
commit
ea28276eec
28
README.md
28
README.md
|
@ -235,6 +235,34 @@ the following config files equivalent:
|
||||||
relink: true
|
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
|
||||||
|
|
||||||
Create commands specify empty directories to be created. This can be useful
|
Create commands specify empty directories to be created. This can be useful
|
||||||
|
|
|
@ -2,3 +2,4 @@ from .clean import Clean
|
||||||
from .create import Create
|
from .create import Create
|
||||||
from .link import Link
|
from .link import Link
|
||||||
from .shell import Shell
|
from .shell import Shell
|
||||||
|
from .copy import Copy
|
||||||
|
|
139
dotbot/plugins/copy.py
Normal file
139
dotbot/plugins/copy.py
Normal file
|
@ -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
|
||||||
|
|
45
test/tests/copy-create.bash
Normal file
45
test/tests/copy-create.bash
Normal file
|
@ -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 <<EOF
|
||||||
|
- copy:
|
||||||
|
~/more/fruits/apple:
|
||||||
|
path: apple
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
test_expect_failure 'copy directory should fail without create option' '
|
||||||
|
run_dotbot <<EOF
|
||||||
|
- copy:
|
||||||
|
~/more/fruits/in:
|
||||||
|
path: box
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'run with create option' '
|
||||||
|
run_dotbot <<EOF
|
||||||
|
- copy:
|
||||||
|
~/more/fruits/apple:
|
||||||
|
path: apple
|
||||||
|
create: true
|
||||||
|
~/more/fruits/unbox/:
|
||||||
|
path: box
|
||||||
|
create: true
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
test_expect_success 'content test' '
|
||||||
|
grep "apple" ~/more/fruits/apple &&
|
||||||
|
grep "banana" ~/more/fruits/unbox/banana
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'tear down' '
|
||||||
|
rm -rf ~/more
|
||||||
|
rm ${DOTFILES}/apple
|
||||||
|
rm -rf ${DOTFILES}/box
|
||||||
|
'
|
39
test/tests/copy-files-and-dir.bash
Normal file
39
test/tests/copy-files-and-dir.bash
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
test_description='Basic copy test'
|
||||||
|
. '../test-lib.bash'
|
||||||
|
|
||||||
|
test_expect_success 'setup' '
|
||||||
|
echo "apple" > ${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 <<EOF
|
||||||
|
- copy:
|
||||||
|
~/apple:
|
||||||
|
~/melon: water
|
||||||
|
~/grape:
|
||||||
|
~/.fruits:
|
||||||
|
path: more/fruits
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'content test' '
|
||||||
|
grep "apple" ~/apple &&
|
||||||
|
grep "watermelon" ~/melon &&
|
||||||
|
grep "grape" ~/grape &&
|
||||||
|
grep "guava" ~/.fruits/guava
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'tear down' '
|
||||||
|
rm ~/apple
|
||||||
|
rm ~/melon
|
||||||
|
rm ~/grape
|
||||||
|
rm -rf ~/.fruits
|
||||||
|
rm ${DOTFILES}/apple
|
||||||
|
rm ${DOTFILES}/water
|
||||||
|
rm ${DOTFILES}/grape
|
||||||
|
rm -rf ${DOTFILES}/fruits
|
||||||
|
'
|
98
test/tests/copy-force.bash
Normal file
98
test/tests/copy-force.bash
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
test_description='Test copy with force option'
|
||||||
|
. '../test-lib.bash'
|
||||||
|
|
||||||
|
test_expect_success 'setup' '
|
||||||
|
echo "apple" > ${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 <<EOF
|
||||||
|
- copy:
|
||||||
|
~/apple:
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
test_expect_success 'content test(not overwrite file)' '
|
||||||
|
grep "banana" ~/apple
|
||||||
|
'
|
||||||
|
|
||||||
|
## destination file already exists, but it is not skippable
|
||||||
|
test_expect_failure 'should fail because not skippable' '
|
||||||
|
run_dotbot <<EOF
|
||||||
|
- copy:
|
||||||
|
~/apple:
|
||||||
|
skippable: false
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
test_expect_success 'content test(file not skippable)' '
|
||||||
|
grep "banana" ~/apple
|
||||||
|
'
|
||||||
|
|
||||||
|
## destination file already exists, use option 'force' to overwrite it
|
||||||
|
test_expect_success 'copy file with force option' '
|
||||||
|
run_dotbot <<eof
|
||||||
|
- copy:
|
||||||
|
~/apple:
|
||||||
|
force: true
|
||||||
|
eof
|
||||||
|
'
|
||||||
|
test_expect_success 'content test(force copy file)' '
|
||||||
|
grep "apple" ~/apple
|
||||||
|
'
|
||||||
|
|
||||||
|
# test single directory
|
||||||
|
|
||||||
|
## destination directory already exists, do not overwrite it
|
||||||
|
test_expect_success 'do not overwrite existing directory' '
|
||||||
|
run_dotbot <<EOF
|
||||||
|
- copy:
|
||||||
|
~/fruits:
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
test_expect_success 'content test(not overwrite directory)' '
|
||||||
|
grep "orange" ~/fruits/box/lemon &&
|
||||||
|
grep "guava" ~/fruits/box/guava
|
||||||
|
'
|
||||||
|
|
||||||
|
## destination directory already exists, but it is not skippable
|
||||||
|
test_expect_failure 'do not overwrite existing directory' '
|
||||||
|
run_dotbot <<EOF
|
||||||
|
- copy:
|
||||||
|
~/fruits:
|
||||||
|
skippable: false
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
test_expect_success 'content test(directory not skippable)' '
|
||||||
|
grep "orange" ~/fruits/box/lemon &&
|
||||||
|
grep "guava" ~/fruits/box/guava
|
||||||
|
'
|
||||||
|
|
||||||
|
## destination directory already exists, use option 'force' to overwrite it
|
||||||
|
test_expect_success 'copy directory with force option' '
|
||||||
|
run_dotbot <<eof
|
||||||
|
- copy:
|
||||||
|
~/fruits:
|
||||||
|
force: true
|
||||||
|
eof
|
||||||
|
'
|
||||||
|
test_expect_success 'content test(force copy directory)' '
|
||||||
|
grep "lemon" ~/fruits/box/lemon &&
|
||||||
|
grep "cherry" ~/fruits/box/cherry &&
|
||||||
|
grep "guava" ~/fruits/box/guava
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'tear down' '
|
||||||
|
rm ~/apple
|
||||||
|
rm ${DOTFILES}/apple
|
||||||
|
rm -rf ~/fruits
|
||||||
|
rm -rf ${DOTFILES}/fruits
|
||||||
|
'
|
Loading…
Reference in a new issue