Search code examples
pythonpython-3.xfile-writingftplib

Python ftplib: open file in write mode?


How can I open file on FTP server in write mode? I know I can write/create file directly (when I have data), but I want to first open it for writing and only then write it as you would do locally using contextmanager.

The reasoning is, I want to create interface that would have unified methods to work with transfer protocol servers. Specifically SFTP and FTP.

So with SFTP its easy (using paramiko):

def open(sftp, fname, mode='r'):
    return sftp.open(fname, mode=mode)

Now I can do this:

with open(sftp, 'some_file.txt', 'w') as f:
    f.write(data)

And then I can read what was written

with open(sftp, 'some_file.txt', 'r') as f:
    print(f.read().decode('utf-8'))

How can I do the same implementation for FTP (using ftplib)?

Reading part for FTP, I was able to implement and I can open file in read mode just like with SFTP. But how can I open it in write mode? ftplib method storbinary asks for data to be provided "immediately". I mean I should already pass data I want to write via open method (but then it would defeat unified method purpose)?

import io

def open(ftp, filename, mode='r'):
    """Open a file on FTP server."""
    def handle_buffer(buffer_data):
        bio.write(buffer_data)

    # Reading implementation
    if mode == 'r':
        bio = io.BytesIO()
        ftp.retrbinary(
            'RETR %s' % filename, callback=handle_buffer)
        bio.seek(0)
        return bio
    # Writing implementation.
    if mode == 'w':
        # how to open in write mode?

update

Let say we have immediate writing implementation in FTP:

bio = io.BytesIO
# Write some data
data = csv.writer(bio)
data.writerows(data_to_export)
bio.seek(0)
# Store. So it looks like storbinary does not open file in w mode, it does everything in one go?
ftp.storbinary("STOR " + file_name, sio)

So the question is how can I separate writing data from just opening file in write mode. Is it even possible with ftplib?


Solution

  • So after some struggle, I was able to make this work. Solution was to implement custom contextmanagers for open method when in read (had to reimplement read mode, because it was only working with plain file reading, but was failing if let say I would try to use csv reader) mode and when in write mode.

    For read mode, I chose to use tempfile, because using other approaches, I was not able to properly read data using different readers (plain file reader, csv reader etc.). Though when using opened tempfile in read mode, everything works as expected.

    For write mode, I was able to utilize memory buffer -> io.BytesIO. So for writing it was not necessary to use tempfile.

    import tempfile
    
    
    class OpenRead(object):
    
        def _open_tempfile(self):
            self.tfile = tempfile.NamedTemporaryFile()
            # Write data on tempfile.
            self.ftp.retrbinary(
                'RETR %s' % self.filename, self.tfile.write)
            # Get back to start of file, so it would be possible to
            # read it.
            self.tfile.seek(0)
            return open(self.tfile.name, 'r')
    
        def __init__(self, ftp, filename):
            self.ftp = ftp
            self.filename = filename
            self.tfile = None
    
        def __enter__(self):
            return self._open_tempfile()
    
        def __exit__(self, exception_type, exception_value, traceback):
            # Remove temporary file.
            self.tfile.close()
    
    class OpenWrite(object):
        def __init__(self, ftp, filename):
            self.ftp = ftp
            self.filename = filename
            self.data = ''
    
        def __enter__(self):
            return self
    
        def __exit__(self, exception_type, exception_value, traceback):
            bio = io.BytesIO()
            if isinstance(self.data, six.string_types):
                self.data = self.data.encode()
            bio.write(self.data)
            bio.seek(0)
            res = self.ftp.storbinary('STOR %s' % self.filename, bio)
            bio.close()
            return res
    
        def write(self, data):
            self.data += data
    
    def open(ftp, filename, mode='r'):
        """Open a file on FTP server."""
        if mode == 'r':
            return OpenRead(ftp, filename)
        if mode == 'w':
            return OpenWrite(ftp, filename)
    

    P.S. this might not work properly without context manager, but for now it is OK solution to me. If anyone has better implementation, they are more than welcome to share it.

    Update

    Decided to use ftputil package instead of standard ftplib. So all this hacking is not needed, because ftputil takes care of it and it actually uses many same named methods as paramiko, that do same thing, so it is much easier to unify protocols usage.