~didrocks/ubuntuone-client/dont-suffer-zg-crash

« back to all changes in this revision

Viewing changes to ubuntuone/u1sync/sync.py

  • Committer: Bazaar Package Importer
  • Author(s): Rodney Dawes
  • Date: 2009-06-30 12:00:00 UTC
  • Revision ID: james.westby@ubuntu.com-20090630120000-by806ovmw3193qe8
Tags: upstream-0.90.3
ImportĀ upstreamĀ versionĀ 0.90.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# ubuntuone.u1sync.sync
 
2
#
 
3
# State update
 
4
#
 
5
# Author: Tim Cole <tim.cole@canonical.com>
 
6
#
 
7
# Copyright 2009 Canonical Ltd.
 
8
#
 
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.
 
12
#
 
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.
 
17
#
 
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."""
 
22
 
 
23
from __future__ import with_statement
 
24
 
 
25
import os
 
26
import hashlib
 
27
 
 
28
EMPTY_HASH = "sha1:%s" % hashlib.sha1().hexdigest()
 
29
UPLOAD_SYMBOL = u"\u25b2".encode("utf-8")
 
30
DOWNLOAD_SYMBOL = u"\u25bc".encode("utf-8")
 
31
CONFLICT_SYMBOL = "!"
 
32
DELETE_SYMBOL = "X"
 
33
 
 
34
from ubuntuone.storageprotocol import request
 
35
from ubuntuone.storageprotocol.dircontent_pb2 import (
 
36
    DIRECTORY, SYMLINK)
 
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
 
41
 
 
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))
 
47
 
 
48
def name_from_path(path):
 
49
    """Returns unicode name from last path component."""
 
50
    return os.path.split(path)[1].decode("UTF-8")
 
51
 
 
52
 
 
53
class NodeSyncError(Exception):
 
54
    """Error syncing node."""
 
55
 
 
56
 
 
57
class NodeCreateError(NodeSyncError):
 
58
    """Error creating node."""
 
59
 
 
60
 
 
61
class NodeUpdateError(NodeSyncError):
 
62
    """Error updating node."""
 
63
 
 
64
 
 
65
class NodeDeleteError(NodeSyncError):
 
66
    """Error deleting node."""
 
67
 
 
68
 
 
69
def sync_tree(merged_tree, original_tree, sync_mode, path, quiet):
 
70
    """Performs actual synchronization."""
 
71
 
 
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) \
 
76
            = partial_parent
 
77
 
 
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)
 
81
        node_uuid = None
 
82
 
 
83
        synced = False
 
84
        if merged_node is not None:
 
85
            if merged_node.node_type == DIRECTORY:
 
86
                if original_node is not None:
 
87
                    synced = True
 
88
                    node_uuid = original_node.uuid
 
89
                else:
 
90
                    if not quiet:
 
91
                        print "%s   %s" % (sync_mode.symbol, display_path)
 
92
                    try:
 
93
                        create_dir = sync_mode.create_directory
 
94
                        node_uuid = create_dir(parent_uuid=parent_uuid,
 
95
                                               path=path)
 
96
                        synced = True
 
97
                    except NodeCreateError, e:
 
98
                        print e
 
99
            elif merged_node.content_hash is None:
 
100
                if not quiet:
 
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
 
108
                else:
 
109
                    conflict_symbol = " "
 
110
                if not quiet:
 
111
                    print "%s %s %s" % (sync_mode.symbol, conflict_symbol,
 
112
                                        display_path)
 
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
 
116
                else:
 
117
                    node_uuid = merged_node.uuid
 
118
                    original_hash = EMPTY_HASH
 
119
                try:
 
120
                    sync_mode.write_file(node_uuid=node_uuid,
 
121
                                         content_hash=
 
122
                                         merged_node.content_hash,
 
123
                                         old_content_hash=original_hash,
 
124
                                         path=path,
 
125
                                         parent_uuid=parent_uuid,
 
126
                                         conflict_info=conflict_info,
 
127
                                         node_type=merged_node.node_type)
 
128
                    synced = True
 
129
                except NodeSyncError, e:
 
130
                    print e
 
131
            else:
 
132
                synced = True
 
133
 
 
134
        return (path, display_path, node_uuid, synced)
 
135
 
 
136
    def post_merge(nodes, partial_result, child_results):
 
137
        """Delete nodes."""
 
138
        (merged_node, original_node) = nodes
 
139
        (path, display_path, node_uuid, synced) = partial_result
 
140
 
 
141
        if merged_node is None:
 
142
            assert original_node is not None
 
143
            if not quiet:
 
144
                print "%s %s %s" % (sync_mode.symbol, DELETE_SYMBOL,
 
145
                                    display_path)
 
146
            try:
 
147
                if original_node.node_type == DIRECTORY:
 
148
                    sync_mode.delete_directory(node_uuid=original_node.uuid,
 
149
                                               path=path)
 
150
                else:
 
151
                    # files or symlinks
 
152
                    sync_mode.delete_file(node_uuid=original_node.uuid,
 
153
                                          path=path)
 
154
                synced = True
 
155
            except NodeDeleteError, e:
 
156
                print e
 
157
 
 
158
        if synced:
 
159
            model_node = merged_node
 
160
        else:
 
161
            model_node = original_node
 
162
 
 
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)
 
167
                                                      in child_iter
 
168
                                                      if child is not None])
 
169
            else:
 
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)
 
176
        else:
 
177
            return None
 
178
 
 
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"")
 
182
 
 
183
def download_tree(merged_tree, local_tree, client, share_uuid, path, dry_run,
 
184
                  quiet):
 
185
    """Downloads a directory."""
 
186
    if dry_run:
 
187
        downloader = DryRun(symbol=DOWNLOAD_SYMBOL)
 
188
    else:
 
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)
 
192
 
 
193
def upload_tree(merged_tree, remote_tree, client, share_uuid, path, dry_run,
 
194
                quiet):
 
195
    """Uploads a directory."""
 
196
    if dry_run:
 
197
        uploader = DryRun(symbol=UPLOAD_SYMBOL)
 
198
    else:
 
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)
 
202
 
 
203
 
 
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."""
 
