示例#1
0
    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 "))

            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())
            ))
示例#2
0
    def _parse_triggers(self):
        """ Parse trigger definitions from config.
        """
        # TODO: Load triggers from their own file (INI or YAML), and watch it with a timer?!
        self.triggers = []
        current = Bunch(nick=None, flags=re.I, sysattr='')
        for token in shlex.split(self.api.config_get_plugin("triggers") or ''):
            if '=' in token:
                key, val = token.split('=', 1)
                current[key] = val

                if key == 'series':
                    missing = [i for i in ('dbname', 'buffer', 'regex') if i not in current]
                    if missing:
                        self.log('triggers: Ignoring series "{0}" due to missing fields: {1}', val, missing, prefix='warn')
                        continue

                    triggerdef = Bunch(current)
                    triggerdef.sysattr = set(i.strip() for i in triggerdef.sysattr.split(','))
                    try:
                        triggerdef.regex = re.compile(triggerdef.regex, triggerdef.flags)
                    except re.error, exc:
                        self.log('Ignoring series "{0}" with malformed regex "{1}": {2}', val, current.regex, exc, prefix='warn')
                    else:
                        self.triggers.append(triggerdef)
                        self.trace("trigger#{0} = {1} ", len(self.triggers), current)
            elif token == 'ignorecase':
                current.flags |= re.I
示例#3
0
class FilterTest(unittest.TestCase):
    DATA = [
        Bunch(name="T1", num=1, flag=True, tags=set("ab")),
        Bunch(name="F0", num=0, flag=False, tags=set()),
        Bunch(name="T11", num=11, flag=True, tags=set("b")),
    ]
    CASES = [
        ("flag=y", "T1 T11"),
        ("num=-1", "F0"),
        ("num=-2", "F0 T1"),
        ("num=+99", ""),
        ("num>10", "T11"),
        ("num>11", ""),
        ("num>=11", "T11"),
        ("num<=11", "F0 T1 T11"),
        ("num<11", "F0 T1"),
        ("num<>1", "F0 T11"),
        ("num!=1", "F0 T11"),
        ("T?", "T1"),
        ("T*", "T1 T11"),
        ("tags=a", "T1"),
        ("tags=b", "T1 T11"),
        ("tags=a,b", "T1 T11"),
        ("tags==b", "T11"),
        ("tags=", "F0"),
        ("tags=!", "T1 T11"),
        ("tags=!a", "F0 T11"),
    ]

    def test_conditions(self):
        for cond, expected in self.CASES:
            keep = matching.ConditionParser(lookup, "name").parse(cond)
            result = set(i.name for i in self.DATA if keep(i))
            expected = set(expected.split())
            assert result == expected, "Expected %r, but got %r, for '%s' [ %s ]" % (expected, result, cond, keep) 
示例#4
0
    def __init__(self, config=None):
        """ Set up InfluxDB logger.
        """
        self.config = config or Bunch()
        self.influxdb = Bunch(config_ini.influxdb)
        self.influxdb.timeout = float(self.influxdb.timeout or '0.250')

        self.LOG = pymagic.get_class_logger(self)
        self.LOG.debug("InfluxDB statistics feed created with config %r" % self.config)
示例#5
0
 def __init__(self, job, pathname):
     """ Create a metafile handler.
     """
     self.job = job
     self.metadata = None
     self.ns = Bunch(
         pathname=os.path.abspath(pathname),
         info_hash=None,
         tracker_alias=None,
     )
示例#6
0
    def load_config(self, namespace=None, rcfile=None):
        """ Load file given in "rcfile".
        """
        if namespace is None:
            namespace = config
        if namespace.scgi_url:
            return  # already have the connection to rTorrent

        # Get and check config file name
        if not rcfile:
            rcfile = getattr(config, "rtorrent_rc", None)
        if not rcfile:
            raise error.UserError(
                "No 'rtorrent_rc' path defined in configuration!")
        if not os.path.isfile(rcfile):
            raise error.UserError("Config file %r doesn't exist!" % (rcfile, ))

        # Parse the file
        self.LOG.debug("Loading rtorrent config from %r" % (rcfile, ))
        rc_vals = Bunch(scgi_local='', scgi_port='')
        with open(rcfile) as handle:
            continued = False
            for line in handle.readlines():
                # Skip comments, continuations, and empty lines
                line = line.strip()
                continued, was_continued = line.endswith('\\'), continued
                if not line or was_continued or line.startswith("#"):
                    continue

                # Be lenient about errors, after all it's not our own config file
                try:
                    key, val = line.split("=", 1)
                except ValueError:
                    self.LOG.warning("Ignored invalid line %r in %r!" %
                                     (line, rcfile))
                    continue
                key, val = key.strip(), val.strip()
                key = self.RTORRENT_RC_ALIASES.get(key, key).replace('.', '_')

                # Copy values we're interested in
                if key in self.RTORRENT_RC_KEYS:
                    self.LOG.debug("rtorrent.rc: %s = %s" % (key, val))
                    rc_vals[key] = val

        # Validate fields
        if rc_vals.scgi_local:
            rc_vals.scgi_local = os.path.expanduser(rc_vals.scgi_local)
            if rc_vals.scgi_local.startswith('/'):
                rc_vals.scgi_local = "scgi://" + rc_vals.scgi_local
        if rc_vals.scgi_port and not rc_vals.scgi_port.startswith("scgi://"):
            rc_vals.scgi_port = "scgi://" + rc_vals.scgi_port

        # Prefer UNIX domain sockets over TCP sockets
        namespace.scgi_url = rc_vals.scgi_local or rc_vals.scgi_port
