diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py index 3664c1e2d..ba6ec285a 100644 --- a/deluge/core/torrent.py +++ b/deluge/core/torrent.py @@ -37,9 +37,12 @@ import os import time import logging +import re from urllib import unquote from urlparse import urlparse +from twisted.internet.defer import Deferred, DeferredList +from twisted.internet.task import LoopingCall from deluge._libtorrent import lt import deluge.common @@ -116,7 +119,6 @@ class Torrent(object): # We use this to return dicts that only contain changes from the previous # {session_id: status_dict, ...} self.prev_status = {} - from twisted.internet.task import LoopingCall self.prev_status_cleanup_loop = LoopingCall(self.cleanup_prev_status) self.prev_status_cleanup_loop.start(10) @@ -125,11 +127,10 @@ class Torrent(object): # Set the torrent_id for this torrent self.torrent_id = str(handle.info_hash()) - # Keep a list of file indexes we're waiting for file_rename alerts on - # This also includes the old_folder and new_folder to know what signal to send + # Keep a list of Deferreds for file indexes we're waiting for file_rename alerts on # This is so we can send one folder_renamed signal instead of multiple # file_renamed signals. - # [(old_folder, new_folder, [*indexes]), ...] + # [{index: Deferred, ...}, ...] self.waiting_on_folder_rename = [] # We store the filename just in case we need to make a copy of the torrentfile @@ -989,8 +990,13 @@ class Torrent(object): self.handle.rename_file(index, filename.encode("utf-8")) def rename_folder(self, folder, new_folder): - """Renames a folder within a torrent. This basically does a file rename - on all of the folders children.""" + """ + Renames a folder within a torrent. This basically does a file rename + on all of the folders children. + + :returns: A deferred which fires when the rename is complete + :rtype: twisted.internet.defer.Deferred + """ log.debug("attempting to rename folder: %s to %s", folder, new_folder) if len(new_folder) < 1: log.error("Attempting to rename a folder with an invalid folder name: %s", new_folder) @@ -998,13 +1004,57 @@ class Torrent(object): new_folder = sanitize_filepath(new_folder, folder=True) - wait_on_folder = (folder, new_folder, []) + def on_file_rename_complete(result, wait_dict, index): + wait_dict.pop(index, None) + + wait_on_folder = {} + self.waiting_on_folder_rename.append(wait_on_folder) for f in self.get_files(): if f["path"].startswith(folder): - # Keep a list of filerenames we're waiting on - wait_on_folder[2].append(f["index"]) + # Keep track of filerenames we're waiting on + wait_on_folder[f["index"]] = Deferred().addBoth(on_file_rename_complete, wait_on_folder, f["index"]) self.handle.rename_file(f["index"], f["path"].replace(folder, new_folder, 1).encode("utf-8")) - self.waiting_on_folder_rename.append(wait_on_folder) + + def on_folder_rename_complete(result, torrent, folder, new_folder): + component.get("EventManager").emit(TorrentFolderRenamedEvent(torrent.torrent_id, folder, new_folder)) + # Empty folders are removed after libtorrent folder renames + self.remove_empty_folders(folder) + torrent.waiting_on_folder_rename = filter(None, torrent.waiting_on_folder_rename) + component.get("TorrentManager").save_resume_data((self.torrent_id,)) + + d = DeferredList(wait_on_folder.values()) + d.addBoth(on_folder_rename_complete, self, folder, new_folder) + return d + + def remove_empty_folders(self, folder): + """ + Recursively removes folders but only if they are empty. + Cleans up after libtorrent folder renames. + + """ + info = self.get_status(['save_path']) + # Regex removes leading slashes that causes join function to ignore save_path + folder_full_path = os.path.join(info['save_path'], re.sub("^/*", "", folder)) + folder_full_path = os.path.normpath(folder_full_path) + + try: + if not os.listdir(folder_full_path): + os.removedirs(folder_full_path) + log.debug("Removed Empty Folder %s", folder_full_path) + else: + for root, dirs, files in os.walk(folder_full_path, topdown=False): + for name in dirs: + try: + os.removedirs(os.path.join(root, name)) + log.debug("Removed Empty Folder %s", os.path.join(root, name)) + except OSError as (errno, strerror): + from errno import ENOTEMPTY + if errno == ENOTEMPTY: + # Error raised if folder is not empty + log.debug("%s", strerror) + + except OSError as (errno, strerror): + log.debug("Cannot Remove Folder: %s (ErrNo %s)", strerror, errno) def cleanup_prev_status(self): """ diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index d6314dda4..25eab66bb 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -41,7 +41,6 @@ import os import shutil import operator import logging -import re from collections import defaultdict from twisted.internet.task import LoopingCall @@ -812,39 +811,6 @@ class TorrentManager(component.Component): except IOError: log.warning("Error trying to save fastresume file") - def remove_empty_folders(self, torrent_id, folder): - """ - Recursively removes folders but only if they are empty. - Cleans up after libtorrent folder renames. - - """ - if torrent_id not in self.torrents: - raise InvalidTorrentError("torrent_id is not in session") - - info = self.torrents[torrent_id].get_status(['save_path']) - # Regex removes leading slashes that causes join function to ignore save_path - folder_full_path = os.path.join(info['save_path'], re.sub("^/*", "", folder)) - folder_full_path = os.path.normpath(folder_full_path) - - try: - if not os.listdir(folder_full_path): - os.removedirs(folder_full_path) - log.debug("Removed Empty Folder %s", folder_full_path) - else: - for root, dirs, files in os.walk(folder_full_path, topdown=False): - for name in dirs: - try: - os.removedirs(os.path.join(root, name)) - log.debug("Removed Empty Folder %s", os.path.join(root, name)) - except OSError as (errno, strerror): - from errno import ENOTEMPTY - if errno == ENOTEMPTY: - # Error raised if folder is not empty - log.debug("%s", strerror) - - except OSError as (errno, strerror): - log.debug("Cannot Remove Folder: %s (ErrNo %s)", strerror, errno) - def get_queue_position(self, torrent_id): """Get queue position of torrent""" return self.torrents[torrent_id].get_queue_position() @@ -1097,24 +1063,12 @@ class TorrentManager(component.Component): except: return - # We need to see if this file index is in a waiting_on_folder list - folder_rename = False - for i, wait_on_folder in enumerate(torrent.waiting_on_folder_rename): - if alert.index in wait_on_folder[2]: - folder_rename = True - if len(wait_on_folder[2]) == 1: - # This is the last alert we were waiting for, time to send signal - component.get("EventManager").emit(TorrentFolderRenamedEvent(torrent_id, wait_on_folder[0], wait_on_folder[1])) - # Empty folders are removed after libtorrent folder renames - self.remove_empty_folders(torrent_id, wait_on_folder[0]) - del torrent.waiting_on_folder_rename[i] - self.save_resume_data((torrent_id,)) - break - # This isn't the last file to be renamed in this folder, so just - # remove the index and continue - torrent.waiting_on_folder_rename[i][2].remove(alert.index) - - if not folder_rename: + # We need to see if this file index is in a waiting_on_folder dict + for wait_on_folder in torrent.waiting_on_folder_rename: + if alert.index in wait_on_folder: + wait_on_folder[alert.index].callback(None) + break + else: # This is just a regular file rename so send the signal component.get("EventManager").emit(TorrentFileRenamedEvent(torrent_id, alert.index, alert.name)) self.save_resume_data((torrent_id,))