diff --git a/pyinfra/operations/files.py b/pyinfra/operations/files.py index 6f9515f5e..c72d784bd 100644 --- a/pyinfra/operations/files.py +++ b/pyinfra/operations/files.py @@ -1312,6 +1312,35 @@ def move(src: str, dest: str, overwrite=False): yield StringCommand("mv", QuoteString(src), QuoteString(dest)) +@operation() +def copy(src: str, dest: str, overwrite=False): + """ + Copy remote file/directory/link into remote directory + + + src: remote file/directory to copy + + dest: remote directory to copy `src` into + + overwrite: whether to overwrite dest, if present + """ + src_is_dir = host.get_fact(Directory, src) + if not host.get_fact(File, src) and not src_is_dir: + raise OperationError(f"src {src} does not exist") + + if not host.get_fact(Directory, dest): + raise OperationError(f"dest {dest} is not an existing directory") + + dest_file_path = os.path.join(dest, os.path.basename(src)) + dest_file_exists = host.get_fact(File, dest_file_path) + if dest_file_exists and not overwrite: + raise OperationError(f"dest {dest_file_path} already exists and `overwrite` is unset") + + cp_cmd = ["cp -r"] + + if overwrite: + cp_cmd.append("-f") + + yield StringCommand(*cp_cmd, QuoteString(src), QuoteString(dest)) + + def _validate_path(path): try: return os.fspath(path) diff --git a/tests/operations/files.copy/copies_directory.json b/tests/operations/files.copy/copies_directory.json new file mode 100644 index 000000000..d09f899d2 --- /dev/null +++ b/tests/operations/files.copy/copies_directory.json @@ -0,0 +1,21 @@ +{ + "kwargs": { + "src": "/tmp/src_dir", + "dest": "/tmp/dest_dir", + "overwrite": false + }, + "facts": { + "files.File": { + "path=/tmp/src_dir/file": true, + + "path=/tmp/src_dir": null, + "path=/tmp/dest_dir/src_dir": null, + "path=/tmp/dest_dir/src_dir/file": null + }, + "files.Directory": { + "path=/tmp/src_dir": true, + "path=/tmp/dest_dir": true + } + }, + "commands": ["cp -r /tmp/src_dir /tmp/dest_dir"] +} diff --git a/tests/operations/files.copy/copies_directory_overwriting.json b/tests/operations/files.copy/copies_directory_overwriting.json new file mode 100644 index 000000000..91a93e663 --- /dev/null +++ b/tests/operations/files.copy/copies_directory_overwriting.json @@ -0,0 +1,21 @@ +{ + "kwargs": { + "src": "/tmp/src_dir", + "dest": "/tmp/dest_dir", + "overwrite": true + }, + "facts": { + "files.File": { + "path=/tmp/src_dir/file": true, + + "path=/tmp/src_dir": null, + "path=/tmp/dest_dir/src_dir": null, + "path=/tmp/dest_dir/src_dir/file": null + }, + "files.Directory": { + "path=/tmp/src_dir": true, + "path=/tmp/dest_dir": true + } + }, + "commands": ["cp -r -f /tmp/src_dir /tmp/dest_dir"] +} diff --git a/tests/operations/files.copy/copies_file.json b/tests/operations/files.copy/copies_file.json new file mode 100644 index 000000000..4f46c02ee --- /dev/null +++ b/tests/operations/files.copy/copies_file.json @@ -0,0 +1,18 @@ +{ + "kwargs": { + "src": "/tmp/src_dir/file", + "dest": "/tmp/dest_dir", + "overwrite": false + }, + "facts": { + "files.File": { + "path=/tmp/src_dir/file": true, + "path=/tmp/dest_dir/file": null + }, + "files.Directory": { + "path=/tmp/dest_dir": true, + "path=/tmp/src_dir/file": null + } + }, + "commands": ["cp -r /tmp/src_dir/file /tmp/dest_dir"] +} diff --git a/tests/operations/files.copy/copies_file_overwriting.json b/tests/operations/files.copy/copies_file_overwriting.json new file mode 100644 index 000000000..7b7494400 --- /dev/null +++ b/tests/operations/files.copy/copies_file_overwriting.json @@ -0,0 +1,18 @@ +{ + "kwargs": { + "src": "/tmp/src_dir/file", + "dest": "/tmp/dest_dir", + "overwrite": true + }, + "facts": { + "files.File": { + "path=/tmp/src_dir/file": true, + "path=/tmp/dest_dir/file": true + }, + "files.Directory": { + "path=/tmp/dest_dir": true, + "path=/tmp/src_dir/file": null + } + }, + "commands": ["cp -r -f /tmp/src_dir/file /tmp/dest_dir"] +} diff --git a/tests/operations/files.copy/invalid_dest.json b/tests/operations/files.copy/invalid_dest.json new file mode 100644 index 000000000..63399aeff --- /dev/null +++ b/tests/operations/files.copy/invalid_dest.json @@ -0,0 +1,21 @@ +{ + "kwargs": { + "src": "/tmp/src_dir/file", + "dest": "/tmp/dest_dir", + "overwrite": false + }, + "facts": { + "files.File": { + "path=/tmp/src_dir/file": true, + "path=/tmp/dest_dir/file": null + }, + "files.Directory": { + "path=/tmp/dest_dir": null, + "path=/tmp/src_dir/file": null + } + }, + "exception": { + "name": "OperationError", + "message": "dest /tmp/dest_dir is not an existing directory" + } +} diff --git a/tests/operations/files.copy/invalid_overwrite.json b/tests/operations/files.copy/invalid_overwrite.json new file mode 100644 index 000000000..d9be3cc8c --- /dev/null +++ b/tests/operations/files.copy/invalid_overwrite.json @@ -0,0 +1,21 @@ +{ + "kwargs": { + "src": "/tmp/src_dir/file", + "dest": "/tmp/dest_dir", + "overwrite": false + }, + "facts": { + "files.File": { + "path=/tmp/src_dir/file": true, + "path=/tmp/dest_dir/file": true + }, + "files.Directory": { + "path=/tmp/dest_dir": true, + "path=/tmp/src_dir/file": null + } + }, + "exception": { + "name": "OperationError", + "message": "dest /tmp/dest_dir/file already exists and `overwrite` is unset" + } +} diff --git a/tests/operations/files.copy/invalid_src.json b/tests/operations/files.copy/invalid_src.json new file mode 100644 index 000000000..b77a44170 --- /dev/null +++ b/tests/operations/files.copy/invalid_src.json @@ -0,0 +1,21 @@ +{ + "kwargs": { + "src": "/tmp/src_dir/file", + "dest": "/tmp/dest_dir", + "overwrite": false + }, + "facts": { + "files.File": { + "path=/tmp/src_dir/file": null, + "path=/tmp/dest_dir/file": null + }, + "files.Directory": { + "path=/tmp/dest_dir": true, + "path=/tmp/src_dir/file": null + } + }, + "exception": { + "name": "OperationError", + "message": "src /tmp/src_dir/file does not exist" + } +}