Beispiel #1
0
class WebApi(JSONComponent):
    """
    The component that implements all the methods required for managing
    the web interface. The complete web json interface also exposes all the
    methods available from the core RPC.
    """

    XSS_VULN_KEYS = ['name', 'message', 'comment', 'tracker_status', 'peers']

    def __init__(self):
        super(WebApi, self).__init__('Web', depend=['SessionProxy'])
        self.hostlist = HostList()
        self.core_config = CoreConfig()
        self.event_queue = EventQueue()
        try:
            self.sessionproxy = component.get('SessionProxy')
        except KeyError:
            self.sessionproxy = SessionProxy()

    def disable(self):
        client.deregister_event_handler('PluginEnabledEvent',
                                        self._json.get_remote_methods)
        client.deregister_event_handler('PluginDisabledEvent',
                                        self._json.get_remote_methods)

        if client.is_standalone():
            component.get('Web.PluginManager').stop()
        else:
            client.disconnect()
            client.set_disconnect_callback(None)

    def enable(self):
        client.register_event_handler('PluginEnabledEvent',
                                      self._json.get_remote_methods)
        client.register_event_handler('PluginDisabledEvent',
                                      self._json.get_remote_methods)

        if client.is_standalone():
            component.get('Web.PluginManager').start()
        else:
            client.set_disconnect_callback(self._on_client_disconnect)
            default_host_id = component.get(
                'DelugeWeb').config['default_daemon']
            if default_host_id:
                return self.connect(default_host_id)

        return defer.succeed(True)

    def _on_client_connect(self, *args):
        """Handles client successfully connecting to the daemon.

        Invokes retrieving the method names and starts webapi and plugins.

        """
        d_methods = self._json.get_remote_methods()
        component.get('Web.PluginManager').start()
        self.start()
        return d_methods

    def _on_client_connect_fail(self, result, host_id):
        log.error(
            'Unable to connect to daemon, check host_id "%s" is correct.',
            host_id)

    def _on_client_disconnect(self, *args):
        component.get('Web.PluginManager').stop()
        return self.stop()

    def start(self):
        self.core_config.start()
        return self.sessionproxy.start()

    def stop(self):
        self.core_config.stop()
        self.sessionproxy.stop()
        return defer.succeed(True)

    @export
    def connect(self, host_id):
        """Connect the web client to a daemon.

        Args:
            host_id (str): The id of the daemon in the host list.

        Returns:
            Deferred: List of methods the daemon supports.
        """
        d = self.hostlist.connect_host(host_id)
        d.addCallback(self._on_client_connect)
        d.addErrback(self._on_client_connect_fail, host_id)
        return d

    @export
    def connected(self):
        """
        The current connection state.

        :returns: True if the client is connected
        :rtype: boolean
        """
        return client.connected()

    @export
    def disconnect(self):
        """
        Disconnect the web interface from the connected daemon.
        """
        d = client.disconnect()

        def on_disconnect(reason):
            return str(reason)

        d.addCallback(on_disconnect)
        return d

    @export
    def update_ui(self, keys, filter_dict):
        """
        Gather the information required for updating the web interface.

        :param keys: the information about the torrents to gather
        :type keys: list
        :param filter_dict: the filters to apply when selecting torrents.
        :type filter_dict: dictionary
        :returns: The torrent and UI information.
        :rtype: dictionary
        """
        d = Deferred()
        ui_info = {
            'connected': client.connected(),
            'torrents': None,
            'filters': None,
            'stats': {
                'max_download':
                self.core_config.get('max_download_speed'),
                'max_upload':
                self.core_config.get('max_upload_speed'),
                'max_num_connections':
                self.core_config.get('max_connections_global'),
            },
        }

        if not client.connected():
            d.callback(ui_info)
            return d

        def got_stats(stats):
            ui_info['stats']['num_connections'] = stats['num_peers']
            ui_info['stats']['upload_rate'] = stats['payload_upload_rate']
            ui_info['stats']['download_rate'] = stats['payload_download_rate']
            ui_info['stats']['download_protocol_rate'] = (
                stats['download_rate'] - stats['payload_download_rate'])
            ui_info['stats']['upload_protocol_rate'] = (
                stats['upload_rate'] - stats['payload_upload_rate'])
            ui_info['stats']['dht_nodes'] = stats['dht_nodes']
            ui_info['stats']['has_incoming_connections'] = stats[
                'has_incoming_connections']

        def got_filters(filters):
            ui_info['filters'] = filters

        def got_free_space(free_space):
            ui_info['stats']['free_space'] = free_space

        def got_external_ip(external_ip):
            ui_info['stats']['external_ip'] = external_ip

        def got_torrents(torrents):
            ui_info['torrents'] = torrents

        def on_complete(result):
            d.callback(ui_info)

        d1 = component.get('SessionProxy').get_torrents_status(
            filter_dict, keys)
        d1.addCallback(got_torrents)

        d2 = client.core.get_filter_tree()
        d2.addCallback(got_filters)

        d3 = client.core.get_session_status([
            'num_peers',
            'payload_download_rate',
            'payload_upload_rate',
            'download_rate',
            'upload_rate',
            'dht_nodes',
            'has_incoming_connections',
        ])
        d3.addCallback(got_stats)

        d4 = client.core.get_free_space(
            self.core_config.get('download_location'))
        d4.addCallback(got_free_space)

        d5 = client.core.get_external_ip()
        d5.addCallback(got_external_ip)

        dl = DeferredList([d1, d2, d3, d4, d5], consumeErrors=True)
        dl.addCallback(on_complete)
        return d

    def _on_got_files(self, torrent, d):
        files = torrent.get('files')
        file_progress = torrent.get('file_progress')
        file_priorities = torrent.get('file_priorities')

        paths = []
        info = {}
        for index, torrent_file in enumerate(files):
            path = xml_escape(torrent_file['path'])
            paths.append(path)
            torrent_file['progress'] = file_progress[index]
            torrent_file['priority'] = file_priorities[index]
            torrent_file['index'] = index
            torrent_file['path'] = path
            info[path] = torrent_file

            # update the directory info
            dirname = os.path.dirname(path)
            while dirname:
                dirinfo = info.setdefault(dirname, {})
                dirinfo['size'] = dirinfo.get('size', 0) + torrent_file['size']
                if 'priority' not in dirinfo:
                    dirinfo['priority'] = torrent_file['priority']
                else:
                    if dirinfo['priority'] != torrent_file['priority']:
                        dirinfo['priority'] = 9

                progresses = dirinfo.setdefault('progresses', [])
                progresses.append(torrent_file['size'] *
                                  torrent_file['progress'] / 100)
                dirinfo['progress'] = sum(progresses) / dirinfo['size'] * 100
                dirinfo['path'] = dirname
                dirname = os.path.dirname(dirname)

        def walk(path, item):
            if item['type'] == 'dir':
                item.update(info[path])
                return item
            else:
                item.update(info[path])
                return item

        file_tree = FileTree2(paths)
        file_tree.walk(walk)
        d.callback(file_tree.get_tree())

    def _on_torrent_status(self, torrent, d):
        for key in self.XSS_VULN_KEYS:
            try:
                if key == 'peers':
                    for peer in torrent[key]:
                        peer['client'] = xml_escape(peer['client'])
                else:
                    torrent[key] = xml_escape(torrent[key])
            except KeyError:
                pass
        d.callback(torrent)

    @export
    def get_torrent_status(self, torrent_id, keys):
        """Get the status for a torrent, filtered by status keys."""
        main_deferred = Deferred()
        d = component.get('SessionProxy').get_torrent_status(torrent_id, keys)
        d.addCallback(self._on_torrent_status, main_deferred)
        return main_deferred

    @export
    def get_torrent_files(self, torrent_id):
        """
        Gets the files for a torrent in tree format

        :param torrent_id: the id of the torrent to retrieve.
        :type torrent_id: string
        :returns: The torrents files in a tree
        :rtype: dictionary
        """
        main_deferred = Deferred()
        d = component.get('SessionProxy').get_torrent_status(
            torrent_id, FILES_KEYS)
        d.addCallback(self._on_got_files, main_deferred)
        return main_deferred

    @export
    def download_torrent_from_url(self, url, cookie=None):
        """
        Download a torrent file from a URL to a temporary directory.

        :param url: the URL of the torrent
        :type url: string
        :returns: the temporary file name of the torrent file
        :rtype: string
        """
        def on_download_success(result):
            log.debug('Successfully downloaded %s to %s', url, result)
            return result

        def on_download_fail(result):
            log.error('Failed to add torrent from url %s', url)
            return result

        tempdir = tempfile.mkdtemp(prefix='delugeweb-')
        tmp_file = os.path.join(tempdir, url.split('/')[-1])
        log.debug('filename: %s', tmp_file)
        headers = {}
        if cookie:
            headers['Cookie'] = cookie
            log.debug('cookie: %s', cookie)
        d = httpdownloader.download_file(url, tmp_file, headers=headers)
        d.addCallbacks(on_download_success, on_download_fail)
        return d

    @export
    def get_torrent_info(self, filename):
        """
        Return information about a torrent on the filesystem.

        :param filename: the path to the torrent
        :type filename: string

        :returns: information about the torrent:

        ::

            {
                "name": the torrent name,
                "files_tree": the files the torrent contains,
                "info_hash" the torrents info_hash
            }

        :rtype: dictionary
        """
        try:
            torrent_info = TorrentInfo(filename.strip(), 2)
            return torrent_info.as_dict('name', 'info_hash', 'files_tree')
        except Exception as ex:
            log.error(ex)
            return False

    @export
    def get_magnet_info(self, uri):
        """Parse a magnet URI for hash and name."""
        return get_magnet_info(uri)

    @export
    def add_torrents(self, torrents):
        """
        Add torrents by file

        :param torrents: A list of dictionaries containing the torrent \
            path and torrent options to add with.
        :type torrents: list

        ::

            json_api.web.add_torrents([{
                "path": "/tmp/deluge-web/some-torrent-file.torrent",
                "options": {"download_location": "/home/deluge/"}
            }])

        """
        deferreds = []

        for torrent in torrents:
            if is_magnet(torrent['path']):
                log.info(
                    'Adding torrent from magnet uri `%s` with options `%r`',
                    torrent['path'],
                    torrent['options'],
                )
                d = client.core.add_torrent_magnet(torrent['path'],
                                                   torrent['options'])
                deferreds.append(d)
            else:
                filename = os.path.basename(torrent['path'])
                with open(torrent['path'], 'rb') as _file:
                    fdump = b64encode(_file.read())
                log.info(
                    'Adding torrent from file `%s` with options `%r`',
                    filename,
                    torrent['options'],
                )
                d = client.core.add_torrent_file_async(filename, fdump,
                                                       torrent['options'])
                deferreds.append(d)
        return DeferredList(deferreds, consumeErrors=False)

    def _get_host(self, host_id):
        """Information about a host from supplied host id.

        Args:
            host_id (str): The id of the host.

        Returns:
            list: The host information, empty list if not found.

        """
        return list(self.hostlist.get_host_info(host_id))

    @export
    def get_hosts(self):
        """
        Return the hosts in the hostlist.
        """
        log.debug('get_hosts called')
        return self.hostlist.get_hosts_info()

    @export
    def get_host_status(self, host_id):
        """
        Returns the current status for the specified host.

        :param host_id: the hash id of the host
        :type host_id: string

        """
        def response(result):
            return result

        return self.hostlist.get_host_status(host_id).addCallback(response)

    @export
    def add_host(self, host, port, username='', password=''):
        """Adds a host to the list.

        Args:
            host (str): The IP or hostname of the deluge daemon.
            port (int): The port of the deluge daemon.
            username (str): The username to login to the daemon with.
            password (str): The password to login to the daemon with.

        Returns:
            tuple: A tuple of (bool, str). If True will contain the host_id, otherwise
                if False will contain the error message.
        """
        try:
            host_id = self.hostlist.add_host(host, port, username, password)
        except ValueError as ex:
            return False, str(ex)
        else:
            return True, host_id

    @export
    def edit_host(self, host_id, host, port, username='', password=''):
        """Edit host details in the hostlist.

        Args:
            host_id (str): The host identifying hash.
            host (str): The IP or hostname of the deluge daemon.
            port (int): The port of the deluge daemon.
            username (str): The username to login to the daemon with.
            password (str): The password to login to the daemon with.

        Returns:
            bool: True if successful, False otherwise.

        """
        return self.hostlist.update_host(host_id, host, port, username,
                                         password)

    @export
    def remove_host(self, host_id):
        """Removes a host from the hostlist.

        Args:
            host_id (str): The host identifying hash.

        Returns:
            bool: True if successful, False otherwise.

        """
        return self.hostlist.remove_host(host_id)

    @export
    def start_daemon(self, port):
        """
        Starts a local daemon.
        """
        client.start_daemon(port, get_config_dir())

    @export
    def stop_daemon(self, host_id):
        """
        Stops a running daemon.

        :param host_id: the hash id of the host
        :type host_id: string
        """
        main_deferred = Deferred()
        host = self._get_host(host_id)
        if not host:
            main_deferred.callback((False, _('Daemon does not exist')))
            return main_deferred

        try:

            def on_connect(connected, c):
                if not connected:
                    main_deferred.callback((False, _('Daemon not running')))
                    return
                c.daemon.shutdown()
                main_deferred.callback((True, ))

            def on_connect_failed(reason):
                main_deferred.callback((False, reason))

            host, port, user, password = host[1:5]
            c = Client()
            d = c.connect(host, port, user, password)
            d.addCallback(on_connect, c)
            d.addErrback(on_connect_failed)
        except Exception:
            main_deferred.callback((False, 'An error occurred'))
        return main_deferred

    @export
    def get_config(self):
        """
        Get the configuration dictionary for the web interface.

        :rtype: dictionary
        :returns: the configuration
        """
        config = component.get('DelugeWeb').config.config.copy()
        del config['sessions']
        del config['pwd_salt']
        del config['pwd_sha1']
        return config

    @export
    def set_config(self, config):
        """
        Sets the configuration dictionary for the web interface.

        :param config: The configuration options to update
        :type config: dictionary
        """
        web_config = component.get('DelugeWeb').config
        for key in config:
            if key in ['sessions', 'pwd_salt', 'pwd_sha1']:
                log.warning('Ignored attempt to overwrite web config key: %s',
                            key)
                continue
            web_config[key] = config[key]

    @export
    def get_plugins(self):
        """All available and enabled plugins within WebUI.

        Note:
            This does not represent all plugins from deluge.client.core.

        Returns:
            dict: A dict containing 'available_plugins' and 'enabled_plugins' lists.

        """

        return {
            'enabled_plugins':
            list(component.get('Web.PluginManager').plugins),
            'available_plugins':
            component.get('Web.PluginManager').available_plugins,
        }

    @export
    def get_plugin_info(self, name):
        """Get the details for a plugin."""
        return component.get('Web.PluginManager').get_plugin_info(name)

    @export
    def get_plugin_resources(self, name):
        """Get the resource data files for a plugin."""
        return component.get('Web.PluginManager').get_plugin_resources(name)

    @export
    def upload_plugin(self, filename, path):
        """Upload a plugin to config."""
        main_deferred = Deferred()

        shutil.copyfile(path,
                        os.path.join(get_config_dir(), 'plugins', filename))
        component.get('Web.PluginManager').scan_for_plugins()

        if client.is_localhost():
            client.core.rescan_plugins()
            return True
        with open(path, 'rb') as _file:
            plugin_data = b64encode(_file.read())

        def on_upload_complete(*args):
            client.core.rescan_plugins()
            component.get('Web.PluginManager').scan_for_plugins()
            main_deferred.callback(True)

        def on_upload_error(*args):
            main_deferred.callback(False)

        d = client.core.upload_plugin(filename, plugin_data)
        d.addCallback(on_upload_complete)
        d.addErrback(on_upload_error)
        return main_deferred

    @export
    def register_event_listener(self, event):
        """
        Add a listener to the event queue.

        :param event: The event name
        :type event: string
        """
        self.event_queue.add_listener(__request__.session_id, event)

    @export
    def deregister_event_listener(self, event):
        """
        Remove an event listener from the event queue.

        :param event: The event name
        :type event: string
        """
        self.event_queue.remove_listener(__request__.session_id, event)

    @export
    def get_events(self):
        """
        Retrieve the pending events for the session.
        """
        return self.event_queue.get_events(__request__.session_id)
