Search code examples
python-3.xsftprsyncparamiko

Copying Directories using Paramiko SFTP


I want to mimic the behavior of the famous Linux rsync command where it copies the entire directory from "test" if the "/" is not specified at the end of a directory and copies everything inside the "test/" when the "/" is present. My local "test" folder is structure like so:

test
.
├── fileA.txt
├── fileB.txt
├── test1
│   └── test3
│       └── file3.txt
└── test2
    └── file2.txt

To copy the entire local test folder to remote server in rsync:

rsync -avzP test username@remotehost:/home/

Inside the remotehost's home directory would be

home
.
|__ test
    ├── fileA.txt
    ├── fileB.txt
    ├── test1
    │   └── test3
    │       └── file3.txt
    └── test2
        └── file2.txt

Example A

To copy everything inside the local "test" folder excluding itself:

rsync -avzP test/ username@remotehost:/home/

The structure for home directory would be:

home/
.
├── fileA.txt
├── fileB.txt
├── test1
│   └── test3
│       └── file3.txt
└── test2
    └── file2.txt

Example B

The code I have isn't working for Example B. I thought about splitting the paths and get rid of the "test" then copy everything inside of it but it only leads me to an endless, nested, for loops. Another idea is to use os.listdir. If it's a directory, list the directory again and copy the contents inside that directory. This is still another endless for loop. The tree structure above is an over simplified example but in real life, we all know that the directory could be 5 levels deep. How can I implement Example B?

def put (self, localpath, remotepath):

        sftp = self.ssh.open_sftp ()

        # Create remote directory if it doesn't exist
        try:
            sftp.stat (remotepath)
        except FileNotFoundError:
            sftp.mkdir (remotepath)

        if os.path.isfile (localpath):
            # Obtain file name from local path & append to remote path
            path = os.path.split (localpath)        # Returns a tuple (directory, filename)
            remote_filename = os.path.join (remotepath, path [1])
            print ('  Copying %s' % remote_filename)
            sftp.put (localpath, remote_filename)

        elif os.path.isdir (localpath):
            p = os.path.join (remotepath, localpath)
            try:
                sftp.stat (p)
            except FileNotFoundError:
                sftp.mkdir (p)

            for dirpath, dirnames, filenames in os.walk (localpath):
                # Traverse into each child directory and create sub directory if it doesn't exist
                if dirnames:
                    for dirname in dirnames:
                        subdir = os.path.join (dirpath, dirname)
                        try:
                            sftp.stat (subdir)
                        except FileNotFoundError:
                            sftp.mkdir (subdir)

                for filename in filenames:
                    local_filename = os.path.join (dirpath, filename)
                    remote_filename = os.path.join (remotepath, local_filename)
                    sftp.put (local_filename, remote_filename)

Solution

  • I figured it out. It's not the prettiest but functional. If anyone has better, cleaner, and more pythonic way of doing so, please share.

    def put (self, localpath, remotepath):
    
        sftp = self.ssh.open_sftp ()
    
        # Create remote directory if it doesn't exist
        try:
            sftp.stat (remotepath)
        except FileNotFoundError:
            sftp.mkdir (remotepath)
    
        if os.path.isfile (localpath):
            # Obtain file name from local path & append to remote path
            path = os.path.split (localpath)        # Returns a tuple (directory, filename)
            remote_filename = os.path.join (remotepath, path [1])
            print ('  Copying %s' % remote_filename)
            sftp.put (localpath, remote_filename)
    
        elif os.path.isdir (localpath):
            if localpath.endswith ('/'):
                for dirpath, dirnames, filenames in os.walk (localpath):
                    # Change local dirpath to match remote path. Ex: local/dir/.. to remote/dir/...
                    remotedir = dirpath.split ('/')             # remotedir = [local, dir1, dir2, ...]
                    remotedir [0] = remotepath.rstrip ('/')     # remotedir = [/remote, dir1, dir2, ...]
                    remotedir = '/'.join (remotedir)
    
                    # Traverse into each child directory and create sub directory if it doesn't exist on remote host
                    if dirnames:
                        for dirname in dirnames:
                            subdir = os.path.join (remotedir, dirname)
    
                            try:
                                sftp.stat (subdir)
                            except FileNotFoundError:
                                sftp.mkdir (subdir)
    
                    for filename in filenames:
                        localdir = os.path.join (dirpath, filename)
                        remotefile = os.path.join (remotedir, filename)
                        print ('  Copying %s' % localdir)
                        sftp.put (localdir, remotefile)
            else:
                # Create path /remote/local/dir1...
                p = os.path.join (remotepath, localpath)
    
                try:
                    sftp.stat (p)
                except FileNotFoundError:
                    sftp.mkdir (p)
    
                for dirpath, dirnames, filenames in os.walk (localpath):
                    if dirnames:
                        for dirname in dirnames:
                            subdir = os.path.join (dirpath, dirname)
                            remotedir = os.path.join (remotepath, subdir)
    
                            try:
                                sftp.stat (remotedir)
                            except FileNotFoundError:
                                sftp.mkdir (remotedir)
    
                    for filename in filenames:
                        local_filename = os.path.join (dirpath, filename)
                        remote_filename = os.path.join (remotepath, local_filename)
                        print (' Copying %s' % local_filename)
                        sftp.put (local_filename, remote_filename)
        else:
            print ('File or directory not found.')
    

    End results:

    Example A:

    >>> ssh.put ('test', '/home/user/')
     Copying test/fileA.txt
     Copying test/fileB.txt
     Copying test/test1/test3/file3.txt
     Copying test/test2/file2.txt
    Completed!
    
    -sh-4.1$ ls -lh test/*
    -rw-r----- 1 user users    0 Sep  1 23:43 test/fileA.txt
    -rw-r----- 1 user users    0 Sep  1 23:43 test/fileB.txt
    
    test/test1:
    total 4.0K
    drwxr-x--- 2 user users 4.0K Sep  1 23:43 test3
    
    test/test2:
    total 0
    -rw-r----- 1 user users 0 Sep  1 23:43 file2.txt
    
    -sh-4.1$ ls -lh test/*/*
    -rw-r----- 1 user users    0 Sep  1 23:43 test/test2/file2.txt
    
    test/test1/test3:
    total 0
    -rw-r----- 1 user users 0 Sep  1 23:43 file3.txt
    -sh-4.1$ 
    

    Example B:

    >>> ssh.put ('test/', '/home/user/')
      Copying test/fileA.txt
      Copying test/fileB.txt
      Copying test/test1/test3/file3.txt
      Copying test/test2/file2.txt
    Completed!
    
    -sh-4.1$ pwd
    /home/user
    -sh-4.1$ ls -lh
    total 108K
    -rw-r----- 1 user users    0 Sep  1 23:43 fileA.txt
    -rw-r----- 1 user users    0 Sep  1 23:43 fileB.txt
    drwxr-x--- 3 user users 4.0K Sep  1 23:43 test1
    drwxr-x--- 2 user users 4.0K Sep  1 23:43 test2
    
    -sh-4.1$ ls -lh test1 test2
    test1:
    total 4.0K
    drwxr-x--- 2 user users 4.0K Sep  1 23:43 test3
    
    test2:
    total 0
    -rw-r----- 1 user users 0 Sep  1 23:43 file2.txt
    
    -sh-4.1$ ls -lh test1/test3/
    total 0
    -rw-r----- 1 user users 0 Sep  1 23:43 file3.txt
    -sh-4.1$