Logo Search packages:      
Sourcecode: ubuntuone-control-panel version File versions  Download package

backend.py

# -*- coding: utf-8 -*-

# Authors: Alejandro J. Cura <alecu@canonical.com>
# Authors: Natalia B. Bidart <nataliabidart@canonical.com>
#
# Copyright 2010 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/>.

"""A backend for the Ubuntu One Control Panel."""

from collections import defaultdict
from functools import wraps

from twisted.internet.defer import inlineCallbacks, returnValue

from ubuntuone.controlpanel import dbus_client
from ubuntuone.controlpanel import replication_client
from ubuntuone.controlpanel.logger import setup_logging, log_call
# pylint: disable=W0611
from ubuntuone.controlpanel.webclient import (UnauthorizedError,
    WebClient, WebClientError)
# pylint: enable=W0611

logger = setup_logging('backend')

ACCOUNT_API = "account/"
QUOTA_API = "quota/"
DEVICES_API = "1.0/devices/"
DEVICE_REMOVE_API = "1.0/devices/remove/%s/%s"
DEVICE_TYPE_PHONE = "Phone"
DEVICE_TYPE_COMPUTER = "Computer"
UPLOAD_KEY = "max_upload_speed"
DOWNLOAD_KEY = "max_download_speed"

FILE_SYNC_DISABLED = 'file-sync-disabled'
FILE_SYNC_DISCONNECTED = 'file-sync-disconnected'
FILE_SYNC_ERROR = 'file-sync-error'
FILE_SYNC_IDLE = 'file-sync-idle'
FILE_SYNC_STARTING = 'file-sync-starting'
FILE_SYNC_STOPPED = 'file-sync-stopped'
FILE_SYNC_SYNCING = 'file-sync-syncing'
FILE_SYNC_UNKNOWN = 'file-sync-unknown'

MSG_KEY = 'message'
STATUS_KEY = 'status'

BOOKMARKS_PKG = 'xul-ext-bindwood'
CONTACTS_PKG = 'evolution-couchdb'


def bool_str(value):
    """Return the string representation of a bool (dbus-compatible)."""
    return 'True' if value else ''


def filter_field(info, field):
    """Return a copy of 'info' where each item has 'field' hidden."""
    result = []
    for item in info:
        item = item.copy()
        item[field] = '<hidden>'
        result.append(item)
    return result


def process_unauthorized(f):
    """Decorator to catch UnauthorizedError from the webclient and act upon."""

    @inlineCallbacks
    @wraps(f)
    def inner(*args, **kwargs):
        """Handle UnauthorizedError and clear credentials."""
        try:
            result = yield f(*args, **kwargs)
        except UnauthorizedError, e:
            logger.exception('process_unauthorized (clearing credentials):')
            yield dbus_client.clear_credentials()
            raise e

        returnValue(result)

    return inner


00096 class ControlBackend(object):
    """The control panel backend."""

    ROOT_TYPE = u'ROOT'
    FOLDER_TYPE = u'UDF'
    SHARE_TYPE = u'SHARE'
    NAME_NOT_SET = u'ENAMENOTSET'
    FREE_BYTES_NOT_AVAILABLE = u'EFREEBYTESNOTAVAILABLE'
    STATUS_DISABLED = {MSG_KEY: '', STATUS_KEY: FILE_SYNC_DISABLED}

00106     def __init__(self, shutdown_func=None):
        """Initialize the webclient."""
        self.shutdown_func = shutdown_func
        self.wc = WebClient(dbus_client.get_credentials)
        self._status_changed_handler = None

        self._volumes = {}  # cache last known volume info
        self.file_sync_disabled = False