208
        self.symbol = symbol
 
209
 
 
210
    def create_directory(self, parent_uuid, path):
 
211
        """Doesn't create a directory."""
 
212
        return None
 
213
 
 
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."""
 
217
        return None
 
218
 
 
219
    def delete_directory(self, node_uuid, path):
 
220
        """Doesn't delete a directory."""
 
221
 
 
222
    def delete_file(self, node_uuid, path):
 
223
        """Doesn't delete a file."""
 
224
 
 
225
 
 
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."""
 
230
        self.client = client
 
231
        self.share_uuid = share_uuid
 
232
        self.symbol = DOWNLOAD_SYMBOL
 
233
 
 
234
    def create_directory(self, parent_uuid, path):
 
235
        """Creates a directory."""
 
236
        try:
 
237
            safe_mkdir(path)
 
238
        except OSError, e:
 
239
            raise NodeCreateError("Error creating local directory %s: %s" % \
 
240
                                  (path, e))
 
241
        return None
 
242
 
 
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."""
 
246
        if conflict_info:
 
247
            # download to conflict file rather than overwriting local changes
 
248
            path = get_conflict_path(path, conflict_info)
 
249
            content_hash = conflict_info[1]
 
250
        try:
 
251
            if node_type == SYMLINK:
 
252
                target = self.client.download_string(share_uuid=
 
253
                                                     self.share_uuid,
 
254
                                                     node_uuid=node_uuid,
 
255
                                                     content_hash=content_hash)
 
256
            else:
 
257
                self.client.download_file(share_uuid=self.share_uuid,
 
258
                                          node_uuid=node_uuid,
 
259
                                          content_hash=content_hash,
 
260
                                          filename=path)
 
261
        except (request.StorageRequestError, UnsupportedOperationError), e:
 
262
            if os.path.exists(path):
 
263
                raise NodeUpdateError("Error downloading content for %s: %s" %\
 
264
                                      (path, e))
 
265
            else:
 
266
                raise NodeCreateError("Error locally creating %s: %s" % \
 
267
                                      (path, e))
 
268
 
 
269
    def delete_directory(self, node_uuid, path):
 
270
        """Deletes a directory."""
 
