Logo Search packages:      
Sourcecode: ubuntuone-client version File versions

filesystem_manager.py

# ubuntuone.syncdaemon.filesystem_manager - FSM
#
# Author: Facundo Batista <facundo@canonical.com>
#
# Copyright 2009 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.
'''Module that implements the File System Manager.'''
from __future__ import with_statement

import os
import time
import shutil
import functools
import logging
import contextlib
import errno

from ubuntuone.syncdaemon import file_shelf
import uuid

METADATA_VERSION = "2"

#
# File System Manager  (FSM)
# --------------------------
#
# The FileSystemManager is the one that interacts with the filesystem, and
# keeps a storage with metadata.  This storage is verterok's FileShelf.
#
# The metadata, in disk, is a dictionary, where the keys are 'mdid's (metadata
# ids), and the values are different parameters, some of them can be modified
# from outside, and some can not.
#
# There're two very important values: path and node_id. The path is the
# pointer to where the file or directory is located in the filesystem, and
# the node_id is the unique identifier from the server. When a new file is
# created (with the .create() method), a mdid is assigned to the path and the
# share, but no node_id yet. When the server assigns the node_id, it needs to
# be set here with the .set_node_id() method.
#
# All the data can be retrieved generally using this three values (mdid, path,
# and node_id/share) using specific get_by_*() methods. For this to be fast,
# two indexes are created at init time, two dictionaries that hold the
# relationships path->mdid, and (share,node_id)->mdid.  In any case, KeyError
# is raised if an incorrect value is passed to the getters. Note that a mdid
# identifies uniquely the MD Object, like also the path; but for the node_id it
# also needs to have the share, as the same "server file" can live in different
# directories in the "client disk".
#
# Once assigned, the path, share and node_id values can not be changed. For any
# other value (except another special one, 'info', see below), three methods
# are provided to set them: set_by_*() (symmetric to the getters). These
# methods receive a first argument to indicate what is modified, and then
# several keyword arguments with all the values to be set.
#
# The 'info' is a special value set by the FileSystemManager itself, that
# records operations and changes made to each node, and as I said before,
# it can only be accesses from outside, not modified.
#
# Another method is provided to retrieve the created objects:
# get_mdobjs_by_share_id, that returns all the objects in that share.
#
# When asked for data, the FSM returns an object that is a thin wrapper to the
# info, only to be easily accessible, like using "mdobj.path", or
# "mdobj.info.is_partial". This object is not alive: it does not get updated
# if something changes in the metadata, and any change in the object is not
# written back to the metadata (this is by design, because of how the
# information flows in the system).
#
# As I said before, the FileSystemManager not only keeps the metadata, but also
# interacts with the filesystem itself. As such, it provides several operations
# on files and directories.
#
# In the process of downloading a file from the server, FSM handles the
# .partial files. With the .create_partial() method the system creates this
# special file where the new content will be downloaded. When it finishes ok,
# the .commit_partial() is called, and that content is moved into the old file.
# If the download fails for any reason, .remove_partial() is called and all is
# back to clean.
#
# Other services are provided:
#
#    .move_to_conflict(): moves a file or dir in problem to the same name but
# adding a .conflict to the name (if .conflict already exists, it will try with
# .conflict.1, .conflict.2, and so on).
#
#    .upload_finished(): sets a new hash in the metadata, marking that the
# new content was uploaded to the server.
#
#    .move_file(): moves a file or directory from one pathname to other.
#
#    .delete_file(): removes a file or directory from disk.
#
# Finally, the FSM has three methods that provides high level information,
# in some cases synthesising their values using some internal values:
#
#    .has_metadata(): returns True if the system has metadata for that path,
# node_id or mdid (note that we may don't have metadata even to an old mdid,
# because it was deleted in the middle)
#
#    .changed(): returns 'local', 'server', or 'none', depending of what
# changed for that node
#
#    .is_dir: returns if the node is a directory.
#
#


# fsm logger
fsm_logger = logging.getLogger('ubuntuone.SyncDaemon.fsm')
logger = functools.partial(fsm_logger.log, logging.INFO)
log_warning = functools.partial(fsm_logger.log, logging.WARNING)

is_forbidden = set("info path node_id share_id is_dir stat".split()
                  ).intersection

00128 class InconsistencyError(Exception):
    '''Inconsistency between internal records and filesystem itself.'''

