~rodrigo-moya/ubuntuone-client/fix-657850

« back to all changes in this revision

Viewing changes to canonical/ubuntuone/storage/syncdaemon/filesystem_manager.py

  • Committer: Rodney Dawes
  • Date: 2009-05-12 13:36:05 UTC
  • Revision ID: rodney.dawes@canonical.com-20090512133605-6aqs6e8xnnmp5u1p
        Import the code
        Hook up lint/trial tests in setup.py
        Use icontool now instead of including the render script
        Add missing python-gnome2-desktop to package dependencies
        Update debian/rules to fix the icon cache issue

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# canonical.ubuntuone.storage.syncdaemon.filesystem_manager - FSM
 
2
#
 
3
# Author: Facundo Batista <facundo@canonical.com>
 
4
#
 
5
# Copyright 2009 Canonical Ltd.
 
6
#
 
7
# This program is free software: you can redistribute it and/or modify it
 
8
# under the terms of the GNU General Public License version 3, as published
 
9
# by the Free Software Foundation.
 
10
#
 
11
# This program is distributed in the hope that it will be useful, but
 
12
# WITHOUT ANY WARRANTY; without even the implied warranties of
 
13
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 
14
# PURPOSE.  See the GNU General Public License for more details.
 
15
#
 
16
# You should have received a copy of the GNU General Public License along
 
17
# with this program.  If not, see <http://www.gnu.org/licenses/>.
 
18
'''Module that implements the File System Manager.'''
 
19
from __future__ import with_statement
 
20
 
 
21
import os
 
22
import time
 
23
import shutil
 
24
import functools
 
25
import logging
 
26
import contextlib
 
27
import errno
 
28
 
 
29
from canonical.ubuntuone.storage.syncdaemon import file_shelf
 
30
import uuid
 
31
 
 
32
METADATA_VERSION = "2"
 
33
 
 
34
#
 
35
# File System Manager  (FSM)
 
36
# --------------------------
 
37
#
 
38
# The FileSystemManager is the one that interacts with the filesystem, and
 
39
# keeps a storage with metadata.  This storage is verterok's FileShelf.
 
40
#
 
41
# The metadata, in disk, is a dictionary, where the keys are 'mdid's (metadata
 
42
# ids), and the values are different parameters, some of them can be modified
 
43
# from outside, and some can not.
 
44
#
 
45
# There're two very important values: path and node_id. The path is the
 
46
# pointer to where the file or directory is located in the filesystem, and
 
47
# the node_id is the unique identifier from the server. When a new file is
 
48
# created (with the .create() method), a mdid is assigned to the path and the
 
49
# share, but no node_id yet. When the server assigns the node_id, it needs to
 
50
# be set here with the .set_node_id() method.
 
51
#
 
52
# All the data can be retrieved generally using this three values (mdid, path,
 
53
# and node_id/share) using specific get_by_*() methods. For this to be fast,
 
54
# two indexes are created at init time, two dictionaries that hold the
 
55
# relationships path->mdid, and (share,node_id)->mdid.  In any case, KeyError
 
56
# is raised if an incorrect value is passed to the getters. Note that a mdid
 
57
# identifies uniquely the MD Object, like also the path; but for the node_id it
 
58
# also needs to have the share, as the same "server file" can live in different
 
59
# directories in the "client disk".
 
60
#
 
61
# Once assigned, the path, share and node_id values can not be changed. For any
 
62
# other value (except another special one, 'info', see below), three methods
 
63
# are provided to set them: set_by_*() (symmetric to the getters). These
 
64
# methods receive a first argument to indicate what is modified, and then
 
65
# several keyword arguments with all the values to be set.
 
66
#
 
67
# The 'info' is a special value set by the FileSystemManager itself, that
 
68
# records operations and changes made to each node, and as I said before,
 
69
# it can only be accesses from outside, not modified.
 
70
#
 
71
# Another method is provided to retrieve the created objects:
 
72
# get_mdobjs_by_share_id, that returns all the objects in that share.
 
73
#
 
74
# When asked for data, the FSM returns an object that is a thin wrapper to the
 
75
# info, only to be easily accessible, like using "mdobj.path", or
 
76
# "mdobj.info.is_partial". This object is not alive: it does not get updated
 
77
# if something changes in the metadata, and any change in the object is not
 
78
# written back to the metadata (this is by design, because of how the
 
79
# information flows in the system).
 
80
#
 
81
# As I said before, the FileSystemManager not only keeps the metadata, but also
 
82
# interacts with the filesystem itself. As such, it provides several operations
 
83
# on files and directories.
 
84
#
 
85
# In the process of downloading a file from the server, FSM handles the
 
86
# .partial files. With the .create_partial() method the system creates this
 
87
# special file where the new content will be downloaded. When it finishes ok,
 
88
# the .commit_partial() is called, and that content is moved into the old file.
 
89
# If the download fails for any reason, .remove_partial() is called and all is
 
90
# back to clean.
 