示例#7
0
    def mainloop(self):
        """ The main loop.
        """
        # Print field definitions?
        if self.options.help_fields:
            self.parser.print_help()
            print_help_fields()
            sys.exit(1)

        # Print usage if no conditions are provided
        if not self.args:
            self.parser.error("No filter conditions given!")

        # Check special action options
        actions = []
        if self.options.ignore:
            actions.append(Bunch(name="ignore", method="ignore", label="IGNORE" if int(self.options.ignore) else "HEED",
                help="commands on torrent", interactive=False, args=(self.options.ignore,)))
        if self.options.prio:
            actions.append(Bunch(name="prio", method="set_prio", label="PRIO" + str(self.options.prio),
                help="for torrent", interactive=False, args=(self.options.prio,)))

        # Check standard action options
        # TODO: Allow certain combinations of actions (like --tag foo --stop etc.)
        #       How do we get a sensible order of execution?
        for action_mode in self.ACTION_MODES:
            if getattr(self.options, action_mode.name):
                if actions:
                    self.parser.error("Options --%s and --%s are mutually exclusive" % (
                        ", --".join(i.name.replace('_', '-') for i in actions),
                        action_mode.name.replace('_', '-'),
                    ))
                if action_mode.argshelp:
                    action_mode.args = (getattr(self.options, action_mode.name),)
                actions.append(action_mode)
        if not actions and self.options.flush:
            actions.append(Bunch(name="flush", method="flush", label="FLUSH",
                help="flush session data", interactive=False, args=()))
            self.options.flush = False # No need to flush twice
        if any(i.interactive for i in actions):
            self.options.interactive = True

        # Reduce results according to index range
        selection = None
        if self.options.select:
            try:
                if '-' in self.options.select:
                    selection = tuple(int(i or default, 10) for i, default in
                        zip(self.options.select.split('-', 1), ("1", "-1")))
                else:
                    selection = 1, int(self.options.select, 10)
            except (ValueError, TypeError), exc:
                self.fatal("Bad selection '%s' (%s)" % (self.options.select, exc))
示例#8
0
文件: jobs.py 项目: supergis/pyrocore
 def __init__(self, config=None):
     """ Set up statistics logger.
     """
     self.config = config or Bunch()
     self.LOG = pymagic.get_class_logger(self)
     self.LOG.debug("Statistics logger created with config %r" %
                    self.config)
示例#9
0
    def test_magic_matching(self):
        item = Bunch(name="foo",
                     date=time.time() - 86401,
                     one=1,
                     year=2011,
                     size=1024**2)
        match = lambda c: matching.ConditionParser(
            lambda _: {
                "matcher": matching.MagicFilter
            }, "name").parse(c).match(item)

        assert match("f??")
        assert match("name=f*")
        assert match("date=+1d")
        assert match("one=y")
        assert match("one=+0")
        assert match("year=+2000")
        assert match("size=1m")
        assert match("size=1024k")
        assert not match("a*")
        assert not match("date=-1d")
        assert not match("one=false")
        assert not match("one=+1")
        assert not match("year=+2525")
        assert not match("size=-1m")
示例#10
0
    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())
                ))
示例#11
0
def expand_template(template, namespace):
    """ Expand the given (preparsed) template.
        Currently, only Tempita templates are supported.

        @param template: The template, in preparsed form, or as a string (which then will be preparsed).
        @param namespace: Custom namespace that is added to the predefined defaults
            and takes precedence over those.
        @return: The expanded template.
        @raise LoggableError: In case of typical errors during template execution.
    """
    # Create helper namespace
    formatters = dict((name.split('_', 1)[1], method)
                      for name, method in globals().items()
                      if name.startswith("fmt_") or name.startswith("filter_"))
    helpers = Bunch()
    helpers.update(formatters)

    # Default templating namespace
    variables = dict(h=helpers, c=config.custom_template_helpers)
    variables.update(formatters)  # redundant, for backwards compatibility

    # Provided namespace takes precedence
    variables.update(namespace)

    # Expand template
    try:
        template = preparse(template)
        return template.substitute(**variables)
    except (AttributeError, ValueError, NameError, TypeError) as exc:
        hint = ''
        if "column" in str(exc):
            try:
                col = int(str(exc).split("column")[1].split()[0])
            except (TypeError, ValueError):
                pass
            else:
                hint = "%svVv\n" % (' ' * (col + 4))

        content = getattr(template, "content", template)
        raise error.LoggableError(
            "%s: %s in template:\n%s%s" %
            (type(exc).__name__, exc, hint, "\n".join(
                "%3d: %s" % (i + 1, line)
                for i, line in enumerate(content.splitlines()))))
示例#12
0
    def _parse_triggers(self):
        """ Parse trigger definitions from config.
        """
        # TODO: Load triggers from their own file (INI or YAML), and watch it with a timer?!
        self.triggers = []
        current = Bunch(nick=None, flags=re.I, sysattr='')
        for token in shlex.split(self.api.config_get_plugin("triggers") or ''):
            if '=' in token:
                key, val = token.split('=', 1)
                current[key] = val

                if key == 'series':
                    missing = [
                        i for i in ('dbname', 'buffer', 'regex')
                        if i not in current
                    ]
                    if missing:
                        self.log(
                            'triggers: Ignoring series "{0}" due to missing fields: {1}',
                            val,
                            missing,
                            prefix='warn')
                        continue

                    triggerdef = Bunch(current)
                    triggerdef.sysattr = set(
                        i.strip() for i in triggerdef.sysattr.split(','))
                    try:
                        triggerdef.regex = re.compile(triggerdef.regex,
                                                      triggerdef.flags)
                    except re.error, exc:
                        self.log(
                            'Ignoring series "{0}" with malformed regex "{1}": {2}',
                            val,
                            current.regex,
                            exc,
                            prefix='warn')
                    else:
                        self.triggers.append(triggerdef)
                        self.trace("trigger#{0} = {1} ", len(self.triggers),
                                   current)
            elif token == 'ignorecase':
                current.flags |= re.I