00131 class Despair(Exception):
    '''This should never happen, we're in an impossible condition!'''


00135 class _MDObject(object):
    '''Wrapper around MD dict.'''
    def __init__(self, **mdobj):
        self.__dict__.update(mdobj)

        # info is a special one
        if "info" in mdobj:
            self.info = _MDObject(**mdobj["info"])

    def __eq__(self, other):
        return self.__dict__ == other.__dict__


00148 class ShareNodeDict(dict):
    '''Cache for node_id and share.'''
    def __getitem__(self, key):
        share_id, node_id = key
        if node_id is None:
            raise ValueError("The node_id can not be None")
        return dict.__getitem__(self, key)

    def __setitem__(self, key, value):
        share_id, node_id = key
        if node_id is None:
            raise ValueError("The node_id can not be None")
        return dict.__setitem__(self, key, value)

    def __contains__(self, key):
        share_id, node_id = key
        if node_id is None:
            raise ValueError("The node_id can not be None")
        return dict.__contains__(self, key)




00171 class FileSystemManager(object):
    '''Keeps the files/dirs metadata and interacts with the filesystem.

    It has a FileShelf where all the metadata is stored, using 'mdid's as
    keys.  'mdid' is 'metadata id'... it's actually an uuid, but we call it
    mdid to don't get confused about names, as we also have the node_id is
    the one assigned by the server.

    At init time two indexes are built in memory:

      - idx_path: relationship path -> mdid
      - idx_node_id: relationship (share_id, node_id) -> mdid
    '''
    def __init__(self, data_dir, vm):
        if not isinstance(data_dir, basestring):
            raise TypeError("data_dir should be a string instead of %s" % \
                            type(data_dir))
        fsmdir = os.path.join(data_dir, 'fsm')
        self.fs = file_shelf.FileShelf(fsmdir)
        self.shares = {}
        self.vm = vm

        # create the indexes
        self._idx_path = {}
        self._idx_node_id = ShareNodeDict()

        # get the metadata version
        self._version_file = os.path.join(data_dir, "metadata_version")
        if os.path.exists(self._version_file):
            with open(self._version_file) as fh:
                md_version = fh.read().strip()
        else:
            md_version = None

        # load the info from the metadata
        if md_version == METADATA_VERSION:
            self._load_metadata_updated()
        else:
            load_method = getattr(self, "_load_metadata_%s" % md_version)
            load_method(md_version)

        logger("initialized: idx_path: %s, idx_node_id: %s, shares: %s" %
               (len(self._idx_path), len(self._idx_node_id), len(self.shares)))

00215     def _load_metadata_None(self, old_version):
        '''Loads metadata from when it wasn't even versioned.'''
        logger("loading metadata from old version %r", old_version)

        for mdid in self.fs.keys():
            mdobj = self.fs[mdid]

            # assure path are bytes (new to version 2)
            try:
                mdobj["path"] = mdobj["path"].encode("utf8")
            except UnicodeDecodeError:
                # this is an invalid path, we shouldn't have it
                del self.fs[mdid]
                continue

            abspath = self.get_abspath(mdobj["share_id"], mdobj["path"])
            self._idx_path[abspath] = mdid
            if mdobj["node_id"] is not None:
                self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])] = mdid

            # assure we have stat info (new to version 1)
            if os.path.exists(abspath):
                newstat = os.stat(abspath)
            else:
                newstat = None
            mdobj["stat"] = newstat
            self.fs[mdid] = mdobj

        # set new version
        with open(self._version_file, "w") as fh:
            fh.write(METADATA_VERSION)

00247     def _load_metadata_1(self, old_version):
        '''Loads metadata from when it wasn't even versioned.'''
        logger("loading metadata from old version %r", old_version)

        for mdid in self.fs.keys():
            mdobj = self.fs[mdid]

            # assure path are bytes (new to version 2)
            try:
                mdobj["path"] = mdobj["path"].encode("utf8")
            except UnicodeDecodeError:
                # this is an invalid path, we shouldn't have it
                del self.fs[mdid]
                continue

            abspath = self.get_abspath(mdobj["share_id"], mdobj["path"])
            self._idx_path[abspath] = mdid
            if mdobj["node_id"] is not None:
                self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])] = mdid
            self.fs[mdid] = mdobj

        # set new version
        with open(self._version_file, "w") as fh:
            fh.write(METADATA_VERSION)

