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

volume_manager.py

# ubuntuone.syncdaemon.volume_manager - manages volumes
#
# Author: Guillermo Gonzalez <guillermo.gonzalez@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/>.
""" The all mighty Volume Manager """
from __future__ import with_statement

import logging
import os
import shutil
import stat
from contextlib import contextmanager
from itertools import ifilter

from ubuntuone.syncdaemon.marker import MDMarker
from ubuntuone.syncdaemon import file_shelf


00032 class Share(object):
    """Representas a share or mount point"""

00035     def __init__(self, path, share_id='', name=None, access_level='View',
                 accepted=False, other_username=None, other_visible_name=None,
                 subtree=None):
        """ Creates the instance.

        The received path should be 'bytes'
        """
        if path is None:
            self.path = None
        else:
            self.path = os.path.normpath(path)
        self.id = str(share_id)
        self.access_level = access_level
        self.accepted = accepted
        self.name = name
        self.other_username = other_username
        self.other_visible_name = other_visible_name
        self.subtree = subtree

    @classmethod
00055     def from_response(cls, share_response, path):
        """ Creates a Share instance from a ShareResponse.

        The received path should be 'bytes'
        """
        share = cls(path, str(share_response.id), share_response.name,
                    share_response.access_level, share_response.accepted,
                    share_response.other_username,
                    share_response.other_visible_name, share_response.subtree)
        return share

    @classmethod
00067     def from_notify_holder(cls, share_notify, path):
        """ Creates a Share instance from a NotifyShareHolder.

        The received path should be 'bytes'
        """
        share = cls(path, share_id=str(share_notify.share_id),
                    name=share_notify.share_name,
                    access_level=share_notify.access_level,
                    other_username=share_notify.from_username,
                    other_visible_name=share_notify.from_visible_name,
                    subtree=share_notify.subtree)
        return share

00080     def can_write(self):
        """ check the access_level of this share,
        returns True if it's 'Modify'.
        """
        return self.access_level == 'Modify'


00087 class VolumeManager(object):
    """Manages shares and mount points."""

    CACHE_VERSION = '1'

00092     def __init__(self, main):
        """Create the instance and populate the shares/d attributes
        from the metadata (if it exists).
        """
        self.log = logging.getLogger('ubuntuone.SyncDaemon.VM')
        self.m = main
        data_dir = os.path.join(self.m.data_dir, 'vm')
        version_file = os.path.join(data_dir, '.version')
        shares_dir = os.path.join(data_dir, 'shares')
        shared_dir = os.path.join(data_dir, 'shared')
        if os.path.exists(data_dir) and not os.path.exists(version_file) \
           and not os.path.exists(shares_dir):
            self.upgrade_shelf_layout(data_dir, shares_dir)
        # write down the version file if it don't exists
        if not os.path.exists(version_file):
            if not os.path.exists(os.path.dirname(version_file)):
                os.makedirs(os.path.dirname(version_file))
            with open(version_file, 'w') as fd:
                fd.write(VolumeManager.CACHE_VERSION)
        if not os.path.exists(shares_dir):
            os.makedirs(shares_dir)
        if not os.path.exists(shared_dir):
            os.makedirs(shared_dir)
        self.shares = ShareFileShelf(shares_dir)
        self.shared = ShareFileShelf(shared_dir)
        if self.shares.get('') is None:
            self.root = Share(self.m.root_dir)
        else:
            self.root = self.shares['']
        self.root.access_level = 'Modify'
        self.root.path = self.m.root_dir
        self.shares[''] = self.root
        with allow_writes(os.path.dirname(self.m.shares_dir)):
            if not os.path.exists(self.m.shares_dir):
                os.makedirs(self.m.shares_dir)
                # make it read only
            os.chmod(self.m.shares_dir, 0555)
        self.marker_share_map = {}
        self.list_shares_retries = 0
        self.retries_limit = 5

