Have FTP backend reconnect on timeout.
[cumulus.git] / python / cumulus / store / ftp.py
1
2 from ftplib        import FTP, all_errors, error_temp
3 from netrc         import netrc, NetrcParseError
4 from cumulus.store import Store, type_patterns, NotFoundError
5
6 class FtpStore (Store):
7     def __init__ (self, url, **kw):
8         self.synced = True
9         try:
10             upw, hp = self.netloc.split ('@')
11         except ValueError:
12             hp = self.netloc
13             upw = 'anonymous'
14         try:
15             host, port = hp.split (':')
16             port = int (port, 10)
17         except ValueError:
18             host = hp
19             port = 21
20         try:
21             user, passwd = upw.split (':')
22         except ValueError:
23             user = upw
24             passwd = None
25             try:
26                 n = netrc ()
27                 try:
28                     user, acct, passwd = n.authenticators (host)
29                 except ValueError:
30                     pass
31             except (IOError, NetrcParseError):
32                 pass
33         self.host   = host
34         self.port   = port
35         self.user   = user
36         self.passwd = passwd
37         self.prefix = self.path [1:] # skip *only* first '/'
38         self.ftp    = FTP ()
39         self.connect ()
40
41     def _get_path (self, type, name):
42         # we are in right directory
43         return name
44
45     def connect (self) :
46         self.ftp.connect (self.host, self.port)
47         self.ftp.login (self.user, self.passwd)
48         self.ftp.cwd (self.prefix)
49     # end def connect
50
51     def list (self, type):
52         self.sync ()
53         files = self.ftp.nlst ()
54         return (f for f in files if type_patterns[type].match (f))
55
56     def get (self, type, name):
57         self.sync ()
58         self.ftp.sendcmd ('TYPE I')
59         sock = self.ftp.transfercmd ('RETR %s' % self._get_path (type, name))
60         self.synced = False
61         return sock.makefile ()
62
63     def put (self, type, name, fp):
64         self.sync ()
65         self.ftp.storbinary ("STOR %s" % self._get_path (type, name), fp)
66
67     def delete (self, type, name):
68         self.sync ()
69         self.ftp.delete (self._get_path (type, name))
70
71     def stat (self, type, name):
72         """ Note that the size-command is non-standard but supported by
73         most ftp servers today. If size returns an error condition we
74         try nlst to detect if the file exists and return an bogus length
75         """
76         self.sync ()
77         fn = self._get_path (type, name)
78         size = None
79         try:
80             # my client doesn't accept size in ascii-mode
81             self.ftp.sendcmd ('TYPE I')
82             size = self.ftp.size (fn)
83             self.ftp.sendcmd ('TYPE A')
84         except all_errors, err:
85             print err
86             pass
87         if size is not None:
88             return {'size': size}
89         print "nlst: %s" % fn, size
90         l = self.ftp.nlst (fn)
91         if l:
92             return {'size': 42}
93         raise NotFoundError, (type, name)
94
95     def sync (self):
96         """ After a get command at end of transfer a 2XX reply is still
97         in the input-queue, we have to get rid of that.
98         We also test here that the connection is still alive. If we get
99         a temporary error 421 ("error_temp") we reconnect: It was
100         probably a timeout.
101         """
102         try :
103             if not self.synced:
104                 self.ftp.voidresp()
105             self.ftp.sendcmd ('TYPE A')
106         except error_temp, err :
107             if not err.message.startswith ('421') :
108                 raise
109             self.connect ()
110         self.synced = True
111
112 Store = FtpStore