1
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
3
# Copyright 2011 Carlos Abalde <carlos.abalde@gmail.com>
5
# This file is part of duplicity.
7
# Duplicity is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by the
9
# Free Software Foundation; either version 2 of the License, or (at your
10
# option) any later version.
12
# Duplicity is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
# General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with duplicity; if not, write to the Free Software Foundation,
19
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25
import duplicity.backend
26
from duplicity.backend import retry
27
from duplicity import log
28
from duplicity.errors import * #@UnusedWildImport
30
class GDocsBackend(duplicity.backend.Backend):
31
"""Connect to remote store using Google Google Documents List API"""
33
ROOT_FOLDER_ID = 'folder%3Aroot'
34
BACKUP_DOCUMENT_TYPE = 'application/binary'
36
def __init__(self, parsed_url):
37
duplicity.backend.Backend.__init__(self, parsed_url)
39
# Import Google Data APIs libraries.
45
import gdata.docs.client
46
import gdata.docs.data
48
raise BackendException('Google Docs backend requires Google Data APIs Python '
49
'Client Library (see http://code.google.com/p/gdata-python-client/).')
51
# Setup client instance.
52
self.client = gdata.docs.client.DocsClient(source='duplicity $version')
53
self.client.ssl = True
54
self.client.http_client.debug = False
55
self.__authorize(parsed_url.username + '@' + parsed_url.hostname, self.get_password())
57
# Fetch destination folder entry (and crete hierarchy if required).
58
folder_names = string.split(parsed_url.path[1:], '/')
60
parent_folder_id = GDocsBackend.ROOT_FOLDER_ID
61
for folder_name in folder_names:
62
entries = self.__fetch_entries(parent_folder_id, 'folder', folder_name)
63
if entries is not None:
65
parent_folder = entries[0]
66
elif len(entries) == 0:
67
parent_folder = self.client.create(gdata.docs.data.FOLDER_LABEL, folder_name, parent_folder)
71
parent_folder_id = parent_folder.resource_id.text
73
raise BackendException("Error while creating destination folder '%s'." % folder_name)
75
raise BackendException("Error while fetching destination folder '%s'." % folder_name)
76
self.folder = parent_folder
79
def put(self, source_path, remote_filename=None, raise_errors = False):
80
"""Transfer source_path to remote_filename"""
81
# Default remote file name.
82
if not remote_filename:
83
remote_filename = source_path.get_filename()
87
# If remote file already exists in destination folder, remove it.
88
entries = self.__fetch_entries(self.folder.resource_id.text,
89
GDocsBackend.BACKUP_DOCUMENT_TYPE,
92
self.client.delete(entry.get_edit_link().href + '?delete=true', force=True)
94
# Set uploader instance. Note that resumable uploads are required in order to
95
# enable uploads for all file types.
96
# (see http://googleappsdeveloper.blogspot.com/2011/05/upload-all-file-types-to-any-google.html)
97
file = source_path.open()
98
uploader = gdata.client.ResumableUploader(
99
self.client, file, GDocsBackend.BACKUP_DOCUMENT_TYPE, os.path.getsize(file.name),
100
chunk_size=gdata.client.ResumableUploader.DEFAULT_CHUNK_SIZE,
101
desired_class=gdata.docs.data.DocsEntry)
104
entry = gdata.docs.data.DocsEntry(title = atom.data.Title(text = remote_filename))
105
uri = '/feeds/upload/create-session/default/private/full?convert=false'
106
entry = uploader.UploadFile(uri, entry = entry)
108
# Move to destination folder.
109
# TODO: any ideas on how to avoid this step?
110
if self.client.Move(entry, self.folder):
111
assert not file.close()
114
self.__handle_error("Failed to move uploaded file '%s' to destination remote folder '%s'"
115
% (source_path.get_filename(), self.folder.title.text), raise_errors)
117
self.__handle_error("Failed to upload file '%s' to remote folder '%s'"
118
% (source_path.get_filename(), self.folder.title.text), raise_errors)
120
self.__handle_error("Failed to initialize upload of file '%s' to remote folder '%s'"
121
% (source_path.get_filename(), self.folder.title.text), raise_errors)
122
assert not file.close()
124
self.__handle_error("Failed to upload file '%s' to remote folder '%s': %s"
125
% (source_path.get_filename(), self.folder.title.text, str(e)), raise_errors)
128
def get(self, remote_filename, local_path, raise_errors = False):
129
"""Get remote filename, saving it to local_path"""
131
entries = self.__fetch_entries(self.folder.resource_id.text,
132
GDocsBackend.BACKUP_DOCUMENT_TYPE,
134
if len(entries) == 1:
136
self.client.Download(entry, local_path.name)
140
self.__handle_error("Failed to find file '%s' in remote folder '%s'"
141
% (remote_filename, self.folder.title.text), raise_errors)
143
self.__handle_error("Failed to download file '%s' in remote folder '%s': %s"
144
% (remote_filename, self.folder.title.text, str(e)), raise_errors)
147
def list(self, raise_errors = False):
148
"""List files in folder"""
150
entries = self.__fetch_entries(self.folder.resource_id.text,
151
GDocsBackend.BACKUP_DOCUMENT_TYPE)
152
return [entry.title.text for entry in entries]
154
self.__handle_error("Failed to fetch list of files in remote folder '%s': %s"
155
% (self.folder.title.text, str(e)), raise_errors)
158
def delete(self, filename_list, raise_errors = False):
159
"""Delete files in filename_list"""
160
for filename in filename_list:
162
entries = self.__fetch_entries(self.folder.resource_id.text,
163
GDocsBackend.BACKUP_DOCUMENT_TYPE,
167
for entry in entries:
168
if not self.client.delete(entry.get_edit_link().href + '?delete=true', force = True):
171
self.__handle_error("Failed to remove file '%s' in remote folder '%s'"
172
% (filename, self.folder.title.text), raise_errors)
174
log.Warn("Failed to fetch file '%s' in remote folder '%s'"
175
% (filename, self.folder.title.text))
177
self.__handle_error("Failed to remove file '%s' in remote folder '%s': %s"
178
% (filename, self.folder.title.text, str(e)), raise_errors)
180
def __handle_error(self, message, raise_errors = True):
182
raise BackendException(message)
184
log.FatalError(message, log.ErrorCode.backend_error)
186
def __authorize(self, email, password, captcha_token = None, captcha_response = None):
188
self.client.client_login(email,
190
source = 'duplicity $version',
192
captcha_token = captcha_token,
193
captcha_response = captcha_response)
194
except gdata.client.CaptchaChallenge, challenge:
195
print('A captcha challenge in required. Please visit ' + challenge.captcha_url)
198
answer = raw_input('Answer to the challenge? ')
199
self.__authorize(email, password, challenge.captcha_token, answer)
200
except gdata.client.BadAuthentication:
201
self.__handle_error('Invalid user credentials given. Be aware that accounts '
202
'that use 2-step verification require creating an application specific '
203
'access code for using this Duplicity backend. Follow the instrucction in '
204
'http://www.google.com/support/accounts/bin/static.py?page=guide.cs&guide=1056283&topic=1056286 '
205
'and create your application-specific password to run duplicity backups.')
207
self.__handle_error('Error while authenticating client: %s.' % str(e))
209
def __fetch_entries(self, folder_id, type, title = None):
211
uri = '/feeds/default/private/full/%s/contents' % folder_id
213
uri += '/-/folder?showfolders=true'
214
elif type == GDocsBackend.BACKUP_DOCUMENT_TYPE:
215
uri += '?showfolders=false'
217
uri += '?showfolders=true'
219
uri += '&title=' + urllib.quote(title) + '&title-exact=true'
223
entries = self.client.get_everything(uri = uri)
225
# When filtering by entry title, API is returning (don't know why) documents in other
226
# folders (apart from folder_id) matching the title, so some extra filtering is required.
229
for entry in entries:
230
if (not type) or (entry.get_document_type() == type):
231
if folder_id != GDocsBackend.ROOT_FOLDER_ID:
232
for link in entry.in_folders():
233
folder_entry = self.client.get_entry(link.href, None, None,
234
desired_class=gdata.docs.data.DocsEntry)
235
if folder_entry and (folder_entry.resource_id.text == folder_id):
237
elif len(entry.in_folders()) == 0:
245
self.__handle_error('Error while fetching remote entries: %s.' % str(e))
247
duplicity.backend.register_backend('gdocs', GDocsBackend)