1
# canonical.ubuntuone.storage.syncdaemon.filesystem_manager - FSM
3
# Author: Facundo Batista <facundo@canonical.com>
5
# Copyright 2009 Canonical Ltd.
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.
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.
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
29
from canonical.ubuntuone.storage.syncdaemon import file_shelf
32
METADATA_VERSION = "2"
35
# File System Manager (FSM)
36
# --------------------------
38
# The FileSystemManager is the one that interacts with the filesystem, and
39
# keeps a storage with metadata. This storage is verterok's FileShelf.
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.
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.
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".
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.
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.
71
# Another method is provided to retrieve the created objects:
72
# get_mdobjs_by_share_id, that returns all the objects in that share.
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).
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.
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
92
# Other services are provided:
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).
98
# .upload_finished(): sets a new hash in the metadata, marking that the
99
# new content was uploaded to the server.
101
# .move_file(): moves a file or directory from one pathname to other.
103
# .delete_file(): removes a file or directory from disk.
105
# Finally, the FSM has three methods that provides high level information,
106
# in some cases synthesising their values using some internal values:
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)
112
# .changed(): returns 'local', 'server', or 'none', depending of what
113
# changed for that node
115
# .is_dir: returns if the node is a directory.
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)
125
is_forbidden = set("info path node_id share_id is_dir stat".split()
128
class InconsistencyError(Exception):
129
'''Inconsistency between internal records and filesystem itself.'''
131
class Despair(Exception):
132
'''This should never happen, we're in an impossible condition!'''
135
class _MDObject(object):
136
'''Wrapper around MD dict.'''
137
def __init__(self, **mdobj):
138
self.__dict__.update(mdobj)
140
# info is a special one
142
self.info = _MDObject(**mdobj["info"])
144
def __eq__(self, other):
145
return self.__dict__ == other.__dict__
148
class ShareNodeDict(dict):
149
'''Cache for node_id and share.'''
150
def __getitem__(self, key):
151
share_id, node_id = key
153
raise ValueError("The node_id can not be None")
154
return dict.__getitem__(self, key)
156
def __setitem__(self, key, value):
157
share_id, node_id = key
159
raise ValueError("The node_id can not be None")
160
return dict.__setitem__(self, key, value)
162
def __contains__(self, key):
163
share_id, node_id = key
165
raise ValueError("The node_id can not be None")
166
return dict.__contains__(self, key)
171
class FileSystemManager(object):
172
'''Keeps the files/dirs metadata and interacts with the filesystem.
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.
179
At init time two indexes are built in memory:
181
- idx_path: relationship path -> mdid
182
- idx_node_id: relationship (share_id, node_id) -> mdid
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" % \
188
fsmdir = os.path.join(data_dir, 'fsm')
189
self.fs = file_shelf.FileShelf(fsmdir)
195
self._idx_node_id = ShareNodeDict()
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()
205
# load the info from the metadata
206
if md_version == METADATA_VERSION:
207
self._load_metadata_updated()
209
load_method = getattr(self, "_load_metadata_%s" % md_version)
210
load_method(md_version)
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)))
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)
219
for mdid in self.fs.keys():
220
mdobj = self.fs[mdid]
222
# assure path are bytes (new to version 2)
224
mdobj["path"] = mdobj["path"].encode("utf8")
225
except UnicodeDecodeError:
226
# this is an invalid path, we shouldn't have it
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
235
# assure we have stat info (new to version 1)
236
if os.path.exists(abspath):
237
newstat = os.stat(abspath)
240
mdobj["stat"] = newstat
241
self.fs[mdid] = mdobj
244
with open(self._version_file, "w") as fh:
245
fh.write(METADATA_VERSION)
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)
251
for mdid in self.fs.keys():
252
mdobj = self.fs[mdid]
254
# assure path are bytes (new to version 2)
256
mdobj["path"] = mdobj["path"].encode("utf8")
257
except UnicodeDecodeError:
258
# this is an invalid path, we shouldn't have it
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
269
with open(self._version_file, "w") as fh:
270
fh.write(METADATA_VERSION)
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
282
def create(self, path, share_id, is_dir=False):
283
'''Creates a new md object.'''
285
raise ValueError("Empty paths are not allowed (got %r)" % path)
287
path = os.path.normpath(path)
288
if path in self._idx_path:
289
raise ValueError("The path %r is already created!" % path)
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)
302
newobj["stat"] = None
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
309
self._idx_path[path] = mdid
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)
323
share_id = mdobj["share_id"]
324
self._idx_node_id[(share_id, node_id)] = mdid
327
mdobj["node_id"] = node_id
328
mdobj["info"]["node_id_assigned"] = time.time()
329
self.fs[mdid] = mdobj
331
logger("set_node_id: path=%r mdid=%r share_id=%r node_id=%r" % (
332
path, mdid, share_id, node_id))
334
def get_mdobjs_by_share_id(self, share_id):
335
'''Returns all the mdids that belongs to a share.'''
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))
343
def get_data_for_server_rescan(self):
344
'''Generates all the (share, node, hash) tuples needed for rescan'''
346
for _, v in self.fs.items():
349
(v['share_id'], v['node_id'], v['server_hash'] or ''))
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)
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)
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)
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))
374
raise ValueError("The following attributes can not be set "
375
"externally: %s" % forbidden)
377
logger("set mdid=%r: %s" % (mdid, kwargs))
378
mdobj = self.fs[mdid]
379
for k, v in kwargs.items():
381
self.fs[mdid] = mdobj
383
def set_by_path(self, path, **kwargs):
384
'''Set some values to the md object with that path.'''
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)
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.'''
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)
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
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]
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)
417
# pylint: disable-msg=W0704
419
with contextlib.nested(from_context, to_context):
420
shutil.move(path_from, path_to)
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)
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)
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"])]
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]
446
self._idx_path[path_to] = mdid
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
457
mdobj["stat"] = os.stat(path_to) # needed if not the same FS
459
logger("Got an OSError while getting the stat of %r", path_to)
460
self.fs[mdid] = mdobj
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:])
469
# change in the index
470
mdid = self._idx_path.pop(path)
471
self._idx_path[newpath] = mdid
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
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))
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"])]
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))
503
with self._enable_share_write(mdobj['share_id'], path):
504
if self.is_dir(path=path):
509
if e.errno == errno.ENOTEMPTY:
511
m = "OSError %s when trying to remove file/dir %r"
512
log_warning(m, e, path)
513
self.delete_metadata(path)
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"
522
while os.path.exists(to_path):
524
to_path = base_to_path + "." + str(ind)
525
with self._enable_share_write(mdobj['share_id'], path):
527
os.rename(path, to_path)
529
if e.errno == errno.ENOENT:
530
m = "Already removed when trying to move to conflict: %r"
535
for p, is_dir in self.get_paths_starting_with(path):
539
# remove inotify watch
541
self.vm.m.event_q.inotify_rm_watch(p)
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"
549
self.delete_metadata(p)
550
mdobj["info"]["last_conflicted"] = time.time()
551
self.fs[mdid] = mdobj
553
def _check_partial(self, mdid):
554
'''Checks consistency between internal flag and FS regarding partial'''
556
mdobj = self.fs[mdid]
557
path = self.get_abspath(mdobj['share_id'], mdobj['path'])
558
partial_in_md = mdobj["info"]["is_partial"]
560
partial_path = os.path.join(path, ".partial")
562
partial_path = path + ".partial"
563
partial_in_disk = os.path.exists(partial_path)
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)
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'])
577
return os.path.join(path, ".partial")
579
return path + ".partial"
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))
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):
597
open(partial_path, "w").close()
599
mdobj["info"]["last_partial_created"] = time.time()
600
mdobj["info"]["is_partial"] = True
601
self.fs[mdid] = mdobj
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))
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")
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))
623
partial_path = self.get_partial_path(self.fs[mdid])
624
fd = open(partial_path, "r")
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]
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))
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))
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
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)]
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")
665
partial_path = path + ".partial"
666
with self._enable_share_write(share_id, partial_path):
667
#pylint: disable-msg=W0704
669
os.remove(partial_path)
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
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
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))
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"]
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)
713
def changed(self, **kwargs):
714
'''Return True if there's metadata for a given object.'''
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)
723
if local_hash == server_hash:
725
return "We broke the Universe! local_hash %r, server_hash %r,"\
726
" is_partial %r" % (local_hash, server_hash, is_partial)
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.")
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:
749
yield (os.path.basename(p), mdobj["is_dir"],
752
return sorted(_get_all())
754
def open_file(self, mdid):
755
'''returns a file like object for reading the contents of the file.'''
756
mdobj = self.fs[mdid]
758
raise ValueError("You can only open files, not directories.")
760
return open(self.get_abspath(mdobj['share_id'], mdobj['path']))
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
768
head, sep, tail = path.rpartition(share.path)
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)
775
def _get_share(self, share_id):
776
""" returns the share with id: share_id. """
777
share = self.shares.get(share_id, None)
779
share = self.vm.shares.get(share_id)
780
self.shares[share_id] = share
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
790
return os.path.join(share_path, path)
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)
797
def get_paths_starting_with(self, base_path):
798
""" return a list of paths that starts with: path. """
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']))
808
class EnableShareWrite(object):
809
""" Context manager to allow write in ro-shares. """
810
#pylint: disable-msg=W0201
812
def __init__(self, share, path):
813
""" create the instance """
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
824
self.parent = os.path.dirname(self.path)
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)
835
def __exit__(self, *exc_info):
836
""" ContextManager API """
838
if os.path.exists(self.path):
839
if os.path.isdir(self.path):
840
os.chmod(self.path, 0555)
842
os.chmod(self.path, 0444)
843
if os.path.exists(self.parent):
844
os.chmod(self.parent, 0555)