91
#
 
92
# Other services are provided:
 
93
#
 
94
#    .move_to_conflict(): moves a file or dir in problem to the same name but
 
95
# adding a .conflict to the name (if .conflict already exists, it will try with
 
96
# .conflict.1, .conflict.2, and so on).
 
97
#
 
98
#    .upload_finished(): sets a new hash in the metadata, marking that the
 
99
# new content was uploaded to the server.
 
100
#
 
101
#    .move_file(): moves a file or directory from one pathname to other.
 
102
#
 
103
#    .delete_file(): removes a file or directory from disk.
 
104
#
 
105
# Finally, the FSM has three methods that provides high level information,
 
106
# in some cases synthesising their values using some internal values:
 
107
#
 
108
#    .has_metadata(): returns True if the system has metadata for that path,
 
109
# node_id or mdid (note that we may don't have metadata even to an old mdid,
 
110
# because it was deleted in the middle)
 
111
#
 
112
#    .changed(): returns 'local', 'server', or 'none', depending of what
 
113
# changed for that node
 
114
#
 
115
#    .is_dir: returns if the node is a directory.
 
116
#
 
117
#
 
118
 
 
119
 
 
120
# fsm logger
 
121
fsm_logger = logging.getLogger('ubuntuone.SyncDaemon.fsm')
 
122
logger = functools.partial(fsm_logger.log, logging.INFO)
 
123
log_warning = functools.partial(fsm_logger.log, logging.WARNING)
 
124
 
 
125
is_forbidden = set("info path node_id share_id is_dir stat".split()
 
126
                  ).intersection
 
127
 
 
128
class InconsistencyError(Exception):
 
129
    '''Inconsistency between internal records and filesystem itself.'''
 
130
 
 
131
class Despair(Exception):
 
132
    '''This should never happen, we're in an impossible condition!'''
 
133
 
 
134
 
 
135
class _MDObject(object):
 
136
    '''Wrapper around MD dict.'''
 
137
    def __init__(self, **mdobj):
 
138
        self.__dict__.update(mdobj)
 
139
 
 
140
        # info is a special one
 
141
        if "info" in mdobj:
 
142
            self.info = _MDObject(**mdobj["info"])
 
143
 
 
144
    def __eq__(self, other):
 
145
        return self.__dict__ == other.__dict__
 
146
 
 
147
 
 
148
class ShareNodeDict(dict):
 
149
    '''Cache for node_id and share.'''
 
150
    def __getitem__(self, key):
 
151
        share_id, node_id = key
 
152
        if node_id is None:
 
153
            raise ValueError("The node_id can not be None")
 
154
        return dict.__getitem__(self, key)
 
155
 
 
156
    def __setitem__(self, key, value):
 
157
        share_id, node_id = key
 
158
        if node_id is None:
 
159
            raise ValueError("The node_id can not be None")
 
160
        return dict.__setitem__(self, key, value)
 
161
 
 
162
    def __contains__(self, key):
 
163
        share_id, node_id = key
 
164
        if node_id is None:
 
165
            raise ValueError("The node_id can not be None")
 
166
        return dict.__contains__(self, key)
 
167
 
 
168
 
 
169
 
 
170
 
 
171
class FileSystemManager(object):
 
172
    '''Keeps the files/dirs metadata and interacts with the filesystem.
 
173
 
 
174
    It has a FileShelf where all the metadata is stored, using 'mdid's as
 
175
    keys.  'mdid' is 'metadata id'... it's actually an uuid, but we call it
 
176
    mdid to don't get confused about names, as we also have the node_id is
 
177
    the one assigned by the server.
 
178
 
 
179
    At init time two indexes are built in memory:
 
180
 
 
181
      - idx_path: relationship path -> mdid
 
182
      - idx_node_id: relationship (share_id, node_id) -> mdid
 
183
    '''
 
184
    def __init__(self, data_dir, vm):
 
185
        if not isinstance(data_dir, basestring):
 
186
            raise TypeError("data_dir should be a string instead of %s" % \
 
187
                            type(data_dir))
 
188
        fsmdir = os.path.join(data_dir, 'fsm')
 
189
        self.fs = file_shelf.FileShelf(fsmdir)
 
190
        self.shares = {}
 
191
        self.vm = vm
 
192
 
 
193
        # create the indexes
 
194
        self._idx_path = {}
 
195
        self._idx_node_id = ShareNodeDict()
 
196
 
 
197
        # get the metadata version
 
198
        self._version_file = os.path.join(data_dir, "metadata_version")
 
199
        if os.path.exists(self._version_file):
 
200
            with open(self._version_file) as fh:
 
201
                md_version = fh.read().strip()
 
202
        else:
 
203
            md_version = None
 
204
 
 
205
        # load the info from the metadata
 
206
        if md_version == METADATA_VERSION:
 
207
            self._load_metadata_updated()
 
208
        else:
 
209
            load_method = getattr(self, "_load_metadata_%s" % md_version)
 
