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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# ubuntuone.u1sync.sync
#
# State update
#
# Author: Tim Cole <tim.cole@canonical.com>
#
# Copyright 2009 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, 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 this program.  If not, see <http://www.gnu.org/licenses/>.
"""After merging, these routines are used to synchronize state locally and on
the server to correspond to the merged result."""

from __future__ import with_statement

import os
import hashlib

EMPTY_HASH = "sha1:%s" % hashlib.sha1().hexdigest()
UPLOAD_SYMBOL = u"\u25b2".encode("utf-8")
DOWNLOAD_SYMBOL = u"\u25bc".encode("utf-8")
CONFLICT_SYMBOL = "!"
DELETE_SYMBOL = "X"

from ubuntuone.storageprotocol import request
from ubuntuone.storageprotocol.dircontent_pb2 import (
    DIRECTORY, SYMLINK)
from ubuntuone.u1sync.genericmerge import (
    MergeNode, generic_merge)
from ubuntuone.u1sync.utils import safe_mkdir
from ubuntuone.u1sync.client import UnsupportedOperationError

def get_conflict_path(path, conflict_info):
    """Returns path for conflict file corresponding to path."""
    dir, name = os.path.split(path)
    unique_id = conflict_info[0]
    return os.path.join(dir, "conflict-%s-%s" % (unique_id, name))

def name_from_path(path):
    """Returns unicode name from last path component."""
    return os.path.split(path)[1].decode("UTF-8")


class NodeSyncError(Exception):
    """Error syncing node."""


class NodeCreateError(NodeSyncError):
    """Error creating node."""


class NodeUpdateError(NodeSyncError):
    """Error updating node."""


class NodeDeleteError(NodeSyncError):
    """Error deleting node."""


def sync_tree(merged_tree, original_tree, sync_mode, path, quiet):
    """Performs actual synchronization."""

    def pre_merge(nodes, name, partial_parent):
        """Create nodes and write content as required."""
        (merged_node, original_node) = nodes
        (parent_path, parent_display_path, parent_uuid, parent_synced) \
            = partial_parent

        utf8_name = name.encode("utf-8")
        path = os.path.join(parent_path, utf8_name)
        display_path = os.path.join(parent_display_path, utf8_name)
        node_uuid = None

        synced = False
        if merged_node is not None:
            if merged_node.node_type == DIRECTORY:
                if original_node is not None:
                    synced = True
                    node_uuid = original_node.uuid
                else:
                    if not quiet:
                        print "%s   %s" % (sync_mode.symbol, display_path)
                    try:
                        create_dir = sync_mode.create_directory
                        node_uuid = create_dir(parent_uuid=parent_uuid,
                                               path=path)
                        synced = True
                    except NodeCreateError, e:
                        print e
            elif merged_node.content_hash is None:
                if not quiet:
                    print "?   %s" % display_path
            elif original_node is None or \
                 original_node.content_hash != merged_node.content_hash or \
                 merged_node.conflict_info is not None:
                conflict_info = merged_node.conflict_info
                if conflict_info is not None:
                    conflict_symbol = CONFLICT_SYMBOL
                else:
                    conflict_symbol = " "
                if not quiet:
                    print "%s %s %s" % (sync_mode.symbol, conflict_symbol,
                                        display_path)
                if original_node is not None:
                    node_uuid = original_node.uuid or merged_node.uuid
                    original_hash = original_node.content_hash or EMPTY_HASH
                else:
                    node_uuid = merged_node.uuid
                    original_hash = EMPTY_HASH
                try:
                    sync_mode.write_file(node_uuid=node_uuid,
                                         content_hash=
                                         merged_node.content_hash,
                                         old_content_hash=original_hash,
                                         path=path,
                                         parent_uuid=parent_uuid,
                                         conflict_info=conflict_info,
                                         node_type=merged_node.node_type)
                    synced = True
                except NodeSyncError, e:
                    print e
            else:
                synced = True

        return (path, display_path, node_uuid, synced)

    def post_merge(nodes, partial_result, child_results):
        """Delete nodes."""
        (merged_node, original_node) = nodes
        (path, display_path, node_uuid, synced) = partial_result

        if merged_node is None:
            assert original_node is not None
            if not quiet:
                print "%s %s %s" % (sync_mode.symbol, DELETE_SYMBOL,
                                    display_path)
            try:
                if original_node.node_type == DIRECTORY:
                    sync_mode.delete_directory(node_uuid=original_node.uuid,
                                               path=path)
                else:
                    # files or symlinks
                    sync_mode.delete_file(node_uuid=original_node.uuid,
                                          path=path)
                synced = True
            except NodeDeleteError, e:
                print e

        if synced:
            model_node = merged_node
        else:
            model_node = original_node

        if model_node is not None:
            if model_node.node_type == DIRECTORY:
                child_iter = child_results.iteritems()
                merged_children = dict([(name, child) for (name, child)
                                                      in child_iter
                                                      if child is not None])
            else:
                # if there are children here it's because they failed to delete
                merged_children = None
            return MergeNode(node_type=model_node.node_type,
                             uuid=model_node.uuid,
                             children=merged_children,
                             content_hash=model_node.content_hash)
        else:
            return None

    return generic_merge(trees=[merged_tree, original_tree],
                         pre_merge=pre_merge, post_merge=post_merge,
                         partial_parent=(path, "", None, True), name=u"")