00115     def _process_file_sync_status(self, status):
        """Process raw file sync status into custom format.

        Return a dictionary with two members:
        * STATUS_KEY: the current status of syncdaemon, can be one of:
            FILE_SYNC_DISABLED, FILE_SYNC_STARTING, FILE_SYNC_DISCONNECTED,
            FILE_SYNC_SYNCING, FILE_SYNC_IDLE, FILE_SYNC_ERROR,
            FILE_SYNC_UNKNOWN
        * MSG_KEY: a non translatable but human readable string of the status.

        """
        if not status:
            self.file_sync_disabled = True
            return self.STATUS_DISABLED

        msg = '%s (%s)' % (status['description'], status['name'])
        result = {MSG_KEY: msg}

        # file synch is enabled
        is_error = bool(status['is_error'])
        is_synching = bool(status['is_connected'])
        is_idle = bool(status['is_online']) and status['queues'] == 'IDLE'
        is_disconnected = status['name'] == 'WAITING' or \
                          (status['name'] == 'READY' and \
                           'Not User' in status['connection'])
        is_starting = status['name'] in ('INIT', 'LOCAL_RESCAN', 'READY')
        is_stopped = status['name'] == 'SHUTDOWN'

        if is_error:
            result[STATUS_KEY] = FILE_SYNC_ERROR
        elif is_idle:
            result[STATUS_KEY] = FILE_SYNC_IDLE
        elif is_synching:
            result[STATUS_KEY] = FILE_SYNC_SYNCING
        elif is_disconnected:
            result[STATUS_KEY] = FILE_SYNC_DISCONNECTED
        elif is_starting:
            self.file_sync_disabled = False
            result[STATUS_KEY] = FILE_SYNC_STARTING
        elif is_stopped:
            result[STATUS_KEY] = FILE_SYNC_STOPPED
        else:
            logger.warning('file_sync_status: unknown (got %r)', status)
            result[STATUS_KEY] = FILE_SYNC_UNKNOWN

        if self.file_sync_disabled:
            return self.STATUS_DISABLED
        else:
            return result

00165     def _set_status_changed_handler(self, handler):
        """Set 'handler' to be called when file sync status changes."""
        logger.debug('setting status_changed_handler to %r', handler)

        def process_and_callback(status):
            """Process syncdaemon's status and callback 'handler'."""
            result = self._process_file_sync_status(status)
            handler(result)

        self._status_changed_handler = process_and_callback
        dbus_client.set_status_changed_handler(process_and_callback)

00177     def _get_status_changed_handler(self):
        """Return the handler to be called when file sync status changes."""
        return self._status_changed_handler

    status_changed_handler = property(_get_status_changed_handler,
                                      _set_status_changed_handler)

    @inlineCallbacks
00185     def _process_device_web_info(self, devices, enabled, limit_bw, limits,
                                 show_notifs):
        """Return a lis of processed devices."""
        result = []
        for d in devices:
            di = {}
            di["type"] = d["kind"]
            di["name"] = d["description"]
            di["configurable"] = ''
            if di["type"] == DEVICE_TYPE_COMPUTER:
                di["device_id"] = di["type"] + d["token"]
            if di["type"] == DEVICE_TYPE_PHONE:
                di["device_id"] = di["type"] + str(d["id"])

            is_local = yield self.device_is_local(di["device_id"])
            di["is_local"] = bool_str(is_local)
            # currently, only local devices are configurable.
            # eventually, more devices will be configurable.
            di["configurable"] = bool_str(is_local and enabled)

            if bool(di["configurable"]):
                di["limit_bandwidth"] = bool_str(limit_bw)
                di["show_all_notifications"] = bool_str(show_notifs)
                upload = limits["upload"]
                download = limits["download"]
                di[UPLOAD_KEY] = str(upload)
                di[DOWNLOAD_KEY] = str(download)

            # date_added is not in the webservice yet (LP: #673668)
            # di["date_added"] = ""

            # missing values (LP: #673668)
            # di["available_services"] = ""
            # di["enabled_services"] = ""

            # make a sanity check
            for key, val in di.iteritems():
                if not isinstance(val, basestring):
                    logger.warning('_process_device_web_info: (key %r), '
                                   'val %r is not a basestring.', key, val)
                    di[key] = repr(val)

            if is_local:  # prepend the local device!
                result.insert(0, di)
            else:
                result.append(di)

        returnValue(result)

    @inlineCallbacks
00235     def get_token(self):
        """Return the token from the credentials."""
        credentials = yield dbus_client.get_credentials()
        returnValue(credentials["token"])

    @inlineCallbacks
