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

« back to all changes in this revision

Viewing changes to ubuntuone/syncdaemon/filesystem_manager.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.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 ubuntuone.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 update_stat(self, mdid, stat):
 
407
        '''Updates the stat of a md object.'''
 
408
        logger("update stat of mdid=%r", mdid)
 
409
        mdobj = self.fs[mdid]
 
410
        mdobj["stat"] = stat
 
411
        self.fs[mdid] = mdobj
 
412
 
 
413
 
 
414
    def move_file(self, new_share_id, path_from, path_to):
 
415
        '''Moves a file/dir from one point to the other.'''
 
416
        path_from = os.path.normpath(path_from)
 
417
        path_to = os.path.normpath(path_to)
 
418
        mdid = self._idx_path[path_from]
 
419
        mdobj = self.fs[mdid]
 
420
 
 
421
        # move the file in the fs
 
422
        from_context = self._enable_share_write(mdobj['share_id'], path_from)
 
423
        to_context = self._enable_share_write(new_share_id, path_to)
 
424
 
 
425
        # pylint: disable-msg=W0704
 
426
        try:
 
427
            with contextlib.nested(from_context, to_context):
 
428
                shutil.move(path_from, path_to)
 
429
        except IOError, e:
 
430
            # file was not yet created
 
431
            m = "IOError %s when trying to move file/dir %r"
 
432
            log_warning(m, e, path_from)
 
433
        self.moved(new_share_id, path_from, path_to)
 
434
 
 
435
    def moved(self, new_share_id, path_from, path_to):
 
436
        """change the metadata of a moved file"""
 
437
        path_from = os.path.normpath(path_from)
 
438
        path_to = os.path.normpath(path_to)
 
439
 
 
440
        # if the move overwrites other file, adjust *that* metadata
 
441
        if path_to in self._idx_path:
 
442
            mdid = self._idx_path.pop(path_to)
 
443
            mdobj = self.fs[mdid]
 
444
            if mdobj["node_id"] is not None:
 
445
                del self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])]
 
446
            del self.fs[mdid]
 
447
 
 
448
        # adjust the metadata of "from" file
 
449
        mdid = self._idx_path.pop(path_from)
 
450
        logger("move_file: mdid=%r path_from=%r path_to=%r",
 
451
                                                    mdid, path_from, path_to)
 
452
        mdobj = self.fs[mdid]
 
453
 
 
454
        self._idx_path[path_to] = mdid
 
455
 
 
456
        # change the path, make it relative to the share_id
 
457
        relpath = self._share_relative_path(new_share_id, path_to)
 
458
        mdobj["path"] = relpath
 
459
        mdobj['share_id'] = new_share_id
 
460
        mdobj["info"]["last_moved_from"] = path_from
 
461
        mdobj["info"]["last_moved_time"] = time.time()
 
462
        # we try to stat, if we fail, so what?
 
463
        #pylint: disable-msg=W0704
 
464
        try:
 
465
            mdobj["stat"] = os.stat(path_to)  # needed if not the same FS
 
466
        except OSError:
 
467
            logger("Got an OSError while getting the stat of %r", path_to)
 
468
        self.fs[mdid] = mdobj
 
469
 
 
470
        if mdobj["is_dir"]:
 
471
            # change the path for all the children of that node
 
472
            path_from = path_from + os.path.sep
 
473
            len_from = len(path_from)
 
474
            for path in (x for x in self._idx_path if x.startswith(path_from)):
 
475
                newpath = os.path.join(path_to, path[len_from:])
 
476
 
 
477
                # change in the index
 
478
                mdid = self._idx_path.pop(path)
 
479
                self._idx_path[newpath] = mdid
 
480
 
 
481
                # and in the object itself
 
482
                mdobj = self.fs[mdid]
 
483
                relpath = self._share_relative_path(new_share_id, newpath)
 
484
                mdobj["path"] = relpath
 
485
                self.fs[mdid] = mdobj
 
486
 
 
487
 
 
488
    def delete_metadata(self, path):
 
489
        '''Deletes the metadata.'''
 
490
        path = os.path.normpath(path)
 
491
        mdid = self._idx_path[path]
 
492
        mdobj = self.fs[mdid]
 
493
        logger("delete metadata: path=%r mdid=%r" % (path, mdid))
 
494
 
 
495
        # adjust all
 