210
            load_method(md_version)
 
211
 
 
212
        logger("initialized: idx_path: %s, idx_node_id: %s, shares: %s" %
 
213
               (len(self._idx_path), len(self._idx_node_id), len(self.shares)))
 
214
 
 
215
    def _load_metadata_None(self, old_version):
 
216
        '''Loads metadata from when it wasn't even versioned.'''
 
217
        logger("loading metadata from old version %r", old_version)
 
218
 
 
219
        for mdid in self.fs.keys():
 
220
            mdobj = self.fs[mdid]
 
221
 
 
222
            # assure path are bytes (new to version 2)
 
223
            try:
 
224
                mdobj["path"] = mdobj["path"].encode("utf8")
 
225
            except UnicodeDecodeError:
 
226
                # this is an invalid path, we shouldn't have it
 
227
                del self.fs[mdid]
 
228
                continue
 
229
 
 
230
            abspath = self.get_abspath(mdobj["share_id"], mdobj["path"])
 
231
            self._idx_path[abspath] = mdid
 
232
            if mdobj["node_id"] is not None:
 
233
                self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])] = mdid
 
234
 
 
235
            # assure we have stat info (new to version 1)
 
236
            if os.path.exists(abspath):
 
237
                newstat = os.stat(abspath)
 
238
            else:
 
239
                newstat = None
 
240
            mdobj["stat"] = newstat
 
241
            self.fs[mdid] = mdobj
 
242
 
 
243
        # set new version
 
244
        with open(self._version_file, "w") as fh:
 
245
            fh.write(METADATA_VERSION)
 
246
 
 
247
    def _load_metadata_1(self, old_version):
 
248
        '''Loads metadata from when it wasn't even versioned.'''
 
249
        logger("loading metadata from old version %r", old_version)
 
250
 
 
251
        for mdid in self.fs.keys():
 
252
            mdobj = self.fs[mdid]
 
253
 
 
254
            # assure path are bytes (new to version 2)
 
255
            try:
 
256
                mdobj["path"] = mdobj["path"].encode("utf8")
 
257
            except UnicodeDecodeError:
 
258
                # this is an invalid path, we shouldn't have it
 
259
                del self.fs[mdid]
 
260
                continue
 
261
 
 
262
            abspath = self.get_abspath(mdobj["share_id"], mdobj["path"])
 
263
            self._idx_path[abspath] = mdid
 
264
            if mdobj["node_id"] is not None:
 
265
                self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])] = mdid
 
266
            self.fs[mdid] = mdobj
 
267
 
 
268
        # set new version
 
269
        with open(self._version_file, "w") as fh:
 
270
            fh.write(METADATA_VERSION)
 
271
 
 
272
    def _load_metadata_updated(self):
 
273
        '''Loads metadata of last version.'''
 
274
        logger("loading updated metadata")
 
275
        for mdid in self.fs.keys():
 
276
            mdobj = self.fs[mdid]
 
277
            abspath = self.get_abspath(mdobj["share_id"], mdobj["path"])
 
278
            self._idx_path[abspath] = mdid
 
279
            if mdobj["node_id"] is not None:
 
280
                self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])] = mdid
 
281
 
 
282
    def create(self, path, share_id, is_dir=False):
 
283
        '''Creates a new md object.'''
 
284
        if not path.strip():
 
285
            raise ValueError("Empty paths are not allowed (got %r)" % path)
 
286
 
 
287
        path = os.path.normpath(path)
 
288
        if path in self._idx_path:
 
289
            raise ValueError("The path %r is already created!" % path)
 
290
 
 
291
        # create it
 
292
        mdid = str(uuid.uuid4())
 
293
        # make path relative to the share_id
 
294
        relpath = self._share_relative_path(share_id, path)
 
295
        newobj = dict(path=relpath, node_id=None, share_id=share_id,
 
296
                      is_dir=is_dir, local_hash=None,
 
297
                      server_hash=None, mdid=mdid)
 
298
        newobj["info"] = dict(created=time.time(), is_partial=False)
 
299
        if os.path.exists(path):
 
300
            newobj["stat"] = os.stat(path)
 
301
        else:
 
302
            newobj["stat"] = None
 
303
 
 
304
        logger("create: path=%r mdid=%r share_id=%r node_id=%r is_dir=%r" % (
 
305
                                          path, mdid, share_id, None, is_dir))
 
306
        self.fs[mdid] = newobj
 
307
 
 
308
        # adjust the index
 
309
        self._idx_path[path] = mdid
 
310
 
 
311
        return mdid
 
312
 
 
313
    def set_node_id(self, path, node_id):
 
314
        '''Sets the node_id to a md object.'''
 
315
        path = os.path.normpath(path)
 
316
        mdid = self._idx_path[path]
 
317
        mdobj = self.fs[mdid]
 
318
        if mdobj["node_id"] is not None:
 
319
            msg = "The path %r already has the node_id %r" % (path, node_id)
 
