Source code for pyrocore.scripts.rtmv

# -*- coding: utf-8 -*-
# pylint: disable=
""" Move seeding data.

    Copyright (c) 2010, 2011 The PyroScope Project <pyroscope.project@gmail.com>
"""
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import

import logging
from collections import defaultdict

from pyrocore import config
from pyrocore.scripts.base import ScriptBase, ScriptBaseWithConfig
from pyrocore.util import os, fmt, xmlrpc
#from pyrocore.torrent import engine


[docs]def pretty_path(path): """ Prettify path for logging. """ path = fmt.to_utf8(path) home_dir = os.path.expanduser("~") if path.startswith(home_dir): path = "~" + path[len(home_dir):] return '"%s"' % (path,)
[docs]class RtorrentMove(ScriptBaseWithConfig): ### Keep things wrapped to fit under this comment... ############################## """ Move data actively seeded in rTorrent. """ # argument description for the usage information ARGS_HELP = "<source>... <target>" # fields needed to find the item PREFETCH_FIELDS = [ "hash", "name", "size", "path", "is_complete", ]
[docs] def add_options(self): """ Add program options. """ super(RtorrentMove, self).add_options() # basic options self.add_bool_option("-n", "--dry-run", help="don't move data, just tell what would happen") self.add_bool_option("-F", "--force-incomplete", help="force a move of incomplete data")
[docs] def resolve_slashed(self, path): """ Resolve symlinked directories if they end in a '/', remove trailing '/' otherwise. """ if path.endswith(os.sep): path = path.rstrip(os.sep) if os.path.islink(path): real = os.path.realpath(path) self.LOG.debug('Resolved "%s/" to "%s"' % (path, real)) path = real return path
[docs] def guarded(self, call, *args): """ Catch exceptions thrown by filesystem calls, and don't really execute them in dry-run mode. """ self.LOG.debug('%s(%s)' % ( call.__name__, ', '.join([pretty_path(i) for i in args]), )) if not self.options.dry_run: try: call(*args) except (EnvironmentError, UnicodeError) as exc: self.fatal('%s(%s) failed [%s]' % ( call.__name__, ', '.join([pretty_path(i) for i in args]), exc, ))
[docs] def mainloop(self): """ The main loop. """ # Print usage if not enough args if len(self.args) < 2: self.parser.print_help() self.parser.exit() # TODO: Add mode to move tied metafiles, without losing the tie # Target handling target = self.args[-1] if "//" in target.rstrip('/'): # Create parts of target path existing, _ = target.split("//", 1) if not os.path.isdir(existing): self.fatal("Path before '//' MUST exists in %s" % (pretty_path(target),)) # Possibly create the rest target = target.replace("//", "/") if not os.path.exists(target): self.guarded(os.makedirs, target) # Preparation # TODO: Handle cases where target is the original download path correctly! # i.e. rtmv foo/ foo AND rtmv foo/ . (in the download dir) proxy = config.engine.open() download_path = os.path.realpath(os.path.expanduser(proxy.directory.default(xmlrpc.NOHASH).rstrip(os.sep))) target = self.resolve_slashed(target) source_paths = [self.resolve_slashed(i) for i in self.args[:-1]] source_realpaths = [os.path.realpath(i) for i in source_paths] source_items = defaultdict(list) # map of source path to item items = list(config.engine.items(prefetch=self.PREFETCH_FIELDS)) # Validate source paths and find matching items for item in items: if not item.path: continue realpath = None try: realpath = os.path.realpath(item.path) except (EnvironmentError, UnicodeError) as exc: self.LOG.warning("Cannot realpath %r (%s)" % (item.path, exc)) # Look if item matches a source path # TODO: Handle download items nested into each other! try: path_idx = source_realpaths.index(realpath or fmt.to_utf8(item.path)) except ValueError: continue if realpath: self.LOG.debug('Item path %s resolved to %s' % (pretty_path(item.path), pretty_path(realpath))) self.LOG.debug('Found "%s" for %s' % (fmt.to_utf8(item.name), pretty_path(source_paths[path_idx]))) source_items[source_paths[path_idx]].append(item) ##for path in source_paths: print path, "==>"; print " " + "\n ".join(i.path for i in source_items[path]) if not os.path.isdir(target) and len(source_paths) > 1: self.fatal("Can't move multiple files to %s which is no directory!" % (pretty_path(target),)) # Actually move the data moved_count = 0 for path in source_paths: item = None # Make sure there's no accidental stale reference if not source_items[path]: self.LOG.warn("No download item found for %s, skipping!" % (pretty_path(path),)) continue if len(source_items[path]) > 1: self.LOG.warn("Can't handle multi-item moving yet, skipping %s!" % (pretty_path(path),)) continue if os.path.islink(path): self.LOG.warn("Won't move symlinks, skipping %s!" % (pretty_path(path),)) continue for item in source_items[path]: if os.path.islink(item.path) and os.path.realpath(item.path) != os.readlink(item.path): self.LOG.warn("Can't handle multi-hop symlinks yet, skipping %s!" % (pretty_path(path),)) continue if not item.is_complete: if self.options.force_incomplete: self.LOG.warn("Moving incomplete item '%s'!" % (item.name,)) else: self.LOG.warn("Won't move incomplete item '%s'!" % (item.name,)) continue moved_count += 1 dst = target if os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(path)) self.LOG.info("Moving to %s..." % (pretty_path(dst),)) # Pause torrent? # was_active = item.is_active and not self.options.dry_run # if was_active: item.pause() # TODO: move across devices # TODO: move using "d.directory.set" instead of symlinks if os.path.islink(item.path): if os.path.abspath(dst) == os.path.abspath(item.path.rstrip(os.sep)): # Moving back to original place self.LOG.debug("Unlinking %s" % (pretty_path(item.path),)) self.guarded(os.remove, item.path) self.guarded(os.rename, path, dst) else: # Moving to another place self.LOG.debug("Re-linking %s" % (pretty_path(item.path),)) self.guarded(os.rename, path, dst) self.guarded(os.remove, item.path) self.guarded(os.symlink, os.path.abspath(dst), item.path) else: # Moving download initially self.LOG.debug("Symlinking %s" % (pretty_path(item.path),)) src1, src2 = os.path.join(download_path, os.path.basename(item.path)), fmt.to_unicode(os.path.realpath(path)) assert src1 == src2, 'Item path %r should match %r!' % (src1, src2) self.guarded(os.rename, item.path, dst) self.guarded(os.symlink, os.path.abspath(dst), item.path) # Resume torrent? # if was_active: sitem.resume() # Print stats self.LOG.debug("XMLRPC stats: %s" % proxy) self.LOG.log(logging.DEBUG if self.options.cron else logging.INFO, "Moved %d path%s (skipped %d)" % ( moved_count, "" if moved_count == 1 else "s", len(source_paths) - moved_count ))
[docs]def run(): #pragma: no cover """ The entry point. """ ScriptBase.setup() RtorrentMove().run()
if __name__ == "__main__": run()