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?
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.