320
            raise ValueError(msg)
 
321
 
 
322
        # adjust the index
 
323
        share_id = mdobj["share_id"]
 
324
        self._idx_node_id[(share_id, node_id)] = mdid
 
325
 
 
326
        # set the node_id
 
327
        mdobj["node_id"] = node_id
 
328
        mdobj["info"]["node_id_assigned"] = time.time()
 
329
        self.fs[mdid] = mdobj
 
330
 
 
331
        logger("set_node_id: path=%r mdid=%r share_id=%r node_id=%r" % (
 
332
                                               path, mdid, share_id, node_id))
 
333
 
 
334
    def get_mdobjs_by_share_id(self, share_id):
 
335
        '''Returns all the mdids that belongs to a share.'''
 
336
        all_mdobjs = []
 
337
        for mdid in self.fs.keys():
 
338
            mdobj = self.fs[mdid]
 
339
            if mdobj["share_id"] == share_id:
 
340
                all_mdobjs.append(_MDObject(**mdobj))
 
341
        return all_mdobjs
 
342
 
 
343
    def get_data_for_server_rescan(self):
 
344
        '''Generates all the (share, node, hash) tuples needed for rescan'''
 
345
        all_data = []
 
346
        for _, v in self.fs.items():
 
347
            if v['node_id']:
 
348
                all_data.append(
 
349
                        (v['share_id'], v['node_id'], v['server_hash'] or ''))
 
350
        return all_data
 
351
 
 
352
    def get_by_mdid(self, mdid):
 
353
        '''Returns the md object according to the mdid.'''
 
354
        mdobj = self.fs[mdid]
 
355
        return _MDObject(**mdobj)
 
356
 
 
357
    def get_by_path(self, path):
 
358
        '''Returns the md object according to the path.'''
 
359
        path = os.path.normpath(path)
 
360
        mdid = self._idx_path[path]
 
361
        mdobj = self.fs[mdid]
 
362
        return _MDObject(**mdobj)
 
363
 
 
364
    def get_by_node_id(self, share_id, node_id):
 
365
        '''Returns the md object according to the node_id and share_id.'''
 
366
        mdid = self._idx_node_id[(share_id, node_id)]
 
367
        mdobj = self.fs[mdid]
 
368
        return _MDObject(**mdobj)
 
369
 
 
370
    def set_by_mdid(self, mdid, **kwargs):
 
371
        '''Set some values to the md object with that mdid.'''
 
372
        forbidden = is_forbidden(set(kwargs))
 
373
        if forbidden:
 
374
            raise ValueError("The following attributes can not be set "
 
375
                             "externally: %s" % forbidden)
 
376
 
 
377
        logger("set mdid=%r: %s" % (mdid, kwargs))
 
378
        mdobj = self.fs[mdid]
 
379
        for k, v in kwargs.items():
 
380
            mdobj[k] = v
 
381
        self.fs[mdid] = mdobj
 
382
 
 
383
    def set_by_path(self, path, **kwargs):
 
384
        '''Set some values to the md object with that path.'''
 
385
        if "mdid" in kwargs:
 
386
            raise ValueError("The mdid is forbidden to set externally")
 
387
        path = os.path.normpath(path)
 
388
        mdid = self._idx_path[path]
 
389
        self.set_by_mdid(mdid, **kwargs)
 
390
 
 
391
    def set_by_node_id(self, node_id, share_id, **kwargs):
 
392
        '''Set some values to the md object with that node_id/share_id.'''
 
393
        if "mdid" in kwargs:
 
394
            raise ValueError("The mdid is forbidden to set externally")
 
395
        mdid = self._idx_node_id[(share_id, node_id)]
 
396
        self.set_by_mdid(mdid, **kwargs)
 
397
 
 
398
    def refresh_stat(self, path):
 
399
        '''Refreshes the stat of a md object.'''
 
400
        logger("refresh stat to path=%r", path)
 
401
        mdid = self._idx_path[path]
 
402
        mdobj = self.fs[mdid]
 
403
        mdobj["stat"] = os.stat(path)
 
404
        self.fs[mdid] = mdobj
 
405
 
 
406
    def move_file(self, new_share_id, path_from, path_to):
 
407
        '''Moves a file/dir from one point to the other.'''
 
408
        path_from = os.path.normpath(path_from)
 
409
        path_to = os.path.normpath(path_to)
 
410
        mdid = self._idx_path[path_from]
 
411
        mdobj = self.fs[mdid]
 
412
 
 
413
        # move the file in the fs
 
414
        from_context = self._enable_share_write(mdobj['share_id'], path_from)
 
415
        to_context = self._enable_share_write(new_share_id, path_to)
 
416
 
 
417
        # pylint: disable-msg=W0704
 
418
        try:
 
419
            with contextlib.nested(from_context, to_context):
 
420
                shutil.move(path_from, path_to)
 
421
        except IOError, e:
 
422
            # file was not yet created
 