00133     def init_root(self):
        """ Creates the root mdid. """
        self._create_share_dir(self.root)
        try:
            mdobj = self.m.fs.get_by_path(self.root.path)
        except KeyError:
            mdobj = self.m.fs.create(path=self.root.path,
                                     share_id='', is_dir=True)
            self.m.fs.set_by_path(path=self.root.path,
                                  local_hash=None, server_hash=None)

00144     def on_server_root(self, root):
        """Asociate server root"""
        self.log.debug('init_root(%s)', root)
        mdobj = self.m.fs.get_by_path(self.root.path)
        if getattr(mdobj, 'node_id', None) is None:
            self.m.fs.set_node_id(self.root.path, root)
        share = self.shares['']
        self.root.subtree = share.subtree = root
        self.shares[''] = share
        self.refresh_shares()
        return mdobj.mdid

00156     def refresh_shares(self):
        """ Reuqest the list of shares to the server. """
        # request the list of shares
        self.m.action_q.list_shares()

00161     def handle_SYS_STATE_CHANGED(self, state):
        """
        The system changed state. If we don't know our root's uuid yet,
        and the AQ is online, ask for it.
        """
        if state.is_connected and self.root.subtree is None:
            try:
                mdobj = self.m.fs.get_by_path(self.root.path)
                mdid = mdobj.mdid
            except KeyError:
                mdid = self.m.fs.create(path=self.root.path, share_id='',
                                         is_dir=True)
                self.m.fs.set_by_path(path=self.root.path,
                                      local_hash=None, server_hash=None)
            self.m.get_root(MDMarker(mdid))

00177     def handle_AQ_SHARES_LIST(self, shares_list):
        """ handle AQ_SHARES_LIST event """
        self.log.debug('handling shares list: ')
        self.list_shares_retries = 0
        shares = []
        shared = []
        for a_share in shares_list.shares:
            share_id = getattr(a_share, 'id',
                               getattr(a_share, 'share_id', None))
            self.log.debug('share %r: id=%s, name=%r', a_share.direction,
                           share_id, a_share.name)
            if a_share.direction == "to_me":
                dir_name = self._build_dirname(a_share.name,
                                               a_share.other_visible_name)
                path = os.path.join(self.m.shares_dir, dir_name)
                share = Share.from_response(a_share, path)
                shares.append(share.id)
                self.add_share(share)
            elif a_share.direction == "from_me":
                try:
                    mdobj = self.m.fs.get_by_node_id("", a_share.subtree)
                    path = self.m.fs.get_abspath(mdobj.share_id, mdobj.path)
                except KeyError:
                    # we don't have the file/md of this shared subtree yet
                    # for the moment ignore this share
                    self.log.warning("we got a share with 'from_me' direction,"
                            " but don't have the node_id in the metadata yet")
                    path = None
                share = Share.from_response(a_share, path)
                shared.append(share.id)
                self.add_shared(share)

        # housekeeping of the shares and shared shelf's each time we get the
        # list of shares
        self.log.debug('deleting dead shares')
        for share in ifilter(lambda item: item and item not in shares,
                             self.shares):
            self.log.debug('deleting share: id=%s', share)
            self.share_deleted(share)
        for share in ifilter(lambda item: item and item not in shared,
                             self.shared):
            self.log.debug('deleting shared: id=%s', share)
            del self.shared[share]

00221     def _build_dirname(self, share_name, visible_name):
        '''Builds the root path using the share information.'''
        dir_name = share_name + u' from ' + visible_name

        # Unicode boundary! the name is Unicode in protocol and server,
        # but here we use bytes for paths
        dir_name = dir_name.encode("utf8")
        return dir_name

00230     def handle_AQ_LIST_SHARES_ERROR(self, error):
        """ handle AQ_LIST_SHARES_ERROR event """
        # just call list_shares again, until we reach the retry limit
        if self.list_shares_retries <= self.retries_limit:
            self.m.action_q.list_shares()
            self.list_shares_retries += 1

