def remove_with_links(path): "Remove a path including any symlink chains leading to it." rm_paths = [] while os.path.islink(path): target = os.readlink(path) rm_paths.append(path) path = target if os.path.exists(path): rm_paths.append(path) else: self._engine.LOG.debug("Real path '%s' doesn't exist," " but %d symlink(s) leading to it will be deleted..." % (path, len(rm_paths))) # Remove the link chain, starting at the real path # (this prevents losing the chain when there's permission problems) for path in reversed(rm_paths): is_dir = os.path.isdir(path) and not os.path.islink(path) self._engine.LOG.debug("Deleting '%s%s'" % (path, '/' if is_dir else '')) if not dry_run: try: (os.rmdir if is_dir else os.remove)(path) except OSError, exc: if exc.errno == errno.ENOENT: # Seems this disappeared somehow inbetween (race condition) self._engine.LOG.info("Path '%s%s' disappeared before it could be deleted" % (path, '/' if is_dir else '')) else: raise
def remove_with_links(path): "Remove a path including any symlink chains leading to it." rm_paths = [] while os.path.islink(path): target = os.readlink(path) rm_paths.append(path) path = target if os.path.exists(path): rm_paths.append(path) else: self._engine.LOG.debug( "Real path '%s' doesn't exist," " but %d symlink(s) leading to it will be deleted..." % (path, len(rm_paths))) # Remove the link chain, starting at the real path # (this prevents losing the chain when there's permission problems) for rm_path in reversed(rm_paths): is_dir = os.path.isdir(rm_path) and not os.path.islink(rm_path) self._engine.LOG.debug("Deleting '%s%s'" % (rm_path, '/' if is_dir else '')) if not dry_run: try: (os.rmdir if is_dir else os.remove)(rm_path) except OSError as exc: if exc.errno == errno.ENOENT: # Seems this disappeared somehow inbetween (race condition) self._engine.LOG.info( "Path '%s%s' disappeared before it could be deleted" % (rm_path, '/' if is_dir else '')) else: raise return rm_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),))
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))