423
            m = "IOError %s when trying to move file/dir %r"
 
424
            log_warning(m, e, path_from)
 
425
        self.moved(new_share_id, path_from, path_to)
 
426
 
 
427
    def moved(self, new_share_id, path_from, path_to):
 
428
        """change the metadata of a moved file"""
 
429
        path_from = os.path.normpath(path_from)
 
430
        path_to = os.path.normpath(path_to)
 
431
 
 
432
        # if the move overwrites other file, adjust *that* metadata
 
433
        if path_to in self._idx_path:
 
434
            mdid = self._idx_path.pop(path_to)
 
435
            mdobj = self.fs[mdid]
 
436
            if mdobj["node_id"] is not None:
 
437
                del self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])]
 
438
            del self.fs[mdid]
 
439
 
 
440
        # adjust the metadata of "from" file
 
441
        mdid = self._idx_path.pop(path_from)
 
442
        logger("move_file: mdid=%r path_from=%r path_to=%r",
 
443
                                                    mdid, path_from, path_to)
 
444
        mdobj = self.fs[mdid]
 
445
 
 
446
        self._idx_path[path_to] = mdid
 
447
 
 
448
        # change the path, make it relative to the share_id
 
449
        relpath = self._share_relative_path(new_share_id, path_to)
 
450
        mdobj["path"] = relpath
 
451
        mdobj['share_id'] = new_share_id
 
452
        mdobj["info"]["last_moved_from"] = path_from
 
453
        mdobj["info"]["last_moved_time"] = time.time()
 
454
        # we try to stat, if we fail, so what?
 
455
        #pylint: disable-msg=W0704
 
456
        try:
 
457
            mdobj["stat"] = os.stat(path_to)  # needed if not the same FS
 
458
        except OSError:
 
459
            logger("Got an OSError while getting the stat of %r", path_to)
 
460
        self.fs[mdid] = mdobj
 
461
 
 
462
        if mdobj["is_dir"]:
 
463
            # change the path for all the children of that node
 
464
            path_from = path_from + os.path.sep
 
465
            len_from = len(path_from)
 
466
            for path in (x for x in self._idx_path if x.startswith(path_from)):
 
467
                newpath = os.path.join(path_to, path[len_from:])
 
468
 
 
469
                # change in the index
 
470
                mdid = self._idx_path.pop(path)
 
471
                self._idx_path[newpath] = mdid
 
472
 
 
473
                # and in the object itself
 
474
                mdobj = self.fs[mdid]
 
475
                relpath = self._share_relative_path(new_share_id, newpath)
 
476
                mdobj["path"] = relpath
 
477
                self.fs[mdid] = mdobj
 
478
 
 
479
 
 
480
    def delete_metadata(self, path):
 
481
        '''Deletes the metadata.'''
 
482
        path = os.path.normpath(path)
 
483
        mdid = self._idx_path[path]
 
484
        mdobj = self.fs[mdid]
 
485
        logger("delete metadata: path=%r mdid=%r" % (path, mdid))
 
486
 
 
487
        # adjust all
 
488
        del self._idx_path[path]
 
489
        if mdobj["node_id"] is not None:
 
490
            del self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])]
 
491
        del self.fs[mdid]
 
492
 
 
493
    def delete_file(self, path):
 
494
        '''Deletes a file/dir and the metadata.'''
 
495
        # adjust the metadata
 
496
        path = os.path.normpath(path)
 
497
        mdid = self._idx_path[path]
 
498
        mdobj = self.fs[mdid]
 
499
        logger("delete: path=%r mdid=%r" % (path, mdid))
 
500
 
 
501
        # delete the file
 
502
        try:
 
503
            with self._enable_share_write(mdobj['share_id'], path):
 
504
                if self.is_dir(path=path):
 
505
                    os.rmdir(path)
 
506
                else:
 
507
                    os.remove(path)
 
508
        except OSError, e:
 
509
            if e.errno == errno.ENOTEMPTY:
 
510
                raise
 
511
            m = "OSError %s when trying to remove file/dir %r"
 
512
            log_warning(m, e, path)
 
513
        self.delete_metadata(path)
 
514
 
 
515
    def move_to_conflict(self, mdid):
 
516
        '''Moves a file/dir to its .conflict.'''
 
517
        mdobj = self.fs[mdid]
 
518
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
519
        logger("move_to_conflict: path=%r mdid=%r" % (path, mdid))
 
520
        base_to_path = to_path =  path + ".conflict"
 
521
        ind = 0
 
522
        while os.path.exists(to_path):
 
523
            ind += 1
 
524
            to_path = base_to_path + "." + str(ind)
 
525
        with self._enable_share_write(mdobj['share_id'], path):
 
526
            try:
 
527
                os.rename(path, to_path)
 
528
            except OSError, e:
 
529
                if e.errno == errno.ENOENT:
 
530
                    m = "Already removed when trying to move to conflict: %r"
 
531
                    log_warning(m, path)
 