00272     def _load_metadata_updated(self):
        '''Loads metadata of last version.'''
        logger("loading updated metadata")
        for mdid in self.fs.keys():
            mdobj = self.fs[mdid]
            abspath = self.get_abspath(mdobj["share_id"], mdobj["path"])
            self._idx_path[abspath] = mdid
            if mdobj["node_id"] is not None:
                self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])] = mdid

00282     def create(self, path, share_id, is_dir=False):
        '''Creates a new md object.'''
        if not path.strip():
            raise ValueError("Empty paths are not allowed (got %r)" % path)

        path = os.path.normpath(path)
        if path in self._idx_path:
            raise ValueError("The path %r is already created!" % path)

        # create it
        mdid = str(uuid.uuid4())
        # make path relative to the share_id
        relpath = self._share_relative_path(share_id, path)
        newobj = dict(path=relpath, node_id=None, share_id=share_id,
                      is_dir=is_dir, local_hash=None,
                      server_hash=None, mdid=mdid)
        newobj["info"] = dict(created=time.time(), is_partial=False)
        if os.path.exists(path):
            newobj["stat"] = os.stat(path)
        else:
            newobj["stat"] = None

        logger("create: path=%r mdid=%r share_id=%r node_id=%r is_dir=%r" % (
                                          path, mdid, share_id, None, is_dir))
        self.fs[mdid] = newobj

        # adjust the index
        self._idx_path[path] = mdid

        return mdid

00313     def set_node_id(self, path, node_id):
        '''Sets the node_id to a md object.'''
        path = os.path.normpath(path)
        mdid = self._idx_path[path]
        mdobj = self.fs[mdid]
        if mdobj["node_id"] is not None:
            msg = "The path %r already has the node_id %r" % (path, node_id)
            raise ValueError(msg)

        # adjust the index
        share_id = mdobj["share_id"]
        self._idx_node_id[(share_id, node_id)] = mdid

        # set the node_id
        mdobj["node_id"] = node_id
        mdobj["info"]["node_id_assigned"] = time.time()
        self.fs[mdid] = mdobj

        logger("set_node_id: path=%r mdid=%r share_id=%r node_id=%r" % (
                                               path, mdid, share_id, node_id))

00334     def get_mdobjs_by_share_id(self, share_id):
        '''Returns all the mdids that belongs to a share.'''
        all_mdobjs = []
        for mdid in self.fs.keys():
            mdobj = self.fs[mdid]
            if mdobj["share_id"] == share_id:
                all_mdobjs.append(_MDObject(**mdobj))
        return all_mdobjs

00343     def get_data_for_server_rescan(self):
        '''Generates all the (share, node, hash) tuples needed for rescan'''
        all_data = []
        for _, v in self.fs.items():
            if v['node_id']:
                all_data.append(
                        (v['share_id'], v['node_id'], v['server_hash'] or ''))
        return all_data

00352     def get_by_mdid(self, mdid):
        '''Returns the md object according to the mdid.'''
        mdobj = self.fs[mdid]
        return _MDObject(**mdobj)

00357     def get_by_path(self, path):
        '''Returns the md object according to the path.'''
        path = os.path.normpath(path)
        mdid = self._idx_path[path]
        mdobj = self.fs[mdid]
        return _MDObject(**mdobj)

00364     def get_by_node_id(self, share_id, node_id):
        '''Returns the md object according to the node_id and share_id.'''
        mdid = self._idx_node_id[(share_id, node_id)]
        mdobj = self.fs[mdid]
        return _MDObject(**mdobj)

00370     def set_by_mdid(self, mdid, **kwargs):
        '''Set some values to the md object with that mdid.'''
        forbidden = is_forbidden(set(kwargs))
        if forbidden:
            raise ValueError("The following attributes can not be set "
                             "externally: %s" % forbidden)

        logger("set mdid=%r: %s" % (mdid, kwargs))
        mdobj = self.fs[mdid]
        for k, v in kwargs.items():
            mdobj[k] = v
        self.fs[mdid] = mdobj

00383     def set_by_path(self, path, **kwargs):
        '''Set some values to the md object with that path.'''
        if "mdid" in kwargs:
            raise ValueError("The mdid is forbidden to set externally")
        path = os.path.normpath(path)
        mdid = self._idx_path[path]
        self.set_by_mdid(mdid, **kwargs)

