Update copyright notices to use a central AUTHORS file.
[cumulus.git] / python / cumulus / store / ftp.py
1 # Cumulus: Efficient Filesystem Backup to the Cloud
2 # Copyright (C) 2009 The Cumulus Developers
3 # See the AUTHORS file for a list of contributors.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 from ftplib        import FTP, all_errors, error_temp
20 from netrc         import netrc, NetrcParseError
21 from cumulus.store import Store, type_patterns, NotFoundError
22
23 class FtpStore (Store):
24     def __init__ (self, url, **kw):
25         self.synced = True
26         try:
27             upw, hp = self.netloc.split ('@')
28         except ValueError:
29             hp = self.netloc
30             upw = 'anonymous'
31         try:
32             host, port = hp.split (':')
33             port = int (port, 10)
34         except ValueError:
35             host = hp
36             port = 21
37         try:
38             user, passwd = upw.split (':')
39         except ValueError:
40             user = upw
41             passwd = None
42             try:
43                 n = netrc ()
44                 try:
45                     user, acct, passwd = n.authenticators (host)
46                 except ValueError:
47                     pass
48             except (IOError, NetrcParseError):
49                 pass
50         self.host   = host
51         self.port   = port
52         self.user   = user
53         self.passwd = passwd
54         self.prefix = self.path [1:] # skip *only* first '/'
55         self.ftp    = FTP ()
56         self.connect ()
57
58     def _get_path (self, type, name):
59         # we are in right directory
60         return name
61
62     def connect (self) :
63         self.ftp.connect (self.host, self.port)
64         self.ftp.login (self.user, self.passwd)
65         self.ftp.cwd (self.prefix)
66     # end def connect
67
68     def list (self, type):
69         self.sync ()
70         files = self.ftp.nlst ()
71         return (f for f in files if type_patterns[type].match (f))
72
73     def get (self, type, name):
74         self.sync ()
75         self.ftp.sendcmd ('TYPE I')
76         sock = self.ftp.transfercmd ('RETR %s' % self._get_path (type, name))
77         self.synced = False
78         return sock.makefile ()
79
80     def put (self, type, name, fp):
81         self.sync ()
82         self.ftp.storbinary ("STOR %s" % self._get_path (type, name), fp)
83
84     def delete (self, type, name):
85         self.sync ()
86         self.ftp.delete (self._get_path (type, name))
87
88     def stat (self, type, name):
89         """ Note that the size-command is non-standard but supported by
90         most ftp servers today. If size returns an error condition we
91         try nlst to detect if the file exists and return an bogus length
92         """
93         self.sync ()
94         fn = self._get_path (type, name)
95         size = None
96         try:
97             # my client doesn't accept size in ascii-mode
98             self.ftp.sendcmd ('TYPE I')
99             size = self.ftp.size (fn)
100             self.ftp.sendcmd ('TYPE A')
101         except all_errors, err:
102             print err
103             pass
104         if size is not None:
105             return {'size': size}
106         print "nlst: %s" % fn, size
107         l = self.ftp.nlst (fn)
108         if l:
109             return {'size': 42}
110         raise NotFoundError, (type, name)
111
112     def sync (self):
113         """ After a get command at end of transfer a 2XX reply is still
114         in the input-queue, we have to get rid of that.
115         We also test here that the connection is still alive. If we get
116         a temporary error 421 ("error_temp") we reconnect: It was
117         probably a timeout.
118         """
119         try :
120             if not self.synced:
121                 self.ftp.voidresp()
122             self.ftp.sendcmd ('TYPE A')
123         except error_temp, err :
124             if not err.message.startswith ('421') :
125                 raise
126             self.connect ()
127         self.synced = True
128
129 Store = FtpStore