Skip to content

Commit e962a5d

Browse files
Danpkittenis
authored andcommitted
Rebased copy remote files branch. Updated copy remote file code to work correctly with tree dir structures. Added tests for recursion on/off and directory structure tests. Updated docstrings. Resolves #40
1 parent 490ecdc commit e962a5d

File tree

3 files changed

+119
-76
lines changed

3 files changed

+119
-76
lines changed

pssh/pssh_client.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -843,13 +843,14 @@ def copy_file(self, local_file, remote_file, recurse=False):
843843
{'recurse' : recurse})
844844
for host in self.hosts]
845845

846-
def _copy_file(self, host, local_file, remote_file, recurse):
846+
def _copy_file(self, host, local_file, remote_file, recurse=False):
847847
"""Make sftp client, copy file"""
848848
self._make_ssh_client(host)
849849
return self.host_clients[host].copy_file(local_file, remote_file,
850850
recurse=recurse)
851851

852-
def copy_file_to_local(self, remote_file, local_file, recurse=False):
852+
def copy_remote_file(self, remote_file, local_file, recurse=False,
853+
suffix_separator='_'):
853854
"""Copy remote file to local file in parallel
854855
855856
:param remote_file: remote filepath to copy to local host
@@ -858,25 +859,34 @@ def copy_file_to_local(self, remote_file, local_file, recurse=False):
858859
:type local_file: str
859860
:param recurse: whether or not to recurse
860861
:type recurse: bool
862+
:param suffix_separator: (Optional) Separator string between \
863+
filename and host, defaults to ``_``. Eg for a ``local_file`` value of \
864+
``my_file`` and default seaparator the resulting filename will be \
865+
``my_file_my_host`` for the file from host ``my_host``
866+
:type suffix_separator: str
861867
.. note ::
862868
Local directories in `local_file` that do not exist will be
863869
created as long as permissions allow.
864870
865871
.. note ::
866872
File names will be de-duplicated by appending the hostname to the
867-
filepath.
873+
filepath separated by ``suffix_separator``.
868874
869875
:rtype: List(:mod:`gevent.Greenlet`) of greenlets for remote copy \
870876
commands
871877
"""
872-
return [self.pool.spawn(self._copy_file_to_local, host, remote_file, local_file, recurse)
873-
for host in self.hosts]
878+
return [self.pool.spawn(
879+
self._copy_remote_file, host, remote_file,
880+
local_file, recurse, suffix_separator=suffix_separator)
881+
for host in self.hosts]
874882

875-
def _copy_file_to_local(self, host, remote_file, local_file, recurse):
883+
def _copy_remote_file(self, host, remote_file, local_file, recurse,
884+
suffix_separator='_'):
876885
"""Make sftp client, copy file to local"""
886+
file_w_suffix = suffix_separator.join([local_file, host])
877887
self._make_ssh_client(host)
878-
return self.host_clients[host].copy_file_to_local(
879-
remote_file, '_'.join([local_file, host]), recurse=recurse)
888+
return self.host_clients[host].copy_remote_file(
889+
remote_file, file_w_suffix, recurse=recurse)
880890

881891
def _make_ssh_client(self, host):
882892
if not host in self.host_clients or not self.host_clients[host]:

pssh/ssh_client.py

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,14 @@ def _mkdir(self, sftp, directory):
298298

299299
def mkdir(self, sftp, directory):
300300
"""Make directory via SFTP channel.
301-
301+
302302
Parent paths in the directory are created if they do not exist.
303-
303+
304304
:param sftp: SFTP client object
305305
:type sftp: :mod:`paramiko.SFTPClient`
306306
:param directory: Remote directory to create
307307
:type directory: str
308-
308+
309309
Catches and logs at error level remote IOErrors on creating directory.
310310
"""
311311
try:
@@ -360,7 +360,7 @@ def copy_file(self, local_file, remote_file, recurse=False):
360360
raise ValueError("Recurse must be true if local_file is a "
361361
"directory.")
362362
sftp = self._make_sftp()
363-
destination = self._parent_path_split(remote_file)
363+
destination = self._parent_paths_split(remote_file)
364364
try:
365365
sftp.stat(destination)
366366
except IOError:
@@ -375,70 +375,71 @@ def copy_file(self, local_file, remote_file, recurse=False):
375375
logger.info("Copied local file %s to remote destination %s:%s",
376376
local_file, self.host, remote_file)
377377