示例#13
0
    def _get_files(self, attrs=None):
        """ Get a list of all files in this download; each entry has the
            attributes C{path} (relative to root), C{size} (in bytes),
            C{mtime}, C{prio} (0=off, 1=normal, 2=high), C{created},
            and C{opened}.

            This is UNCACHED, use C{fetch("files")} instead.

            @param attrs: Optional list of additional attributes to fetch.
        """
        try:
            # Get info for all files
            f_multicall = self._engine._rpc.f.multicall
            f_params = [
                self._fields["hash"],
                0,
                "f.path=",
                "f.size_bytes=",
                "f.last_touched=",
                "f.priority=",
                "f.is_created=",
                "f.is_open=",
            ]
            for attr in (attrs or []):
                f_params.append("f.%s=" % attr)
            rpc_result = f_multicall(*tuple(f_params))
        except xmlrpc.ERRORS as exc:
            raise error.EngineError(
                "While %s torrent #%s: %s" %
                ("getting files for", self._fields["hash"], exc))
        else:
            #self._engine.LOG.debug("files result: %r" % rpc_result)

            # Return results
            result = [
                Bunch(
                    path=i[0],
                    size=i[1],
                    mtime=i[2] / 1000000.0,
                    prio=i[3],
                    created=i[4],
                    opened=i[5],
                ) for i in rpc_result
            ]

            if attrs:
                for idx, attr in enumerate(attrs):
                    if attr.startswith("get_"):
                        attr = attr[4:]
                    for item, rpc_item in zip(result, rpc_result):
                        item[attr] = rpc_item[6 + idx]

            return result
示例#14
0
def add_fast_resume(meta, datapath):
    """ Add fast resume data to a metafile dict.
    """
    # Get list of files
    files = meta["info"].get("files", None)
    single = files is None
    if single:
        if os.path.isdir(datapath):
            datapath = os.path.join(datapath, meta["info"]["name"])
        files = [
            Bunch(
                path=[os.path.abspath(datapath)],
                length=meta["info"]["length"],
            )
        ]

    # Prepare resume data
    resume = meta.setdefault("libtorrent_resume", {})
    resume["bitfield"] = len(meta["info"]["pieces"]) // 20
    resume["files"] = []
    piece_length = meta["info"]["piece length"]
    offset = 0

    for fileinfo in files:
        # Get the path into the filesystem
        filepath = os.sep.join(fileinfo["path"])
        if not single:
            filepath = os.path.join(datapath,
                                    fmt.to_utf8(filepath.strip(os.sep)))

        # Check file size
        if os.path.getsize(filepath) != fileinfo["length"]:
            raise OSError(
                errno.EINVAL,
                "File size mismatch for %r [is %d, expected %d]" % (
                    filepath,
                    os.path.getsize(filepath),
                    fileinfo["length"],
                ))

        # Add resume data for this file
        resume["files"].append(
            dict(
                priority=1,
                mtime=int(os.path.getmtime(filepath)),
                completed=(offset + fileinfo["length"] + piece_length - 1) //
                piece_length - offset // piece_length,
            ))
        offset += fileinfo["length"]

    return meta
示例#15
0
    def __call__(self, environ, start_response):
        req = Request(environ)
        self.LOG.debug("Incoming request at %r" % (req.path_info, ))

        for regex, controller, kwargs in self.routes:
            match = regex.match(req.path_info)
            if match:
                req.urlvars = Bunch(kwargs)
                req.urlvars.update(match.groupdict())
                self.LOG.debug("controller=%r; vars=%r; req=%r; env=%r" %
                               (controller, req.urlvars, req, environ))
                return controller(environ, start_response)

        return exc.HTTPNotFound()(environ, start_response)
示例#16
0
文件: watch.py 项目: zapras/pyrocore
    def addinfo(self):
        """ Add known facts to templating namespace.
        """
        # Basic values
        self.ns.watch_path = self.job.config.path
        self.ns.relpath = None
        for watch in self.job.config.path:
            if self.ns.pathname.startswith(watch.rstrip('/') + '/'):
                self.ns.relpath = os.path.dirname(
                    self.ns.pathname)[len(watch.rstrip('/')) + 1:]
                break

        # Build indicator flags for target state from filename
        flags = self.ns.pathname.split(os.sep)
        flags.extend(flags[-1].split('.'))
        self.ns.flags = set(i for i in flags if i)

        # Metafile stuff
        announce = self.metadata.get("announce", None)
        if announce:
            self.ns.tracker_alias = configuration.map_announce2alias(announce)

        main_file = self.ns.info_name
        if "files" in self.metadata["info"]:
            main_file = list(
                sorted((i["length"], i["path"][-1])
                       for i in self.metadata["info"]["files"]))[-1][1]
        self.ns.filetype = os.path.splitext(main_file)[1]

        # Add name traits
        kind, info = traits.name_trait(self.ns.info_name, add_info=True)
        self.ns.traits = Bunch(info)
        self.ns.traits.kind = kind
        self.ns.label = '/'.join(
            traits.detect_traits(name=self.ns.info_name,
                                 alias=self.ns.tracker_alias,
                                 filetype=self.ns.filetype)).strip('/')

        # Finally, expand commands from templates
        self.ns.commands = []
        for key, cmd in sorted(self.job.custom_cmds.items()):
            try:
                self.ns.commands.append(
                    formatting.expand_template(cmd, self.ns))
            except error.LoggableError as exc:
                self.job.LOG.error("While expanding '%s' custom command: %s" %
                                   (key, exc))