00391     def set_by_node_id(self, node_id, share_id, **kwargs):
        '''Set some values to the md object with that node_id/share_id.'''
        if "mdid" in kwargs:
            raise ValueError("The mdid is forbidden to set externally")
        mdid = self._idx_node_id[(share_id, node_id)]
        self.set_by_mdid(mdid, **kwargs)

00398     def refresh_stat(self, path):
        '''Refreshes the stat of a md object.'''
        logger("refresh stat to path=%r", path)
        mdid = self._idx_path[path]
        mdobj = self.fs[mdid]
        mdobj["stat"] = os.stat(path)
        self.fs[mdid] = mdobj

00406     def update_stat(self, mdid, stat):
        '''Updates the stat of a md object.'''
        logger("update stat of mdid=%r", mdid)
        mdobj = self.fs[mdid]
        mdobj["stat"] = stat
        self.fs[mdid] = mdobj


00414     def move_file(self, new_share_id, path_from, path_to):
        '''Moves a file/dir from one point to the other.'''
        path_from = os.path.normpath(path_from)
        path_to = os.path.normpath(path_to)
        mdid = self._idx_path[path_from]
        mdobj = self.fs[mdid]

        # move the file in the fs
        from_context = self._enable_share_write(mdobj['share_id'], path_from)
        to_context = self._enable_share_write(new_share_id, path_to)

        # pylint: disable-msg=W0704
        try:
            with contextlib.nested(from_context, to_context):
                shutil.move(path_from, path_to)
        except IOError, e:
            # file was not yet created
            m = "IOError %s when trying to move file/dir %r"
            log_warning(m, e, path_from)
        self.moved(new_share_id, path_from, path_to)

00435     def moved(self, new_share_id, path_from, path_to):
        """change the metadata of a moved file"""
        path_from = os.path.normpath(path_from)
        path_to = os.path.normpath(path_to)

        # if the move overwrites other file, adjust *that* metadata
        if path_to in self._idx_path:
            mdid = self._idx_path.pop(path_to)
            mdobj = self.fs[mdid]
            if mdobj["node_id"] is not None:
                del self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])]
            del self.fs[mdid]

        # adjust the metadata of "from" file
        mdid = self._idx_path.pop(path_from)
        logger("move_file: mdid=%r path_from=%r path_to=%r",
                                                    mdid, path_from, path_to)
        mdobj = self.fs[mdid]

        self._idx_path[path_to] = mdid

        # change the path, make it relative to the share_id
        relpath = self._share_relative_path(new_share_id, path_to)
        mdobj["path"] = relpath
        mdobj['share_id'] = new_share_id
        mdobj["info"]["last_moved_from"] = path_from
        mdobj["info"]["last_moved_time"] = time.time()
        # we try to stat, if we fail, so what?
        #pylint: disable-msg=W0704
        try:
            mdobj["stat"] = os.stat(path_to)  # needed if not the same FS
        except OSError:
            logger("Got an OSError while getting the stat of %r", path_to)
        self.fs[mdid] = mdobj

        if mdobj["is_dir"]:
            # change the path for all the children of that node
            path_from = path_from + os.path.sep
            len_from = len(path_from)
            for path in (x for x in self._idx_path if x.startswith(path_from)):
                newpath = os.path.join(path_to, path[len_from:])

                # change in the index
                mdid = self._idx_path.pop(path)
                self._idx_path[newpath] = mdid

                # and in the object itself
                mdobj = self.fs[mdid]
                relpath = self._share_relative_path(new_share_id, newpath)
                mdobj["path"] = relpath
                self.fs[mdid] = mdobj


00488     def delete_metadata(self, path):
        '''Deletes the metadata.'''
        path = os.path.normpath(path)
        mdid = self._idx_path[path]
        mdobj = self.fs[mdid]
        logger("delete metadata: path=%r mdid=%r" % (path, mdid))

        # adjust all
        del self._idx_path[path]
        if mdobj["node_id"] is not None:
            del self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])]
        del self.fs[mdid]

00501     def delete_file(self, path):
        '''Deletes a file/dir and the metadata.'''
        # adjust the metadata
        path = os.path.normpath(path)
        mdid = self._idx_path[path]
        mdobj = self.fs[mdid]
        logger("delete: path=%r mdid=%r" % (path, mdid))

        # delete the file
        try:
            with self._enable_share_write(mdobj['share_id'], path):
                if self.is_dir(path=path):
                    os.rmdir(path)
                else:
                    os.remove(path)
        except OSError, e:
            if e.errno == errno.ENOTEMPTY:
                raise
            m = "OSError %s when trying to remove file/dir %r"
            log_warning(m, e, path)
        self.delete_metadata(path)