271
        try:
 
272
            os.rmdir(path)
 
273
        except OSError, e:
 
274
            raise NodeDeleteError("Error locally deleting %s: %s" % (path, e))
 
275
 
 
276
    def delete_file(self, node_uuid, path):
 
277
        """Deletes a file."""
 
278
        try:
 
279
            os.unlink(path)
 
280
        except OSError, e:
 
281
            raise NodeDeleteError("Error locally deleting %s: %s" % (path, e))
 
282
 
 
283
 
 
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."""
 
288
        self.client = client
 
289
        self.share_uuid = share_uuid
 
290
        self.symbol = UPLOAD_SYMBOL
 
291
 
 
292
    def create_directory(self, parent_uuid, path):
 
293
        """Creates a directory on the server."""
 
294
        name = name_from_path(path)
 
295
        try:
 
296
            return self.client.create_directory(share_uuid=self.share_uuid,
 
297
                                                parent_uuid=parent_uuid,
 
298
                                                name=name)
 
299
        except (request.StorageRequestError, UnsupportedOperationError), e:
 
300
            raise NodeCreateError("Error remotely creating %s: %s" % \
 
301
                                  (path, e))
 
302
 
 
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."""
 
306
        if conflict_info:
 
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)
 
310
            try:
 
311
                self.client.move(share_uuid=self.share_uuid,
 
312
                                 parent_uuid=parent_uuid,
 
313
                                 name=conflict_name,
 
314
                                 node_uuid=node_uuid)
 
315
            except (request.StorageRequestError, UnsupportedOperationError), e:
 
316
                raise NodeUpdateError("Error remotely renaming %s to %s: %s" %\
 
317
                                      (path, conflict_path, e))
 
318
            node_uuid = None
 
319
            old_content_hash = EMPTY_HASH
 
320
 
 
321
        if node_type == SYMLINK:
 
322
            try:
 
323
                target = os.readlink(path)
 
324
            except OSError, e:
 
325
                raise NodeCreateError("Error retrieving link target " \
 
326
                                      "for %s: %s" % (path, e))
 
327
        else:
 
328
            target = None
 
329
 
 
330
        name = name_from_path(path)
 
331
        if node_uuid is None:
 
332
            try:
 
333
                if node_type == SYMLINK:
 
334
                    node_uuid = self.client.create_symlink(share_uuid=
 
335
                                                           self.share_uuid,
 
336
                                                           parent_uuid=
 
337
                                                           parent_uuid,
 
338
                                                           name=name,
 
339
                                                           target=target)
 
340
                    old_content_hash = content_hash
 
341
                else:
 
342
                    node_uuid = self.client.create_file(share_uuid=
 
343
                                                        self.share_uuid,
 
344
                                                        parent_uuid=
 
345
                                                        parent_uuid,
 
346
                                                        name=name)
 
347
            except (request.StorageRequestError, UnsupportedOperationError), e:
 
348
                raise NodeCreateError("Error remotely creating %s: %s" % \
 
349
                                      (path, e))
 
350
 
 
351
        if old_content_hash != content_hash:
 
352
            try:
 
353
                if node_type == SYMLINK:
 
354
                    self.client.upload_string(share_uuid=self.share_uuid,
 
355
                                              node_uuid=node_uuid,
 
356
                                              content_hash=content_hash,
 
357
                                              old_content_hash=
 
358
                                              old_content_hash,
 
359
                                              content=target)
 
360
                else:
 
361
                    self.client.upload_file(share_uuid=self.share_uuid,
 
362
                                            node_uuid=node_uuid,
 
363
                                            content_hash=content_hash,
 
364
                                            old_content_hash=old_content_hash,
 
365
                                            filename=path)
 
366
            except (request.StorageRequestError, UnsupportedOperationError), e:
 
367
                raise NodeUpdateError("Error uploading content for %s: %s" % \
 
368
                                      (path, e))
 
369
 
 
370
    def delete_directory(self, node_uuid, path):
 
371
        """Deletes a directory."""
 
372
        try:
 
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))
 
376
 
 
377
    def delete_file(self, node_uuid, path):
 
378
        """Deletes a file."""
 
379
        try:
 
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))