Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions pyinfra/operations/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should this raise an OperationError. For copy() to be idempotent, the destination file existing, should just be a noop right? Alternatively, you could compare hashes to see if the src file has changed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed a noop would be a better fit here, although I still feel that an error should be raised in case dest_file_path already exists and is different from src. So maybe something like this?

    if dest_file_exists and not overwrite:
        if _file_equal(src, dest_file_path):
            host.noop(f"{dest_file_path} already exists")
        else:
            raise OperationError(f"{dest_file_path} already exists and is different than src")

Or maybe there's a better way to handle that case?

Also, in the snippet I use the _file_equal function, but that seems to be intended for local-remote hash comparisons.
Are you aware of any functions that might better fit this use case? Should I just create a new internal function based on this one for remote-remote hash comparison (what could be a nice name for it?)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you that the noop should only happen when the existing file at destination is the same.

I don't know if the _file_equal function can be used for remote-remote hash comparisons. @Fizzadar do you know?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed _file_equal is for local <> remote, a new function seems best here, basically fact x2 and compare.


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)
Expand Down
21 changes: 21 additions & 0 deletions tests/operations/files.copy/copies_directory.json
Original file line number Diff line number Diff line change
@@ -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"]
}
21 changes: 21 additions & 0 deletions tests/operations/files.copy/copies_directory_overwriting.json
Original file line number Diff line number Diff line change
@@ -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"]
}
18 changes: 18 additions & 0 deletions tests/operations/files.copy/copies_file.json
Original file line number Diff line number Diff line change
@@ -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"]
}
18 changes: 18 additions & 0 deletions tests/operations/files.copy/copies_file_overwriting.json
Original file line number Diff line number Diff line change
@@ -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"]
}
21 changes: 21 additions & 0 deletions tests/operations/files.copy/invalid_dest.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
21 changes: 21 additions & 0 deletions tests/operations/files.copy/invalid_overwrite.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
21 changes: 21 additions & 0 deletions tests/operations/files.copy/invalid_src.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading