1
# ubuntuone.u1sync.sync
5
# Author: Tim Cole <tim.cole@canonical.com>
7
# Copyright 2009 Canonical Ltd.
9
# This program is free software: you can redistribute it and/or modify it
10
# under the terms of the GNU General Public License version 3, as published
11
# by the Free Software Foundation.
13
# This program is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranties of
15
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16
# PURPOSE. See the GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License along
19
# with this program. If not, see <http://www.gnu.org/licenses/>.
20
"""After merging, these routines are used to synchronize state locally and on
21
the server to correspond to the merged result."""
23
from __future__ import with_statement
28
EMPTY_HASH = "sha1:%s" % hashlib.sha1().hexdigest()
29
UPLOAD_SYMBOL = u"\u25b2".encode("utf-8")
30
DOWNLOAD_SYMBOL = u"\u25bc".encode("utf-8")
34
from ubuntuone.storageprotocol import request
35
from ubuntuone.storageprotocol.dircontent_pb2 import (
37
from ubuntuone.u1sync.genericmerge import (
38
MergeNode, generic_merge)
39
from ubuntuone.u1sync.utils import safe_mkdir
40
from ubuntuone.u1sync.client import UnsupportedOperationError
42
def get_conflict_path(path, conflict_info):
43
"""Returns path for conflict file corresponding to path."""
44
dir, name = os.path.split(path)
45
unique_id = conflict_info[0]
46
return os.path.join(dir, "conflict-%s-%s" % (unique_id, name))
48
def name_from_path(path):
49
"""Returns unicode name from last path component."""
50
return os.path.split(path)[1].decode("UTF-8")
53
class NodeSyncError(Exception):
54
"""Error syncing node."""
57
class NodeCreateError(NodeSyncError):
58
"""Error creating node."""
61
class NodeUpdateError(NodeSyncError):
62
"""Error updating node."""
65
class NodeDeleteError(NodeSyncError):
66
"""Error deleting node."""
69
def sync_tree(merged_tree, original_tree, sync_mode, path, quiet):
70
"""Performs actual synchronization."""
72
def pre_merge(nodes, name, partial_parent):
73
"""Create nodes and write content as required."""
74
(merged_node, original_node) = nodes
75
(parent_path, parent_display_path, parent_uuid, parent_synced) \
78
utf8_name = name.encode("utf-8")
79
path = os.path.join(parent_path, utf8_name)
80
display_path = os.path.join(parent_display_path, utf8_name)
84
if merged_node is not None:
85
if merged_node.node_type == DIRECTORY:
86
if original_node is not None:
88
node_uuid = original_node.uuid
91
print "%s %s" % (sync_mode.symbol, display_path)
93
create_dir = sync_mode.create_directory
94
node_uuid = create_dir(parent_uuid=parent_uuid,
97
except NodeCreateError, e:
99
elif merged_node.content_hash is None:
101
print "? %s" % display_path
102
elif original_node is None or \
103
original_node.content_hash != merged_node.content_hash or \
104
merged_node.conflict_info is not None:
105
conflict_info = merged_node.conflict_info
106
if conflict_info is not None:
107
conflict_symbol = CONFLICT_SYMBOL
109
conflict_symbol = " "
111
print "%s %s %s" % (sync_mode.symbol, conflict_symbol,
113
if original_node is not None:
114
node_uuid = original_node.uuid or merged_node.uuid
115
original_hash = original_node.content_hash or EMPTY_HASH
117
node_uuid = merged_node.uuid
118
original_hash = EMPTY_HASH
120
sync_mode.write_file(node_uuid=node_uuid,
122
merged_node.content_hash,
123
old_content_hash=original_hash,
125
parent_uuid=parent_uuid,
126
conflict_info=conflict_info,
127
node_type=merged_node.node_type)
129
except NodeSyncError, e:
134
return (path, display_path, node_uuid, synced)
136
def post_merge(nodes, partial_result, child_results):
138
(merged_node, original_node) = nodes
139
(path, display_path, node_uuid, synced) = partial_result
141
if merged_node is None:
142
assert original_node is not None
144
print "%s %s %s" % (sync_mode.symbol, DELETE_SYMBOL,
147
if original_node.node_type == DIRECTORY:
148
sync_mode.delete_directory(node_uuid=original_node.uuid,
152
sync_mode.delete_file(node_uuid=original_node.uuid,
155
except NodeDeleteError, e:
159
model_node = merged_node
161
model_node = original_node
163
if model_node is not None:
164
if model_node.node_type == DIRECTORY:
165
child_iter = child_results.iteritems()
166
merged_children = dict([(name, child) for (name, child)
168
if child is not None])
170
# if there are children here it's because they failed to delete
171
merged_children = None
172
return MergeNode(node_type=model_node.node_type,
173
uuid=model_node.uuid,
174
children=merged_children,
175
content_hash=model_node.content_hash)
179
return generic_merge(trees=[merged_tree, original_tree],
180
pre_merge=pre_merge, post_merge=post_merge,
181
partial_parent=(path, "", None, True), name=u"")
183
def download_tree(merged_tree, local_tree, client, share_uuid, path, dry_run,
185
"""Downloads a directory."""
187
downloader = DryRun(symbol=DOWNLOAD_SYMBOL)
189
downloader = Downloader(client=client, share_uuid=share_uuid)
190
return sync_tree(merged_tree=merged_tree, original_tree=local_tree,
191
sync_mode=downloader, path=path, quiet=quiet)
193
def upload_tree(merged_tree, remote_tree, client, share_uuid, path, dry_run,
195
"""Uploads a directory."""
197
uploader = DryRun(symbol=UPLOAD_SYMBOL)
199
uploader = Uploader(client=client, share_uuid=share_uuid)
200
return sync_tree(merged_tree=merged_tree, original_tree=remote_tree,
201
sync_mode=uploader, path=path, quiet=quiet)
204
class DryRun(object):
205
"""A class which implements the sync interface but does nothing."""
206
def __init__(self, symbol):
207
"""Initializes a DryRun instance."""
210
def create_directory(self, parent_uuid, path):
211
"""Doesn't create a directory."""
214
def write_file(self, node_uuid, old_content_hash, content_hash,
215
parent_uuid, path, conflict_info, node_type):
216
"""Doesn't write a file."""
219
def delete_directory(self, node_uuid, path):
220
"""Doesn't delete a directory."""
222
def delete_file(self, node_uuid, path):
223
"""Doesn't delete a file."""
226
class Downloader(object):
227
"""A class which implements the download half of syncing."""
228
def __init__(self, client, share_uuid):
229
"""Initializes a Downloader instance."""
231
self.share_uuid = share_uuid
232
self.symbol = DOWNLOAD_SYMBOL
234
def create_directory(self, parent_uuid, path):
235
"""Creates a directory."""
239
raise NodeCreateError("Error creating local directory %s: %s" % \
243
def write_file(self, node_uuid, old_content_hash, content_hash,
244
parent_uuid, path, conflict_info, node_type):
245
"""Creates a file and downloads new content for it."""
247
# download to conflict file rather than overwriting local changes
248
path = get_conflict_path(path, conflict_info)
249
content_hash = conflict_info[1]
251
if node_type == SYMLINK:
252
target = self.client.download_string(share_uuid=
255
content_hash=content_hash)
257
self.client.download_file(share_uuid=self.share_uuid,
259
content_hash=content_hash,
261
except (request.StorageRequestError, UnsupportedOperationError), e:
262
if os.path.exists(path):
263
raise NodeUpdateError("Error downloading content for %s: %s" %\
266
raise NodeCreateError("Error locally creating %s: %s" % \
269
def delete_directory(self, node_uuid, path):
270
"""Deletes a directory."""
274
raise NodeDeleteError("Error locally deleting %s: %s" % (path, e))
276
def delete_file(self, node_uuid, path):
277
"""Deletes a file."""
281
raise NodeDeleteError("Error locally deleting %s: %s" % (path, e))
284
class Uploader(object):
285
"""A class which implements the upload half of syncing."""
286
def __init__(self, client, share_uuid):
287
"""Initializes an uploader instance."""
289
self.share_uuid = share_uuid
290
self.symbol = UPLOAD_SYMBOL
292
def create_directory(self, parent_uuid, path):
293
"""Creates a directory on the server."""
294
name = name_from_path(path)
296
return self.client.create_directory(share_uuid=self.share_uuid,
297
parent_uuid=parent_uuid,
299
except (request.StorageRequestError, UnsupportedOperationError), e:
300
raise NodeCreateError("Error remotely creating %s: %s" % \
303
def write_file(self, node_uuid, old_content_hash, content_hash,
304
parent_uuid, path, conflict_info, node_type):
305
"""Creates a file on the server and uploads new content for it."""
307
# move conflicting file out of the way on the server
308
conflict_path = get_conflict_path(path, conflict_info)
309
conflict_name = name_from_path(conflict_path)
311
self.client.move(share_uuid=self.share_uuid,
312
parent_uuid=parent_uuid,
315
except (request.StorageRequestError, UnsupportedOperationError), e:
316
raise NodeUpdateError("Error remotely renaming %s to %s: %s" %\
317
(path, conflict_path, e))
319
old_content_hash = EMPTY_HASH
321
if node_type == SYMLINK:
323
target = os.readlink(path)
325
raise NodeCreateError("Error retrieving link target " \
326
"for %s: %s" % (path, e))
330
name = name_from_path(path)
331
if node_uuid is None:
333
if node_type == SYMLINK:
334
node_uuid = self.client.create_symlink(share_uuid=
340
old_content_hash = content_hash
342
node_uuid = self.client.create_file(share_uuid=
347
except (request.StorageRequestError, UnsupportedOperationError), e:
348
raise NodeCreateError("Error remotely creating %s: %s" % \
351
if old_content_hash != content_hash:
353
if node_type == SYMLINK:
354
self.client.upload_string(share_uuid=self.share_uuid,
356
content_hash=content_hash,
361
self.client.upload_file(share_uuid=self.share_uuid,
363
content_hash=content_hash,
364
old_content_hash=old_content_hash,
366
except (request.StorageRequestError, UnsupportedOperationError), e:
367
raise NodeUpdateError("Error uploading content for %s: %s" % \
370
def delete_directory(self, node_uuid, path):
371
"""Deletes a directory."""
373
self.client.unlink(share_uuid=self.share_uuid, node_uuid=node_uuid)
374
except (request.StorageRequestError, UnsupportedOperationError), e:
375
raise NodeDeleteError("Error remotely deleting %s: %s" % (path, e))
377
def delete_file(self, node_uuid, path):
378
"""Deletes a file."""
380
self.client.unlink(share_uuid=self.share_uuid, node_uuid=node_uuid)
381
except (request.StorageRequestError, UnsupportedOperationError), e:
382
raise NodeDeleteError("Error remotely deleting %s: %s" % (path, e))