496
        del self._idx_path[path]
 
497
        if mdobj["node_id"] is not None:
 
498
            del self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])]
 
499
        del self.fs[mdid]
 
500
 
 
501
    def delete_file(self, path):
 
502
        '''Deletes a file/dir and the metadata.'''
 
503
        # adjust the metadata
 
504
        path = os.path.normpath(path)
 
505
        mdid = self._idx_path[path]
 
506
        mdobj = self.fs[mdid]
 
507
        logger("delete: path=%r mdid=%r" % (path, mdid))
 
508
 
 
509
        # delete the file
 
510
        try:
 
511
            with self._enable_share_write(mdobj['share_id'], path):
 
512
                if self.is_dir(path=path):
 
513
                    os.rmdir(path)
 
514
                else:
 
515
                    os.remove(path)
 
516
        except OSError, e:
 
517
            if e.errno == errno.ENOTEMPTY:
 
518
                raise
 
519
            m = "OSError %s when trying to remove file/dir %r"
 
520
            log_warning(m, e, path)
 
521
        self.delete_metadata(path)
 
522
 
 
523
    def move_to_conflict(self, mdid):
 
524
        '''Moves a file/dir to its .conflict.'''
 
525
        mdobj = self.fs[mdid]
 
526
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
527
        logger("move_to_conflict: path=%r mdid=%r" % (path, mdid))
 
528
        base_to_path = to_path =  path + ".conflict"
 
529
        ind = 0
 
530
        while os.path.exists(to_path):
 
531
            ind += 1
 
532
            to_path = base_to_path + "." + str(ind)
 
533
        with self._enable_share_write(mdobj['share_id'], path):
 
534
            try:
 
535
                os.rename(path, to_path)
 
536
            except OSError, e:
 
537
                if e.errno == errno.ENOENT:
 
538
                    m = "Already removed when trying to move to conflict: %r"
 
539
                    log_warning(m, path)
 
540
                else:
 
541
                    raise
 
542
 
 
543
        for p, is_dir in self.get_paths_starting_with(path):
 
544
            if p == path:
 
545
                continue
 
546
            if is_dir:
 
547
                # remove inotify watch
 
548
                try:
 
549
                    self.vm.m.event_q.inotify_rm_watch(p)
 
550
                except TypeError, e:
 
551
                    # pyinotify has an ugly error management, if we can call
 
552
                    # it that, :(. We handle this here because it's possible
 
553
                    # and correct that the path is not there anymore
 
554
                    m = "Error %s when trying to remove the watch on %r"
 
555
                    log_warning(m, e, path)
 
556
 
 
557
            self.delete_metadata(p)
 
558
        mdobj["info"]["last_conflicted"] = time.time()
 
559
        self.fs[mdid] = mdobj
 
560
 
 
561
    def _check_partial(self, mdid):
 
562
        '''Checks consistency between internal flag and FS regarding partial'''
 
563
        # get the values
 
564
        mdobj = self.fs[mdid]
 
565
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
566
        partial_in_md = mdobj["info"]["is_partial"]
 
567
        if mdobj["is_dir"]:
 
568
            partial_path = os.path.join(path, ".partial")
 
569
        else:
 
570
            partial_path = path + ".partial"
 
571
        partial_in_disk = os.path.exists(partial_path)
 
572
 
 
573
        # check and return
 
574
        if partial_in_md != partial_in_disk:
 
575
            msg = "'partial' inconsistency for object with mdid %r!  In disk:"\
 
576
                  " %s, In MD: %s" % (mdid, partial_in_disk, partial_in_md)
 
577
            raise InconsistencyError(msg)
 
578
        return partial_in_md
 
579
 
 
580
    def get_partial_path(self, mdobj):
 
581
        '''Gets the path of the .partial file for a given mdobj'''
 
582
        is_dir = mdobj["is_dir"]
 
583
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
584
        if is_dir:
 
585
            return os.path.join(path, ".partial")
 
586
        else:
 
587
            return path + ".partial"
 
588
 
 
589
    def create_partial(self, node_id, share_id):
 
590
        '''Creates a .partial in disk and set the flag in metadata.'''
 
591
        mdid = self._idx_node_id[(share_id, node_id)]
 
592
        if self._check_partial(mdid):
 