class ConnectionManager(BaseMode, PopupsHandler):

    def __init__(self, stdscr, encoding=None):
        PopupsHandler.__init__(self)
        self.statuses = {}
        self.all_torrents = None
        self.hostlist = HostList()
        self.update_hosts_status()
        BaseMode.__init__(self, stdscr, encoding=encoding)
        self.update_select_host_popup()

    def update_select_host_popup(self):
        selected_index = self.popup.current_selection() if self.popup else None

        popup = SelectablePopup(
            self, _('Select Host'), self._host_selected, border_off_west=1, active_wrap=True)
        popup.add_header(
            "{!white,black,bold!}'Q'=%s, 'a'=%s, 'D'=%s" %
            (_('Quit'), _('Add Host'), _('Delete Host')), space_below=True)
        self.push_popup(popup, clear=True)

        for host_entry in self.hostlist.get_hosts_info():
            host_id, hostname, port, user = host_entry
            args = {'data': host_id, 'foreground': 'red'}
            state = 'Offline'
            if host_id in self.statuses:
                state = 'Online'
                args.update({'data': self.statuses[host_id], 'foreground': 'green'})
            host_str = '%s:%d [%s]' % (hostname, port, state)
            self.popup.add_line(host_id, host_str, selectable=True, use_underline=True, **args)

        if selected_index:
            self.popup.set_selection(selected_index)
        self.inlist = True
        self.refresh()

    def update_hosts_status(self):
        for host_entry in self.hostlist.get_hosts_info():
            def on_host_status(status_info):
                self.statuses[status_info[0]] = status_info
                self.update_select_host_popup()
            self.hostlist.get_host_status(host_entry[0]).addCallback(on_host_status)

    def _on_connected(self, result):
        def on_console_start(result):
            component.get('ConsoleUI').set_mode('TorrentList')
        d = component.get('ConsoleUI').start_console()
        d.addCallback(on_console_start)

    def _on_connect_fail(self, result):
        self.report_message('Failed to connect!', result)
        self.refresh()
        if hasattr(result, 'getTraceback'):
            log.exception(result)

    def _host_selected(self, selected_host, *args, **kwargs):
        if selected_host in self.statuses:
            d = self.hostlist.connect_host(selected_host)
            d.addCallback(self._on_connected)
            d.addErrback(self._on_connect_fail)

    def _do_add(self, result, **kwargs):
        if not result or kwargs.get('close', False):
            self.pop_popup()
        else:
            self.add_host(result['hostname']['value'], result['port']['value'],
                          result['username']['value'], result['password']['value'])

    def add_popup(self):
        self.inlist = False
        popup = InputPopup(
            self,
            _('Add Host (Up & Down arrows to navigate, Esc to cancel)'),
            border_off_north=1,
            border_off_east=1,
            close_cb=self._do_add)
        popup.add_text_input('hostname', _('Hostname:'))
        popup.add_text_input('port', _('Port:'))
        popup.add_text_input('username', _('Username:'******'password', _('Password:'******'Adding host: %s', hostname)
        try:
            self.hostlist.add_host(hostname, port, username, password)
        except ValueError as ex:
            self.report_message(_('Error adding host'), '%s: %s' % (hostname, ex))
        else:
            self.update_select_host_popup()

    def delete_host(self, host_id):
        log.info('Deleting host: %s', host_id)
        self.hostlist.remove_host(host_id)
        self.update_select_host_popup()

    @overrides(component.Component)
    def start(self):
        self.refresh()

    @overrides(component.Component)
    def update(self):
        self.update_hosts_status()

    @overrides(BaseMode)
    def pause(self):
        self.pop_popup()
        BaseMode.pause(self)

    @overrides(BaseMode)
    def resume(self):
        BaseMode.resume(self)
        self.refresh()

    @overrides(BaseMode)
    def refresh(self):
        if self.mode_paused():
            return

        self.stdscr.erase()
        self.draw_statusbars()
        self.stdscr.noutrefresh()

        if not self.popup:
            self.update_select_host_popup()

        self.popup.refresh()
        curses.doupdate()

    @overrides(BaseMode)
    def on_resize(self, rows, cols):
        BaseMode.on_resize(self, rows, cols)

        if self.popup:
            self.popup.handle_resize()

        self.stdscr.erase()
        self.refresh()

    @overrides(BaseMode)
    def read_input(self):
        c = self.stdscr.getch()

        if is_printable_chr(c):
            if chr(c) == 'Q':
                component.get('ConsoleUI').quit()
            elif self.inlist:
                if chr(c) == 'q':
                    return
                elif chr(c) == 'D':
                    host_id = self.popup.current_selection()[1]
                    self.delete_host(host_id)
                    return
                elif chr(c) == 'a':
                    self.add_popup()
                    return

        if self.popup:
            if self.popup.handle_read(c) and self.popup.closed():
                self.pop_popup()
            self.refresh()
Beispiel #3
0
class ConnectionManager(BaseMode, PopupsHandler):
    def __init__(self, stdscr, encoding=None):
        PopupsHandler.__init__(self)
        self.statuses = {}
        self.all_torrents = None
        self.hostlist = HostList()
        self.update_hosts_status()
        BaseMode.__init__(self, stdscr, encoding=encoding)
        self.update_select_host_popup()

    def update_select_host_popup(self):
        selected_index = self.popup.current_selection() if self.popup else None

        popup = SelectablePopup(self,
                                _('Select Host'),
                                self._host_selected,
                                border_off_west=1,
                                active_wrap=True)
        popup.add_header("{!white,black,bold!}'Q'=%s, 'a'=%s, 'D'=%s" %
                         (_('Quit'), _('Add Host'), _('Delete Host')),
                         space_below=True)
        self.push_popup(popup, clear=True)

        for host_entry in self.hostlist.get_hosts_info():
            host_id, hostname, port, user = host_entry
            args = {'data': host_id, 'foreground': 'red'}
            state = 'Offline'
            if host_id in self.statuses:
                state = 'Online'
                args.update({
                    'data': self.statuses[host_id],
                    'foreground': 'green'
                })
            host_str = '%s:%d [%s]' % (hostname, port, state)
            self.popup.add_line(host_id,
                                host_str,
                                selectable=True,
                                use_underline=True,
                                **args)

        if selected_index:
            self.popup.set_selection(selected_index)
        self.inlist = True
        self.refresh()

    def update_hosts_status(self):
        for host_entry in self.hostlist.get_hosts_info():

            def on_host_status(status_info):
                self.statuses[status_info[0]] = status_info
                self.update_select_host_popup()

            self.hostlist.get_host_status(
                host_entry[0]).addCallback(on_host_status)

    def _on_connected(self, result):
        def on_console_start(result):
            component.get('ConsoleUI').set_mode('TorrentList')

        d = component.get('ConsoleUI').start_console()
        d.addCallback(on_console_start)

    def _on_connect_fail(self, result):
        self.report_message('Failed to connect!', result)
        self.refresh()
        if hasattr(result, 'getTraceback'):
            log.exception(result)

    def _host_selected(self, selected_host, *args, **kwargs):
        if selected_host in self.statuses:
            d = self.hostlist.connect_host(selected_host)
            d.addCallback(self._on_connected)
            d.addErrback(self._on_connect_fail)

    def _do_add(self, result, **kwargs):
        if not result or kwargs.get('close', False):
            self.pop_popup()
        else:
            self.add_host(result['hostname']['value'], result['port']['value'],
                          result['username']['value'],
                          result['password']['value'])

    def add_popup(self):
        self.inlist = False
        popup = InputPopup(
            self,
            _('Add Host (Up & Down arrows to navigate, Esc to cancel)'),
            border_off_north=1,
            border_off_east=1,
            close_cb=self._do_add)
        popup.add_text_input('hostname', _('Hostname:'))
        popup.add_text_input('port', _('Port:'))
        popup.add_text_input('username', _('Username:'******'password', _('Password:'******'Adding host: %s', hostname)
        try:
            self.hostlist.add_host(hostname, port, username, password)
        except ValueError as ex:
            self.report_message(_('Error adding host'),
                                '%s: %s' % (hostname, ex))
        else:
            self.update_select_host_popup()

    def delete_host(self, host_id):
        log.info('Deleting host: %s', host_id)
        self.hostlist.remove_host(host_id)
        self.update_select_host_popup()

    @overrides(component.Component)
    def start(self):
        self.refresh()

    @overrides(component.Component)
    def update(self):
        self.update_hosts_status()

    @overrides(BaseMode)
    def pause(self):
        self.pop_popup()
        BaseMode.pause(self)

    @overrides(BaseMode)
    def resume(self):
        BaseMode.resume(self)
        self.refresh()

    @overrides(BaseMode)
    def refresh(self):
        if self.mode_paused():
            return

        self.stdscr.erase()
        self.draw_statusbars()
        self.stdscr.noutrefresh()

        if not self.popup:
            self.update_select_host_popup()

        self.popup.refresh()
        curses.doupdate()

    @overrides(BaseMode)
    def on_resize(self, rows, cols):
        BaseMode.on_resize(self, rows, cols)

        if self.popup:
            self.popup.handle_resize()

        self.stdscr.erase()
        self.refresh()

    @overrides(BaseMode)
    def read_input(self):
        c = self.stdscr.getch()

        if is_printable_chr(c):
            if chr(c) == 'Q':
                component.get('ConsoleUI').quit()
            elif self.inlist:
                if chr(c) == 'q':
                    return
                elif chr(c) == 'D':
                    host_id = self.popup.current_selection()[1]
                    self.delete_host(host_id)
                    return
                elif chr(c) == 'a':
                    self.add_popup()
                    return

        if self.popup:
            if self.popup.handle_read(c) and self.popup.closed():
                self.pop_popup()
            self.refresh()
Beispiel #4
0
class WebApi(JSONComponent):
    """
    The component that implements all the methods required for managing
    the web interface. The complete web json interface also exposes all the
    methods available from the core RPC.
    """

    def __init__(self):
        super(WebApi, self).__init__('Web', depend=['SessionProxy'])
        self.hostlist = HostList()
        self.core_config = CoreConfig()
        self.event_queue = EventQueue()
        try:
            self.sessionproxy = component.get('SessionProxy')
        except KeyError:
            self.sessionproxy = SessionProxy()

    def disable(self):
        client.deregister_event_handler('PluginEnabledEvent', self._json.get_remote_methods)
        client.deregister_event_handler('PluginDisabledEvent', self._json.get_remote_methods)

        if client.is_standalone():
            component.get('Web.PluginManager').stop()
        else:
            client.disconnect()
            client.set_disconnect_callback(None)

    def enable(self):
        client.register_event_handler('PluginEnabledEvent', self._json.get_remote_methods)
        client.register_event_handler('PluginDisabledEvent', self._json.get_remote_methods)

        if client.is_standalone():
            component.get('Web.PluginManager').start()
        else:
            client.set_disconnect_callback(self._on_client_disconnect)
            default_host_id = component.get('DelugeWeb').config['default_daemon']
            if default_host_id:
                return self.connect(default_host_id)

        return defer.succeed(True)

    def _on_client_connect(self, *args):
        """Handles client successfully connecting to the daemon.

        Invokes retrieving the method names and starts webapi and plugins.

        """
        d_methods = self._json.get_remote_methods()
        component.get('Web.PluginManager').start()
        self.start()
        return d_methods

    def _on_client_disconnect(self, *args):
        component.get('Web.PluginManager').stop()
        return self.stop()

    def start(self):
        self.core_config.start()
        return self.sessionproxy.start()

    def stop(self):
        self.core_config.stop()
        self.sessionproxy.stop()
        return defer.succeed(True)

    @export
    def connect(self, host_id):
        """Connect the web client to a daemon.

        Args:
            host_id (str): The id of the daemon in the host list.

        Returns:
            Deferred: List of methods the daemon supports.
        """
        return self.hostlist.connect_host(host_id).addCallback(self._on_client_connect)

    @export
    def connected(self):
        """
        The current connection state.

        :returns: True if the client is connected
        :rtype: booleon
        """
        return client.connected()

    @export
    def disconnect(self):
        """
        Disconnect the web interface from the connected daemon.
        """
        d = client.disconnect()

        def on_disconnect(reason):
            return str(reason)
        d.addCallback(on_disconnect)
        return d

    @export
    def update_ui(self, keys, filter_dict):
        """
        Gather the information required for updating the web interface.

        :param keys: the information about the torrents to gather
        :type keys: list
        :param filter_dict: the filters to apply when selecting torrents.
        :type filter_dict: dictionary
        :returns: The torrent and ui information.
        :rtype: dictionary
        """
        d = Deferred()
        ui_info = {
            'connected': client.connected(),
            'torrents': None,
            'filters': None,
            'stats': {
                'max_download': self.core_config.get('max_download_speed'),
                'max_upload': self.core_config.get('max_upload_speed'),
                'max_num_connections': self.core_config.get('max_connections_global')
            }
        }

        if not client.connected():
            d.callback(ui_info)
            return d

        def got_stats(stats):
            ui_info['stats']['num_connections'] = stats['num_peers']
            ui_info['stats']['upload_rate'] = stats['payload_upload_rate']
            ui_info['stats']['download_rate'] = stats['payload_download_rate']
            ui_info['stats']['download_protocol_rate'] = stats['download_rate'] - stats['payload_download_rate']
            ui_info['stats']['upload_protocol_rate'] = stats['upload_rate'] - stats['payload_upload_rate']
            ui_info['stats']['dht_nodes'] = stats['dht_nodes']
            ui_info['stats']['has_incoming_connections'] = stats['has_incoming_connections']

        def got_filters(filters):
            ui_info['filters'] = filters

        def got_free_space(free_space):
            ui_info['stats']['free_space'] = free_space

        def got_external_ip(external_ip):
            ui_info['stats']['external_ip'] = external_ip

        def got_torrents(torrents):
            ui_info['torrents'] = torrents

        def on_complete(result):
            d.callback(ui_info)

        d1 = component.get('SessionProxy').get_torrents_status(filter_dict, keys)
        d1.addCallback(got_torrents)

        d2 = client.core.get_filter_tree()
        d2.addCallback(got_filters)

        d3 = client.core.get_session_status([
            'num_peers',
            'payload_download_rate',
            'payload_upload_rate',
            'download_rate',
            'upload_rate',
            'dht_nodes',
            'has_incoming_connections'
        ])
        d3.addCallback(got_stats)

        d4 = client.core.get_free_space(self.core_config.get('download_location'))
        d4.addCallback(got_free_space)

        d5 = client.core.get_external_ip()
        d5.addCallback(got_external_ip)

        dl = DeferredList([d1, d2, d3, d4, d5], consumeErrors=True)
        dl.addCallback(on_complete)
        return d

    def _on_got_files(self, torrent, d):
        files = torrent.get('files')
        file_progress = torrent.get('file_progress')
        file_priorities = torrent.get('file_priorities')

        paths = []
        info = {}
        for index, torrent_file in enumerate(files):
            path = torrent_file['path']
            paths.append(path)
            torrent_file['progress'] = file_progress[index]
            torrent_file['priority'] = file_priorities[index]
            torrent_file['index'] = index
            torrent_file['path'] = path
            info[path] = torrent_file

            # update the directory info
            dirname = os.path.dirname(path)
            while dirname:
                dirinfo = info.setdefault(dirname, {})
                dirinfo['size'] = dirinfo.get('size', 0) + torrent_file['size']
                if 'priority' not in dirinfo:
                    dirinfo['priority'] = torrent_file['priority']
                else:
                    if dirinfo['priority'] != torrent_file['priority']:
                        dirinfo['priority'] = 9

                progresses = dirinfo.setdefault('progresses', [])
                progresses.append(torrent_file['size'] * torrent_file['progress'] / 100)
                dirinfo['progress'] = sum(progresses) / dirinfo['size'] * 100
                dirinfo['path'] = dirname
                dirname = os.path.dirname(dirname)

        def walk(path, item):
            if item['type'] == 'dir':
                item.update(info[path])
                return item
            else:
                item.update(info[path])
                return item

        file_tree = FileTree2(paths)
        file_tree.walk(walk)
        d.callback(file_tree.get_tree())

    @export
    def get_torrent_status(self, torrent_id, keys):
        return component.get('SessionProxy').get_torrent_status(torrent_id, keys)

    @export
    def get_torrent_files(self, torrent_id):
        """
        Gets the files for a torrent in tree format

        :param torrent_id: the id of the torrent to retrieve.
        :type torrent_id: string
        :returns: The torrents files in a tree
        :rtype: dictionary
        """
        main_deferred = Deferred()
        d = component.get('SessionProxy').get_torrent_status(torrent_id, FILES_KEYS)
        d.addCallback(self._on_got_files, main_deferred)
        return main_deferred

    @export
    def download_torrent_from_url(self, url, cookie=None):
        """
        Download a torrent file from a url to a temporary directory.

        :param url: the url of the torrent
        :type url: string
        :returns: the temporary file name of the torrent file
        :rtype: string
        """

        def on_download_success(result):
            log.debug('Successfully downloaded %s to %s', url, result)
            return result

        def on_download_fail(result):
            log.error('Failed to add torrent from url %s', url)
            return result

        tempdir = tempfile.mkdtemp(prefix='delugeweb-')
        tmp_file = os.path.join(tempdir, url.split('/')[-1])
        log.debug('filename: %s', tmp_file)
        headers = {}
        if cookie:
            headers['Cookie'] = cookie
            log.debug('cookie: %s', cookie)
        d = httpdownloader.download_file(url, tmp_file, headers=headers)
        d.addCallbacks(on_download_success, on_download_fail)
        return d

    @export
    def get_torrent_info(self, filename):
        """
        Return information about a torrent on the filesystem.

        :param filename: the path to the torrent
        :type filename: string

        :returns: information about the torrent:

        ::

            {
                "name": the torrent name,
                "files_tree": the files the torrent contains,
                "info_hash" the torrents info_hash
            }

        :rtype: dictionary
        """
        try:
            torrent_info = TorrentInfo(filename.strip(), 2)
            return torrent_info.as_dict('name', 'info_hash', 'files_tree')
        except Exception as ex:
            log.error(ex)
            return False

    @export
    def get_magnet_info(self, uri):
        return get_magnet_info(uri)

    @export
    def add_torrents(self, torrents):
        """
        Add torrents by file

        :param torrents: A list of dictionaries containing the torrent \
            path and torrent options to add with.
        :type torrents: list

        ::

            json_api.web.add_torrents([{
                "path": "/tmp/deluge-web/some-torrent-file.torrent",
                "options": {"download_location": "/home/deluge/"}
            }])

        """
        deferreds = []

        for torrent in torrents:
            if is_magnet(torrent['path']):
                log.info('Adding torrent from magnet uri `%s` with options `%r`',
                         torrent['path'], torrent['options'])
                d = client.core.add_torrent_magnet(torrent['path'], torrent['options'])
                deferreds.append(d)
            else:
                filename = os.path.basename(torrent['path'])
                with open(torrent['path'], 'rb') as _file:
                    fdump = base64.encodestring(_file.read())
                log.info('Adding torrent from file `%s` with options `%r`',
                         filename, torrent['options'])
                d = client.core.add_torrent_file(filename, fdump, torrent['options'])
                deferreds.append(d)
        return DeferredList(deferreds, consumeErrors=False)

    def _get_host(self, host_id):
        """Information about a host from supplied host id.

        Args:
            host_id (str): The id of the host.

        Returns:
            list: The host information, empty list if not found.

        """
        return list(self.hostlist.get_host_info(host_id))

    @export
    def get_hosts(self):
        """
        Return the hosts in the hostlist.
        """
        log.debug('get_hosts called')
        return self.hostlist.get_hosts_info()

    @export
    def get_host_status(self, host_id):
        """
        Returns the current status for the specified host.

        :param host_id: the hash id of the host
        :type host_id: string

        """
        def response(result):
            return result

        return self.hostlist.get_host_status(host_id).addCallback(response)

    @export
    def add_host(self, host, port, username='', password=''):
        """Adds a host to the list.

        Args:
            host (str): The IP or hostname of the deluge daemon.
            port (int): The port of the deluge daemon.
            username (str): The username to login to the daemon with.
            password (str): The password to login to the daemon with.

        Returns:
            tuple: A tuple of (bool, str). If True will contain the host_id, otherwise
                if False will contain the error message.
        """
        try:
            host_id = self.hostlist.add_host(host, port, username, password)
        except ValueError as ex:
            return False, str(ex)
        else:
            return True, host_id

    @export
    def edit_host(self, host_id, host, port, username='', password=''):
        """Edit host details in the hostlist.

        Args:
            host_id (str): The host identifying hash.
            host (str): The IP or hostname of the deluge daemon.
            port (int): The port of the deluge daemon.
            username (str): The username to login to the daemon with.
            password (str): The password to login to the daemon with.

        Returns:
            bool: True if succesful, False otherwise.

        """
        return self.hostlist.update_host(host_id, host, port, username, password)

    @export
    def remove_host(self, host_id):
        """Removes a host from the hostlist.

        Args:
            host_id (str): The host identifying hash.

        Returns:
            bool: True if succesful, False otherwise.

        """
        return self.hostlist.remove_host(host_id)

    @export
    def start_daemon(self, port):
        """
        Starts a local daemon.
        """
        client.start_daemon(port, get_config_dir())

    @export
    def stop_daemon(self, host_id):
        """
        Stops a running daemon.

        :param host_id: the hash id of the host
        :type host_id: string
        """
        main_deferred = Deferred()
        host = self._get_host(host_id)
        if not host:
            main_deferred.callback((False, _('Daemon does not exist')))
            return main_deferred

        try:
            def on_connect(connected, c):
                if not connected:
                    main_deferred.callback((False, _('Daemon not running')))
                    return
                c.daemon.shutdown()
                main_deferred.callback((True, ))

            def on_connect_failed(reason):
                main_deferred.callback((False, reason))

            host, port, user, password = host[1:5]
            c = Client()
            d = c.connect(host, port, user, password)
            d.addCallback(on_connect, c)
            d.addErrback(on_connect_failed)
        except Exception:
            main_deferred.callback((False, 'An error occurred'))
        return main_deferred

    @export
    def get_config(self):
        """
        Get the configuration dictionary for the web interface.

        :rtype: dictionary
        :returns: the configuration
        """
        config = component.get('DelugeWeb').config.config.copy()
        del config['sessions']
        del config['pwd_salt']
        del config['pwd_sha1']
        return config

    @export
    def set_config(self, config):
        """
        Sets the configuration dictionary for the web interface.

        :param config: The configuration options to update
        :type config: dictionary
        """
        web_config = component.get('DelugeWeb').config
        for key in config:
            if key in ['sessions', 'pwd_salt', 'pwd_sha1']:
                log.warn('Ignored attempt to overwrite web config key: %s', key)
                continue
            web_config[key] = config[key]

    @export
    def get_plugins(self):
        """All available and enabled plugins within WebUI.

        Note:
            This does not represent all plugins from deluge.client.core.

        Returns:
            dict: A dict containing 'available_plugins' and 'enabled_plugins' lists.

        """

        return {
            'enabled_plugins': list(component.get('Web.PluginManager').plugins),
            'available_plugins': component.get('Web.PluginManager').available_plugins
        }

    @export
    def get_plugin_info(self, name):
        return component.get('Web.PluginManager').get_plugin_info(name)

    @export
    def get_plugin_resources(self, name):
        return component.get('Web.PluginManager').get_plugin_resources(name)

    @export
    def upload_plugin(self, filename, path):
        main_deferred = Deferred()

        shutil.copyfile(path, os.path.join(get_config_dir(), 'plugins', filename))
        component.get('Web.PluginManager').scan_for_plugins()

        if client.is_localhost():
            client.core.rescan_plugins()
            return True
        with open(path, 'rb') as _file:
            plugin_data = base64.encodestring(_file.read())

        def on_upload_complete(*args):
            client.core.rescan_plugins()
            component.get('Web.PluginManager').scan_for_plugins()
            main_deferred.callback(True)

        def on_upload_error(*args):
            main_deferred.callback(False)

        d = client.core.upload_plugin(filename, plugin_data)
        d.addCallback(on_upload_complete)
        d.addErrback(on_upload_error)
        return main_deferred

    @export
    def register_event_listener(self, event):
        """
        Add a listener to the event queue.

        :param event: The event name
        :type event: string
        """
        self.event_queue.add_listener(__request__.session_id, event)

    @export
    def deregister_event_listener(self, event):
        """
        Remove an event listener from the event queue.

        :param event: The event name
        :type event: string
        """
        self.event_queue.remove_listener(__request__.session_id, event)

    @export
    def get_events(self):
        """
        Retrieve the pending events for the session.
        """
        return self.event_queue.get_events(__request__.session_id)