# -*- coding: utf-8 -*-
# pylint: disable=I0011,C0103
""" rTorrent Watch Jobs.
Copyright (c) 2012 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 with_statement
from __future__ import absolute_import
# TODO: Re-tie metafiles when they're moved in the tree
import time
import logging
import asyncore
from pyrobase import logutil
from pyrobase.parts import Bunch
from pyrocore import error
from pyrocore import config as configuration
from pyrocore.util import os, fmt, xmlrpc, pymagic, metafile, traits
from pyrocore.torrent import matching, formatting
from pyrocore.scripts.base import ScriptBase, ScriptBaseWithConfig
try:
import pyinotify
except ImportError as exc:
pyinotify = Bunch(WatchManager=None, ProcessEvent=object, _import_error=str(exc)) # bogus pylint: disable=C0103
[docs]class RemoteWatch(object):
""" rTorrent remote torrent file watch.
"""
def __init__(self, config=None):
""" Set up remote watcher.
"""
self.config = config or {}
self.LOG = pymagic.get_class_logger(self)
self.LOG.debug("Remote watcher created with config %r" % self.config)
[docs] def run(self):
""" Check remote watch target.
"""
# TODO: ftp. ssh, and remote rTorrent instance (extra view?) as sources!
# config:
# local_dir storage path (default local sessiondir + '/remote-watch-' + jobname
# target URL of target to watch
[docs]class TreeWatchHandler(pyinotify.ProcessEvent):
""" inotify event handler for rTorrent folder tree watch.
See https://github.com/seb-m/pyinotify/.
"""
METAFILE_EXT = (".torrent", ".load", ".start", ".queue")
[docs] def my_init(self, **kw):
self.job = kw["job"] # pylint: disable=W0201
[docs] def handle_path(self, event):
""" Handle a path-related event.
"""
self.job.LOG.debug("Notification %r" % event)
if event.dir:
return
if any(event.pathname.endswith(i) for i in self.METAFILE_EXT):
MetafileHandler(self.job, event.pathname).handle()
elif os.path.basename(event.pathname) == "watch.ini":
self.job.LOG.info("NOT YET Reloading watch config for '%s'" % event.path)
# TODO: Load new metadata
[docs] def process_IN_CLOSE_WRITE(self, event):
""" File written.
"""
# <Event dir=False name=xx path=/var/torrent/watch/tmp pathname=/var/torrent/watch/tmp/xx>
self.handle_path(event)
[docs] def process_IN_MOVED_TO(self, event):
""" File moved into tree.
"""
# <Event dir=False name=yy path=/var/torrent/watch/tmp pathname=/var/torrent/watch/tmp/yy>
self.handle_path(event)
[docs] def process_default(self, event):
""" Fallback.
"""
if self.job.LOG.isEnabledFor(logging.DEBUG):
# On debug level, we subscribe to ALL events, so they're expected in that case ;)
if self.job.config.trace_inotify:
self.job.LOG.debug("Ignored inotify event:\n %r" % event)
else:
self.job.LOG.warning("Unexpected inotify event %r" % event)
[docs]class TreeWatch(object):
""" rTorrent folder tree watch via inotify.
"""
def __init__(self, config=None):
self.config = config or {}
self.LOG = pymagic.get_class_logger(self)
self.LOG.debug("Tree watcher created with config %r" % self.config)
self.manager = None
self.handler = None
self.notifier = None
bool_param = lambda key, default: matching.truth(self.config.get(key, default), "job.%s.%s" % (self.config.job_name, key))
if not self.config.path:
raise error.UserError("You need to set 'job.%s.path' in the configuration!" % self.config.job_name)
self.config.quiet = bool_param("quiet", False)
self.config.queued = bool_param("queued", False)
self.config.trace_inotify = bool_param("trace_inotify", False)
self.config.path = set([os.path.abspath(os.path.expanduser(path.strip()).rstrip(os.sep))
for path in self.config.path.split(os.pathsep)
])
for path in self.config.path:
if not os.path.isdir(path):
raise error.UserError("Path '%s' is not a directory!" % path)
# Assemble custom commands
self.custom_cmds = {}
for key, val in self.config.items():
if key.startswith("cmd."):
_, key = key.split('.', 1)
if key in self.custom_cmds:
raise error.UserError("Duplicate custom command definition '%s'"
" (%r already registered, you also added %r)!" % (key, self.custom_cmds[key], val))
self.custom_cmds[key] = formatting.preparse(val)
self.LOG.debug("custom commands = %r" % self.custom_cmds)
# Get client proxy
self.proxy = xmlrpc.RTorrentProxy(configuration.scgi_url)
self.proxy._set_mappings() # pylint: disable=W0212
if self.config.active:
self.setup()
[docs] def setup(self):
""" Set up inotify manager.
See https://github.com/seb-m/pyinotify/.
"""
if not pyinotify.WatchManager:
raise error.UserError("You need to install 'pyinotify' to use %s (%s)!" % (
self.__class__.__name__, pyinotify._import_error)) # pylint: disable=E1101, W0212
self.manager = pyinotify.WatchManager()
self.handler = TreeWatchHandler(job=self)
self.notifier = pyinotify.AsyncNotifier(self.manager, self.handler)
if self.LOG.isEnabledFor(logging.DEBUG):
mask = pyinotify.ALL_EVENTS
else:
mask = pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO # bogus pylint: disable=E1101
# Add all configured base dirs
for path in self.config.path:
self.manager.add_watch(path.strip(), mask, rec=True, auto_add=True)
[docs] def run(self):
""" Regular maintenance and fallback task.
"""
# TODO: Maybe do some stats logging here, once per hour or so
# TODO: We can handle files that were not valid bencode here, from a Queue! And watch.ini reloading.
# XXX: Add a check that the notifier is working, by creating / deleting a file
# XXX: Also check for unhandled files
# TODO: XXX: Especially on startup, we need to walk the directory tree
# and check for files not loaded (by checking hashes)!
# Or maybe rename them during loading?! Makes detection easy.
# TODO: Move untied *.torrent.loaded in the tree to *.torrent.dead
[docs]class TreeWatchCommand(ScriptBaseWithConfig):
### Keep things wrapped to fit under this comment... ##############################
"""
Use tree watcher directly from cmd line, call it like this:
python -m pyrocore.torrent.watch <DIR>
If the argument is a file, the templating namespace for that metafile is
dumped (for testing and debugging purposes).
"""
# log level for user-visible standard logging
STD_LOG_LEVEL = logging.DEBUG
# argument description for the usage information
ARGS_HELP = "<directory>"
OPTIONAL_CFG_FILES = ["torque.ini"]
[docs] def mainloop(self):
""" The main loop.
"""
# Print usage if not enough args or bad options
if len(self.args) < 1:
self.parser.error("You have to provide the root directory of your watch tree, or a metafile path!")
configuration.engine.load_config()
pathname = os.path.abspath(self.args[0])
if os.path.isdir(pathname):
watch = TreeWatch(Bunch(path=pathname, job_name="watch", active=True, dry_run=True, load_mode=None))
asyncore.loop(timeout=~0, use_poll=True)
else:
config = Bunch()
config.update(dict((key.split('.', 2)[-1], val)
for key, val in configuration.torque.items()
if key.startswith("job.treewatch.")
))
config.update(dict(path=os.path.dirname(os.path.dirname(pathname)), job_name="treewatch", active=False, dry_run=True))
watch = TreeWatch(config)
handler = MetafileHandler(watch, pathname)
ok = handler.parse()
self.LOG.debug("Metafile '%s' would've %sbeen loaded" % (pathname, "" if ok else "NOT "))
if ok:
handler.addinfo()
post_process = str if self.options.verbose else logutil.shorten
self.LOG.info("Templating values are:\n %s" % "\n ".join("%s=%s" % (key, post_process(repr(val)))
for key, val in sorted(handler.ns.items())
))
@classmethod
[docs] def main(cls): #pragma: no cover
""" The entry point.
"""
ScriptBase.setup()
cls().run()
if __name__ == "__main__":
TreeWatchCommand.main()