Implement FTP backend and other code cleanups.
[cumulus.git] / python / cumulus / store / ftp.py
1
2 from ftplib        import FTP, all_errors
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.ftp = FTP ()
34         self.ftp.connect (host, port)
35         self.ftp.login (user, passwd)
36         self.prefix = self.path [1:] # skip *only* first '/'
37         self.ftp.cwd (self.prefix)
38
39     def _get_path (self, type, name):
40         # we are in right directory
41         return name
42
43     def list (self, type):
44         self.sync ()
45         files = self.ftp.nlst ()
46         return (f for f in files if type_patterns[type].match (f))
47
48     def get (self, type, name):
49         self.sync ()
50         sock = self.ftp.transfercmd ('RETR %s' % self._get_path (type, name))
51         self.synced = False
52         return sock.makefile ()
53
54     def put (self, type, name, fp):
55         self.sync ()
56         self.ftp.storbinary ("STOR %s" % self._get_path (type, name), fp)
57
58     def delete (self, type, name):
59         self.sync ()
60         self.ftp.delete (self._get_path (type, name))
61
62     def stat (self, type, name):
63         """ Note that the size-command is non-standard but supported by
64         most ftp servers today. If size returns an error condition we
65         try nlst to detect if the file exists and return an bogus length
66         """
67         self.sync ()
68         fn = self._get_path (type, name)
69         size = None
70         try:
71             # my client doesn't accept size in ascii-mode
72             self.ftp.sendcmd ('TYPE I')
73             size = self.ftp.size (fn)
74             self.ftp.sendcmd ('TYPE A')
75         except all_errors, err:
76             print err
77             pass
78         if size is not None:
79             return {'size': size}
80         print "nlst: %s" % fn, size
81         l = self.ftp.nlst (fn)
82         if l:
83             return {'size': 42}
84         raise NotFoundError, (type, name)
85
86     def sync (self):
87         """ After a get command at end of transfer a 2XX reply is still
88         in the input-queue, we have to get rid of that
89         """
90         if not self.synced:
91             self.ftp.voidresp()
92         self.synced = True
93
94 Store = FtpStore