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()
class ConnectionManager(component.Component): def __init__(self): component.Component.__init__(self, 'ConnectionManager') self.gtkui_config = ConfigManager('gtkui.conf') self.hostlist = HostList() self.running = False # Component overrides def start(self): pass def stop(self): # Close this dialog when we are shutting down if self.running: self.connection_manager.response(gtk.RESPONSE_CLOSE) def shutdown(self): pass # Public methods def show(self): """Show the ConnectionManager dialog.""" self.builder = gtk.Builder() self.builder.add_from_file( resource_filename('deluge.ui.gtkui', os.path.join('glade', 'connection_manager.ui'))) self.connection_manager = self.builder.get_object('connection_manager') self.connection_manager.set_transient_for( component.get('MainWindow').window) # Create status pixbufs if not HOSTLIST_PIXBUFS: for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT): HOSTLIST_PIXBUFS.append( self.connection_manager.render_icon( stock_id, gtk.ICON_SIZE_MENU)) # Setup the hostlist liststore and treeview self.treeview = self.builder.get_object('treeview_hostlist') self.liststore = self.builder.get_object('liststore_hostlist') render = gtk.CellRendererPixbuf() column = gtk.TreeViewColumn(_('Status'), render) column.set_cell_data_func(render, cell_render_status, HOSTLIST_COL_STATUS) self.treeview.append_column(column) render = gtk.CellRendererText() column = gtk.TreeViewColumn(_('Host'), render, text=HOSTLIST_COL_HOST) host_data = (HOSTLIST_COL_HOST, HOSTLIST_COL_PORT, HOSTLIST_COL_USER) column.set_cell_data_func(render, cell_render_host, host_data) column.set_expand(True) self.treeview.append_column(column) column = gtk.TreeViewColumn(_('Version'), gtk.CellRendererText(), text=HOSTLIST_COL_VERSION) self.treeview.append_column(column) # Load any saved host entries self._load_liststore() # Set widgets to values from gtkui config. self._load_widget_config() self._update_widget_buttons() # Connect the signals to the handlers self.builder.connect_signals(self) self.treeview.get_selection().connect( 'changed', self.on_hostlist_selection_changed) # Set running True before update status call. self.running = True if windows_check(): # Call to simulate() required to workaround showing daemon status (see #2813) reactor.simulate() self._update_host_status() # Trigger the on_selection_changed code and select the first host if possible self.treeview.get_selection().unselect_all() if len(self.liststore): self.treeview.get_selection().select_path(0) # Run the dialog self.connection_manager.run() # Dialog closed so cleanup. self.running = False self.connection_manager.destroy() del self.builder del self.connection_manager del self.liststore del self.treeview def _load_liststore(self): """Load saved host entries""" for host_entry in self.hostlist.get_hosts_info(): host_id, host, port, username = host_entry self.liststore.append([host_id, host, port, username, '', '', '']) def _load_widget_config(self): """Set the widgets to show the correct options from the config.""" self.builder.get_object('chk_autoconnect').set_active( self.gtkui_config['autoconnect']) self.builder.get_object('chk_autostart').set_active( self.gtkui_config['autostart_localhost']) self.builder.get_object('chk_donotshow').set_active( not self.gtkui_config['show_connection_manager_on_start']) def _update_host_status(self): """Updates the host status""" if not self.running: # Callback likely fired after the window closed. return def on_host_status(status_info, row): if self.running and row: row[HOSTLIST_COL_STATUS] = status_info[1] row[HOSTLIST_COL_VERSION] = status_info[2] self._update_widget_buttons() deferreds = [] for row in self.liststore: host_id = row[HOSTLIST_COL_ID] d = self.hostlist.get_host_status(host_id) try: d.addCallback(on_host_status, row) except AttributeError: on_host_status(d, row) else: deferreds.append(d) defer.DeferredList(deferreds) def _update_widget_buttons(self): """Updates the dialog button states.""" self.builder.get_object('button_refresh').set_sensitive( len(self.liststore)) self.builder.get_object('button_startdaemon').set_sensitive(False) self.builder.get_object('button_connect').set_sensitive(False) self.builder.get_object('button_connect').set_label(_('C_onnect')) self.builder.get_object('button_edithost').set_sensitive(False) self.builder.get_object('button_removehost').set_sensitive(False) self.builder.get_object('button_startdaemon').set_sensitive(False) self.builder.get_object('image_startdaemon').set_from_stock( gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU) self.builder.get_object('label_startdaemon').set_text_with_mnemonic( '_Start Daemon') model, row = self.treeview.get_selection().get_selected() if row: self.builder.get_object('button_edithost').set_sensitive(True) self.builder.get_object('button_removehost').set_sensitive(True) else: return # Get selected host info. __, host, port, __, __, status, __ = model[row] try: gethostbyname(host) except gaierror as ex: log.error('Error resolving host %s to ip: %s', row[HOSTLIST_COL_HOST], ex.args[1]) self.builder.get_object('button_connect').set_sensitive(False) return log.debug('Host Status: %s, %s', host, status) # Check to see if the host is online if status == 'Connected' or status == 'Online': self.builder.get_object('button_connect').set_sensitive(True) self.builder.get_object('image_startdaemon').set_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU) self.builder.get_object( 'label_startdaemon').set_text_with_mnemonic(_('_Stop Daemon')) self.builder.get_object('button_startdaemon').set_sensitive(False) if status == 'Connected': # Display a disconnect button if we're connected to this host self.builder.get_object('button_connect').set_label( _('_Disconnect')) self.builder.get_object('button_removehost').set_sensitive( False) # Currently can only stop daemon when connected to it self.builder.get_object('button_startdaemon').set_sensitive( True) elif host in LOCALHOST: # If localhost we can start the dameon. self.builder.get_object('button_startdaemon').set_sensitive(True) def start_daemon(self, port, config): """Attempts to start local daemon process and will show an ErrorDialog if not. Args: port (int): Port for the daemon to listen on. config (str): Config path to pass to daemon. Returns: bool: True is successfully started the daemon, False otherwise. """ if client.start_daemon(port, config): log.debug('Localhost daemon started') reactor.callLater(1, self._update_host_status) return True else: ErrorDialog( _('Unable to start daemon!'), _('Check deluged package is installed and logs for further details' )).run() return False # Signal handlers def _connect(self, host_id, username=None, password=None, try_counter=0): def do_connect(result, username=None, password=None, *args): log.debug('Attempting to connect to daemon...') for host_entry in self.hostlist.config['hosts']: if host_entry[0] == host_id: __, host, port, host_user, host_pass = host_entry username = username if username else host_user password = password if password else host_pass d = client.connect(host, port, username, password) d.addCallback(self._on_connect, host_id) d.addErrback(self._on_connect_fail, host_id, try_counter) return d if client.connected(): return client.disconnect().addCallback(do_connect, username, password) else: return do_connect(None, username, password) def _on_connect(self, daemon_info, host_id): log.debug('Connected to daemon: %s', host_id) if self.gtkui_config['autoconnect']: self.gtkui_config['autoconnect_host_id'] = host_id if self.running: # When connected to a client, and then trying to connect to another, # this component will be stopped(while the connect deferred is # running), so, self.connection_manager will be deleted. # If that's not the case, close the dialog. self.connection_manager.response(gtk.RESPONSE_OK) component.start() def _on_connect_fail(self, reason, host_id, try_counter): log.debug('Failed to connect: %s', reason.value) if reason.check(AuthenticationRequired, BadLoginError): log.debug('PasswordRequired exception') dialog = AuthenticationDialog(reason.value.message, reason.value.username) def dialog_finished(response_id): if response_id == gtk.RESPONSE_OK: self._connect(host_id, dialog.get_username(), dialog.get_password()) return dialog.run().addCallback(dialog_finished) elif reason.check(IncompatibleClient): return ErrorDialog(_('Incompatible Client'), reason.value.message).run() if try_counter: log.info('Retrying connection.. Retries left: %s', try_counter) return reactor.callLater(0.5, self._connect, host_id, try_counter=try_counter - 1) msg = str(reason.value) if not self.gtkui_config['autostart_localhost']: msg += '\n' + _('Auto-starting the daemon locally is not enabled. ' 'See "Options" on the "Connection Manager".') ErrorDialog(_('Failed To Connect'), msg).run() def on_button_connect_clicked(self, widget=None): """Button handler for connect to or disconnect from daemon.""" model, row = self.treeview.get_selection().get_selected() if not row: return host_id, host, port, __, __, status, __ = model[row] # If status is connected then connect button disconnects instead. if status == 'Connected': def on_disconnect(reason): self._update_host_status() return client.disconnect().addCallback(on_disconnect) try_counter = 0 auto_start = self.builder.get_object('chk_autostart').get_active() if auto_start and host in LOCALHOST and status == 'Offline': # Start the local daemon and then connect with retries set. if self.start_daemon(port, get_config_dir()): try_counter = 6 else: # Don't attempt to connect to offline daemon. return self._connect(host_id, try_counter=try_counter) def on_button_close_clicked(self, widget): self.connection_manager.response(gtk.RESPONSE_CLOSE) def _run_addhost_dialog(self, edit_host_info=None): """Create and runs the add host dialog. Supplying edit_host_info changes the dialog to an edit dialog. Args: edit_host_info (list): A list of (host, port, user, pass) to edit. Returns: list: The new host info values (host, port, user, pass). """ self.builder.add_from_file( resource_filename( 'deluge.ui.gtkui', os.path.join('glade', 'connection_manager.addhost.ui'))) dialog = self.builder.get_object('addhost_dialog') dialog.set_transient_for(self.connection_manager) hostname_entry = self.builder.get_object('entry_hostname') port_spinbutton = self.builder.get_object('spinbutton_port') username_entry = self.builder.get_object('entry_username') password_entry = self.builder.get_object('entry_password') if edit_host_info: dialog.set_title(_('Edit Host')) hostname_entry.set_text(edit_host_info[0]) port_spinbutton.set_value(edit_host_info[1]) username_entry.set_text(edit_host_info[2]) password_entry.set_text(edit_host_info[3]) response = dialog.run() new_host_info = [] if response: new_host_info.append(hostname_entry.get_text()) new_host_info.append(port_spinbutton.get_value_as_int()) new_host_info.append(username_entry.get_text()) new_host_info.append(password_entry.get_text()) dialog.destroy() return new_host_info def on_button_addhost_clicked(self, widget): log.debug('on_button_addhost_clicked') host_info = self._run_addhost_dialog() if host_info: hostname, port, username, password = host_info try: host_id = self.hostlist.add_host(hostname, port, username, password) except ValueError as ex: ErrorDialog(_('Error Adding Host'), ex).run() else: self.liststore.append([ host_id, hostname, port, username, password, 'Offline', '' ]) self._update_host_status() def on_button_edithost_clicked(self, widget=None): log.debug('on_button_edithost_clicked') model, row = self.treeview.get_selection().get_selected() status = model[row][HOSTLIST_COL_STATUS] host_id = model[row][HOSTLIST_COL_ID] if status == 'Connected': def on_disconnect(reason): self._update_host_status() client.disconnect().addCallback(on_disconnect) return host_info = [ self.liststore[row][HOSTLIST_COL_HOST], self.liststore[row][HOSTLIST_COL_PORT], self.liststore[row][HOSTLIST_COL_USER], self.liststore[row][HOSTLIST_COL_PASS] ] new_host_info = self._run_addhost_dialog(edit_host_info=host_info) if new_host_info: hostname, port, username, password = new_host_info try: self.hostlist.update_host(host_id, hostname, port, username, password) except ValueError as ex: ErrorDialog(_('Error Updating Host'), ex).run() else: self.liststore[ row] = host_id, hostname, port, username, password, '', '' self._update_host_status() def on_button_removehost_clicked(self, widget): log.debug('on_button_removehost_clicked') # Get the selected rows model, row = self.treeview.get_selection().get_selected() self.hostlist.remove_host(model[row][HOSTLIST_COL_ID]) self.liststore.remove(row) # Update the hostlist self._update_host_status() def on_button_startdaemon_clicked(self, widget): log.debug('on_button_startdaemon_clicked') if not self.liststore.iter_n_children(None): # There is nothing in the list, so lets create a localhost entry try: self.hostlist.add_default_host() except ValueError as ex: log.error('Error adding default host: %s', ex) else: self.start_daemon(DEFAULT_PORT, get_config_dir()) finally: return paths = self.treeview.get_selection().get_selected_rows()[1] if len(paths): __, host, port, user, password, status, __ = self.liststore[ paths[0]] else: return if host not in LOCALHOST: return def on_daemon_status_change(result): """Daemon start/stop callback""" reactor.callLater(0.7, self._update_host_status) if status in ('Online', 'Connected'): # Button will stop the daemon if status is online or connected. def on_connect(d, c): """Client callback to call daemon shutdown""" c.daemon.shutdown().addCallback(on_daemon_status_change) if client.connected() and (host, port, user) == client.connection_info(): client.daemon.shutdown().addCallback(on_daemon_status_change) elif user and password: c = Client() c.connect(host, port, user, password).addCallback(on_connect, c) else: # Otherwise button will start the daemon. self.start_daemon(port, get_config_dir()) def on_button_refresh_clicked(self, widget): self._update_host_status() def on_hostlist_row_activated(self, tree, path, view_column): self.on_button_connect_clicked() def on_hostlist_selection_changed(self, treeselection): self._update_widget_buttons() def on_chk_toggled(self, widget): self.gtkui_config['autoconnect'] = self.builder.get_object( 'chk_autoconnect').get_active() self.gtkui_config['autostart_localhost'] = self.builder.get_object( 'chk_autostart').get_active() self.gtkui_config[ 'show_connection_manager_on_start'] = not self.builder.get_object( 'chk_donotshow').get_active() def on_entry_host_paste_clipboard(self, widget): text = get_clipboard_text() log.debug('on_entry_proxy_host_paste-clipboard: got paste: %s', text) text = text if '//' in text else '//' + text parsed = urlparse(text) if parsed.hostname: widget.set_text(parsed.hostname) widget.emit_stop_by_name('paste-clipboard') if parsed.port: self.builder.get_object('spinbutton_port').set_value(parsed.port) if parsed.username: self.builder.get_object('entry_username').set_text(parsed.username) if parsed.password: self.builder.get_object('entry_password').set_text(parsed.password)
class ConnectionManager(component.Component): def __init__(self): component.Component.__init__(self, 'ConnectionManager') self.gtkui_config = ConfigManager('gtkui.conf') self.hostlist = HostList() self.running = False # Component overrides def start(self): pass def stop(self): # Close this dialog when we are shutting down if self.running: self.connection_manager.response(gtk.RESPONSE_CLOSE) def shutdown(self): pass # Public methods def show(self): """Show the ConnectionManager dialog.""" self.builder = gtk.Builder() self.builder.add_from_file(resource_filename( 'deluge.ui.gtkui', os.path.join('glade', 'connection_manager.ui'))) self.connection_manager = self.builder.get_object('connection_manager') self.connection_manager.set_transient_for(component.get('MainWindow').window) # Create status pixbufs if not HOSTLIST_PIXBUFS: for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT): HOSTLIST_PIXBUFS.append( self.connection_manager.render_icon(stock_id, gtk.ICON_SIZE_MENU)) # Setup the hostlist liststore and treeview self.treeview = self.builder.get_object('treeview_hostlist') self.liststore = self.builder.get_object('liststore_hostlist') render = gtk.CellRendererPixbuf() column = gtk.TreeViewColumn(_('Status'), render) column.set_cell_data_func(render, cell_render_status, HOSTLIST_COL_STATUS) self.treeview.append_column(column) render = gtk.CellRendererText() column = gtk.TreeViewColumn(_('Host'), render, text=HOSTLIST_COL_HOST) host_data = (HOSTLIST_COL_HOST, HOSTLIST_COL_PORT, HOSTLIST_COL_USER) column.set_cell_data_func(render, cell_render_host, host_data) column.set_expand(True) self.treeview.append_column(column) column = gtk.TreeViewColumn(_('Version'), gtk.CellRendererText(), text=HOSTLIST_COL_VERSION) self.treeview.append_column(column) # Load any saved host entries self._load_liststore() # Set widgets to values from gtkui config. self._load_widget_config() self._update_widget_buttons() # Connect the signals to the handlers self.builder.connect_signals(self) self.treeview.get_selection().connect('changed', self.on_hostlist_selection_changed) # Set running True before update status call. self.running = True if windows_check(): # Call to simulate() required to workaround showing daemon status (see #2813) reactor.simulate() self._update_host_status() # Trigger the on_selection_changed code and select the first host if possible self.treeview.get_selection().unselect_all() if len(self.liststore): self.treeview.get_selection().select_path(0) # Run the dialog self.connection_manager.run() # Dialog closed so cleanup. self.running = False self.connection_manager.destroy() del self.builder del self.connection_manager del self.liststore del self.treeview def _load_liststore(self): """Load saved host entries""" for host_entry in self.hostlist.get_hosts_info(): host_id, host, port, username = host_entry self.liststore.append([host_id, host, port, username, '', '', '']) def _load_widget_config(self): """Set the widgets to show the correct options from the config.""" self.builder.get_object('chk_autoconnect').set_active( self.gtkui_config['autoconnect']) self.builder.get_object('chk_autostart').set_active( self.gtkui_config['autostart_localhost']) self.builder.get_object('chk_donotshow').set_active( not self.gtkui_config['show_connection_manager_on_start']) def _update_host_status(self): """Updates the host status""" if not self.running: # Callback likely fired after the window closed. return def on_host_status(status_info, row): if self.running and row: row[HOSTLIST_COL_STATUS] = status_info[1] row[HOSTLIST_COL_VERSION] = status_info[2] self._update_widget_buttons() deferreds = [] for row in self.liststore: host_id = row[HOSTLIST_COL_ID] d = self.hostlist.get_host_status(host_id) try: d.addCallback(on_host_status, row) except AttributeError: on_host_status(d, row) else: deferreds.append(d) defer.DeferredList(deferreds) def _update_widget_buttons(self): """Updates the dialog button states.""" self.builder.get_object('button_refresh').set_sensitive(len(self.liststore)) self.builder.get_object('button_startdaemon').set_sensitive(False) self.builder.get_object('button_connect').set_sensitive(False) self.builder.get_object('button_connect').set_label(_('C_onnect')) self.builder.get_object('button_edithost').set_sensitive(False) self.builder.get_object('button_removehost').set_sensitive(False) self.builder.get_object('button_startdaemon').set_sensitive(False) self.builder.get_object('image_startdaemon').set_from_stock( gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU) self.builder.get_object('label_startdaemon').set_text_with_mnemonic('_Start Daemon') model, row = self.treeview.get_selection().get_selected() if row: self.builder.get_object('button_edithost').set_sensitive(True) self.builder.get_object('button_removehost').set_sensitive(True) else: return # Get selected host info. __, host, port, __, __, status, __ = model[row] try: gethostbyname(host) except gaierror as ex: log.error('Error resolving host %s to ip: %s', row[HOSTLIST_COL_HOST], ex.args[1]) self.builder.get_object('button_connect').set_sensitive(False) return log.debug('Host Status: %s, %s', host, status) # Check to see if the host is online if status == 'Connected' or status == 'Online': self.builder.get_object('button_connect').set_sensitive(True) self.builder.get_object('image_startdaemon').set_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU) self.builder.get_object('label_startdaemon').set_text_with_mnemonic(_('_Stop Daemon')) self.builder.get_object('button_startdaemon').set_sensitive(False) if status == 'Connected': # Display a disconnect button if we're connected to this host self.builder.get_object('button_connect').set_label(_('_Disconnect')) self.builder.get_object('button_removehost').set_sensitive(False) # Currently can only stop daemon when connected to it self.builder.get_object('button_startdaemon').set_sensitive(True) elif host in LOCALHOST: # If localhost we can start the dameon. self.builder.get_object('button_startdaemon').set_sensitive(True) def start_daemon(self, port, config): """Attempts to start local daemon process and will show an ErrorDialog if not. Args: port (int): Port for the daemon to listen on. config (str): Config path to pass to daemon. Returns: bool: True is successfully started the daemon, False otherwise. """ if client.start_daemon(port, config): log.debug('Localhost daemon started') reactor.callLater(1, self._update_host_status) return True else: ErrorDialog( _('Unable to start daemon!'), _('Check deluged package is installed and logs for further details')).run() return False # Signal handlers def _connect(self, host_id, username=None, password=None, try_counter=0): def do_connect(result, username=None, password=None, *args): log.debug('Attempting to connect to daemon...') for host_entry in self.hostlist.config['hosts']: if host_entry[0] == host_id: __, host, port, host_user, host_pass = host_entry username = username if username else host_user password = password if password else host_pass d = client.connect(host, port, username, password) d.addCallback(self._on_connect, host_id) d.addErrback(self._on_connect_fail, host_id, try_counter) return d if client.connected(): return client.disconnect().addCallback(do_connect, username, password) else: return do_connect(None, username, password) def _on_connect(self, daemon_info, host_id): log.debug('Connected to daemon: %s', host_id) if self.gtkui_config['autoconnect']: self.gtkui_config['autoconnect_host_id'] = host_id if self.running: # When connected to a client, and then trying to connect to another, # this component will be stopped(while the connect deferred is # running), so, self.connection_manager will be deleted. # If that's not the case, close the dialog. self.connection_manager.response(gtk.RESPONSE_OK) component.start() def _on_connect_fail(self, reason, host_id, try_counter): log.debug('Failed to connect: %s', reason.value) if reason.check(AuthenticationRequired, BadLoginError): log.debug('PasswordRequired exception') dialog = AuthenticationDialog(reason.value.message, reason.value.username) def dialog_finished(response_id): if response_id == gtk.RESPONSE_OK: self._connect(host_id, dialog.get_username(), dialog.get_password()) return dialog.run().addCallback(dialog_finished) elif reason.check(IncompatibleClient): return ErrorDialog(_('Incompatible Client'), reason.value.message).run() if try_counter: log.info('Retrying connection.. Retries left: %s', try_counter) return reactor.callLater(0.5, self._connect, host_id, try_counter=try_counter - 1) msg = str(reason.value) if not self.gtkui_config['autostart_localhost']: msg += '\n' + _('Auto-starting the daemon locally is not enabled. ' 'See "Options" on the "Connection Manager".') ErrorDialog(_('Failed To Connect'), msg).run() def on_button_connect_clicked(self, widget=None): """Button handler for connect to or disconnect from daemon.""" model, row = self.treeview.get_selection().get_selected() if not row: return host_id, host, port, __, __, status, __ = model[row] # If status is connected then connect button disconnects instead. if status == 'Connected': def on_disconnect(reason): self._update_host_status() return client.disconnect().addCallback(on_disconnect) try_counter = 0 auto_start = self.builder.get_object('chk_autostart').get_active() if auto_start and host in LOCALHOST and status == 'Offline': # Start the local daemon and then connect with retries set. if self.start_daemon(port, get_config_dir()): try_counter = 6 else: # Don't attempt to connect to offline daemon. return self._connect(host_id, try_counter=try_counter) def on_button_close_clicked(self, widget): self.connection_manager.response(gtk.RESPONSE_CLOSE) def _run_addhost_dialog(self, edit_host_info=None): """Create and runs the add host dialog. Supplying edit_host_info changes the dialog to an edit dialog. Args: edit_host_info (list): A list of (host, port, user, pass) to edit. Returns: list: The new host info values (host, port, user, pass). """ self.builder.add_from_file(resource_filename( 'deluge.ui.gtkui', os.path.join('glade', 'connection_manager.addhost.ui'))) dialog = self.builder.get_object('addhost_dialog') dialog.set_transient_for(self.connection_manager) hostname_entry = self.builder.get_object('entry_hostname') port_spinbutton = self.builder.get_object('spinbutton_port') username_entry = self.builder.get_object('entry_username') password_entry = self.builder.get_object('entry_password') if edit_host_info: dialog.set_title(_('Edit Host')) hostname_entry.set_text(edit_host_info[0]) port_spinbutton.set_value(edit_host_info[1]) username_entry.set_text(edit_host_info[2]) password_entry.set_text(edit_host_info[3]) response = dialog.run() new_host_info = [] if response: new_host_info.append(hostname_entry.get_text()) new_host_info.append(port_spinbutton.get_value_as_int()) new_host_info.append(username_entry.get_text()) new_host_info.append(password_entry.get_text()) dialog.destroy() return new_host_info def on_button_addhost_clicked(self, widget): log.debug('on_button_addhost_clicked') host_info = self._run_addhost_dialog() if host_info: hostname, port, username, password = host_info try: host_id = self.hostlist.add_host(hostname, port, username, password) except ValueError as ex: ErrorDialog(_('Error Adding Host'), ex).run() else: self.liststore.append([host_id, hostname, port, username, password, 'Offline', '']) self._update_host_status() def on_button_edithost_clicked(self, widget=None): log.debug('on_button_edithost_clicked') model, row = self.treeview.get_selection().get_selected() status = model[row][HOSTLIST_COL_STATUS] host_id = model[row][HOSTLIST_COL_ID] if status == 'Connected': def on_disconnect(reason): self._update_host_status() client.disconnect().addCallback(on_disconnect) return host_info = [ self.liststore[row][HOSTLIST_COL_HOST], self.liststore[row][HOSTLIST_COL_PORT], self.liststore[row][HOSTLIST_COL_USER], self.liststore[row][HOSTLIST_COL_PASS]] new_host_info = self._run_addhost_dialog(edit_host_info=host_info) if new_host_info: hostname, port, username, password = new_host_info try: self.hostlist.update_host(host_id, hostname, port, username, password) except ValueError as ex: ErrorDialog(_('Error Updating Host'), ex).run() else: self.liststore[row] = host_id, hostname, port, username, password, '', '' self._update_host_status() def on_button_removehost_clicked(self, widget): log.debug('on_button_removehost_clicked') # Get the selected rows model, row = self.treeview.get_selection().get_selected() self.hostlist.remove_host(model[row][HOSTLIST_COL_ID]) self.liststore.remove(row) # Update the hostlist self._update_host_status() def on_button_startdaemon_clicked(self, widget): log.debug('on_button_startdaemon_clicked') if not self.liststore.iter_n_children(None): # There is nothing in the list, so lets create a localhost entry try: self.hostlist.add_default_host() except ValueError as ex: log.error('Error adding default host: %s', ex) else: self.start_daemon(DEFAULT_PORT, get_config_dir()) finally: return paths = self.treeview.get_selection().get_selected_rows()[1] if len(paths): __, host, port, user, password, status, __ = self.liststore[paths[0]] else: return if host not in LOCALHOST: return def on_daemon_status_change(result): """Daemon start/stop callback""" reactor.callLater(0.7, self._update_host_status) if status in ('Online', 'Connected'): # Button will stop the daemon if status is online or connected. def on_connect(d, c): """Client callback to call daemon shutdown""" c.daemon.shutdown().addCallback(on_daemon_status_change) if client.connected() and (host, port, user) == client.connection_info(): client.daemon.shutdown().addCallback(on_daemon_status_change) elif user and password: c = Client() c.connect(host, port, user, password).addCallback(on_connect, c) else: # Otherwise button will start the daemon. self.start_daemon(port, get_config_dir()) def on_button_refresh_clicked(self, widget): self._update_host_status() def on_hostlist_row_activated(self, tree, path, view_column): self.on_button_connect_clicked() def on_hostlist_selection_changed(self, treeselection): self._update_widget_buttons() def on_chk_toggled(self, widget): self.gtkui_config['autoconnect'] = self.builder.get_object('chk_autoconnect').get_active() self.gtkui_config['autostart_localhost'] = self.builder.get_object('chk_autostart').get_active() self.gtkui_config['show_connection_manager_on_start'] = not self.builder.get_object( 'chk_donotshow').get_active() def on_entry_host_paste_clipboard(self, widget): text = get_clipboard_text() log.debug('on_entry_proxy_host_paste-clipboard: got paste: %s', text) text = text if '//' in text else '//' + text parsed = urlparse(text) if parsed.hostname: widget.set_text(parsed.hostname) widget.emit_stop_by_name('paste-clipboard') if parsed.port: self.builder.get_object('spinbutton_port').set_value(parsed.port) if parsed.username: self.builder.get_object('entry_username').set_text(parsed.username) if parsed.password: self.builder.get_object('entry_password').set_text(parsed.password)
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()
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)