diff --git a/src/pyinfra/operations/files.py b/src/pyinfra/operations/files.py index 2d59cc85e..ab5ba6cdc 100644 --- a/src/pyinfra/operations/files.py +++ b/src/pyinfra/operations/files.py @@ -751,6 +751,15 @@ def _file_equal(local_path: str | IO[Any] | None, remote_path: str) -> bool: return False +def _remote_file_equal(remote_path_a: str, remote_path_b: str) -> bool: + for fact in [Sha1File, Md5File, Sha256File]: + sum_a = host.get_fact(fact, path=remote_path_a) + sum_b = host.get_fact(fact, path=remote_path_b) + if sum_a and sum_b: + return sum_a == sum_b + return False + + @operation( # We don't (currently) cache the local state, so there's nothing we can # update to flag the local file as present. @@ -1312,6 +1321,39 @@ 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: + if _remote_file_equal(src, dest_file_path): + host.noop(f"{dest_file_path} already exists") + return + else: + raise OperationError(f"{dest_file_path} already exists and is different than src") + + 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..19b5d0b92 --- /dev/null +++ b/tests/operations/files.copy/copies_directory.json @@ -0,0 +1,22 @@ +{ + "require_platform": ["Darwin", "Linux"], + "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..0bc7c53f8 --- /dev/null +++ b/tests/operations/files.copy/copies_directory_overwriting.json @@ -0,0 +1,21 @@ +{ + "require_platform": ["Darwin", "Linux"], + "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..f37991ff1 --- /dev/null +++ b/tests/operations/files.copy/copies_file.json @@ -0,0 +1,19 @@ +{ + "require_platform": ["Darwin", "Linux"], + "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..9c37cadb7 --- /dev/null +++ b/tests/operations/files.copy/copies_file_overwriting.json @@ -0,0 +1,19 @@ +{ + "require_platform": ["Darwin", "Linux"], + "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..9ff71a326 --- /dev/null +++ b/tests/operations/files.copy/invalid_overwrite.json @@ -0,0 +1,26 @@ +{ + "require_platform": ["Darwin", "Linux"], + "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 + }, + "files.Sha1File": { + "path=/tmp/src_dir/file": "abc", + "path=/tmp/dest_dir/file": "xyz" + } + }, + "exception": { + "name": "OperationError", + "message": "/tmp/dest_dir/file already exists and is different than src" + } +} 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" + } +} diff --git a/tests/operations/files.copy/noop_dest_file_exists.json b/tests/operations/files.copy/noop_dest_file_exists.json new file mode 100644 index 000000000..5bc769ade --- /dev/null +++ b/tests/operations/files.copy/noop_dest_file_exists.json @@ -0,0 +1,24 @@ +{ + "require_platform": ["Darwin", "Linux"], + "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 + }, + "files.Sha1File": { + "path=/tmp/src_dir/file": "abc", + "path=/tmp/dest_dir/file": "abc" + } + }, + "commands": [], + "noop_description": "/tmp/dest_dir/file already exists" +}