00241     def device_is_local(self, device_id):
        """Return whether 'device_id' is the local devicew or not."""
        dtype, did = self.type_n_id(device_id)
        local_token = yield self.get_token()
        is_local = (dtype == DEVICE_TYPE_COMPUTER and did == local_token)
        returnValue(is_local)

    @log_call(logger.debug)
    @process_unauthorized
    @inlineCallbacks
00251     def account_info(self):
        """Get the user account info."""
        result = {}

        account_info = yield self.wc.call_api(ACCOUNT_API)
        logger.debug('account_info from api call: %r', account_info)

        if "current_plan" in account_info:
            desc = account_info["current_plan"]
        elif "subscription" in account_info \
                 and "description" in account_info["subscription"]:
            desc = account_info["subscription"]["description"]
        else:
            desc = ''

        result["type"] = desc
        result["name"] = account_info["nickname"]
        result["email"] = account_info["email"]

        quota_info = yield self.wc.call_api(QUOTA_API)
        result["quota_total"] = str(quota_info["total"])
        result["quota_used"] = str(quota_info["used"])

        returnValue(result)

    @log_call(logger.debug)
    @process_unauthorized
    @inlineCallbacks
00279     def devices_info(self):
        """Get the user devices info."""
        result = limit_bw = limits = show_notifs = None
        enabled = yield dbus_client.files_sync_enabled()
        if enabled:
            limit_bw = yield dbus_client.bandwidth_throttling_enabled()
            show_notifs = yield dbus_client.show_all_notifications_enabled()
            limits = yield dbus_client.get_throttling_limits()

        logger.debug('devices_info: file sync enabled? %s limit_bw %s, limits '
                     '%s, show_notifs %s',
                     enabled, limit_bw, limits, show_notifs)

        try:
            devices = yield self.wc.call_api(DEVICES_API)
        except UnauthorizedError:
            raise
        except WebClientError:
            logger.exception('devices_info: web client failure:')
        else:
            result = yield self._process_device_web_info(devices, enabled,
                                                         limit_bw, limits,
                                                         show_notifs)
        if result is None:
            logger.info('devices_info: result is None after calling '
                        'devices/ API, building the local device.')
            credentials = yield dbus_client.get_credentials()
            local_device = {}
            local_device["type"] = DEVICE_TYPE_COMPUTER
            local_device["name"] = credentials['name']
            device_id = local_device["type"] + credentials["token"]
            local_device["device_id"] = device_id
            local_device["is_local"] = bool_str(True)
            local_device["configurable"] = bool_str(enabled)
            if bool(local_device["configurable"]):
                local_device["limit_bandwidth"] = bool_str(limit_bw)
                show_notifs = bool_str(show_notifs)
                local_device["show_all_notifications"] = show_notifs
                upload = limits["upload"]
                download = limits["download"]
                local_device[UPLOAD_KEY] = str(upload)
                local_device[DOWNLOAD_KEY] = str(download)
            result = [local_device]
        else:
            logger.info('devices_info: result is not None after calling '
                        'devices/ API: %r',
                        filter_field(result, field='device_id'))

        returnValue(result)

00329     def type_n_id(self, device_id):
        """Return the device type and id, as used by the /devices api."""
        if device_id.startswith(DEVICE_TYPE_COMPUTER):
            return DEVICE_TYPE_COMPUTER, device_id[8:]
        if device_id.startswith(DEVICE_TYPE_PHONE):
            return DEVICE_TYPE_PHONE, device_id[5:]
        return "No device", device_id

    @log_call(logger.info, with_args=False)
    @inlineCallbacks