532
                else:
 
533
                    raise
 
534
 
 
535
        for p, is_dir in self.get_paths_starting_with(path):
 
536
            if p == path:
 
537
                continue
 
538
            if is_dir:
 
539
                # remove inotify watch
 
540
                try:
 
541
                    self.vm.m.event_q.inotify_rm_watch(p)
 
542
                except TypeError, e:
 
543
                    # pyinotify has an ugly error management, if we can call
 
544
                    # it that, :(. We handle this here because it's possible
 
545
                    # and correct that the path is not there anymore
 
546
                    m = "Error %s when trying to remove the watch on %r"
 
547
                    log_warning(m, path)
 
548
 
 
549
            self.delete_metadata(p)
 
550
        mdobj["info"]["last_conflicted"] = time.time()
 
551
        self.fs[mdid] = mdobj
 
552
 
 
553
    def _check_partial(self, mdid):
 
554
        '''Checks consistency between internal flag and FS regarding partial'''
 
555
        # get the values
 
556
        mdobj = self.fs[mdid]
 
557
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
558
        partial_in_md = mdobj["info"]["is_partial"]
 
559
        if mdobj["is_dir"]:
 
560
            partial_path = os.path.join(path, ".partial")
 
561
        else:
 
562
            partial_path = path + ".partial"
 
563
        partial_in_disk = os.path.exists(partial_path)
 
564
 
 
565
        # check and return
 
566
        if partial_in_md != partial_in_disk:
 
567
            msg = "'partial' inconsistency for object with mdid %r!  In disk:"\
 
568
                  " %s, In MD: %s" % (mdid, partial_in_disk, partial_in_md)
 
569
            raise InconsistencyError(msg)
 
570
        return partial_in_md
 
571
 
 
572
    def get_partial_path(self, mdobj):
 
573
        '''Gets the path of the .partial file for a given mdobj'''
 
574
        is_dir = mdobj["is_dir"]
 
575
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
576
        if is_dir:
 
577
            return os.path.join(path, ".partial")
 
578
        else:
 
579
            return path + ".partial"
 
580
 
 
581
    def create_partial(self, node_id, share_id):
 
582
        '''Creates a .partial in disk and set the flag in metadata.'''
 
583
        mdid = self._idx_node_id[(share_id, node_id)]
 
584
        if self._check_partial(mdid):
 
585
            raise ValueError("The object with share_id %r and node_id %r is "
 
586
                             "already partial!" % (share_id, node_id))
 
587
 
 
588
        # create an empty partial and set the flag
 
589
        mdobj = self.fs[mdid]
 
590
        is_dir = mdobj["is_dir"]
 
591
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
592
        partial_path = self.get_partial_path(mdobj)
 
593
        with self._enable_share_write(share_id, partial_path):
 
594
            # if we don't have the dir yet, create it
 
595
            if is_dir and not os.path.exists(path):
 
596
                os.mkdir(path)
 
597
            open(partial_path, "w").close()
 
598
 
 
599
        mdobj["info"]["last_partial_created"] = time.time()
 
600
        mdobj["info"]["is_partial"] = True
 
601
        self.fs[mdid] = mdobj
 
602
 
 
603
    def get_partial_for_writing(self, node_id, share_id):
 
604
        '''Get a write-only fd to a partial file'''
 
605
        mdid = self._idx_node_id[(share_id, node_id)]
 
606
        if not self._check_partial(mdid):
 
607
            raise ValueError("The object with share_id %r and node_id %r is "
 
608
                             "not partial!" % (share_id, node_id))
 
609
 
 
610
        mdobj = self.fs[mdid]
 
611
        partial_path = self.get_partial_path(mdobj)
 
612
        with self._enable_share_write(share_id, partial_path):
 
613
            fh = open(partial_path, "w")
 
614
        return fh
 
615
 
 
616
    def get_partial(self, node_id, share_id):
 
617
        '''Gets a read-only fd to a partial file.'''
 
618
        mdid = self._idx_node_id[(share_id, node_id)]
 
619
        if not self._check_partial(mdid):
 
620
            raise ValueError("The object with share_id %r and node_id %r is "
 
621
                             "not partial!" % (share_id, node_id))
 
622
 
 
623
        partial_path = self.get_partial_path(self.fs[mdid])
 
624
        fd = open(partial_path, "r")
 
625
        return fd
 
626
 
 
627
    def commit_partial(self, node_id, share_id, local_hash):
 
628
        '''Creates a .partial in disk and set the flag in metadata.'''
 
629
        mdid = self._idx_node_id[(share_id, node_id)]
 
630
        mdobj = self.fs[mdid]
 
631
        if mdobj["is_dir"]:
 
632
            raise ValueError("Directory partials can not be commited!")
 
633
        if not self._check_partial(mdid):
 
634
            raise ValueError("The object with share_id %r and node_id %r is "
 
635
                             "not partial!" % (share_id, node_id))
 
