# -*- 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)