diff --git a/deluge/core/core.py b/deluge/core/core.py index 29b43f363..96932cea7 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -326,22 +326,48 @@ class Core(component.Component): @export def remove_torrent(self, torrent_id, remove_data): - """ - Removes a torrent from the session. + """Removes a single torrent from the session. - :param torrent_id: the torrent_id of the torrent to remove - :type torrent_id: string - :param remove_data: if True, remove the data associated with this torrent - :type remove_data: boolean - :returns: True if removed successfully - :rtype: bool + Args: + torrent_id (str): The torrent ID to remove. + remove_data (bool): If True, also remove the downloaded data. - :raises InvalidTorrentError: if the torrent_id does not exist in the session + Returns: + bool: True if removed successfully. + + Raises: + InvalidTorrentError: If the torrent ID does not exist in the session. """ log.debug("Removing torrent %s from the core.", torrent_id) return self.torrentmanager.remove(torrent_id, remove_data) + @export + def remove_torrents(self, torrent_ids, remove_data): + """Remove multiple torrents from the session. + + Args: + torrent_ids (list): The torrent IDs to remove. + remove_data (bool, optional): If True, also remove the downloaded data. + + Returns: + list: a list containing all the errors, empty list if no errors occured + + """ + log.info("Removing %d torrents from core.", len(torrent_ids)) + + def do_remove_torrents(): + errors = [] + for torrent_id in torrent_ids: + try: + self.torrentmanager.remove(torrent_id, remove_data=remove_data, save_state=False) + except InvalidTorrentError, ite: + errors.append((torrent_id, ite)) + # Save the session state + self.torrentmanager.save_state() + return errors + return task.deferLater(reactor, 0, do_remove_torrents) + @export def get_session_status(self, keys): """ diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index 79d1ca9c7..7f5aea60c 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -523,12 +523,13 @@ class TorrentManager(component.Component): from_state and "loaded" or "added") return torrent.torrent_id - def remove(self, torrent_id, remove_data=False): - """Remove specified torrent from the session. + def remove(self, torrent_id, remove_data=False, save_state=True): + """Remove a torrent from the session. Args: - torrent_id (str): The torrent to remove. + torrent_id (str): The torrent ID to remove. remove_data (bool, optional): If True, remove the downloaded data, defaults to False. + save_state (bool, optional): If True, save the session state after removal, defaults to True. Returns: bool: True if removed successfully, False if not. @@ -538,16 +539,16 @@ class TorrentManager(component.Component): """ try: - torrent_name = self.torrents[torrent_id].get_status(["name"])["name"] + torrent = self.torrents[torrent_id] except KeyError: - raise InvalidTorrentError("torrent_id not in session") + raise InvalidTorrentError("torrent_id '%s' not in session." % torrent_id) # Emit the signal to the clients component.get("EventManager").emit(PreTorrentRemovedEvent(torrent_id)) try: - self.session.remove_torrent(self.torrents[torrent_id].handle, 1 if remove_data else 0) - except (RuntimeError, KeyError) as ex: + self.session.remove_torrent(torrent.handle, 1 if remove_data else 0) + except RuntimeError as ex: log.warning("Error removing torrent: %s", ex) return False @@ -555,10 +556,11 @@ class TorrentManager(component.Component): self.resume_data.pop(torrent_id, None) # Remove the .torrent file in the state - self.torrents[torrent_id].delete_torrentfile() + torrent.delete_torrentfile() # Remove the torrent file from the user specified directory - filename = self.torrents[torrent_id].filename + torrent_name = torrent.get_status(["name"])["name"] + filename = torrent.filename if self.config["copy_torrent_file"] and self.config["del_copy_torrent_file"] and filename: users_torrent_file = os.path.join(self.config["torrentfiles_location"], filename) log.info("Delete user's torrent file: %s", users_torrent_file) @@ -568,25 +570,22 @@ class TorrentManager(component.Component): log.warning("Unable to remove copy torrent file: %s", ex) # Remove from set if it wasn't finished - if not self.torrents[torrent_id].is_finished: + if not torrent.is_finished: try: self.queued_torrents.remove(torrent_id) except KeyError: log.debug("%s isn't in queued torrents set?", torrent_id) + raise InvalidTorrentError("%s isn't in queued torrents set?" % torrent_id) # Remove the torrent from deluge's session - try: - del self.torrents[torrent_id] - except (KeyError, ValueError): - return False + del self.torrents[torrent_id] - # Save the session state - self.save_state() + if save_state: + self.save_state() # Emit the signal to the clients component.get("EventManager").emit(TorrentRemovedEvent(torrent_id)) - log.info("Torrent %s removed by user: %s", torrent_name, - component.get("RPCServer").get_session_user()) + log.info("Torrent %s removed by user: %s", torrent_name, component.get("RPCServer").get_session_user()) return True def load_state(self): diff --git a/deluge/tests/test_core.py b/deluge/tests/test_core.py index a96d21272..2b7cca121 100644 --- a/deluge/tests/test_core.py +++ b/deluge/tests/test_core.py @@ -1,3 +1,4 @@ +import base64 import os from hashlib import sha1 as sha @@ -11,9 +12,10 @@ from twisted.web.server import Site from twisted.web.static import File import deluge.component as component -import deluge.error +import deluge.core.torrent from deluge.core.core import Core from deluge.core.rpcserver import RPCServer +from deluge.error import InvalidTorrentError from deluge.ui.web.common import compress from . import common @@ -104,7 +106,6 @@ class CoreTestCase(BaseTestCase): def test_add_torrent_file(self): options = {} filename = os.path.join(os.path.dirname(__file__), "test.torrent") - import base64 torrent_id = self.core.add_torrent_file(filename, base64.encodestring(open(filename).read()), options) # Get the info hash from the test.torrent @@ -168,16 +169,53 @@ class CoreTestCase(BaseTestCase): def test_remove_torrent(self): options = {} filename = os.path.join(os.path.dirname(__file__), "test.torrent") - import base64 torrent_id = self.core.add_torrent_file(filename, base64.encodestring(open(filename).read()), options) - - self.assertRaises(deluge.error.InvalidTorrentError, self.core.remove_torrent, "torrentidthatdoesntexist", True) - - ret = self.core.remove_torrent(torrent_id, True) - - self.assertTrue(ret) + removed = self.core.remove_torrent(torrent_id, True) + self.assertTrue(removed) self.assertEquals(len(self.core.get_session_state()), 0) + def test_remove_torrent_invalid(self): + d = self.core.remove_torrents(["torrentidthatdoesntexist"], True) + + def test_true(val): + self.assertTrue(val[0][0] == "torrentidthatdoesntexist") + + self.assertTrue(type(val[0][1]) == InvalidTorrentError) + d.addCallback(test_true) + return d + + def test_remove_torrents(self): + options = {} + filename = os.path.join(os.path.dirname(__file__), "test.torrent") + torrent_id = self.core.add_torrent_file(filename, base64.encodestring(open(filename).read()), options) + filename2 = os.path.join(os.path.dirname(__file__), "unicode_filenames.torrent") + torrent_id2 = self.core.add_torrent_file(filename2, base64.encodestring(open(filename2).read()), options) + d = self.core.remove_torrents([torrent_id, torrent_id2], True) + + def test_ret(val): + self.assertTrue(val == []) + d.addCallback(test_ret) + + def test_session_state(val): + self.assertEquals(len(self.core.get_session_state()), 0) + d.addCallback(test_session_state) + return d + + def test_remove_torrents_invalid(self): + options = {} + filename = os.path.join(os.path.dirname(__file__), "test.torrent") + torrent_id = self.core.add_torrent_file(filename, base64.encodestring(open(filename).read()), options) + d = self.core.remove_torrents(["invalidid1", "invalidid2", torrent_id], False) + + def test_ret(val): + self.assertTrue(len(val) == 2) + self.assertTrue(val[0][0] == "invalidid1") + self.assertTrue(type(val[0][1]) == InvalidTorrentError) + self.assertTrue(val[1][0] == "invalidid2") + self.assertTrue(type(val[1][1]) == InvalidTorrentError) + d.addCallback(test_ret) + return d + def test_get_session_status(self): status = self.core.get_session_status(["upload_rate", "download_rate"]) self.assertEquals(type(status), dict) diff --git a/deluge/tests/test_torrentmanager.py b/deluge/tests/test_torrentmanager.py new file mode 100644 index 000000000..c2430fcd9 --- /dev/null +++ b/deluge/tests/test_torrentmanager.py @@ -0,0 +1,44 @@ +import base64 +import os +import warnings + +from twisted.trial import unittest + +import common +from deluge import component +from deluge.core.core import Core +from deluge.core.rpcserver import RPCServer +from deluge.error import InvalidTorrentError + +warnings.filterwarnings("ignore", category=RuntimeWarning) +warnings.resetwarnings() + + +class TorrentmanagerTestCase(unittest.TestCase): + + def setUp(self): # NOQA + common.set_tmp_config_dir() + self.rpcserver = RPCServer(listen=False) + self.core = Core() + self.torrentManager = self.core.torrentmanager + return component.start() + + def tearDown(self): # NOQA + def on_shutdown(result): + component._ComponentRegistry.components = {} + del self.rpcserver + del self.core + del self.torrentManager + return component.shutdown().addCallback(on_shutdown) + + def test_remove_torrent(self): + filename = os.path.join(os.path.dirname(__file__), "test.torrent") + torrent_id = self.core.add_torrent_file(filename, base64.encodestring(open(filename).read()), {}) + self.assertTrue(self.torrentManager.remove(torrent_id, False)) + + def test_remove_torrent_false(self): + """Test when remove_torrent returns False""" + raise unittest.SkipTest("") + + def test_remove_invalid_torrent(self): + self.assertRaises(InvalidTorrentError, self.torrentManager.remove, "torrentidthatdoesntexist") diff --git a/deluge/ui/gtkui/removetorrentdialog.py b/deluge/ui/gtkui/removetorrentdialog.py index acc7ff7c6..0cc4c5f60 100644 --- a/deluge/ui/gtkui/removetorrentdialog.py +++ b/deluge/ui/gtkui/removetorrentdialog.py @@ -67,8 +67,14 @@ class RemoveTorrentDialog(object): # Unselect all to avoid issues with the selection changed event component.get("TorrentView").treeview.get_selection().unselect_all() - for torrent_id in self.__torrent_ids: - client.core.remove_torrent(torrent_id, remove_data) + def on_removed_finished(errors): + if errors: + log.info("Error(s) occured when trying to delete torrent(s).") + for t_id, e_msg in errors: + log.warn("Error removing torrent %s : %s" % (t_id, e_msg)) + + d = client.core.remove_torrents(self.__torrent_ids, remove_data) + d.addCallback(on_removed_finished) def run(self): """