593
            raise ValueError("The object with share_id %r and node_id %r is "
 
594
                             "already partial!" % (share_id, node_id))
 
595
 
 
596
        # create an empty partial and set the flag
 
597
        mdobj = self.fs[mdid]
 
598
        is_dir = mdobj["is_dir"]
 
599
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
600
        partial_path = self.get_partial_path(mdobj)
 
601
        with self._enable_share_write(share_id, partial_path):
 
602
            # if we don't have the dir yet, create it
 
603
            if is_dir and not os.path.exists(path):
 
604
                os.mkdir(path)
 
605
            open(partial_path, "w").close()
 
606
 
 
607
        mdobj["info"]["last_partial_created"] = time.time()
 
608
        mdobj["info"]["is_partial"] = True
 
609
        self.fs[mdid] = mdobj
 
610
 
 
611
    def get_partial_for_writing(self, node_id, share_id):
 
612
        '''Get a write-only fd to a partial file'''
 
613
        mdid = self._idx_node_id[(share_id, node_id)]
 
614
        if not self._check_partial(mdid):
 
615
            raise ValueError("The object with share_id %r and node_id %r is "
 
616
                             "not partial!" % (share_id, node_id))
 
617
 
 
618
        mdobj = self.fs[mdid]
 
619
        partial_path = self.get_partial_path(mdobj)
 
620
        with self._enable_share_write(share_id, partial_path):
 
621
            fh = open(partial_path, "w")
 
622
        return fh
 
623
 
 
624
    def get_partial(self, node_id, share_id):
 
625
        '''Gets a read-only fd to a partial file.'''
 
626
        mdid = self._idx_node_id[(share_id, node_id)]
 
627
        if not self._check_partial(mdid):
 
628
            raise ValueError("The object with share_id %r and node_id %r is "
 
629
                             "not partial!" % (share_id, node_id))
 
630
 
 
631
        partial_path = self.get_partial_path(self.fs[mdid])
 
632
        fd = open(partial_path, "r")
 
633
        return fd
 
634
 
 
635
    def commit_partial(self, node_id, share_id, local_hash):
 
636
        '''Creates a .partial in disk and set the flag in metadata.'''
 
637
        mdid = self._idx_node_id[(share_id, node_id)]
 
638
        mdobj = self.fs[mdid]
 
639
        if mdobj["is_dir"]:
 
640
            raise ValueError("Directory partials can not be commited!")
 
641
        if not self._check_partial(mdid):
 
642
            raise ValueError("The object with share_id %r and node_id %r is "
 
643
                             "not partial!" % (share_id, node_id))
 
644
 
 
645
        # move the .partial to the real path, and set the md info
 
646
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
647
        logger("commit_partial: path=%r mdid=%r share_id=%r node_id=%r" %
 
648
                                            (path, mdid, share_id, node_id))
 
649
 
 
650
        partial_path = path + ".partial"
 
651
        partial_context =  self._enable_share_write(share_id, partial_path)
 
652
        path_context = self._enable_share_write(share_id, path)
 
653
        with contextlib.nested(partial_context, path_context):
 
654
            shutil.move(path + ".partial", path)
 
655
        mdobj["local_hash"] = local_hash
 
656
        mdobj["info"]["last_downloaded"] = time.time()
 
657
        mdobj["info"]["is_partial"] = False
 
658
        mdobj["stat"] = os.stat(path)
 
659
        self.fs[mdid] = mdobj
 
660
 
 
661
    def remove_partial(self, node_id, share_id):
 
662
        '''Removes a .partial in disk and set the flag in metadata.'''
 
663
        mdid = self._idx_node_id[(share_id, node_id)]
 
664
 
 
665
        # delete the .partial, and set the md info
 
666
        mdobj = self.fs[mdid]
 
667
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
668
        logger("remove_partial: path=%r mdid=%r share_id=%r node_id=%r" %
 
669
                                            (path, mdid, share_id, node_id))
 
670
        if self.is_dir(path=path):
 
671
            partial_path = os.path.join(path, ".partial")
 
672
        else:
 
673
            partial_path = path + ".partial"
 
674
        with self._enable_share_write(share_id, partial_path):
 
675
            #pylint: disable-msg=W0704
 
676
            try:
 
677
                os.remove(partial_path)
 
678
            except OSError, e:
 
679
                # we only remove it if its there.
 
