# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # # Copyright 2012 Paul Barker # Copyright 2011 Canonical Ltd # Authors: Michael Terry # # This file is part of duplicity. # # Duplicity is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. # # Duplicity is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with duplicity. If not, see . import os import json import urlparse import httplib import urllib import oauth2 import shutil import duplicity.backend from duplicity.backend import retry from duplicity.errors import BackendException, TemporaryLoadException from duplicity import log from duplicity import util def http_request(url, method, body='', headers={}): urlinfo = urlparse.urlparse(url) host = urlinfo.netloc path = urlinfo.path if urlinfo.query: path += '?' + urlinfo.query conn = httplib.HTTPSConnection(host, timeout=900) conn.request(method, path, body, headers) response = conn.getresponse() # Check for error if is_bad(response.status): return response.status, None return response.status, response class U1Backend(duplicity.backend.Backend): """ Backend for Ubuntu One, through the use of the ubuntone module and a REST API. See https://one.ubuntu.com/developer/ for REST documentation. """ def __init__(self, url): duplicity.backend.Backend.__init__(self, url) if self.parsed_url.scheme == 'u1+http': # Use the default Ubuntu One host self.parsed_url.hostname = "one.ubuntu.com" else: assert self.parsed_url.scheme == 'u1' # Store the supplied path as the remote volume to write to self.remote_volume = self.parsed_url.path.lstrip('/') self.api_base = "https://%s/api/file_storage/v1" % self.parsed_url.hostname # This next line *should* work, but isn't set up correctly server-side yet #self.content_base = self.api_base self.content_base = "https://files.%s" % self.parsed_url.hostname if not self.oauth_from_env(): log.FatalError(_("Could not obtain Ubuntu One credentials"), log.ErrorCode.backend_error) # Create volume in case it doesn't exist yet self.create_volume() def parse_error(self, response, ignore=None): from duplicity import log status = int(response.status) if status >= 200 and status < 300: return None if ignore and status in ignore: return None if status == 400: code = log.ErrorCode.backend_permission_denied elif status == 404: code = log.ErrorCode.backend_not_found elif status == 507: code = log.ErrorCode.backend_no_space else: code = log.ErrorCode.backend_error return code def handle_error(self, raise_error, op, response, file1=None, file2=None, ignore=None): code = self.parse_error(response, ignore) if code is None: return status = int(response.status) if file1: file1 = file1.encode("utf8") else: file1 = None if file2: file2 = file2.encode("utf8") else: file2 = None extra = ' '.join([util.escape(x) for x in [file1, file2] if x]) extra = ' '.join([op, extra]) msg = _("Got status code %s") % status if response.get_header('x-oops-id') is not None: msg += '\nOops-ID: %s' % response.get_header('x-oops-id') if response.get_header('content-type') == 'application/json': node = json.load(response) if node.get('error'): msg = node.get('error') if raise_error: if status == 503: raise TemporaryLoadException(msg) else: raise BackendException(msg) else: log.FatalError(msg, code, extra) def oauth_from_env(self): try: consumer_key = os.environ['U1_CONSUMER_KEY'] consumer_secret = os.environ['U1_CONSUMER_SECRET'] token = os.environ['U1_TOKEN'] token_secret = os.environ['U1_TOKEN_SECRET'] self.oauth_init(consumer_key, consumer_secret, token, token_secret) except: return False return True def resource_path(self, path, volume='Ubuntu One'): if not path[0] == '/': path = '/' + path vol = urllib.quote(volume) path = urllib.quote(path) return self.api_base + '/~/' + vol + path def volume_path(self, volume='Ubuntu One'): vol = urllib.quote(volume) return self.api_base + '/volumes/~/' + vol def content_path(self, path, volume='Ubuntu One'): if not path[0] == '/': path = '/' + path vol = urllib.quote(volume) path = urllib.quote(path) return self.content_base + '/content/~/' + vol + path def oauth_init(self, consumer_key, consumer_secret, token, token_secret): self.consumer = oauth2.Consumer(consumer_key, consumer_secret) self.token = oauth2.Token(token, token_secret) def oauth_request(self, url, method, body=''): oauth_request = oauth2.Request.from_consumer_and_token( self.consumer, self.token, method, url) oauth_request.sign_request( oauth2.SignatureMethod_PLAINTEXT(), self.consumer, self.token) headers = oauth_request.to_header() urlinfo = urlparse.urlparse(url) host = urlinfo.netloc path = urlinfo.path if urlinfo.query: path += '?' + urlinfo.query conn = httplib.HTTPSConnection(host) conn.request(method, path, body, headers) response = conn.getresponse() return response.status, response @retry def create_volume(self, raise_errors=False): url = self.volume_path(self.remote_volume) status, response = self.oauth_request(url, 'PUT') self.handle_error(raise_errors, 'put', response, url) @retry def put(self, source_path, remote_filename = None, raise_errors=False): if not remote_filename: remote_filename = source_path.get_filename() url = self.content_path(remote_filename, self.remote_volume) body = open(source_path.name, 'rb') if not hasattr(body, 'len'): # Special handling - we need to read the lot into memory body = body.read() status, response = self.oauth_request(url, 'PUT', body) self.handle_error(raise_errors, 'put', response, source_path.name, url) @retry def get(self, filename, local_path, raise_errors=False): """Get file and put in local_path (Path object)""" url = self.content_path(filename, self.remote_volume) status, response = self.oauth_request(url, 'GET') self.handle_error(raise_errors, 'get', response, url, filename) # Copy from response to local file dest = open(local_path.name, 'wb') shutil.copyfileobj(response, dest) dest.close() local_path.setdata() @retry def list(self, raise_errors=False): """List files in that directory""" path = '/' # TODO: Fix path url = self.resource_path(path, self.remote_volume) status, response = self.oauth_request(url, 'GET') self.handle_error(raise_errors, 'list', response, url) info = json.load(response) filelist = [] if info['kind'] == 'directory': if info['has_children']: tmp_path = info['path'] # Repeat request to get children url = self.resource_path(path, self.remote_volume) + '?include_children=true' status, response = self.oauth_request(url, 'GET') self.handle_error(raise_errors, 'list', response, url) parent = json.load(response) for info in parent['children']: filelist += [os.path.basename(info['path'])] else: filelist += [os.path.basename(info['path'])] return filelist @retry def delete(self, filename_list, raise_errors=False): """Delete all files in filename list""" for filename in filename_list: url = self.resource_path(filename, self.remote_volume) status, response = self.oauth_request(url, 'DELETE') self.handle_error(raise_errors, 'delete', response, url, ignore=[404]) @retry def _query_file_info(self, filename, raise_errors=False): """Query attributes on filename""" url = self.resource_path(filename, self.remote_volume) status, response = self.oauth_request(url, 'GET') code = self.parse_error(response) if code is not None: if code == log.ErrorCode.backend_not_found: return {'size': -1} elif raise_errors: self.handle_error(raise_errors, 'query', response, url, filename) else: return {'size': None} info = json.load(response) size = info['size'] return {'size': size} duplicity.backend.register_backend("u1", U1Backend) duplicity.backend.register_backend("u1+http", U1Backend)