示例#17
0
def format_item(format_spec, item, defaults=None):
    """ Format an item according to the given output format.
        The format can be gioven as either an interpolation string,
        or a Tempita template (which has to start with "E{lb}E{lb}"),

        @param format_spec: The output format.
        @param item: The object, which is automatically wrapped for interpolation.
        @param defaults: Optional default values.
    """
    template_engine = getattr(format_spec, "__engine__", None)

    # TODO: Make differences between engines transparent
    if template_engine == "tempita" or (not template_engine
                                        and format_spec.startswith("{{")):
        # Set item, or field names for column titles
        namespace = dict(headers=not bool(item))
        if item:
            namespace["d"] = item
        else:
            namespace["d"] = Bunch()
            for name in engine.FieldDefinition.FIELDS:
                namespace["d"][name] = name.upper()

            # Justify headers to width of a formatted value
            namespace.update(
                (name[4:], lambda x, m=method: str(x).rjust(len(str(m(0)))))
                for name, method in globals().items()
                if name.startswith("fmt_"))

        return expand_template(format_spec, namespace)
    else:
        # Interpolation
        format_spec = getattr(format_spec, "fmt", format_spec)

        if item is None:
            # For headers, ensure we only have string formats
            format_spec = re.sub(
                r"(\([_.a-zA-Z0-9]+\)[-#+0 ]?[0-9]*?)[.0-9]*[diouxXeEfFgG]",
                lambda m: m.group(1) + 's', format_spec)

        return format_spec % OutputMapping(item, defaults)
示例#18
0
        return '.'.join(parts.netloc.split(':')[0].split('.')[-2:])
    except IndexError:
        return parts.netloc


# Remember predefined names
_PREDEFINED = tuple(_ for _ in globals() if not _.startswith('_'))

# Set some defaults to shut up pydev / pylint;
# these later get overwritten by loading the config
debug = False
config_dir = None
scgi_local = ""
scgi_port = ""
scgi_url = ""
throttle_names = set(("NONE", "NULL"))
engine = Bunch(open=lambda: None)
formats = {}
sort_fields = ""
announce = {}
config_validator_callbacks = []
custom_field_factories = []
xmlrpc = {}
output_header_ecma48 = ""
output_header_frequency = 1
waif_pattern_list = []
traits_by_alias = {}
torque = {}
log_execute = None
magnet_watch = None
示例#19
0
文件: rtorrent.py 项目: cork/pyrocore
            for attr in (attrs or []):
                f_params.append("f.%s=" % attr)
            rpc_result = f_multicall(*tuple(f_params))
        except xmlrpc.ERRORS, exc:
            raise error.EngineError(
                "While %s torrent #%s: %s" %
                ("getting files for", self._fields["hash"], exc))
        else:
            #self._engine.LOG.debug("files result: %r" % rpc_result)

            # Return results
            result = [
                Bunch(
                    path=i[0],
                    size=i[1],
                    mtime=i[2] / 1000000.0,
                    prio=i[3],
                    created=i[4],
                    opened=i[5],
                ) for i in rpc_result
            ]

            if attrs:
                for idx, attr in enumerate(attrs):
                    if attr.startswith("get_"):
                        attr = attr[4:]
                    for item, rpc_item in zip(result, rpc_result):
                        item[attr] = rpc_item[6 + idx]

            return result

    def _memoize(self, name, getter, *args, **kwargs):
示例#20
0
 def __init__(self, **kwargs):
     self.LOG = pymagic.get_class_logger(self)
     self.cfg = Bunch(kwargs)
示例#21
0
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


