> class CompressFileProducer(ftpserver.FileProducer):
> def more(self):
> chunk = ftpserver.FileProducer.more(self)
[snip]
> ftpserver.FileProducer = CompressFileProducer
Doing this creates an infinite loop - the easiest way to fix it is to
store away a reference to the old class before monkey-patching it.
e.g. change:
ftpserver.FileProducer = CompressFileProducer
to:
CompressFileProducer._baseProducer = ftpserver.FileProducer
ftpserver.FileProducer = CompressFileProducer
and then change:
chunk = ftpserver.FileProducer.more(self)
to:
chunk = CompressFileProducer._baseProducer.more(self)
However doing that then throws up other errors, because you're trying
to use GzipFile in a streaming fashion (pyftpdlib reads and transmits
files chunk-by-chunk, instead of reading the whole file at once), and
python's gzip module unfortunately doesn't support streaming:
http://www.google.co.uk/search?q=python+stream+gzip
However the gzip module is in turn based on the zlib module, and the
zlib module *does* support streaming, so it should be possible to poke
around in the gzip.py sourcecode and pull out enough functionallity to
get streaming gzip compression working.
Or you might be abe to get
https://fedorahosted.org/spacewalk/browser/projects/python-gzipstream
working (but there's no documentation, so dunno if it supports
streaming compression or only streaming decompression).
Andrew
OMG! I tried playing around with the code from
http://stackoverflow.com/questions/2192529/python-creating-a-streaming-gzipd-file-like
and I actually got it working! :-)
So then I spent a while tidying it up and making it neater. Here's the
new version:
from pyftpdlib import ftpserver
import gzip
import os
class CompressedFileProducer(object):
"""Producer wrapper for transparently gzip-compressed file[-like]
objects."""
read_buffer_size = send_buffer_size = 65536
class StreamBuffer(object):
"""A file-like object for streaming writes."""
def __init__(self, chunksize=-1):
self.buffer = ''
self.chunksize = chunksize
def isfull(self):
if self.chunksize < 0:
return len(self.buffer) > 0
else:
return len(self.buffer) >= self.chunksize
def isempty(self):
return len(self.buffer) == 0
def readchunk(self):
if self.chunksize < 0:
ret, self.buffer = self.buffer, ''
else:
ret, self.buffer = self.buffer[:self.chunksize],
self.buffer[self.chunksize:]
return ret
def write(self, data):
self.buffer += data
def flush(self):
pass
def close(self):
pass
def __init__(self, file, type):
"""Initialize the producer and check the TYPE.
- (file) file: the file[-like] object.
- (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
"""
self.done = False
self.file = file
self.stream = CompressedFileProducer.StreamBuffer(self.send_buffer_size)
if type == 'i':
self.zipper =
gzip.GzipFile(filename=os.path.basename(file.name), mode='wb',
fileobj=self.stream, mtime=os.fstat(file.fileno()).st_mtime)
else:
raise TypeError("unsupported type")
def more(self):
"""Attempt to send a chunk of data of at least size
self.send_buffer_size."""
if self.stream.isfull():
return self.stream.readchunk()
if self.done:
if not self.stream.isempty():
return self.stream.readchunk()
else:
return ''
while not self.done:
try:
data = self.file.read(self.read_buffer_size)
except OSError, err:
raise _FileReadWriteError(err)
if data:
self.zipper.write(data)
self.zipper.flush()
if self.stream.isfull():
break
else:
self.done = True
if not self.file.closed:
self.file.close()
self.zipper.close()
return self.stream.readchunk()
def main():
ftpserver.FileProducer = CompressedFileProducer
authorizer = ftpserver.DummyAuthorizer()
authorizer.add_user('user', password="123", homedir='/home/user',
perm='elrmafdw')
authorizer.add_anonymous(homedir='/tmp')
handler = ftpserver.FTPHandler
handler.authorizer = authorizer
address = ('127.0.0.1', 21)
server = ftpserver.FTPServer(address, handler)
server.serve_forever()
if __name__ == '__main__':
main()
Seems to work well for me, but it would be nice if other people could test it.
Giampaolo: Is this code "good enough" to be added to the
pyftpdlib/contrib/ directory? I'm quite pleased with how it worked
out, but any constructive criticism would be welcome.
Andrew
Fair enough. I just thought it might be a nice example of a custom
FileProducer. But of course it's your call.
> which (I guess) also requires a customized client on the
> other end which is able to decompress the stream.
Nah, you can download the files with any regular client, and then
expand them with any gzip program. Although you may need to use the -c
option to stop gzip complaining about "unknown suffix". And I'm
definitely not gonna add on-the-fly filename-changing to pyftpdlib ;)
Andrew
Just a minor comment - there's actually a newer version of that draft RFC
http://tools.ietf.org/html/draft-preston-ftpext-deflate-04
Andrew
I'm afraid that's not the way the FTP protocol works... when you type
"get file.txt" in your FTP client, your FTP client actually sets up a
data connection to the server, and then sends a "RETR file.txt"
command. So "get file.txt lzma" doesn't make any sense.
I think the way to do what you're trying to do, would be to set up a
custom OPTS command which allows you to turn lzma-compression on or
off, and then just use regular get requests.
If you havn't already done so, I reccomend reading about the MODE Z
that Giampaolo already mentioned
http://tools.ietf.org/html/draft-preston-ftpext-deflate-04
to get an idea of how this sort of thing works.
Andrew