Package duplicity :: Package backends :: Module webdavbackend
[hide private]
[frames] | no frames]

Source Code for Module duplicity.backends.webdavbackend

  1  # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- 
  2  # 
  3  # Copyright 2002 Ben Escoto <ben@emerose.org> 
  4  # Copyright 2007 Kenneth Loafman <kenneth@loafman.com> 
  5  # 
  6  # This file is part of duplicity. 
  7  # 
  8  # Duplicity is free software; you can redistribute it and/or modify it 
  9  # under the terms of the GNU General Public License as published by the 
 10  # Free Software Foundation; either version 2 of the License, or (at your 
 11  # option) any later version. 
 12  # 
 13  # Duplicity is distributed in the hope that it will be useful, but 
 14  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 16  # General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with duplicity; if not, write to the Free Software Foundation, 
 20  # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 21   
 22  import base64 
 23  import httplib 
 24  import re 
 25  import urllib 
 26  import urllib2 
 27  import xml.dom.minidom 
 28   
 29  import duplicity.backend 
 30  from duplicity import globals 
 31  from duplicity import log 
 32  from duplicity.errors import * #@UnusedWildImport 
 33  from duplicity import urlparse_2_5 as urlparser 
 34   
35 -class CustomMethodRequest(urllib2.Request):
36 """ 37 This request subclass allows explicit specification of 38 the HTTP request method. Basic urllib2.Request class 39 chooses GET or POST depending on self.has_data() 40 """
41 - def __init__(self, method, *args, **kwargs):
42 self.method = method 43 urllib2.Request.__init__(self, *args, **kwargs)
44
45 - def get_method(self):
46 return self.method
47 48
49 -class WebDAVBackend(duplicity.backend.Backend):
50 """Backend for accessing a WebDAV repository. 51 52 webdav backend contributed in 2006 by Jesper Zedlitz <jesper@zedlitz.de> 53 """ 54 listbody = """\ 55 <?xml version="1.0" encoding="utf-8" ?> 56 <D:propfind xmlns:D="DAV:"> 57 <D:allprop/> 58 </D:propfind> 59 60 """ 61 62 """Connect to remote store using WebDAV Protocol"""
63 - def __init__(self, parsed_url):
64 duplicity.backend.Backend.__init__(self, parsed_url) 65 self.headers = {'Connection': 'keep-alive'} 66 self.parsed_url = parsed_url 67 self.digest_challenge = None 68 self.digest_auth_handler = None 69 70 if parsed_url.path: 71 foldpath = re.compile('/+') 72 self.directory = foldpath.sub('/', parsed_url.path + '/' ) 73 else: 74 self.directory = '/' 75 76 log.Info("Using WebDAV host %s" % (parsed_url.hostname,)) 77 log.Info("Using WebDAV port %s" % (parsed_url.port,)) 78 log.Info("Using WebDAV directory %s" % (self.directory,)) 79 log.Info("Using WebDAV protocol %s" % (globals.webdav_proto,)) 80 81 if parsed_url.scheme == 'webdav': 82 self.conn = httplib.HTTPConnection(parsed_url.hostname, parsed_url.port) 83 elif parsed_url.scheme == 'webdavs': 84 self.conn = httplib.HTTPSConnection(parsed_url.hostname, parsed_url.port) 85 else: 86 raise BackendException("Unknown URI scheme: %s" % (parsed_url.scheme))
87
88 - def _getText(self,nodelist):
89 rc = "" 90 for node in nodelist: 91 if node.nodeType == node.TEXT_NODE: 92 rc = rc + node.data 93 return rc
94
95 - def close(self):
96 self.conn.close()
97
98 - def request(self, method, path, data=None):
99 """ 100 Wraps the connection.request method to retry once if authentication is 101 required 102 """ 103 quoted_path = urllib.quote(path) 104 105 if self.digest_challenge is not None: 106 self.headers['Authorization'] = self.get_digest_authorization(path) 107 self.conn.request(method, quoted_path, data, self.headers) 108 response = self.conn.getresponse() 109 if response.status == 401: 110 response.close() 111 self.headers['Authorization'] = self.get_authorization(response, quoted_path) 112 self.conn.request(method, quoted_path, data, self.headers) 113 response = self.conn.getresponse() 114 115 return response
116
117 - def get_authorization(self, response, path):
118 """ 119 Fetches the auth header based on the requested method (basic or digest) 120 """ 121 try: 122 auth_hdr = response.getheader('www-authenticate', '') 123 token, challenge = auth_hdr.split(' ', 1) 124 except ValueError: 125 return None 126 if token.lower() == 'basic': 127 return self.get_basic_authorization() 128 else: 129 self.digest_challenge = self.parse_digest_challenge(challenge) 130 return self.get_digest_authorization(path)
131
132 - def parse_digest_challenge(self, challenge_string):
133 return urllib2.parse_keqv_list(urllib2.parse_http_list(challenge_string))
134
135 - def get_basic_authorization(self):
136 """ 137 Returns the basic auth header 138 """ 139 auth_string = '%s:%s' % (self.parsed_url.username, self.get_password()) 140 return 'Basic %s' % base64.encodestring(auth_string).strip()
141
142 - def get_digest_authorization(self, path):
143 """ 144 Returns the digest auth header 145 """ 146 u = self.parsed_url 147 if self.digest_auth_handler is None: 148 pw_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() 149 pw_manager.add_password(None, self.conn.host, u.username, self.get_password()) 150 self.digest_auth_handler = urllib2.HTTPDigestAuthHandler(pw_manager) 151 152 # building a dummy request that gets never sent, 153 # needed for call to auth_handler.get_authorization 154 scheme = u.scheme == 'webdavs' and 'https' or 'http' 155 hostname = u.port and "%s:%s" % (u.hostname, u.port) or u.hostname 156 dummy_url = "%s://%s%s" % (scheme, hostname, path) 157 dummy_req = CustomMethodRequest(self.conn._method, dummy_url) 158 auth_string = self.digest_auth_handler.get_authorization(dummy_req, self.digest_challenge) 159 return 'Digest %s' % auth_string
160
161 - def list(self):
162 """List files in directory""" 163 for n in range(1, globals.num_retries+1): 164 log.Info("Listing directory %s on WebDAV server" % (self.directory,)) 165 self.headers['Depth'] = "1" 166 response = self.request("PROPFIND", self.directory, self.listbody) 167 del self.headers['Depth'] 168 # if the target collection does not exist, create it. 169 if response.status == 404: 170 log.Info("Directory '%s' being created." % self.directory) 171 res = self.request("MKCOL", self.directory) 172 log.Info("WebDAV MKCOL status: %s %s" % (res.status, res.reason)) 173 continue 174 if response.status == 207: 175 document = response.read() 176 break 177 log.Info("WebDAV PROPFIND attempt #%d failed: %s %s" % (n, response.status, response.reason)) 178 if n == globals.num_retries +1: 179 log.Warn("WebDAV backend giving up after %d attempts to PROPFIND %s" % (globals.num_retries, self.directory)) 180 raise BackendException((response.status, response.reason)) 181 182 log.Info("%s" % (document,)) 183 dom = xml.dom.minidom.parseString(document) 184 result = [] 185 for href in dom.getElementsByTagName('d:href') + dom.getElementsByTagName('D:href'): 186 filename = self.__taste_href(href) 187 if filename: 188 result.append(filename) 189 return result
190
191 - def __taste_href(self, href):
192 """ 193 Internal helper to taste the given href node and, if 194 it is a duplicity file, collect it as a result file. 195 196 @return: A matching filename, or None if the href did not match. 197 """ 198 raw_filename = self._getText(href.childNodes).strip() 199 parsed_url = urlparser.urlparse(urllib.unquote(raw_filename)) 200 filename = parsed_url.path 201 log.Debug("webdav path decoding and translation: " 202 "%s -> %s" % (raw_filename, filename)) 203 204 # at least one WebDAV server returns files in the form 205 # of full URL:s. this may or may not be 206 # according to the standard, but regardless we 207 # feel we want to bail out if the hostname 208 # does not match until someone has looked into 209 # what the WebDAV protocol mandages. 210 if not parsed_url.hostname is None \ 211 and not (parsed_url.hostname == self.parsed_url.hostname): 212 m = "Received filename was in the form of a "\ 213 "full url, but the hostname (%s) did "\ 214 "not match that of the webdav backend "\ 215 "url (%s) - aborting as a conservative "\ 216 "safety measure. If this happens to you, "\ 217 "please report the problem"\ 218 "" % (parsed_url.hostname, 219 self.parsed_url.hostname) 220 raise BackendException(m) 221 222 if filename.startswith(self.directory): 223 filename = filename.replace(self.directory,'',1) 224 return filename 225 else: 226 return None
227
228 - def get(self, remote_filename, local_path):
229 """Get remote filename, saving it to local_path""" 230 url = self.directory + remote_filename 231 target_file = local_path.open("wb") 232 for n in range(1, globals.num_retries+1): 233 log.Info("Retrieving %s from WebDAV server" % (url ,)) 234 response = self.request("GET", url) 235 if response.status == 200: 236 target_file.write(response.read()) 237 assert not target_file.close() 238 local_path.setdata() 239 return 240 log.Info("WebDAV GET attempt #%d failed: %s %s" % (n, response.status, response.reason)) 241 log.Warn("WebDAV backend giving up after %d attempts to GET %s" % (globals.num_retries, url)) 242 raise BackendException((response.status, response.reason))
243
244 - def put(self, source_path, remote_filename = None):
245 """Transfer source_path to remote_filename""" 246 if not remote_filename: 247 remote_filename = source_path.get_filename() 248 url = self.directory + remote_filename 249 source_file = source_path.open("rb") 250 for n in range(1, globals.num_retries+1): 251 log.Info("Saving %s on WebDAV server" % (url ,)) 252 response = self.request("PUT", url, source_file.read()) 253 if response.status == 201: 254 response.read() 255 assert not source_file.close() 256 return 257 log.Info("WebDAV PUT attempt #%d failed: %s %s" % (n, response.status, response.reason)) 258 log.Warn("WebDAV backend giving up after %d attempts to PUT %s" % (globals.num_retries, url)) 259 raise BackendException((response.status, response.reason))
260
261 - def delete(self, filename_list):
262 """Delete files in filename_list""" 263 for filename in filename_list: 264 url = self.directory + filename 265 for n in range(1, globals.num_retries+1): 266 log.Info("Deleting %s from WebDAV server" % (url ,)) 267 response = self.request("DELETE", url) 268 if response.status == 204: 269 response.read() 270 break 271 log.Info("WebDAV DELETE attempt #%d failed: %s %s" % (n, response.status, response.reason)) 272 if n == globals.num_retries +1: 273 log.Warn("WebDAV backend giving up after %d attempts to DELETE %s" % (globals.num_retries, url)) 274 raise BackendException((response.status, response.reason))
275 276 duplicity.backend.register_backend("webdav", WebDAVBackend) 277 duplicity.backend.register_backend("webdavs", WebDAVBackend) 278