00237     def handle_SV_SHARE_CHANGED(self, message, share):
        """ handle SV_SHARE_CHANGED event """
        if message == 'changed':
            if share.share_id not in self.shares:
                self.log.debug("New share notification, share_id: %s",
                         share.share_id)
                dir_name = self._build_dirname(share.share_name,
                                               share.from_visible_name)
                path = os.path.join(self.m.shares_dir, dir_name)
                share = Share.from_notify_holder(share, path)
                self.add_share(share)
            else:
                self.log.debug('share changed! %s', share.share_id)
                self.share_changed(share)
        elif message == 'deleted':
            self.log.debug('share deleted! %s', share.share_id)
            self.share_deleted(share.share_id)

00255     def handle_AQ_CREATE_SHARE_OK(self, share_id, marker):
        """ handle AQ_CREATE_SHARE_OK event. """
        share = self.marker_share_map.get(marker)
        if share is None:
            self.m.action_q.list_shares()
        else:
            share.id = share_id
            self.add_shared(share)
            if marker in self.marker_share_map:
                del self.marker_share_map[marker]

00266     def handle_AQ_CREATE_SHARE_ERROR(self, marker, error):
        """ handle AQ_CREATE_SHARE_ERROR event. """
        if marker in self.marker_share_map:
            del self.marker_share_map[marker]

00271     def handle_SV_SHARE_ANSWERED(self, share_id, answer):
        """ handle SV_SHARE_ANSWERED event. """
        share = self.shared.get(share_id, None)
        if share is None:
            # oops, we got an answer for a share we don't have,
            # probably created from the web.
            # refresh the share list
            self.refresh_shares()
        else:
            share.accepted = True if answer == 'Yes' else False
            self.shared[share_id] = share

00283     def add_share(self, share):
        """ Add a share to the share list, and creates the fs mdobj. """
        self.log.info('Adding new share with id: %s - path: %r',
                      share.id, share.path)
        if share.id in self.shares:
            del self.shares[share.id]
        self.shares[share.id] = share
        if share.accepted:
            self._create_fsm_object(share)
            self._create_share_dir(share)
            self.m.action_q.query([(share.id, str(share.subtree), "")])

00295     def accept_share(self, share_id, answer):
        """ Calls AQ.accept_share with answer ('Yes'/'No')."""
        self.log.debug("Accept share, with id: %s - answer: %s ",
                       share_id, answer)
        share = self.shares[share_id]
        share.accepted = answer
        self.shares[share_id] =  share
        answer_str = "Yes" if answer else "No"
        d = self.m.action_q.answer_share(share_id, answer_str)
        def answer_ok(result):
            """ create the share, fsm object, and request a query. """
            if answer:
                self._create_fsm_object(share)
                self._create_share_dir(share)
                self.m.action_q.query([(share.id, str(share.subtree), "")])
        d.addCallback(answer_ok)
        return d

00313     def share_deleted(self, share_id):
        """ process the share deleted event. """
        self.log.debug("Share (id: %s) deleted. ", share_id)
        share = self.shares.get(share_id, None)
        if share is None:
            # we don't have this share, ignore it
            self.log.warning("Got a share deleted notification (%r), "
                             "but don't have the share", share_id)
        else:
            self._delete_fsm_object(share)
            del self.shares[share_id]

00325     def share_changed(self, share_holder):
        """ process the share changed event """
        share = self.shares.get(share_holder.share_id, None)
        if share is None:
            # we don't have this share, ignore it
            self.log.warning("Got a share changed notification (%r), "
                             "but don't have the share", share_holder.share_id)
        else:
            share.access_level = share_holder.access_level
            self.shares[share_holder.share_id] = share

00336     def _create_share_dir(self, share):
        """ Creates the share root dir, and set the permissions. """
        # XXX: verterok: This is going to be moved into fsm
        # if the share don't exists, create it
        if not os.path.exists(share.path):
            with allow_writes(os.path.dirname(share.path)):
                os.mkdir(share.path)
            # add the watch after the mkdir
            if share.can_write():
                self.log.debug('adding inotify watch to: %s', share.path)
                self.m.event_q.inotify_add_watch(share.path)
        # if it's a ro share, change the perms
        if not share.can_write():
            os.chmod(share.path, 0555)