378-
def _copy_dir_to_local(self, remote_dir, local_dir):
379-
"""Copies the remote directory to the local host."""
380-
sftp = self._make_sftp()
381-
file_list = sftp.listdir(remote_dir)
382-
for file_name in file_list:
383-
remote_path = os.path.join(remote_dir, file_name)
384-
local_path = os.path.join(local_dir, file_name)
385-
self.copy_file_to_local(remote_path, local_path, recurse=True)
386-
387-
def copy_file_to_local(self, remote_file, local_file, recurse=False):
378+
def copy_remote_file(self, remote_file, local_file, recurse=False,
379+
sftp=None):
388380
"""Copy remote file to local host via SFTP/SCP
389381
390-
Copy is done natively using SFTP/SCP version 2 protocol, no scp command \
382+
Copy is done natively using SFTP/SCP version 2, no scp command \
391383
is used or required.
392384
393-
:param remote_file: Remote filepath to copy the file from.
385+
:param remote_file: Remote filepath to copy from
394386
:type remote_file: str
395-
:param local_file: Local filepath where the file will be copied.
387+
:param local_file: Local filepath where file(s) will be copied to
396388
:type local_file: str
397-
:param recurse: Whether or not to recursively copy directories.
389+
:param recurse: Whether or not to recursively copy directories
398390
:type recurse: bool
399391
400392
:raises: :mod:`ValueError` when a directory is supplied to remote_file \
401393
and recurse is not set
402-
:raises: :mod:`OSError` on OS errors creating directories or file
403-
:raises: :mod:`IOError` on IO errors creating directories or file
394+
:raises: :mod:`IOError` on I/O errors creating directories or file
395+
:raises: :mod:`OSError` on OS errors like permission denied
404396
"""
405-
sftp = self._make_sftp()
397+
sftp = self._make_sftp() if not sftp else sftp
406398
try:
407-
sftp.listdir(remote_file)
408-
except (OSError, IOError):
409-
remote_dir_exists = False
399+
file_list = sftp.listdir(remote_file)
400+
except IOError:
401+
# remote_file is not dir
402+
pass
410403
else:
411-
remote_dir_exists = True
412-
if remote_dir_exists and recurse:
413-
return self._copy_dir_to_local(remote_file, local_file)
414-
elif remote_dir_exists and not recurse:
415-
raise ValueError("Recurse must be true if remote_file is a "
416-
"directory.")
417-
destination = self._parent_path_split(local_file)
404+
if not recurse:
405+
raise ValueError("Recurse must be true if remote_file is a "
406+
"directory.")
407+
return self._copy_remote_dir(file_list, remote_file,
408+
local_file, sftp)
409+
destination = self._parent_paths_split(local_file)
418410
self._make_local_dir(destination)
419411
try:
420412
sftp.get(remote_file, local_file)
421-
except Exception, error:
422-
logger.error("Error occured copying file %s from remote destination %s:%s - %s",
413+
except Exception as error:
414+
logger.error("Error occured copying file %s from remote destination"
415+
" %s:%s - %s",
423416
local_file, self.host, remote_file, error)
424-
raise error
425-
else:
426-
logger.info("Copied local file %s from remote destination %s:%s",
427-
local_file, self.host, remote_file)
417+
raise
418+
logger.info("Copied local file %s from remote destination %s:%s",
419+
local_file, self.host, remote_file)
420+
421+
def _copy_remote_dir(self, file_list, remote_dir, local_dir, sftp):
422+
for file_name in file_list:
423+
remote_path = os.path.join(remote_dir, file_name)
424+
local_path = os.path.join(local_dir, file_name)
425+
self.copy_remote_file(remote_path, local_path, sftp=sftp,
426+
recurse=True)
428427

429428
def _make_local_dir(self, dirpath):
430-
if not os.path.exists(dirpath):
431-
try:
432-
os.makedirs(dirpath)
433-
except OSError:
434-
logger.error("Unable to create local directory structure for "
435-
"directory %s", dirpath)
436-
raise
429+
if os.path.exists(dirpath):
430+
return
431+
try:
432+
os.makedirs(dirpath)
433+
except OSError:
434+
logger.error("Unable to create local directory structure for "
435+
"directory %s", dirpath)
436+
raise
437437

438-
def _parent_path_split(self, file_path):
438+
def _parent_paths_split(self, file_path):
439439
try:
440-
destination = [_dir for _dir in file_path.split(os.path.sep)
441-
if _dir][:-1][0]
440+
destination = os.path.sep.join(
441+
[_dir for _dir in file_path.split(os.path.sep)
442+
if _dir][:-1])
442443
except IndexError:
443444
destination = ''
444445
if file_path.startswith(os.path.sep) or not destination:

tests/test_pssh_client.py

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -451,31 +451,63 @@ def test_pssh_client_copy_file_failure(self):
451451
for path in [local_test_path, remote_test_path]:
452452
shutil.rmtree(path)
453453

454-
def test_pssh_copy_file_to_local(self):
454+
def test_pssh_copy_remote_file(self):
455455
"""Test parallel copy file to local host"""
456456
test_file_data = 'test'
457-
remote_filename = 'test_file'
458-
local_test_dir, local_filename = 'local_test_dir', 'test_file_copy'
459-
local_filename = os.path.sep.join([local_test_dir, local_filename])
460-
test_file = open(remote_filename, 'w')
461-
test_file.writelines([test_file_data + os.linesep])
462-
test_file.close()
463-
server = start_server({ self.fake_cmd : self.fake_resp },
464-
self.listen_socket)
457+
local_test_path = 'directory_test_local_remote_copied'
458+
remote_test_path = 'directory_test_remote_copy'
459+
local_copied_dir = '_'.join([local_test_path, self.host])
460+
new_local_copied_dir = '.'.join([local_test_path, self.host])
461+
for path in [local_test_path, remote_test_path, local_copied_dir,
462+
new_local_copied_dir]:
463+
try:
464+
shutil.rmtree(path)
465+
except OSError:
466+
try:
467+
os.unlink(path)
468+
except Exception:
469+
pass
470+
pass
471+
os.mkdir(remote_test_path)
472+
local_file_paths = []
473+
for i in range(0, 10):
474+
remote_file_path_dir = os.path.join(remote_test_path, 'dir_foo' + str(i))
475+
os.mkdir(remote_file_path_dir)
476+
remote_file_path = os.path.join(remote_file_path_dir, 'foo' + str(i))
477+
local_file_path = os.path.join(local_copied_dir, 'dir_foo' + str(i), 'foo' + str(i))
478+
local_file_paths.append(local_file_path)
479+
test_file = open(remote_file_path, 'w')
480+
test_file.write(test_file_data)
481+
test_file.close()
465482
client = ParallelSSHClient([self.host], port=self.listen_port,
466483
pkey=self.user_key)
467-
cmds = client.copy_file_to_local(remote_filename, local_filename)
468-
cmds[0].get()
469-
local_filename += '_' + self.host
470-
self.assertTrue(os.path.isdir(local_test_dir),
471-
msg="SFTP create local directory failed")
472-
self.assertTrue(os.path.isfile(local_filename),
473-
msg="SFTP copy failed")
474-
for filepath in [remote_filename, local_filename]:
475-
os.unlink(filepath)
476-
shutil.rmtree(local_test_dir)
477-
del client
478-
server.join()
484+
cmds = client.copy_remote_file(remote_test_path, local_test_path)
485+
for cmd in cmds:
486+
self.assertRaises(ValueError, cmd.get)
487+
cmds = client.copy_remote_file(remote_test_path, local_test_path,
488+
recurse=True)
489+
for cmd in cmds:
490+
cmd.get()
491+
try:
492+
self.assertTrue(os.path.isdir(local_copied_dir))
493+
for path in local_file_paths:
494+
self.assertTrue(os.path.isfile(path))
495+
except Exception:
496+
shutil.rmtree(remote_test_path)
497+
finally:
498+
shutil.rmtree(local_copied_dir)
499+
cmds = client.copy_remote_file(remote_test_path, local_test_path,
500+
suffix_separator='.', recurse=True)
501+
for cmd in cmds:
502+
cmd.get()
503+
new_local_copied_dir = '.'.join([local_test_path, self.host])
504+
try:
505+
for path in local_file_paths:
506+
path = path.replace(local_copied_dir, new_local_copied_dir)
507+
self.assertTrue(os.path.isfile(path))
508+
finally:
509+
shutil.rmtree(new_local_copied_dir)
510+
shutil.rmtree(remote_test_path)
479511

480512
def test_pssh_pool_size(self):
481513
"""Test setting pool size to non default values"""

0 commit comments

Comments
 (0)