00523     def move_to_conflict(self, mdid):
        '''Moves a file/dir to its .conflict.'''
        mdobj = self.fs[mdid]
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
        logger("move_to_conflict: path=%r mdid=%r" % (path, mdid))
        base_to_path = to_path =  path + ".conflict"
        ind = 0
        while os.path.exists(to_path):
            ind += 1
            to_path = base_to_path + "." + str(ind)
        with self._enable_share_write(mdobj['share_id'], path):
            try:
                os.rename(path, to_path)
            except OSError, e:
                if e.errno == errno.ENOENT:
                    m = "Already removed when trying to move to conflict: %r"
                    log_warning(m, path)
                else:
                    raise

        for p, is_dir in self.get_paths_starting_with(path):
            if p == path:
                continue
            if is_dir:
                # remove inotify watch
                try:
                    self.vm.m.event_q.inotify_rm_watch(p)
                except TypeError, e:
                    # pyinotify has an ugly error management, if we can call
                    # it that, :(. We handle this here because it's possible
                    # and correct that the path is not there anymore
                    m = "Error %s when trying to remove the watch on %r"
                    log_warning(m, e, path)

            self.delete_metadata(p)
        mdobj["info"]["last_conflicted"] = time.time()
        self.fs[mdid] = mdobj

00561     def _check_partial(self, mdid):
        '''Checks consistency between internal flag and FS regarding partial'''
        # get the values
        mdobj = self.fs[mdid]
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
        partial_in_md = mdobj["info"]["is_partial"]
        if mdobj["is_dir"]:
            partial_path = os.path.join(path, ".partial")
        else:
            partial_path = path + ".partial"
        partial_in_disk = os.path.exists(partial_path)

        # check and return
        if partial_in_md != partial_in_disk:
            msg = "'partial' inconsistency for object with mdid %r!  In disk:"\
                  " %s, In MD: %s" % (mdid, partial_in_disk, partial_in_md)
            raise InconsistencyError(msg)
        return partial_in_md

00580     def get_partial_path(self, mdobj):
        '''Gets the path of the .partial file for a given mdobj'''
        is_dir = mdobj["is_dir"]
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
        if is_dir:
            return os.path.join(path, ".partial")
        else:
            return path + ".partial"

00589     def create_partial(self, node_id, share_id):
        '''Creates a .partial in disk and set the flag in metadata.'''
        mdid = self._idx_node_id[(share_id, node_id)]
        if self._check_partial(mdid):
            raise ValueError("The object with share_id %r and node_id %r is "
                             "already partial!" % (share_id, node_id))

        # create an empty partial and set the flag
        mdobj = self.fs[mdid]
        is_dir = mdobj["is_dir"]
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
        partial_path = self.get_partial_path(mdobj)
        with self._enable_share_write(share_id, partial_path):
            # if we don't have the dir yet, create it
            if is_dir and not os.path.exists(path):
                os.mkdir(path)
            open(partial_path, "w").close()

        mdobj["info"]["last_partial_created"] = time.time()
        mdobj["info"]["is_partial"] = True
        self.fs[mdid] = mdobj

00611     def get_partial_for_writing(self, node_id, share_id):
        '''Get a write-only fd to a partial file'''
        mdid = self._idx_node_id[(share_id, node_id)]
        if not self._check_partial(mdid):
            raise ValueError("The object with share_id %r and node_id %r is "
                             "not partial!" % (share_id, node_id))

        mdobj = self.fs[mdid]
        partial_path = self.get_partial_path(mdobj)
        with self._enable_share_write(share_id, partial_path):
            fh = open(partial_path, "w")
        return fh

00624     def get_partial(self, node_id, share_id):
        '''Gets a read-only fd to a partial file.'''
        mdid = self._idx_node_id[(share_id, node_id)]
        if not self._check_partial(mdid):
            raise ValueError("The object with share_id %r and node_id %r is "
                             "not partial!" % (share_id, node_id))

        partial_path = self.get_partial_path(self.fs[mdid])
        fd = open(partial_path, "r")
        return fd