00351     def _create_fsm_object(self, share):
        """ Creates the mdobj for this share in fs manager. """
        try:
            self.m.fs.get_by_path(share.path)
        except KeyError:
            self.m.fs.create(path=share.path, share_id=share.id, is_dir=True)
            self.m.fs.set_node_id(share.path, share.subtree)
            self.m.fs.set_by_path(path=share.path, **dict(local_hash=None,
                                                          server_hash=None,))

00361     def _delete_fsm_object(self, share):
        """ Deletes the share and it files/folders metadata from fsm. """
        #XXX: partially implemented, this should be moved into fsm?.
        # should delete all the files in the share?
        if share.can_write():
            try:
                self.m.event_q.inotify_rm_watch(share.path)
            except (ValueError, RuntimeError, 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
                self.log.warning("Error %s when trying to remove the watch"
                                 " on %r", e, share.path)
        # delete all the metadata but dont touch the files/folders
        for path, is_dir in self.m.fs.get_paths_starting_with(share.path):
            self.m.fs.delete_metadata(path)

00378     def create_share(self, path, username, name, access_level):
        """ create a share for the specified path, username, name """
        self.log.debug('create share(%r, %s, %s, %s)',
                       path, username, name, access_level)
        mdobj = self.m.fs.get_by_path(path)
        mdid = self.m.fs.get_by_node_id(mdobj.share_id, mdobj.node_id)
        marker = MDMarker(mdid)
        share = Share(self.m.fs.get_abspath("", mdobj.path), share_id=marker,
                      name=name, access_level=access_level,
                      other_username=username, other_visible_name=None,
                      subtree=mdobj.node_id)
        self.marker_share_map[marker] = share
        self.m.action_q.create_share(mdobj.node_id, username, name,
                                     access_level, marker)

00393     def add_shared(self, share):
        """ Add a share with direction == from_me """
        self.log.info('New shared subtree: id: %s - path: %r',
                      share.id, share.path)
        current_share = self.shared.get(share.id)
        if current_share is None:
            self.shared[share.id] = share
        else:
            for k in share.__dict__:
                setattr(current_share, k, getattr(share, k))
            self.shared[share.id] = current_share

00405     def upgrade_shelf_layout(self, data_dir, shares_dir):
        """ Upgrade the shelf layout"""
        self.log.debug('Upgrading the share shelf layout')
        # the shelf already exists, and don't have a .version file
        # first backup the old data
        backup = os.path.join(data_dir, '0.bkp')
        if not os.path.exists(backup):
            os.makedirs(backup)
        for dirname, dirs, files in os.walk(data_dir):
            if dirname == data_dir:
                for dir in dirs:
                    if dir != os.path.basename(backup):
                        shutil.move(os.path.join(dirname, dir),
                                    os.path.join(backup, dir))
        # regenerate the shelf using the new layout using the backup as src
        old_shelf = ShareFileShelf(backup)
        os.makedirs(shares_dir)
        new_shelf = ShareFileShelf(shares_dir)
        for key in old_shelf.keys():
            new_shelf[key] = old_shelf[key]


@contextmanager
def allow_writes(path):
    """ a very simple context manager to allow writting in RO dirs. """
    prev_mod = stat.S_IMODE(os.stat(path).st_mode)
    os.chmod(path, 0755)
    yield
    os.chmod(path, prev_mod)


00436 class ShareFileShelf(file_shelf.FileShelf):
    """ Custom file shelf that allow '' as key, it's replaced by the string:
    root_node_id.
    """

00441     def __init__(self, *args, **kwargs):
        """ Create the instance. """
        super(ShareFileShelf, self).__init__(*args, **kwargs)
        self.key = 'root_node_id'

00446     def key_file(self, key):
        """ override default key_file, to handle key == ''"""
        if key == '':
            key = self.key
        return super(ShareFileShelf, self).key_file(key)

00452     def keys(self):
        """ override default keys, to handle key == ''"""
        for key in super(ShareFileShelf, self).keys():
            if key == self.key:
                yield ''
            else:
                yield key


Generated by  Doxygen 1.6.0   Back to index