1 from twisted.internet import reactor, defer
2 from twisted.internet.protocol import Protocol, ClientCreator
3 from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
5 from os import SEEK_END
7 # XXX: did I ever actually test supportPartial?
8 class FTPProgressDownloader(Protocol):
9 """Download to a file from FTP and keep track of progress."""
11 def __init__(self, host, port, path, fileOrName, username = 'anonymous', \
12 password = 'my@email.com', writeProgress = None, passive = True, \
13 supportPartial = False, *args, **kwargs):
19 self.resume = supportPartial
22 self.currentlength = 0
23 self.totallength = None
24 if writeProgress and type(writeProgress) is not list:
25 writeProgress = [ writeProgress ]
26 self.writeProgress = writeProgress
29 if isinstance(fileOrName, str):
30 self.filename = fileOrName
33 self.file = fileOrName
35 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
37 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
39 self.deferred = defer.Deferred()
41 def controlConnectionMade(self, ftpclient):
42 # We need the client locally
43 self.ftpclient = ftpclient
45 # Try to fetch filesize
49 def sizeRcvd(self, msgs):
51 code, msg = msgs[0].split()
53 self.totallength = int(msg)
54 for cb in self.writeProgress or [ ]:
55 cb(0, self.totallength)
57 # We know the size, so start fetching
60 # Error while reading size, try to list it
63 def ftpFetchSize(self):
64 d = self.ftpclient.queueStringCommand('SIZE ' + self.path)
65 d.addCallback(self.sizeRcvd).addErrback(self.ftpFetchList)
68 def listRcvd(self, *args):
69 # Quit if file not found
70 if not len(self.filelist.files):
71 self.connectionFailed()
74 self.totallength = self.filelist.files[0]['size']
75 for cb in self.writeProgress or [ ]:
76 cb(0, self.totallength)
81 # We know the size, so start fetching
84 def ftpFetchList(self, *args, **kwargs):
85 self.filelist = FTPFileListProtocol()
86 d = self.ftpclient.list(self.path, self.filelist)
87 d.addCallback(self.listRcvd).addErrback(self.connectionFailed)
91 file = open(self.filename, 'ab')
93 file = open(self.filename, 'wb')
95 return (file, file.tell())
97 def ftpFetchFile(self):
101 if self.file is None:
103 self.file, offset = self.openFile()
105 # TODO: handle exception
108 offset = self.resume and offset or 0
110 d = self.ftpclient.retrieveFile(self.path, self, offset = offset)
111 d.addCallback(self.ftpFinish).addErrback(self.connectionFailed)
113 def dataReceived(self, data):
117 if self.writeProgress:
118 self.currentlength += len(data)
119 for cb in self.writeProgress:
120 cb(self.currentlength, self.totallength)
122 # XXX: why did i always seek? do we really need this?
124 self.file.seek(0, SEEK_END)
126 self.file.write(data)
128 # TODO: handle exception
132 def ftpFinish(self, code = 0, message = None):
133 self.ftpclient.quit()
134 if self.file is not None:
136 self.deferred.callback(code)
138 def connectionFailed(self, reason = None):
139 if self.file is not None:
141 self.deferred.errback(reason)