class MetafileHandler(object):
    """ Handler for loading metafiles into rTorrent.
    """

    def __init__(self, job, pathname):
        """ Create a metafile handler.
        """
        self.job = job
        self.metadata = None
        self.ns = Bunch(
            pathname=os.path.abspath(pathname),
            info_hash=None,
            tracker_alias=None,
示例#22
0
    def _validate_config(self):
        """ Handle and check configuration.
        """
        groups = dict(
            job=defaultdict(Bunch),
            httpd=defaultdict(Bunch),
        )

        for key, val in config.torque.items():
            # Auto-convert numbers and bools
            if val.isdigit():
                config.torque[key] = val = int(val)
            elif val.lower() in (matching.TRUE | matching.FALSE):
                val = matching.truth(str(val), key)

            # Assemble grouped parameters
            stem = key.split('.', 1)[0]
            if key == "httpd.active":
                groups[stem]["active"] = val
            elif stem in groups:
                try:
                    stem, name, param = key.split('.', 2)
                except (TypeError, ValueError):
                    self.fatal(
                        "Bad %s configuration key %r (expecting %s.NAME.PARAM)"
                        % (stem, key, stem))
                else:
                    groups[stem][name][param] = val

        for key, val in groups.iteritems():
            setattr(self, key.replace("job", "jobs"), Bunch(val))

        # Validate httpd config
        if self.httpd.active:
            if self.httpd.waitress.url_scheme not in ("http", "https"):
                self.fatal("HTTP URL scheme must be either 'http' or 'https'")
            if not isinstance(self.httpd.waitress.port, int) or not (
                    1024 <= self.httpd.waitress.port < 65536):
                self.fatal("HTTP port must be a 16 bit number >= 1024")

        # Validate jobs
        for name, params in self.jobs.items():
            for key in ("handler", "schedule"):
                if key not in params:
                    self.fatal(
                        "Job '%s' is missing the required 'job.%s.%s' parameter"
                        % (name, name, key))

            bool_param = lambda k, default, p=params: matching.truth(
                p.get(k, default), "job.%s.%s" % (name, k))

            params.job_name = name
            params.dry_run = bool_param("dry_run",
                                        False) or self.options.dry_run
            params.active = bool_param("active", True)
            params.schedule = self._parse_schedule(params.schedule)

            if params.active:
                try:
                    params.handler = pymagic.import_name(params.handler)
                except ImportError as exc:
                    self.fatal("Bad handler name '%s' for job '%s':\n    %s" %
                               (params.handler, name, exc))
示例#23
0
    # Return 2nd level domain name if no alias found
    try:
        return '.'.join(parts.netloc.split(':')[0].split('.')[-2:])
    except IndexError:
        return parts.netloc


# Remember predefined names
_PREDEFINED = tuple(_ for _ in globals() if not _.startswith('_'))

# Set some defaults to shut up pydev / pylint;
# these later get overwritten by loading the config
debug = False
config_dir = None
scgi_url = ""
engine = Bunch(open=lambda: None)
fast_query = 0
formats = {}
sort_fields = ""
announce = {}
config_validator_callbacks = []
custom_field_factories = []
custom_template_helpers = Bunch()
xmlrpc = {}
output_header_ecma48 = ""
output_header_frequency = 1
waif_pattern_list = []
traits_by_alias = {}
torque = {}
magnet_watch = None
influxdb = {}
示例#24
0
class RtorrentControl(ScriptBaseWithConfig):
    ### Keep things wrapped to fit under this comment... ##############################
    """
        Control and inspect rTorrent from the command line.

        Filter expressions take the form "<field>=<value>", and all expressions must
        be met (AND). If a field name is omitted, "name" is assumed. You can also use
        uppercase OR to build a list of alternative conditions.

        For numeric fields, a leading "+" means greater than, a leading "-" means less
        than. For string fields, the value is a glob pattern (*, ?, [a-z], [!a-z]), or
        a regex match enclosed by slashes. All string comparisons are case-ignoring.
        Multiple values separated by a comma indicate several possible choices (OR).
        "!" in front of a filter value negates it (NOT).

        See https://pyrocore.readthedocs.io/en/latest/usage.html#rtcontrol for more.

        Examples:
          - All 1:1 seeds         ratio=+1
          - All active torrents   xfer=+0
          - All seeding torrents  up=+0
          - Slow torrents         down=+0 down=-5k
          - Older than 2 weeks    completed=+2w
          - Big stuff             size=+4g
          - 1:1 seeds not on NAS  ratio=+1 'realpath=!/mnt/*'
          - Music                 kind=flac,mp3
    """

    # argument description for the usage information
    ARGS_HELP = "<filter>..."

    # additonal stuff appended after the command handler's docstring
    ADDITIONAL_HELP = [
        "",
        "",
        "Use --help to get a list of all options.",
        "Use --help-fields to list all fields and their description.",
    ]

    # additional values for output formatting
    FORMATTER_DEFAULTS = dict(now=time.time(), )

    # choices for --ignore
    IGNORE_OPTIONS = ('0', '1')

    # choices for --prio
    PRIO_OPTIONS = ('0', '1', '2', '3')

    # choices for --alter
    ALTER_MODES = ('append', 'remove')

    # action options that perform some change on selected items
    ACTION_MODES = (
        Bunch(name="start", options=("--start", ), help="start torrent"),
        Bunch(name="close",
              options=("--close", "--stop"),
              help="stop torrent",
              method="stop"),
        Bunch(name="hash_check",
              label="HASH",
              options=("-H", "--hash-check"),
              help="hash-check torrent",
              interactive=True),
        # TODO: Bunch(name="announce", options=("--announce",), help="announce right now", interactive=True),
        # TODO: --pause, --resume?
        # TODO: implement --clean-partial
        #self.add_bool_option("--clean-partial",
        #    help="remove partially downloaded 'off'ed files (also stops downloads)")
        Bunch(name="delete",
              options=("--delete", ),
              help="remove torrent from client",
              interactive=True),
        Bunch(name="purge",
              options=("--purge", "--delete-partial"),
              help="delete PARTIAL data files and remove torrent from client",
              interactive=True),
        Bunch(name="cull",
              options=("--cull", "--exterminate", "--delete-all"),
              help="delete ALL data files and remove torrent from client",
              interactive=True),
        Bunch(
            name="throttle",
            options=(
                "-T",
                "--throttle",
            ),
            argshelp="NAME",
            method="set_throttle",
            help="assign to named throttle group (NULL=unlimited, NONE=global)",
            interactive=True),
        Bunch(name="tag",
              options=("--tag", ),
              argshelp='"TAG +TAG -TAG..."',
              help="add or remove tag(s)",
              interactive=False),
        Bunch(name="custom",
              label="SET_CUSTOM",
              options=("--custom", ),
              argshelp='KEY=VALUE',
              method="set_custom",
              help="set value of 'custom_KEY' field (KEY might also be 1..5)",
              interactive=False),
        Bunch(name="exec",
              label="EXEC",
              options=("--exec", "--xmlrpc"),
              argshelp='CMD',
              method="execute",
              help="execute XMLRPC command pattern",
              interactive=True),
        # TODO: --move / --link output_format / the formatted result is the target path
        #           if the target contains a '//' in place of a '/', directories
        #           after that are auto-created
        #           "--move tracker_dated", with a custom output format
        #           like "tracker_dated = ~/done//$(alias)s/$(completed).7s",
        #           will move to ~/done/OBT/2010-08 for example
        #        self.add_value_option("--move", "TARGET",
        #            help="move data to given target directory (implies -i, can be combined with --delete)")
        # TODO: --copy, and --move/--link across devices
    )

    def __init__(self):
        """ Initialize rtcontrol.
        """
        super(RtorrentControl, self).__init__()

        self.prompt = PromptDecorator(self)
        self.plain_output_format = False
        self.raw_output_format = None

    def add_options(self):
        """ Add program options.
        """
        super(RtorrentControl, self).add_options()

        # basic options
        self.add_bool_option(
            "--help-fields",
            help="show available fields and their description")
        self.add_bool_option(
            "-n",
            "--dry-run",
            help="don't commit changes, just tell what would happen")
        self.add_bool_option("--detach",
                             help="run the process in the background")
        self.prompt.add_options()

        # output control
        self.add_bool_option("-S",
                             "--shell",
                             help="escape output following shell rules")
        self.add_bool_option(
            "-0",
            "--nul",
            "--print0",
            help="use a NUL character instead of a linebreak after items")
        self.add_bool_option("-c",
                             "--column-headers",
                             help="print column headers")
        self.add_bool_option("-+",
                             "--stats",
                             help="add sum / avg / median of numerical fields")
        self.add_bool_option(
            "--summary",
            help="print only statistical summary, without the items")
        #self.add_bool_option("-f", "--full",
        #    help="print full torrent details")
        self.add_bool_option(
            "--json",
            help=
            "dump default fields of all items as JSON (use '-o f1,f2,...' to specify fields)"
        )
        self.add_value_option(
            "-o",
            "--output-format",
            "FORMAT",
            help="specify display format (use '-o-' to disable item display)")
        self.add_value_option(
            "-O",
            "--output-template",
            "FILE",
            help="pass control of output formatting to the specified template")
        self.add_value_option(
            "-s",
            "--sort-fields",
            "[-]FIELD[,...] [-s...]",
            action='append',
            default=[],
            help=
            "fields used for sorting, descending if prefixed with a '-'; '-s*' uses output field list"
        )
        self.add_bool_option("-r",
                             "--reverse-sort",
                             help="reverse the sort order")
        self.add_value_option(
            "-A",
            "--anneal",
            "MODE [-A...]",
            type='choice',
            action='append',
            default=[],
            choices=('dupes+', 'dupes-', 'dupes=', 'invert', 'unique'),
            help="modify result set using some pre-defined methods")
        self.add_value_option(
            "-/",
            "--select",
            "[N-]M",
            help="select result subset by item position (counting from 1)")
        self.add_bool_option(
            "-V",
            "--view-only",
            help="show search result only in default ncurses view")
        self.add_value_option(
            "--to-view",
            "--to",
            "NAME",
            help="show search result only in named ncurses view")
        self.add_bool_option("--append-view",
                             "--append",
                             help="DEPRECATED: use '--alter append' instead")
        self.add_value_option(
            "--alter-view",
            "--alter",
            "MODE",
            type='choice',
            default=None,
            choices=self.ALTER_MODES,
            help=
            "alter view according to mode: {} (modifies -V and --to behaviour)"
            .format(', '.join(self.ALTER_MODES)))
        self.add_bool_option(
            "--tee-view",
            "--tee",
            help=
            "ADDITIONALLY show search results in ncurses view (modifies -V and --to behaviour)"
        )
        self.add_value_option(
            "--from-view",
            "--from",
            "NAME",
            help=
            "select only items that are on view NAME (NAME can be an info hash to quickly select a single item)"
        )
        self.add_value_option(
            "-M",
            "--modify-view",
            "NAME",
            help=
            "get items from given view and write result back to it (short-cut to combine --from-view and --to-view)"
        )
        self.add_value_option(
            "-Q",
            "--fast-query",
            "LEVEL",
            type='choice',
            default='=',
            choices=('=', '0', '1', '2'),
            help=
            "enable query optimization (=: use config; 0: off; 1: safe; 2: danger seeker)"
        )
        self.add_value_option("--call",
                              "CMD",
                              help="call an OS command pattern in the shell")
        self.add_value_option("--spawn",
                              "CMD [--spawn ...]",
                              action="append",
                              default=[],
                              help="execute OS command pattern(s) directly")
        # TODO: implement -S
        #        self.add_bool_option("-S", "--summary",
        #            help="print statistics")

        # torrent state change (actions)
        for action in self.ACTION_MODES:
            action.setdefault("label", action.name.upper())
            action.setdefault("method", action.name)
            action.setdefault("interactive", False)
            action.setdefault("argshelp", "")
            action.setdefault("args", ())
            if action.argshelp:
                self.add_value_option(
                    *action.options + (action.argshelp, ), **{
                        "help":
                        action.help +
                        (" (implies -i)" if action.interactive else "")
                    })
            else:
                self.add_bool_option(
                    *action.options, **{
                        "help":
                        action.help +
                        (" (implies -i)" if action.interactive else "")
                    })
        self.add_value_option("--ignore",
                              "|".join(self.IGNORE_OPTIONS),
                              type="choice",
                              choices=self.IGNORE_OPTIONS,
                              help="set 'ignore commands' status on torrent")
        self.add_value_option("--prio",
                              "|".join(self.PRIO_OPTIONS),
                              type="choice",
                              choices=self.PRIO_OPTIONS,
                              help="set priority of torrent")
        self.add_bool_option(
            "-F",
            "--flush",
            help="flush changes immediately (save session data)")

    def help_completion_fields(self):
        """ Return valid field names.
        """
        for name, field in sorted(engine.FieldDefinition.FIELDS.items()):
            if issubclass(field._matcher, matching.BoolFilter):
                yield "%s=no" % (name, )
                yield "%s=yes" % (name, )
                continue
            elif issubclass(field._matcher, matching.PatternFilter):
                yield "%s=" % (name, )
                yield "%s=/" % (name, )
                yield "%s=?" % (name, )
                yield "%s=\"'*'\"" % (name, )
                continue
            elif issubclass(field._matcher, matching.NumericFilterBase):
                for i in range(10):
                    yield "%s=%d" % (name, i)
            else:
                yield "%s=" % (name, )

            yield r"%s=+" % (name, )
            yield r"%s=-" % (name, )

        yield "custom_"
        yield "kind_"

    # TODO: refactor to engine.TorrentProxy as format() method
    def format_item(self, item, defaults=None, stencil=None):
        """ Format an item.
        """
        from pyrobase.osutil import shell_escape

        try:
            item_text = fmt.to_console(
                formatting.format_item(self.options.output_format, item,
                                       defaults))
        except (NameError, ValueError, TypeError), exc:
            self.fatal(
                "Trouble with formatting item %r\n\n  FORMAT = %r\n\n  REASON ="
                % (item, self.options.output_format), exc)
            raise  # in --debug mode

        if self.options.shell:
            item_text = '\t'.join(
                shell_escape(i) for i in item_text.split('\t'))

        # Justify headers according to stencil
        if stencil:
            item_text = '\t'.join(
                i.ljust(len(s))
                for i, s in zip(item_text.split('\t'), stencil))

        return item_text
示例#25
0
class MetafileHandler(object):
    """ Handler for loading metafiles into rTorrent.
    """

    def __init__(self, job, pathname):
        """ Create a metafile handler.
        """
        self.job = job
        self.metadata = None
        self.ns = Bunch(
            pathname=os.path.abspath(pathname),
            info_hash=None,
            tracker_alias=None,
        )


    def parse(self):
        """ Parse metafile and check pre-conditions.
        """
        try:
            if not os.path.getsize(self.ns.pathname):
                # Ignore 0-byte dummy files (Firefox creates these while downloading)
                self.job.LOG.warn("Ignoring 0-byte metafile '%s'" % (self.ns.pathname,))
                return
            self.metadata = metafile.checked_open(self.ns.pathname)
        except EnvironmentError as exc:
            self.job.LOG.error("Can't read metafile '%s' (%s)" % (
                self.ns.pathname, str(exc).replace(": '%s'" % self.ns.pathname, ""),
            ))
            return
        except ValueError as exc:
            self.job.LOG.error("Invalid metafile '%s': %s" % (self.ns.pathname, exc))
            return

        self.ns.info_hash = metafile.info_hash(self.metadata)
        self.ns.info_name = self.metadata["info"]["name"]
        self.job.LOG.info("Loaded '%s' from metafile '%s'" % (self.ns.info_name, self.ns.pathname))

        # Check whether item is already loaded
        try:
            name = self.job.proxy.d.name(self.ns.info_hash, fail_silently=True)
        except xmlrpc.HashNotFound:
            pass
        except xmlrpc.ERRORS as exc:
            if exc.faultString != "Could not find info-hash.":
                self.job.LOG.error("While checking for #%s: %s" % (self.ns.info_hash, exc))
                return
        else:
            self.job.LOG.warn("Item #%s '%s' already added to client" % (self.ns.info_hash, name))
            return

        return True


    def addinfo(self):
        """ Add known facts to templating namespace.
        """
        # Basic values
        self.ns.watch_path = self.job.config.path
        self.ns.relpath = None
        for watch in self.job.config.path:
            if self.ns.pathname.startswith(watch.rstrip('/') + '/'):
                self.ns.relpath = os.path.dirname(self.ns.pathname)[len(watch.rstrip('/'))+1:]
                break

        # Build indicator flags for target state from filename
        flags = self.ns.pathname.split(os.sep)
        flags.extend(flags[-1].split('.'))
        self.ns.flags = set(i for i in flags if i)

        # Metafile stuff
        announce = self.metadata.get("announce", None)
        if announce:
            self.ns.tracker_alias = configuration.map_announce2alias(announce)

        main_file = self.ns.info_name
        if "files" in self.metadata["info"]:
            main_file = list(sorted((i["length"], i["path"][-1])
                for i in self.metadata["info"]["files"]))[-1][1]
        self.ns.filetype = os.path.splitext(main_file)[1]

        # Add name traits
        kind, info = traits.name_trait(self.ns.info_name, add_info=True)
        self.ns.traits = Bunch(info)
        self.ns.traits.kind = kind
        self.ns.label = '/'.join(traits.detect_traits(
            name=self.ns.info_name, alias=self.ns.tracker_alias, filetype=self.ns.filetype)).strip('/')

        # Finally, expand commands from templates
        self.ns.commands = []
        for key, cmd in sorted(self.job.custom_cmds.items()):
            try:
                self.ns.commands.append(formatting.expand_template(cmd, self.ns))
            except error.LoggableError as exc:
                self.job.LOG.error("While expanding '%s' custom command: %s" % (key, exc))


    def load(self):
        """ Load metafile into client.
        """
        if not self.ns.info_hash and not self.parse():
            return

        self.addinfo()

        # TODO: dry_run
        try:
            # TODO: Scrub metafile if requested

            # Determine target state
            start_it = self.job.config.load_mode.lower() in ("start", "started")
            queue_it = self.job.config.queued

            if "start" in self.ns.flags:
                start_it = True
            elif "load" in self.ns.flags:
                start_it = False

            if "queue" in self.ns.flags:
                queue_it = True

            # Load metafile into client
            load_cmd = self.job.proxy.load.verbose
            if queue_it:
                if not start_it:
                    self.ns.commands.append("d.priority.set=0")
            elif start_it:
                load_cmd = self.job.proxy.load.start_verbose

            self.job.LOG.debug("Templating values are:\n    %s" % "\n    ".join("%s=%s" % (key, repr(val))
                for key, val in sorted(self.ns.items())
            ))

            load_cmd(xmlrpc.NOHASH, self.ns.pathname, *tuple(self.ns.commands))
            time.sleep(.05) # let things settle

            # Announce new item
            if not self.job.config.quiet:
                msg = "%s: Loaded '%s' from '%s/'%s%s" % (
                    self.job.__class__.__name__,
                    fmt.to_utf8(self.job.proxy.d.name(self.ns.info_hash, fail_silently=True)),
                    os.path.dirname(self.ns.pathname).rstrip(os.sep),
                    " [queued]" if queue_it else "",
                    (" [startable]"  if queue_it else " [started]") if start_it else " [normal]",
                )
                self.job.proxy.log(xmlrpc.NOHASH, msg)

            # TODO: Evaluate fields and set client values
            # TODO: Add metadata to tied file if requested

            # TODO: Execute commands AFTER adding the item, with full templating
            # Example: Labeling - add items to a persistent view, i.e. "postcmd = view.set_visible={{label}}"
            #   could also be done automatically from the path, see above under "flags" (autolabel = True)
            #   and add traits to the flags, too, in that case

        except xmlrpc.ERRORS as exc:
            self.job.LOG.error("While loading #%s: %s" % (self.ns.info_hash, exc))


    def handle(self):
        """ Handle metafile.
        """
        if self.parse():
            self.load()
示例#26
0
    def do_session(self):
        """Restore state from session files."""
        def filenames():
            'Helper'
            for arg in self.args:
                if os.path.isdir(arg):
                    for name in glob.glob(os.path.join(arg, '*.torrent.rtorrent')):
                        yield name
                elif arg == '@-':
                    for line in sys.stdin.read().splitlines():
                        if line.strip():
                            yield line.strip()
                elif arg.startswith('@'):
                    if not os.path.isfile(arg[1:]):
                        self.parser.error("File not found (or not a file): {}".format(arg[1:]))
                    with io.open(arg[1:], encoding='utf-8') as handle:
                        for line in handle:
                            if line.strip():
                                yield line.strip()
                else:
                    yield arg

        proxy = self.open()
        for filename in filenames():
            # Check filename and extract infohash
            self.LOG.debug("Reading '%s'...", filename)
            match = re.match(r'(?:.+?[-._])?([a-fA-F0-9]{40})(?:[-._].+?)?\.torrent\.rtorrent',
                             os.path.basename(filename))
            if not match:
                self.LOG.warn("Skipping badly named session file '%s'...", filename)
                continue
            infohash = match.group(1)

            # Read bencoded data
            try:
                with open(filename, 'rb') as handle:
                    raw_data = handle.read()
                data = Bunch(bencode.bdecode(raw_data))
            except EnvironmentError as exc:
                self.LOG.warn("Can't read '%s' (%s)" % (
                    filename, str(exc).replace(": '%s'" % filename, ""),
                ))
                continue

            ##print(infohash, '=', repr(data))
            if 'state_changed' not in data:
                self.LOG.warn("Skipping invalid session file '%s'...", filename)
                continue

            # Restore metadata
            was_active = proxy.d.is_active(infohash)
            proxy.d.ignore_commands.set(infohash, data.ignore_commands)
            proxy.d.priority.set(infohash, data.priority)

            if proxy.d.throttle_name(infohash) != data.throttle_name:
                proxy.d.pause(infohash)
                proxy.d.throttle_name.set(infohash, data.throttle_name)

            if proxy.d.directory(infohash) != data.directory:
                proxy.d.stop(infohash)
                proxy.d.directory_base.set(infohash, data.directory)

            for i in range(5):
                key = 'custom%d' % (i + 1)
                getattr(proxy.d, key).set(infohash, data[key])

            for key, val in data.custom.items():
                proxy.d.custom.set(infohash, key, val)

            for name in data.views:
                try:
                    proxy.view.set_visible(infohash, name)
                except xmlrpclib.Fault as exc:
                    if 'Could not find view' not in str(exc):
                        raise

            if was_active and not proxy.d.is_active(infohash):
                (proxy.d.resume if proxy.d.is_open(infohash) else proxy.d.start)(infohash)
            proxy.d.save_full_session(infohash)

            """ TODO:
示例#27
0
from pyrobase import fmt

if not os.path.supports_unicode_filenames:
    # Make a Unicode-aware copy of os and os.path
    from pyrobase.parts import Bunch

    def _encode_path(text):
        """ Return a string suitable for calling file system functions.
        """
        if isinstance(text, str):
            return text
        else:
            return text.encode("UTF-8")

    # copy "os" identifiers
    _os = Bunch()
    for _key in os.__all__:
        _os[_key] = getattr(os, _key)

    def _wrap(name, func):
        "Wrapping helper."
        setattr(_os, name, func)
        func.__name__ = name
        func.__doc__ = getattr(os, name).__doc__

    # wrap some os functions
    _wrap("makedirs", lambda path, o=os: o.makedirs(_encode_path(path)))
    _wrap("readlink", lambda path, o=os: o.readlink(_encode_path(path)))
    _wrap("remove", lambda path, o=os: o.remove(_encode_path(path)))
    _wrap(
        "rename",