680
                m = "OSError %s when trying to remove partial_path %r"
 
681
                log_warning(m, e, partial_path)
 
682
        mdobj["info"]["last_partial_removed"] = time.time()
 
683
        mdobj["info"]["is_partial"] = False
 
684
        self.fs[mdid] = mdobj
 
685
 
 
686
    def upload_finished(self, mdid, server_hash):
 
687
        '''Sets the metadata with timestamp and server hash.'''
 
688
        mdobj = self.fs[mdid]
 
689
        mdobj["info"]["last_uploaded"] = time.time()
 
690
        mdobj["server_hash"] = server_hash
 
691
        self.fs[mdid] = mdobj
 
692
 
 
693
    def _get_mdid_from_args(self, kwargs, parent):
 
694
        '''Parses the kwargs and gets the mdid.'''
 
695
        if len(kwargs) == 1 and "path" in kwargs:
 
696
            path = os.path.normpath(kwargs["path"])
 
697
            return self._idx_path[path]
 
698
        if len(kwargs) == 1 and "mdid" in kwargs:
 
699
            return kwargs["mdid"]
 
700
        if len(kwargs) == 2 and "node_id" in kwargs and "share_id" in kwargs:
 
701
            return self._idx_node_id[(kwargs["share_id"], kwargs["node_id"])]
 
702
        raise TypeError("Incorrect arguments for %r: %r" % (parent, kwargs))
 
703
 
 
704
    def is_dir(self, **kwargs):
 
705
        '''Return True if the path of a given object is a directory.'''
 
706
        mdid = self._get_mdid_from_args(kwargs, "is_dir")
 
707
        mdobj = self.fs[mdid]
 
708
        return mdobj["is_dir"]
 
709
 
 
710
    def has_metadata(self, **kwargs):
 
711
        '''Return True if there's metadata for a given object.'''
 
712
        if len(kwargs) == 1 and "path" in kwargs:
 
713
            path = os.path.normpath(kwargs["path"])
 
714
            return path in self._idx_path
 
715
        if len(kwargs) == 1 and "mdid" in kwargs:
 
716
            return kwargs["mdid"] in self.fs
 
717
        if len(kwargs) == 2 and "node_id" in kwargs and "share_id" in kwargs:
 
718
            return (kwargs["share_id"], kwargs["node_id"]) in self._idx_node_id
 
719
        raise TypeError("Incorrect arguments for 'has_metadata': %r" % kwargs)
 
720
 
 
721
    def changed(self, **kwargs):
 
722
        '''Return True if there's metadata for a given object.'''
 
723
        # get the values
 
724
        mdid = self._get_mdid_from_args(kwargs, "changed")
 
725
        mdobj = self.fs[mdid]
 
726
        is_partial = mdobj["info"]["is_partial"]
 
727
        local_hash = mdobj.get("local_hash", False)
 
728
        server_hash = mdobj.get("server_hash", False)
 
729
 
 
730
        # return the status
 
731
        if local_hash == server_hash:
 
732
            if is_partial:
 
733
                return "We broke the Universe! local_hash %r, server_hash %r,"\
 
734
                       " is_partial %r" % (local_hash, server_hash, is_partial)
 
735
            else:
 
736
                return 'NONE'
 
737
        else:
 
738
            if is_partial:
 
739
                return 'SERVER'
 
740
            else:
 
741
                return 'LOCAL'
 
742
        return
 
743
 
 
744
    def dir_content(self, path):
 
745
        '''Returns the content of the directory in a server-comparable way.'''
 
746
        path = os.path.normpath(path)
 
747
        mdid = self._idx_path[path]
 
748
        mdobj = self.fs[mdid]
 
749
        if not mdobj["is_dir"]:
 
750
            raise ValueError("You can ask dir_content only on a directory.")
 
751
 
 
752
        def _get_all():
 
753
            '''find the mdids that match'''
 
754
            for p, m in self._idx_path.iteritems():
 
755
                if os.path.dirname(p) == path and p != path:
 
756
                    mdobj = self.fs[m]
 
757
                    yield (os.path.basename(p), mdobj["is_dir"],
 
758
                                                            mdobj["node_id"])
 
759
 
 
760
        return sorted(_get_all())
 
761
 
 
762
    def open_file(self, mdid):
 
763
        '''returns a file like object for reading the contents of the file.'''
 
