77
75
raise BackendException("Error while fetching destination folder '%s'." % folder_name)
78
76
self.folder = parent_folder
81
def put(self, source_path, remote_filename=None, raise_errors=False):
82
"""Transfer source_path to remote_filename"""
83
# Default remote file name.
84
if not remote_filename:
85
remote_filename = source_path.get_filename()
89
# If remote file already exists in destination folder, remove it.
90
entries = self.__fetch_entries(self.folder.resource_id.text,
91
GDocsBackend.BACKUP_DOCUMENT_TYPE,
94
self.client.delete(entry.get_edit_link().href + '?delete=true', force=True)
96
# Set uploader instance. Note that resumable uploads are required in order to
97
# enable uploads for all file types.
98
# (see http://googleappsdeveloper.blogspot.com/2011/05/upload-all-file-types-to-any-google.html)
99
file = source_path.open()
100
uploader = gdata.client.ResumableUploader(
101
self.client, file, GDocsBackend.BACKUP_DOCUMENT_TYPE, os.path.getsize(file.name),
102
chunk_size=gdata.client.ResumableUploader.DEFAULT_CHUNK_SIZE,
103
desired_class=gdata.docs.data.Resource)
106
entry = gdata.docs.data.Resource(title=atom.data.Title(text=remote_filename))
107
uri = self.folder.get_resumable_create_media_link().href + '?convert=false'
108
entry = uploader.UploadFile(uri, entry=entry)
110
self.__handle_error("Failed to upload file '%s' to remote folder '%s'"
111
% (source_path.get_filename(), self.folder.title.text), raise_errors)
113
self.__handle_error("Failed to initialize upload of file '%s' to remote folder '%s'"
114
% (source_path.get_filename(), self.folder.title.text), raise_errors)
115
assert not file.close()
116
except Exception as e:
117
self.__handle_error("Failed to upload file '%s' to remote folder '%s': %s"
118
% (source_path.get_filename(), self.folder.title.text, str(e)), raise_errors)
121
def get(self, remote_filename, local_path, raise_errors=False):
122
"""Get remote filename, saving it to local_path"""
124
entries = self.__fetch_entries(self.folder.resource_id.text,
125
GDocsBackend.BACKUP_DOCUMENT_TYPE,
127
if len(entries) == 1:
129
self.client.DownloadResource(entry, local_path.name)
133
self.__handle_error("Failed to find file '%s' in remote folder '%s'"
134
% (remote_filename, self.folder.title.text), raise_errors)
135
except Exception as e:
136
self.__handle_error("Failed to download file '%s' in remote folder '%s': %s"
137
% (remote_filename, self.folder.title.text, str(e)), raise_errors)
140
def _list(self, raise_errors=False):
141
"""List files in folder"""
143
entries = self.__fetch_entries(self.folder.resource_id.text,
144
GDocsBackend.BACKUP_DOCUMENT_TYPE)
145
return [entry.title.text for entry in entries]
146
except Exception as e:
147
self.__handle_error("Failed to fetch list of files in remote folder '%s': %s"
148
% (self.folder.title.text, str(e)), raise_errors)
151
def delete(self, filename_list, raise_errors=False):
152
"""Delete files in filename_list"""
153
for filename in filename_list:
155
entries = self.__fetch_entries(self.folder.resource_id.text,
156
GDocsBackend.BACKUP_DOCUMENT_TYPE,
160
for entry in entries:
161
if not self.client.delete(entry.get_edit_link().href + '?delete=true', force=True):
164
self.__handle_error("Failed to remove file '%s' in remote folder '%s'"
165
% (filename, self.folder.title.text), raise_errors)
167
log.Warn("Failed to fetch file '%s' in remote folder '%s'"
168
% (filename, self.folder.title.text))
169
except Exception as e:
170
self.__handle_error("Failed to remove file '%s' in remote folder '%s': %s"
171
% (filename, self.folder.title.text, str(e)), raise_errors)
173
def __handle_error(self, message, raise_errors=True):
175
raise BackendException(message)
177
log.FatalError(message, log.ErrorCode.backend_error)
179
def __authorize(self, email, password, captcha_token=None, captcha_response=None):
78
def _put(self, source_path, remote_filename):
79
self._delete(remote_filename)
81
# Set uploader instance. Note that resumable uploads are required in order to
82
# enable uploads for all file types.
83
# (see http://googleappsdeveloper.blogspot.com/2011/05/upload-all-file-types-to-any-google.html)
84
file = source_path.open()
85
uploader = gdata.client.ResumableUploader(
86
self.client, file, GDocsBackend.BACKUP_DOCUMENT_TYPE, os.path.getsize(file.name),
87
chunk_size=gdata.client.ResumableUploader.DEFAULT_CHUNK_SIZE,
88
desired_class=gdata.docs.data.Resource)
91
entry = gdata.docs.data.Resource(title=atom.data.Title(text=remote_filename))
92
uri = self.folder.get_resumable_create_media_link().href + '?convert=false'
93
entry = uploader.UploadFile(uri, entry=entry)
95
raise BackendException("Failed to upload file '%s' to remote folder '%s'"
96
% (source_path.get_filename(), self.folder.title.text))
98
raise BackendException("Failed to initialize upload of file '%s' to remote folder '%s'"
99
% (source_path.get_filename(), self.folder.title.text))
100
assert not file.close()
102
def _get(self, remote_filename, local_path):
103
entries = self._fetch_entries(self.folder.resource_id.text,
104
GDocsBackend.BACKUP_DOCUMENT_TYPE,
106
if len(entries) == 1:
108
self.client.DownloadResource(entry, local_path.name)
110
raise BackendException("Failed to find file '%s' in remote folder '%s'"
111
% (remote_filename, self.folder.title.text))
114
entries = self._fetch_entries(self.folder.resource_id.text,
115
GDocsBackend.BACKUP_DOCUMENT_TYPE)
116
return [entry.title.text for entry in entries]
118
def _delete(self, filename):
119
entries = self._fetch_entries(self.folder.resource_id.text,
120
GDocsBackend.BACKUP_DOCUMENT_TYPE,
122
for entry in entries:
123
self.client.delete(entry.get_edit_link().href + '?delete=true', force=True)
125
def _authorize(self, email, password, captcha_token=None, captcha_response=None):
181
127
self.client.client_login(email,
190
136
while not answer:
191
137
answer = raw_input('Answer to the challenge? ')
192
self.__authorize(email, password, challenge.captcha_token, answer)
138
self._authorize(email, password, challenge.captcha_token, answer)
193
139
except gdata.client.BadAuthentication:
194
self.__handle_error('Invalid user credentials given. Be aware that accounts '
195
'that use 2-step verification require creating an application specific '
196
'access code for using this Duplicity backend. Follow the instrucction in '
197
'http://www.google.com/support/accounts/bin/static.py?page=guide.cs&guide=1056283&topic=1056286 '
198
'and create your application-specific password to run duplicity backups.')
199
except Exception as e:
200
self.__handle_error('Error while authenticating client: %s.' % str(e))
140
raise BackendException('Invalid user credentials given. Be aware that accounts '
141
'that use 2-step verification require creating an application specific '
142
'access code for using this Duplicity backend. Follow the instruction in '
143
'http://www.google.com/support/accounts/bin/static.py?page=guide.cs&guide=1056283&topic=1056286 '
144
'and create your application-specific password to run duplicity backups.')
202
def __fetch_entries(self, folder_id, type, title=None):
146
def _fetch_entries(self, folder_id, type, title=None):
204
148
uri = '/feeds/default/private/full/%s/contents' % folder_id
205
149
if type == 'folder':
212
156
uri += '&title=' + urllib.quote(title) + '&title-exact=true'
216
entries = self.client.get_all_resources(uri=uri)
218
# When filtering by entry title, API is returning (don't know why) documents in other
219
# folders (apart from folder_id) matching the title, so some extra filtering is required.
222
for entry in entries:
223
resource_type = entry.get_resource_type()
225
or (type == 'folder' and resource_type == 'folder') \
226
or (type == GDocsBackend.BACKUP_DOCUMENT_TYPE and resource_type != 'folder'):
228
if folder_id != GDocsBackend.ROOT_FOLDER_ID:
229
for link in entry.in_collections():
230
folder_entry = self.client.get_entry(link.href, None, None,
231
desired_class=gdata.docs.data.Resource)
232
if folder_entry and (folder_entry.resource_id.text == folder_id):
234
elif len(entry.in_collections()) == 0:
241
except Exception as e:
242
self.__handle_error('Error while fetching remote entries: %s.' % str(e))
159
entries = self.client.get_all_resources(uri=uri)
161
# When filtering by entry title, API is returning (don't know why) documents in other
162
# folders (apart from folder_id) matching the title, so some extra filtering is required.
165
for entry in entries:
166
resource_type = entry.get_resource_type()
168
or (type == 'folder' and resource_type == 'folder') \
169
or (type == GDocsBackend.BACKUP_DOCUMENT_TYPE and resource_type != 'folder'):
171
if folder_id != GDocsBackend.ROOT_FOLDER_ID:
172
for link in entry.in_collections():
173
folder_entry = self.client.get_entry(link.href, None, None,
174
desired_class=gdata.docs.data.Resource)
175
if folder_entry and (folder_entry.resource_id.text == folder_id):
177
elif len(entry.in_collections()) == 0:
244
185
duplicity.backend.register_backend('gdocs', GDocsBackend)