636
 
 
637
        # move the .partial to the real path, and set the md info
 
638
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
639
        logger("commit_partial: path=%r mdid=%r share_id=%r node_id=%r" %
 
640
                                            (path, mdid, share_id, node_id))
 
641
 
 
642
        partial_path = path + ".partial"
 
643
        partial_context =  self._enable_share_write(share_id, partial_path)
 
644
        path_context = self._enable_share_write(share_id, path)
 
645
        with contextlib.nested(partial_context, path_context):
 
646
            shutil.move(path + ".partial", path)
 
647
        mdobj["local_hash"] = local_hash
 
648
        mdobj["info"]["last_downloaded"] = time.time()
 
649
        mdobj["info"]["is_partial"] = False
 
650
        mdobj["stat"] = os.stat(path)
 
651
        self.fs[mdid] = mdobj
 
652
 
 
653
    def remove_partial(self, node_id, share_id):
 
654
        '''Removes a .partial in disk and set the flag in metadata.'''
 
655
        mdid = self._idx_node_id[(share_id, node_id)]
 
656
 
 
657
        # delete the .partial, and set the md info
 
658
        mdobj = self.fs[mdid]
 
659
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
660
        logger("remove_partial: path=%r mdid=%r share_id=%r node_id=%r" %
 
661
                                            (path, mdid, share_id, node_id))
 
662
        if self.is_dir(path=path):
 
663
            partial_path = os.path.join(path, ".partial")
 
664
        else:
 
665
            partial_path = path + ".partial"
 
666
        with self._enable_share_write(share_id, partial_path):
 
667
            #pylint: disable-msg=W0704
 
668
            try:
 
669
                os.remove(partial_path)
 
670
            except OSError, e:
 
671
                # we only remove it if its there.
 
672
                m = "OSError %s when trying to remove partial_path %r"
 
673
                log_warning(m, e, partial_path)
 
674
        mdobj["info"]["last_partial_removed"] = time.time()
 
675
        mdobj["info"]["is_partial"] = False
 
676
        self.fs[mdid] = mdobj
 
677
 
 
678
    def upload_finished(self, mdid, server_hash):
 
679
        '''Sets the metadata with timestamp and server hash.'''
 
680
        mdobj = self.fs[mdid]
 
681
        mdobj["info"]["last_uploaded"] = time.time()
 
682
        mdobj["server_hash"] = server_hash
 
683
        self.fs[mdid] = mdobj
 
684
 
 
685
    def _get_mdid_from_args(self, kwargs, parent):
 
686
        '''Parses the kwargs and gets the mdid.'''
 
687
        if len(kwargs) == 1 and "path" in kwargs:
 
688
            path = os.path.normpath(kwargs["path"])
 
689
            return self._idx_path[path]
 
690
        if len(kwargs) == 1 and "mdid" in kwargs:
 
691
            return kwargs["mdid"]
 
692
        if len(kwargs) == 2 and "node_id" in kwargs and "share_id" in kwargs:
 
693
            return self._idx_node_id[(kwargs["share_id"], kwargs["node_id"])]
 
694
        raise TypeError("Incorrect arguments for %r: %r" % (parent, kwargs))
 
695
 
 
696
    def is_dir(self, **kwargs):
 
697
        '''Return True if the path of a given object is a directory.'''
 
698
        mdid = self._get_mdid_from_args(kwargs, "is_dir")
 
699
        mdobj = self.fs[mdid]
 
700
        return mdobj["is_dir"]
 
701
 
 
702
    def has_metadata(self, **kwargs):
 
703
        '''Return True if there's metadata for a given object.'''
 
704
        if len(kwargs) == 1 and "path" in kwargs:
 
705
            path = os.path.normpath(kwargs["path"])
 
706
            return path in self._idx_path
 
707
        if len(kwargs) == 1 and "mdid" in kwargs:
 
708
            return kwargs["mdid"] in self.fs
 
709
        if len(kwargs) == 2 and "node_id" in kwargs and "share_id" in kwargs:
 
710
            return (kwargs["share_id"], kwargs["node_id"]) in self._idx_node_id
 
711
        raise TypeError("Incorrect arguments for 'has_metadata': %r" % kwargs)
 
712
 
 
713
    def changed(self, **kwargs):
 
714
        '''Return True if there's metadata for a given object.'''
 
715
        # get the values
 
716
        mdid = self._get_mdid_from_args(kwargs, "changed")
 
717
        mdobj = self.fs[mdid]
 
718
        is_partial = mdobj["info"]["is_partial"]
 
719
        local_hash = mdobj.get("local_hash", False)
 
720
        server_hash = mdobj.get("server_hash", False)
 
721
 
 
722
        # return the status
 
723
        if local_hash == server_hash:
 
724
            if is_partial:
 
725
                return "We broke the Universe! local_hash %r, server_hash %r,"\
 
726
                       " is_partial %r" % (local_hash, server_hash, is_partial)
 
727
            else:
 
