From 8695344969a35ebb8df1c9129cf997f37c368e23 Mon Sep 17 00:00:00 2001 From: douglas-raillard-arm Date: Thu, 18 Jun 2020 12:25:17 +0100 Subject: [PATCH] target/{host,ssh}: Align push/pull with cp/mv behaviour When pushing or pulling a folder, replicate the mv/cp/scp/adb behaviour, which is: * splitting the destination into (existing, new) components * if {new} component is empty, set it to the basename of the source. * mkdir {new} if necessary * merge the hierarchies of {src} and {existing}/{new} --- devlib/host.py | 35 +++++++++++++++++++++++++---------- devlib/utils/ssh.py | 41 +++++++++++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/devlib/host.py b/devlib/host.py index cb7a280..ccf66e0 100644 --- a/devlib/host.py +++ b/devlib/host.py @@ -64,19 +64,34 @@ class LocalConnection(ConnectionBase): self.unrooted = unrooted self.password = password - def push(self, sources, dest, timeout=None, as_root=False): # pylint: disable=unused-argument - self.logger.debug('copying {} to {}'.format(sources, dest)) - for source in sources: + + def _copy_path(self, source, dest): + self.logger.debug('copying {} to {}'.format(source, dest)) + if os.path.isdir(source): + # Behave similarly as cp, scp, adb push, etc. by creating a new + # folder instead of merging hierarchies + if os.path.exists(dest): + dest = os.path.join(dest, os.path.basename(os.path.normpath(src))) + + # Use distutils copy_tree since it behaves the same as + # shutils.copytree except that it won't fail if some folders + # already exist. + # + # Mirror the behavior of all other targets which only copy the + # content without metadata + copy_tree(source, dest, preserve_mode=False, preserve_times=False) + else: shutil.copy(source, dest) - def pull(self, sources, dest, timeout=None, as_root=False): # pylint: disable=unused-argument + def _copy_paths(self, sources, dest): for source in sources: - self.logger.debug('copying {} to {}'.format(source, dest)) - if os.path.isdir(source): - # Use distutils to allow copying into an existing directory structure. - copy_tree(source, dest) - else: - shutil.copy(source, dest) + self._copy_path(source, dest) + + def push(self, sources, dest, timeout=None, as_root=False): # pylint: disable=unused-argument + self._copy_paths(sources, dest) + + def pull(self, sources, dest, timeout=None, as_root=False): # pylint: disable=unused-argument + self._copy_paths(sources, dest) # pylint: disable=unused-argument def execute(self, command, timeout=None, check_exit_code=True, diff --git a/devlib/utils/ssh.py b/devlib/utils/ssh.py index 7765422..05bd623 100644 --- a/devlib/utils/ssh.py +++ b/devlib/utils/ssh.py @@ -451,28 +451,45 @@ class SshConnection(SshConnectionBase): try: sftp.put(src, dst) # Maybe the dst was a folder - except OSError as e: - logger.debug('sftp transfer error: {}'.format(repr(e))) - # This might fail if the folder already exists - with contextlib.suppress(IOError): - sftp.mkdir(dst) - + except OSError as orig_excep: + # If dst was an existing folder, we add the src basename to create + # a new destination for the file as cp would do new_dst = os.path.join( dst, os.path.basename(src), ) logger.debug('Trying: {} -> {}'.format(src, new_dst)) - sftp.put(src, new_dst) + try: + sftp.put(src, new_dst) + # This still failed, which either means: + # * There are some missing folders in the dirnames + # * Something else SFTP-related is wrong + except OSError as e: + # Raise the original exception, as it is closer to what the + # user asked in the first place + raise orig_excep + @classmethod + def _path_exists(cls, sftp, path): + try: + sftp.lstat(path) + except FileNotFoundError: + return False + else: + return True @classmethod def _push_folder(cls, sftp, src, dst): # Behave like the "mv" command or adb push: a new folder is created - # inside the destination folder, rather than merging the trees. - dst = os.path.join( - dst, - os.path.basename(os.path.normpath(src)), - ) + # inside the destination folder, rather than merging the trees, but + # only if the destination already exists. Otherwise, it is use as-is as + # the new hierarchy name. + if cls._path_exists(sftp, dst): + dst = os.path.join( + dst, + os.path.basename(os.path.normpath(src)), + ) + return cls._push_folder_internal(sftp, src, dst) @classmethod