[PATCH 2/2] sftp-client: add a "cp" helper to copy files

Mike Frysinger vapier at gentoo.org
Fri Dec 24 15:17:11 AEDT 2021


Use the new copy-data extension for efficient remote file copies.
At the very least, this provides a quick method for testing.
---
Rebased onto the latest tree.

 sftp-client.c | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++
 sftp-client.h |   3 ++
 sftp.1        |  14 +++++-
 sftp.c        |  14 ++++++
 4 files changed, 150 insertions(+), 1 deletion(-)

diff --git a/sftp-client.c b/sftp-client.c
index 6c33a836d6b9..ebb2125544a5 100644
--- a/sftp-client.c
+++ b/sftp-client.c
@@ -103,6 +103,7 @@ struct sftp_conn {
 #define SFTP_EXT_LSETSTAT	0x00000020
 #define SFTP_EXT_LIMITS		0x00000040
 #define SFTP_EXT_PATH_EXPAND	0x00000080
+#define SFTP_EXT_COPY_DATA	0x00000100
 	u_int exts;
 	u_int64_t limit_kbps;
 	struct bwlimit bwlimit_in, bwlimit_out;
@@ -534,6 +535,10 @@ do_init(int fd_in, int fd_out, u_int transfer_buflen, u_int num_requests,
 		    strcmp((char *)value, "1") == 0) {
 			ret->exts |= SFTP_EXT_PATH_EXPAND;
 			known = 1;
+		} else if (strcmp(name, "copy-data") == 0 &&
+		    strcmp((char *)value, "1") == 0) {
+			ret->exts |= SFTP_EXT_COPY_DATA;
+			known = 1;
 		}
 		if (known) {
 			debug2("Server supports extension \"%s\" revision %s",
@@ -1060,6 +1065,121 @@ do_expand_path(struct sftp_conn *conn, const char *path)
 	return do_realpath_expand(conn, path, 1);
 }
 
+int
+do_copy(struct sftp_conn *conn, const char *oldpath, const char *newpath)
+{
+	Attrib junk, *a;
+	struct sshbuf *msg;
+	u_char *old_handle, *new_handle;
+	u_int mode, status, id;
+	size_t old_handle_len, new_handle_len;
+	int r;
+
+	/* Return if the extension is not supported */
+	if ((conn->exts & SFTP_EXT_COPY_DATA) == 0) {
+		error("Server does not support copy-data extension");
+		return -1;
+	}
+
+	/* Make sure the file exists, and we can copy its perms */
+	if ((a = do_stat(conn, oldpath, 0)) == NULL)
+		return -1;
+
+	/* Do not preserve set[ug]id here, as we do not preserve ownership */
+	if (a->flags & SSH2_FILEXFER_ATTR_PERMISSIONS) {
+		mode = a->perm & 0777;
+
+		if (!S_ISREG(a->perm)) {
+			error("Cannot copy non-regular file: %s", oldpath);
+			return -1;
+		}
+	} else {
+		/* NB: The user's umask will apply to this */
+		mode = 0666;
+	}
+
+	/* Set up the new perms for the new file */
+	attrib_clear(a);
+	a->perm = mode;
+	a->flags |= SSH2_FILEXFER_ATTR_PERMISSIONS;
+
+	if ((msg = sshbuf_new()) == NULL)
+		fatal("%s: sshbuf_new failed", __func__);
+
+	attrib_clear(&junk); /* Send empty attributes */
+
+	/* Open the old file for reading */
+	id = conn->msg_id++;
+	if ((r = sshbuf_put_u8(msg, SSH2_FXP_OPEN)) != 0 ||
+	    (r = sshbuf_put_u32(msg, id)) != 0 ||
+	    (r = sshbuf_put_cstring(msg, oldpath)) != 0 ||
+	    (r = sshbuf_put_u32(msg, SSH2_FXF_READ)) != 0 ||
+	    (r = encode_attrib(msg, &junk)) != 0)
+		fatal("%s: buffer error: %s", __func__, ssh_err(r));
+	send_msg(conn, msg);
+	debug3("Sent message SSH2_FXP_OPEN I:%u P:%s", id, oldpath);
+
+	sshbuf_reset(msg);
+
+	old_handle = get_handle(conn, id, &old_handle_len,
+	    "remote open(\"%s\")", oldpath);
+	if (old_handle == NULL) {
+		sshbuf_free(msg);
+		return -1;
+	}
+
+	/* Open the new file for writing */
+	id = conn->msg_id++;
+	if ((r = sshbuf_put_u8(msg, SSH2_FXP_OPEN)) != 0 ||
+	    (r = sshbuf_put_u32(msg, id)) != 0 ||
+	    (r = sshbuf_put_cstring(msg, newpath)) != 0 ||
+	    (r = sshbuf_put_u32(msg, SSH2_FXF_WRITE|SSH2_FXF_CREAT|
+	    SSH2_FXF_TRUNC)) != 0 ||
+	    (r = encode_attrib(msg, a)) != 0)
+		fatal("%s: buffer error: %s", __func__, ssh_err(r));
+	send_msg(conn, msg);
+	debug3("Sent message SSH2_FXP_OPEN I:%u P:%s", id, newpath);
+
+	sshbuf_reset(msg);
+
+	new_handle = get_handle(conn, id, &new_handle_len,
+	    "remote open(\"%s\")", newpath);
+	if (new_handle == NULL) {
+		sshbuf_free(msg);
+		free(old_handle);
+		return -1;
+	}
+
+	/* Copy the file data */
+	id = conn->msg_id++;
+	if ((r = sshbuf_put_u8(msg, SSH2_FXP_EXTENDED)) != 0 ||
+	    (r = sshbuf_put_u32(msg, id)) != 0 ||
+	    (r = sshbuf_put_cstring(msg, "copy-data")) != 0 ||
+	    (r = sshbuf_put_string(msg, old_handle, old_handle_len)) != 0 ||
+	    (r = sshbuf_put_u64(msg, 0)) != 0 ||
+	    (r = sshbuf_put_u64(msg, 0)) != 0 ||
+	    (r = sshbuf_put_string(msg, new_handle, new_handle_len)) != 0 ||
+	    (r = sshbuf_put_u64(msg, 0)) != 0)
+		fatal("%s: buffer error: %s", __func__, ssh_err(r));
+	send_msg(conn, msg);
+	debug3("Sent message copy-data \"%s\" 0 0 -> \"%s\" 0",
+	       oldpath, newpath);
+
+	status = get_status(conn, id);
+	if (status != SSH2_FX_OK)
+		error("Couldn't copy file \"%s\" to \"%s\": %s", oldpath,
+		    newpath, fx2txt(status));
+
+	/* Clean up everything */
+	sshbuf_free(msg);
+	do_close(conn, old_handle, old_handle_len);
+	do_close(conn, new_handle, new_handle_len);
+	free(old_handle);
+	free(new_handle);
+
+	return status == SSH2_FX_OK ? 0 : -1;
+}
+
 int
 do_rename(struct sftp_conn *conn, const char *oldpath, const char *newpath,
     int force_legacy)
diff --git a/sftp-client.h b/sftp-client.h
index 7d0bd12ae5a3..f9911d35c949 100644
--- a/sftp-client.h
+++ b/sftp-client.h
@@ -125,6 +125,9 @@ int do_statvfs(struct sftp_conn *, const char *, struct sftp_statvfs *, int);
 /* Rename 'oldpath' to 'newpath' */
 int do_rename(struct sftp_conn *, const char *, const char *, int);
 
+/* Copy 'oldpath' to 'newpath' */
+int do_copy(struct sftp_conn *, const char *, const char *);
+
 /* Link 'oldpath' to 'newpath' */
 int do_hardlink(struct sftp_conn *, const char *, const char *);
 
diff --git a/sftp.1 b/sftp.1
index 7eebeeacbf3f..72b61b14a4bc 100644
--- a/sftp.1
+++ b/sftp.1
@@ -144,7 +144,7 @@ will abort if any of the following
 commands fail:
 .Ic get , put , reget , reput , rename , ln ,
 .Ic rm , mkdir , chdir , ls ,
-.Ic lchdir , chmod , chown ,
+.Ic lchdir , copy , cp , chmod , chown ,
 .Ic chgrp , lpwd , df , symlink ,
 and
 .Ic lmkdir .
@@ -400,6 +400,18 @@ If the
 flag is specified, then symlinks will not be followed.
 Note that this is only supported by servers that implement
 the "lsetstat at openssh.com" extension.
+.It Ic copy Ar oldpath Ar newpath
+Copy remote file from
+.Ar oldpath
+to
+.Ar newpath .
+.Pp
+Note that this is only supported by servers that implement the "copy-data"
+extension.
+.It Ic cp Ar oldpath Ar newpath
+Alias to
+.Ic copy
+command.
 .It Xo Ic df
 .Op Fl hi
 .Op Ar path
diff --git a/sftp.c b/sftp.c
index 8cb5917a91f1..416d57fcb302 100644
--- a/sftp.c
+++ b/sftp.c
@@ -137,6 +137,7 @@ enum sftp_command {
 	I_CHGRP,
 	I_CHMOD,
 	I_CHOWN,
+	I_COPY,
 	I_DF,
 	I_GET,
 	I_HELP,
@@ -180,6 +181,8 @@ static const struct CMD cmds[] = {
 	{ "chgrp",	I_CHGRP,	REMOTE	},
 	{ "chmod",	I_CHMOD,	REMOTE	},
 	{ "chown",	I_CHOWN,	REMOTE	},
+	{ "copy",	I_COPY,		REMOTE	},
+	{ "cp",		I_COPY,		REMOTE	},
 	{ "df",		I_DF,		REMOTE	},
 	{ "dir",	I_LS,		REMOTE	},
 	{ "exit",	I_QUIT,		NOARGS	},
@@ -286,6 +289,8 @@ help(void)
 	    "chgrp [-h] grp path                Change group of file 'path' to 'grp'\n"
 	    "chmod [-h] mode path               Change permissions of file 'path' to 'mode'\n"
 	    "chown [-h] own path                Change owner of file 'path' to 'own'\n"
+	    "copy oldpath newpath               Copy remote file\n"
+	    "cp oldpath newpath                 Copy remote file\n"
 	    "df [-hi] [path]                    Display statistics for current directory or\n"
 	    "                                   filesystem containing 'path'\n"
 	    "exit                               Quit sftp\n"
@@ -1369,6 +1374,10 @@ parse_args(const char **cpp, int *ignore_errors, int *disable_echo, int *aflag,
 		if ((optidx = parse_link_flags(cmd, argv, argc, sflag)) == -1)
 			return -1;
 		goto parse_two_paths;
+	case I_COPY:
+		if ((optidx = parse_no_flags(cmd, argv, argc)) == -1)
+			return -1;
+		goto parse_two_paths;
 	case I_RENAME:
 		if ((optidx = parse_rename_flags(cmd, argv, argc, lflag)) == -1)
 			return -1;
@@ -1536,6 +1545,11 @@ parse_dispatch_command(struct sftp_conn *conn, const char *cmd, char **pwd,
 		err = process_put(conn, path1, path2, *pwd, pflag,
 		    rflag, aflag, fflag);
 		break;
+	case I_COPY:
+		path1 = make_absolute(path1, *pwd);
+		path2 = make_absolute(path2, *pwd);
+		err = do_copy(conn, path1, path2);
+		break;
 	case I_RENAME:
 		path1 = make_absolute(path1, *pwd);
 		path2 = make_absolute(path2, *pwd);
-- 
2.33.0



More information about the openssh-unix-dev mailing list