728
                return 'NONE'
 
729
        else:
 
730
            if is_partial:
 
731
                return 'SERVER'
 
732
            else:
 
733
                return 'LOCAL'
 
734
        return
 
735
 
 
736
    def dir_content(self, path):
 
737
        '''Returns the content of the directory in a server-comparable way.'''
 
738
        path = os.path.normpath(path)
 
739
        mdid = self._idx_path[path]
 
740
        mdobj = self.fs[mdid]
 
741
        if not mdobj["is_dir"]:
 
742
            raise ValueError("You can ask dir_content only on a directory.")
 
743
 
 
744
        def _get_all():
 
745
            '''find the mdids that match'''
 
746
            for p, m in self._idx_path.iteritems():
 
747
                if os.path.dirname(p) == path and p != path:
 
748
                    mdobj = self.fs[m]
 
749
                    yield (os.path.basename(p), mdobj["is_dir"],
 
750
                                                            mdobj["node_id"])
 
751
 
 
752
        return sorted(_get_all())
 
753
 
 
754
    def open_file(self, mdid):
 
755
        '''returns a file like object for reading the contents of the file.'''
 
756
        mdobj = self.fs[mdid]
 
757
        if mdobj["is_dir"]:
 
758
            raise ValueError("You can only open files, not directories.")
 
759
 
 
760
        return open(self.get_abspath(mdobj['share_id'], mdobj['path']))
 
761
 
 
762
    def _share_relative_path(self, share_id, path):
 
763
        """ returns the relative path from the share_id. """
 
764
        share = self._get_share(share_id)
 
765
        if path == share.path:
 
766
            # the relaitve path is the fullpath
 
767
            return share.path
 
768
        head, sep, tail = path.rpartition(share.path)
 
769
        if sep == '':
 
770
            raise ValueError("'%s' isn't a child of '%s'" % (path, share.path))
 
771
        relpath = tail.lstrip(os.path.sep)
 
772
        # remove the initial os.path.sep
 
773
        return relpath.lstrip(os.path.sep)
 
774
 
 
775
    def _get_share(self, share_id):
 
776
        """ returns the share with id: share_id. """
 
777
        share = self.shares.get(share_id, None)
 
778
        if share is None:
 
779
            share = self.vm.shares.get(share_id)
 
780
            self.shares[share_id] = share
 
781
        return share
 
782
 
 
783
    def get_abspath(self, share_id, path):
 
784
        """ return the absolute path: share.path + path"""
 
785
        share_path = self._get_share(share_id).path
 
786
        if share_path == path:
 
787
            # the relaitve path is the fullpath
 
788
            return share_path
 
789
        else:
 
790
            return os.path.join(share_path, path)
 
791
 
 
792
    def _enable_share_write(self, share_id, path):
 
793
        """ helper to create a EnableShareWrite context manager. """
 
794
        share = self._get_share(share_id)
 
795
        return EnableShareWrite(share, path)
 
796
 
 
797
    def get_paths_starting_with(self, base_path):
 
798
        """ return a list of paths that starts with: path. """
 
799
        all_paths = []
 
800
        for path in self._idx_path:
 
801
            if path.startswith(base_path):
 
802
                mdid = self._idx_path[path]
 
803
                mdobj = self.fs[mdid]
 
804
                all_paths.append((path, mdobj['is_dir']))
 
805
        return all_paths
 
806
 
 
807
 
 
808
class EnableShareWrite(object):
 
809
    """ Context manager to allow write in ro-shares. """
 
810
    #pylint: disable-msg=W0201
 
811
 
 
812
    def __init__(self, share, path):
 
813
        """ create the instance """
 
814
        self.share = share
 
815
        self.path = path
 
816
 
 
817
    def __enter__(self):
 
818
        """ ContextManager API """
 
819
        self.ro = not self.share.can_write()
 
820
        if os.path.isdir(self.path) and \
 
821
           os.path.normpath(self.path) == os.path.normpath(self.share.path):
 
822
            self.parent = self.path
 
823
        else:
 
824
            self.parent = os.path.dirname(self.path)
 
825
 
 
826
        if self.ro:
 
827
            if not os.path.exists(self.parent):
 
828
                # if we don't have the parent yet, create it
 
829
                with EnableShareWrite(self.share, self.parent):
 
830
                    os.mkdir(self.parent)
 
831
            os.chmod(self.parent, 0755)
 
832
            if os.path.exists(self.path) and not os.path.isdir(self.path):
 
833
                os.chmod(self.path, 0744)
 
834
 
 
835
    def __exit__(self, *exc_info):
 
836
        """ ContextManager API """
 
837
        if self.ro:
 
838
            if os.path.exists(self.path):
 
839
                if os.path.isdir(self.path):
 
840
                    os.chmod(self.path, 0555)
 
841
                else:
 
842
                    os.chmod(self.path, 0444)
 
843
            if os.path.exists(self.parent):
 
844
                os.chmod(self.parent, 0555)