def download_tree(merged_tree, local_tree, client, share_uuid, path, dry_run,
                  quiet):
    """Downloads a directory."""
    if dry_run:
        downloader = DryRun(symbol=DOWNLOAD_SYMBOL)
    else:
        downloader = Downloader(client=client, share_uuid=share_uuid)
    return sync_tree(merged_tree=merged_tree, original_tree=local_tree,
                     sync_mode=downloader, path=path, quiet=quiet)

def upload_tree(merged_tree, remote_tree, client, share_uuid, path, dry_run,
                quiet):
    """Uploads a directory."""
    if dry_run:
        uploader = DryRun(symbol=UPLOAD_SYMBOL)
    else:
        uploader = Uploader(client=client, share_uuid=share_uuid)
    return sync_tree(merged_tree=merged_tree, original_tree=remote_tree,
                     sync_mode=uploader, path=path, quiet=quiet)


class DryRun(object):
    """A class which implements the sync interface but does nothing."""
    def __init__(self, symbol):
        """Initializes a DryRun instance."""
        self.symbol = symbol

    def create_directory(self, parent_uuid, path):
        """Doesn't create a directory."""
        return None

    def write_file(self, node_uuid, old_content_hash, content_hash,
                   parent_uuid, path, conflict_info, node_type):
        """Doesn't write a file."""
        return None

    def delete_directory(self, node_uuid, path):
        """Doesn't delete a directory."""

    def delete_file(self, node_uuid, path):
        """Doesn't delete a file."""


class Downloader(object):
    """A class which implements the download half of syncing."""
    def __init__(self, client, share_uuid):
        """Initializes a Downloader instance."""
        self.client = client
        self.share_uuid = share_uuid
        self.symbol = DOWNLOAD_SYMBOL

    def create_directory(self, parent_uuid, path):
        """Creates a directory."""
        try:
            safe_mkdir(path)
        except OSError, e:
            raise NodeCreateError("Error creating local directory %s: %s" % \
                                  (path, e))
        return None

    def write_file(self, node_uuid, old_content_hash, content_hash,
                   parent_uuid, path, conflict_info, node_type):
        """Creates a file and downloads new content for it."""
        if conflict_info:
            # download to conflict file rather than overwriting local changes
            path = get_conflict_path(path, conflict_info)
            content_hash = conflict_info[1]
        try:
            if node_type == SYMLINK:
                target = self.client.download_string(share_uuid=
                                                     self.share_uuid,
                                                     node_uuid=node_uuid,
                                                     content_hash=content_hash)
            else:
                self.client.download_file(share_uuid=self.share_uuid,
                                          node_uuid=node_uuid,
                                          content_hash=content_hash,
                                          filename=path)
        except (request.StorageRequestError, UnsupportedOperationError), e:
            if os.path.exists(path):
                raise NodeUpdateError("Error downloading content for %s: %s" %\
                                      (path, e))
            else:
                raise NodeCreateError("Error locally creating %s: %s" % \
                                      (path, e))

    def delete_directory(self, node_uuid, path):
        """Deletes a directory."""
        try:
            os.rmdir(path)
        except OSError, e:
            raise NodeDeleteError("Error locally deleting %s: %s" % (path, e))

    def delete_file(self, node_uuid, path):
        """Deletes a file."""
        try:
            os.unlink(path)
        except OSError, e:
            raise NodeDeleteError("Error locally deleting %s: %s" % (path, e))


