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