764
        mdobj = self.fs[mdid]
 
765
        if mdobj["is_dir"]:
 
766
            raise ValueError("You can only open files, not directories.")
 
767
 
 
768
        return open(self.get_abspath(mdobj['share_id'], mdobj['path']))
 
769
 
 
770
    def create_file(self, mdid):
 
771
        '''create the file.'''
 
772
        mdobj = self.fs[mdid]
 
773
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
 
774
        with self._enable_share_write(mdobj['share_id'], path):
 
775
            if mdobj["is_dir"]:
 
776
                os.mkdir(path)
 
777
            else:
 
778
                # use os.open so this wont raise IN_CLOSE_WRITE
 
779
                fd = os.open(path, os.O_CREAT)
 
780
                os.close(fd)
 
781
 
 
782
    def _share_relative_path(self, share_id, path):
 
783
        """ returns the relative path from the share_id. """
 
784
        share = self._get_share(share_id)
 
785
        if path == share.path:
 
786
            # the relaitve path is the fullpath
 
787
            return share.path
 
788
        head, sep, tail = path.rpartition(share.path)
 
789
        if sep == '':
 
790
            raise ValueError("'%s' isn't a child of '%s'" % (path, share.path))
 
791
        relpath = tail.lstrip(os.path.sep)
 
792
        # remove the initial os.path.sep
 
793
        return relpath.lstrip(os.path.sep)
 
794
 
 
795
    def _get_share(self, share_id):
 
796
        """ returns the share with id: share_id. """
 
797
        share = self.shares.get(share_id, None)
 
798
        if share is None:
 
799
            share = self.vm.shares.get(share_id)
 
800
            self.shares[share_id] = share
 
801
        return share
 
802
 
 
803
    def get_abspath(self, share_id, path):
 
804
        """ return the absolute path: share.path + path"""
 
805
        share_path = self._get_share(share_id).path
 
806
        if share_path == path:
 
807
            # the relaitve path is the fullpath
 
808
            return share_path
 
809
        else:
 
810
            return os.path.join(share_path, path)
 
811
 
 
812
    def _enable_share_write(self, share_id, path):
 
813
        """ helper to create a EnableShareWrite context manager. """
 
814
        share = self._get_share(share_id)
 
815
        return EnableShareWrite(share, path)
 
816
 
 
817
    def get_paths_starting_with(self, base_path):
 
818
        """ return a list of paths that starts with: path. """
 
819
        all_paths = []
 
820
        for path in self._idx_path:
 
821
            if path.startswith(base_path):
 
822
                mdid = self._idx_path[path]
 
823
                mdobj = self.fs[mdid]
 
824
                all_paths.append((path, mdobj['is_dir']))
 
825
        return all_paths
 
826
 
 
827
 
 
828
class EnableShareWrite(object):
 
829
    """ Context manager to allow write in ro-shares. """
 
830
    #pylint: disable-msg=W0201
 
831
 
 
832
    def __init__(self, share, path):
 
833
        """ create the instance """
 
834
        self.share = share
 
835
        self.path = path
 
836
 
 
837
    def __enter__(self):
 
838
        """ ContextManager API """
 
839
        self.ro = not self.share.can_write()
 
840
        if os.path.isdir(self.path) and \
 
841
           os.path.normpath(self.path) == os.path.normpath(self.share.path):
 
842
            self.parent = self.path
 
843
        else:
 
844
            self.parent = os.path.dirname(self.path)
 
845
 
 
846
        if self.ro:
 
847
            if not os.path.exists(self.parent):
 
848
                # if we don't have the parent yet, create it
 
849
                with EnableShareWrite(self.share, self.parent):
 
850
                    os.mkdir(self.parent)
 
851
            os.chmod(self.parent, 0755)
 
852
            if os.path.exists(self.path) and not os.path.isdir(self.path):
 
853
                os.chmod(self.path, 0744)
 
854
 
 
855
    def __exit__(self, *exc_info):
 
856
        """ ContextManager API """
 
857
        if self.ro:
 
858
            if os.path.exists(self.path):
 
859
                if os.path.isdir(self.path):
 
860
                    os.chmod(self.path, 0555)
 
861
                else:
 
862
                    os.chmod(self.path, 0444)
 
863
            if os.path.exists(self.parent):
 
864
                os.chmod(self.parent, 0555)