00635     def commit_partial(self, node_id, share_id, local_hash):
        '''Creates a .partial in disk and set the flag in metadata.'''
        mdid = self._idx_node_id[(share_id, node_id)]
        mdobj = self.fs[mdid]
        if mdobj["is_dir"]:
            raise ValueError("Directory partials can not be commited!")
        if not self._check_partial(mdid):
            raise ValueError("The object with share_id %r and node_id %r is "
                             "not partial!" % (share_id, node_id))

        # move the .partial to the real path, and set the md info
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
        logger("commit_partial: path=%r mdid=%r share_id=%r node_id=%r" %
                                            (path, mdid, share_id, node_id))

        partial_path = path + ".partial"
        partial_context =  self._enable_share_write(share_id, partial_path)
        path_context = self._enable_share_write(share_id, path)
        with contextlib.nested(partial_context, path_context):
            shutil.move(path + ".partial", path)
        mdobj["local_hash"] = local_hash
        mdobj["info"]["last_downloaded"] = time.time()
        mdobj["info"]["is_partial"] = False
        mdobj["stat"] = os.stat(path)
        self.fs[mdid] = mdobj

00661     def remove_partial(self, node_id, share_id):
        '''Removes a .partial in disk and set the flag in metadata.'''
        mdid = self._idx_node_id[(share_id, node_id)]

        # delete the .partial, and set the md info
        mdobj = self.fs[mdid]
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
        logger("remove_partial: path=%r mdid=%r share_id=%r node_id=%r" %
                                            (path, mdid, share_id, node_id))
        if self.is_dir(path=path):
            partial_path = os.path.join(path, ".partial")
        else:
            partial_path = path + ".partial"
        with self._enable_share_write(share_id, partial_path):
            #pylint: disable-msg=W0704
            try:
                os.remove(partial_path)
            except OSError, e:
                # we only remove it if its there.
                m = "OSError %s when trying to remove partial_path %r"
                log_warning(m, e, partial_path)
        mdobj["info"]["last_partial_removed"] = time.time()
        mdobj["info"]["is_partial"] = False
        self.fs[mdid] = mdobj

00686     def upload_finished(self, mdid, server_hash):
        '''Sets the metadata with timestamp and server hash.'''
        mdobj = self.fs[mdid]
        mdobj["info"]["last_uploaded"] = time.time()
        mdobj["server_hash"] = server_hash
        self.fs[mdid] = mdobj

00693     def _get_mdid_from_args(self, kwargs, parent):
        '''Parses the kwargs and gets the mdid.'''
        if len(kwargs) == 1 and "path" in kwargs:
            path = os.path.normpath(kwargs["path"])
            return self._idx_path[path]
        if len(kwargs) == 1 and "mdid" in kwargs:
            return kwargs["mdid"]
        if len(kwargs) == 2 and "node_id" in kwargs and "share_id" in kwargs:
            return self._idx_node_id[(kwargs["share_id"], kwargs["node_id"])]
        raise TypeError("Incorrect arguments for %r: %r" % (parent, kwargs))

00704     def is_dir(self, **kwargs):
        '''Return True if the path of a given object is a directory.'''
        mdid = self._get_mdid_from_args(kwargs, "is_dir")
        mdobj = self.fs[mdid]
        return mdobj["is_dir"]

00710     def has_metadata(self, **kwargs):
        '''Return True if there's metadata for a given object.'''
        if len(kwargs) == 1 and "path" in kwargs:
            path = os.path.normpath(kwargs["path"])
            return path in self._idx_path
        if len(kwargs) == 1 and "mdid" in kwargs:
            return kwargs["mdid"] in self.fs
        if len(kwargs) == 2 and "node_id" in kwargs and "share_id" in kwargs:
            return (kwargs["share_id"], kwargs["node_id"]) in self._idx_node_id
        raise TypeError("Incorrect arguments for 'has_metadata': %r" % kwargs)

00721     def changed(self, **kwargs):
        '''Return True if there's metadata for a given object.'''
        # get the values
        mdid = self._get_mdid_from_args(kwargs, "changed")
        mdobj = self.fs[mdid]
        is_partial = mdobj["info"]["is_partial"]
        local_hash = mdobj.get("local_hash", False)
        server_hash = mdobj.get("server_hash", False)

        # return the status
        if local_hash == server_hash:
            if is_partial:
                return "We broke the Universe! local_hash %r, server_hash %r,"\
                       " is_partial %r" % (local_hash, server_hash, is_partial)
            else:
                return 'NONE'
        else:
            if is_partial:
                return 'SERVER'
            else:
                return 'LOCAL'
        return