class Uploader(object):
    """A class which implements the upload half of syncing."""
    def __init__(self, client, share_uuid):
        """Initializes an uploader instance."""
        self.client = client
        self.share_uuid = share_uuid
        self.symbol = UPLOAD_SYMBOL

    def create_directory(self, parent_uuid, path):
        """Creates a directory on the server."""
        name = name_from_path(path)
        try:
            return self.client.create_directory(share_uuid=self.share_uuid,
                                                parent_uuid=parent_uuid,
                                                name=name)
        except (request.StorageRequestError, UnsupportedOperationError), e:
            raise NodeCreateError("Error remotely creating %s: %s" % \
                                  (path, e))

    def write_file(self, node_uuid, old_content_hash, content_hash,
                   parent_uuid, path, conflict_info, node_type):
        """Creates a file on the server and uploads new content for it."""
        if conflict_info:
            # move conflicting file out of the way on the server
            conflict_path = get_conflict_path(path, conflict_info)
            conflict_name = name_from_path(conflict_path)
            try:
                self.client.move(share_uuid=self.share_uuid,
                                 parent_uuid=parent_uuid,
                                 name=conflict_name,
                                 node_uuid=node_uuid)
            except (request.StorageRequestError, UnsupportedOperationError), e:
                raise NodeUpdateError("Error remotely renaming %s to %s: %s" %\
                                      (path, conflict_path, e))
            node_uuid = None
            old_content_hash = EMPTY_HASH

        if node_type == SYMLINK:
            try:
                target = os.readlink(path)
            except OSError, e:
                raise NodeCreateError("Error retrieving link target " \
                                      "for %s: %s" % (path, e))
        else:
            target = None

        name = name_from_path(path)
        if node_uuid is None:
            try:
                if node_type == SYMLINK:
                    node_uuid = self.client.create_symlink(share_uuid=
                                                           self.share_uuid,
                                                           parent_uuid=
                                                           parent_uuid,
                                                           name=name,
                                                           target=target)
                    old_content_hash = content_hash
                else:
                    node_uuid = self.client.create_file(share_uuid=
                                                        self.share_uuid,
                                                        parent_uuid=
                                                        parent_uuid,
                                                        name=name)
            except (request.StorageRequestError, UnsupportedOperationError), e:
                raise NodeCreateError("Error remotely creating %s: %s" % \
                                      (path, e))

        if old_content_hash != content_hash:
            try:
                if node_type == SYMLINK:
                    self.client.upload_string(share_uuid=self.share_uuid,
                                              node_uuid=node_uuid,
                                              content_hash=content_hash,
                                              old_content_hash=
                                              old_content_hash,
                                              content=target)
                else:
                    self.client.upload_file(share_uuid=self.share_uuid,
                                            node_uuid=node_uuid,
                                            content_hash=content_hash,
                                            old_content_hash=old_content_hash,
                                            filename=path)
            except (request.StorageRequestError, UnsupportedOperationError), e:
                raise NodeUpdateError("Error uploading content for %s: %s" % \
                                      (path, e))

    def delete_directory(self, node_uuid, path):
        """Deletes a directory."""
        try:
            self.client.unlink(share_uuid=self.share_uuid, node_uuid=node_uuid)
        except (request.StorageRequestError, UnsupportedOperationError), e:
            raise NodeDeleteError("Error remotely deleting %s: %s" % (path, e))

    def delete_file(self, node_uuid, path):
        """Deletes a file."""
        try:
            self.client.unlink(share_uuid=self.share_uuid, node_uuid=node_uuid)
        except (request.StorageRequestError, UnsupportedOperationError), e:
            raise NodeDeleteError("Error remotely deleting %s: %s" % (path, e))