00339     def change_device_settings(self, device_id, settings):
        """Change the settings for the given device."""
        is_local = yield self.device_is_local(device_id)

        if is_local and "show_all_notifications" in settings:
            show_all_notif = bool(settings["show_all_notifications"])
            if not show_all_notif:
                yield dbus_client.disable_show_all_notifications()
            else:
                yield dbus_client.enable_show_all_notifications()

        if is_local and "limit_bandwidth" in settings:
            limit_bw = bool(settings["limit_bandwidth"])
            if not limit_bw:
                yield dbus_client.disable_bandwidth_throttling()
            else:
                yield dbus_client.enable_bandwidth_throttling()

        if is_local and (UPLOAD_KEY in settings or
                         DOWNLOAD_KEY in settings):
            current_limits = yield dbus_client.get_throttling_limits()
            limits = {
                "download": current_limits["download"],
                "upload": current_limits["upload"],
            }
            if UPLOAD_KEY in settings:
                limits["upload"] = settings[UPLOAD_KEY]
            if DOWNLOAD_KEY in settings:
                limits["download"] = settings[DOWNLOAD_KEY]
            dbus_client.set_throttling_limits(limits)

        # still pending: more work on the settings dict (LP: #673674)
        returnValue(device_id)

    @log_call(logger.warning, with_args=False)
    @process_unauthorized
    @inlineCallbacks
00376     def remove_device(self, device_id):
        """Remove a device's tokens from the sso server."""
        dtype, did = self.type_n_id(device_id)
        is_local = yield self.device_is_local(device_id)

        api = DEVICE_REMOVE_API % (dtype.lower(), did)
        yield self.wc.call_api(api)

        if is_local:
            logger.warning('remove_device: device is local! removing and '
                           'clearing credentials.')
            yield dbus_client.clear_credentials()

        returnValue(device_id)

    @log_call(logger.debug)
    @inlineCallbacks
00393     def file_sync_status(self):
        """Return the status of the file sync service."""
        enabled = yield dbus_client.files_sync_enabled()
        if enabled:
            status = yield dbus_client.get_current_status()
        else:
            status = {}
        returnValue(self._process_file_sync_status(status))

    @log_call(logger.debug)
    @inlineCallbacks
00404     def enable_files(self):
        """Enable the files service."""
        yield dbus_client.set_files_sync_enabled(True)
        self.file_sync_disabled = False

    @log_call(logger.debug)
    @inlineCallbacks
00411     def disable_files(self):
        """Enable the files service."""
        yield dbus_client.set_files_sync_enabled(False)
        self.file_sync_disabled = True

    @log_call(logger.debug)
    @inlineCallbacks
00418     def connect_files(self):
        """Connect the files service."""
        yield dbus_client.connect_file_sync()

    @log_call(logger.debug)
    @inlineCallbacks
00424     def disconnect_files(self):
        """Disconnect the files service."""
        yield dbus_client.disconnect_file_sync()

    @log_call(logger.debug)
    @inlineCallbacks
00430     def restart_files(self):
        """restart the files service."""
        yield dbus_client.stop_file_sync()
        yield dbus_client.start_file_sync()

    @log_call(logger.debug)
    @inlineCallbacks
00437     def start_files(self):
        """start the files service."""
        yield dbus_client.start_file_sync()

    @log_call(logger.debug)
    @inlineCallbacks
00443     def stop_files(self):
        """stop the files service."""
        yield dbus_client.stop_file_sync()

    @log_call(logger.debug)
    @inlineCallbacks