00744     def dir_content(self, path):
        '''Returns the content of the directory in a server-comparable way.'''
        path = os.path.normpath(path)
        mdid = self._idx_path[path]
        mdobj = self.fs[mdid]
        if not mdobj["is_dir"]:
            raise ValueError("You can ask dir_content only on a directory.")

        def _get_all():
            '''find the mdids that match'''
            for p, m in self._idx_path.iteritems():
                if os.path.dirname(p) == path and p != path:
                    mdobj = self.fs[m]
                    yield (os.path.basename(p), mdobj["is_dir"],
                                                            mdobj["node_id"])

        return sorted(_get_all())

00762     def open_file(self, mdid):
        '''returns a file like object for reading the contents of the file.'''
        mdobj = self.fs[mdid]
        if mdobj["is_dir"]:
            raise ValueError("You can only open files, not directories.")

        return open(self.get_abspath(mdobj['share_id'], mdobj['path']))

00770     def create_file(self, mdid):
        '''create the file.'''
        mdobj = self.fs[mdid]
        path = self.get_abspath(mdobj['share_id'], mdobj['path'])
        with self._enable_share_write(mdobj['share_id'], path):
            if mdobj["is_dir"]:
                os.mkdir(path)
            else:
                # use os.open so this wont raise IN_CLOSE_WRITE
                fd = os.open(path, os.O_CREAT)
                os.close(fd)

00782     def _share_relative_path(self, share_id, path):
        """ returns the relative path from the share_id. """
        share = self._get_share(share_id)
        if path == share.path:
            # the relaitve path is the fullpath
            return share.path
        head, sep, tail = path.rpartition(share.path)
        if sep == '':
            raise ValueError("'%s' isn't a child of '%s'" % (path, share.path))
        relpath = tail.lstrip(os.path.sep)
        # remove the initial os.path.sep
        return relpath.lstrip(os.path.sep)

00795     def _get_share(self, share_id):
        """ returns the share with id: share_id. """
        share = self.shares.get(share_id, None)
        if share is None:
            share = self.vm.shares.get(share_id)
            self.shares[share_id] = share
        return share

00803     def get_abspath(self, share_id, path):
        """ return the absolute path: share.path + path"""
        share_path = self._get_share(share_id).path
        if share_path == path:
            # the relaitve path is the fullpath
            return share_path
        else:
            return os.path.join(share_path, path)

00812     def _enable_share_write(self, share_id, path):
        """ helper to create a EnableShareWrite context manager. """
        share = self._get_share(share_id)
        return EnableShareWrite(share, path)

00817     def get_paths_starting_with(self, base_path):
        """ return a list of paths that starts with: path. """
        all_paths = []
        for path in self._idx_path:
            if path.startswith(base_path):
                mdid = self._idx_path[path]
                mdobj = self.fs[mdid]
                all_paths.append((path, mdobj['is_dir']))
        return all_paths


00828 class EnableShareWrite(object):
    """ Context manager to allow write in ro-shares. """
    #pylint: disable-msg=W0201

00832     def __init__(self, share, path):
        """ create the instance """
        self.share = share
        self.path = path

00837     def __enter__(self):
        """ ContextManager API """
        self.ro = not self.share.can_write()
        if os.path.isdir(self.path) and \
           os.path.normpath(self.path) == os.path.normpath(self.share.path):
            self.parent = self.path
        else:
            self.parent = os.path.dirname(self.path)

        if self.ro:
            if not os.path.exists(self.parent):
                # if we don't have the parent yet, create it
                with EnableShareWrite(self.share, self.parent):
                    os.mkdir(self.parent)
            os.chmod(self.parent, 0755)
            if os.path.exists(self.path) and not os.path.isdir(self.path):
                os.chmod(self.path, 0744)

00855     def __exit__(self, *exc_info):
        """ ContextManager API """
        if self.ro:
            if os.path.exists(self.path):
                if os.path.isdir(self.path):
                    os.chmod(self.path, 0555)
                else:
                    os.chmod(self.path, 0444)
            if os.path.exists(self.parent):
                os.chmod(self.parent, 0555)

Generated by  Doxygen 1.6.0   Back to index