00449     def volumes_info(self):
        """Get the volumes info."""
        self._volumes = {}

        try:
            account = yield self.account_info()
        except Exception:  # pylint: disable=W0703
            logger.exception('volumes_info: quota could not be retrieved:')
            free_bytes = self.FREE_BYTES_NOT_AVAILABLE
        else:
            free_bytes = int(account['quota_total']) - \
                         int(account['quota_used'])

        root_dir = yield dbus_client.get_root_dir()
        shares_dir = yield dbus_client.get_shares_dir()
        shares_dir_link = yield dbus_client.get_shares_dir_link()
        folders = yield dbus_client.get_folders()
        shares = yield dbus_client.get_shares()

        root_volume = {u'volume_id': u'', u'path': root_dir,
                       u'subscribed': 'True', u'type': self.ROOT_TYPE}
        self._volumes[u''] = root_volume

        # group shares by the offering user
        shares_result = defaultdict(list)
        for share in shares:
            if not bool(share['accepted']):
                continue

            share[u'type'] = self.SHARE_TYPE

            vid = share['volume_id']
            if vid in self._volumes:
                logger.warning('volumes_info: share %r already in the volumes '
                               'list (%r).', vid, self._volumes[vid])
            self._volumes[vid] = share

            nicer_path = share[u'path'].replace(shares_dir, shares_dir_link)
            share[u'path'] = nicer_path

            username = share['other_visible_name']
            if not username:
                username = u'%s (%s)' % (share['other_username'],
                                         self.NAME_NOT_SET)

            shares_result[username].append(share)

        for folder in folders:
            folder[u'type'] = self.FOLDER_TYPE

            vid = folder['volume_id']
            if vid in self._volumes:
                logger.warning('volumes_info: udf %r already in the volumes '
                               'list (%r).', vid, self._volumes[vid])
            self._volumes[vid] = folder

        result = [(u'', unicode(free_bytes), [root_volume] + folders)]

        for other_username, shares in shares_result.iteritems():
            send_freebytes = any(s['access_level'] == 'Modify' for s in shares)
            if send_freebytes:
                free_bytes = shares[0][u'free_bytes']
            else:
                free_bytes = self.FREE_BYTES_NOT_AVAILABLE
            result.append((other_username, free_bytes, shares))

        returnValue(result)

    @log_call(logger.debug)
    @inlineCallbacks
00519     def change_volume_settings(self, volume_id, settings):
        """Change settings for 'volume_id'.

        Currently, only supported setting is boolean 'subscribed'.

        """
        if 'subscribed' in settings:
            subscribed = bool(settings['subscribed'])
            if subscribed:
                yield self.subscribe_volume(volume_id)
            else:
                yield self.unsubscribe_volume(volume_id)

    @inlineCallbacks
00533     def subscribe_volume(self, volume_id):
        """Subscribe to 'volume_id'."""
        if self._volumes[volume_id][u'type'] == self.FOLDER_TYPE:
            yield dbus_client.subscribe_folder(volume_id)
        elif self._volumes[volume_id][u'type'] == self.SHARE_TYPE:
            yield dbus_client.subscribe_share(volume_id)

    @inlineCallbacks
00541     def unsubscribe_volume(self, volume_id):
        """Unsubscribe from 'volume_id'."""
        if self._volumes[volume_id][u'type'] == self.FOLDER_TYPE:
            yield dbus_client.unsubscribe_folder(volume_id)
        elif self._volumes[volume_id][u'type'] == self.SHARE_TYPE:
            yield dbus_client.unsubscribe_share(volume_id)

    @log_call(logger.debug)
    @inlineCallbacks
00550     def replications_info(self):
        """Get the user replications info."""
        replications = yield replication_client.get_replications()
        exclusions = yield replication_client.get_exclusions()

        result = []
        for rep in replications:
            dependency = ''
            if rep == replication_client.BOOKMARKS:
                dependency = BOOKMARKS_PKG
            elif rep == replication_client.CONTACTS:
                dependency = CONTACTS_PKG

            repd = {
                "replication_id": rep,
                "name": rep,  # this may change to be more user friendly
                "enabled": bool_str(rep not in exclusions),
                "dependency": dependency,
            }
            result.append(repd)

        returnValue(result)

    @log_call(logger.info)
    @inlineCallbacks
00575     def change_replication_settings(self, replication_id, settings):
        """Change the settings for the given replication."""
        if 'enabled' in settings:
            if bool(settings['enabled']):
                yield replication_client.replicate(replication_id)
            else:
                yield replication_client.exclude(replication_id)
        returnValue(replication_id)

    @log_call(logger.debug)
00585     def query_bookmark_extension(self):
        """True if the bookmark extension has been installed."""
        # still pending (LP: #673672)
        returnValue(False)

    @log_call(logger.debug)
00591     def install_bookmarks_extension(self):
        """Install the extension to sync bookmarks."""
        # still pending (LP: #673673)
        returnValue(None)

    @log_call(logger.info)
00597     def shutdown(self):
        """Stop this service."""
        # do any other needed cleanup
        if self.shutdown_func is not None:
            self.shutdown_func()

Generated by  Doxygen 1.6.0   Back to index