diff options
| -rw-r--r-- | pyload/AddonManager.py | 98 | ||||
| -rw-r--r-- | pyload/api/AddonApi.py | 36 | ||||
| -rw-r--r-- | pyload/plugins/Addon.py | 65 | ||||
| -rw-r--r-- | pyload/plugins/Hoster.py | 4 | ||||
| -rw-r--r-- | pyload/plugins/addons/ExtractArchive.py | 47 | ||||
| -rw-r--r-- | pyload/remote/apitypes.py | 18 | ||||
| -rw-r--r-- | pyload/remote/apitypes_debug.py | 9 | ||||
| -rw-r--r-- | pyload/remote/pyload.thrift | 19 | ||||
| -rw-r--r-- | pyload/utils/PluginLoader.py | 2 | ||||
| -rw-r--r-- | pyload/utils/fs.py | 7 | ||||
| -rw-r--r-- | pyload/web/cnl_app.py | 4 | 
11 files changed, 198 insertions, 111 deletions
| diff --git a/pyload/AddonManager.py b/pyload/AddonManager.py index 7935ff112..5c5524061 100644 --- a/pyload/AddonManager.py +++ b/pyload/AddonManager.py @@ -17,14 +17,23 @@  import __builtin__ +from gettext import gettext +from copy import copy  from thread import start_new_thread  from threading import RLock +from collections import defaultdict +from new_collections import namedtuple +  from types import MethodType +from pyload.Api import AddonService, AddonInfo  from pyload.threads.AddonThread import AddonThread  from utils import lock, to_string +AddonTuple = namedtuple('AddonTuple', 'instances events handler') + +  class AddonManager:      """ Manages addons, loading, unloading.  """ @@ -35,10 +44,13 @@ class AddonManager:          __builtin__.addonManager = self #needed to let addons register themselves          self.log = self.core.log -        # TODO: multiuser, addons can store the user itself, probably not needed here -        self.plugins = {} -        self.methods = {} # dict of names and list of methods usable by rpc -        self.events = {} # Contains event that will be registered + +        # TODO: multiuser addons + +        # maps plugin names to info tuple +        self.plugins = defaultdict(lambda: AddonTuple([], [], {})) +        # Property hash mapped to meta data +        self.info_props = {}          self.lock = RLock()          self.createIndex() @@ -46,11 +58,16 @@ class AddonManager:          # manage addons on config change          self.listenTo("config:changed", self.manageAddon) +    def iterAddons(self): +        """ Yields (name, meta_data) of all addons """ +        return self.plugins.iteritems() +      @lock      def callInHooks(self, event, eventName, *args):          """  Calls a method in all addons and catch / log errors"""          for plugin in self.plugins.itervalues(): -            self.call(plugin, event, *args) +            for inst in plugin.instances: +                self.call(inst, event, *args)          self.dispatchEvent(eventName, *args)      def call(self, addon, f, *args): @@ -78,7 +95,7 @@ class AddonManager:                      if not pluginClass: continue                      plugin = pluginClass(self.core, self) -                    self.plugins[pluginClass.__name__] = plugin +                    self.plugins[pluginClass.__name__].instances.append(plugin)                      # hide internals from printing                      if not internal and plugin.isActivated(): @@ -96,7 +113,7 @@ class AddonManager:          self.log.info(_("Deactivated addons: %s") % ", ".join(sorted(deactive)))      def manageAddon(self, plugin, name, value): -        # TODO: user +        # TODO: multi user          # check if section was a plugin          if plugin not in self.core.pluginManager.getPlugins("addons"): @@ -120,18 +137,18 @@ class AddonManager:          self.log.debug("Plugin loaded: %s" % plugin)          plugin = pluginClass(self.core, self) -        self.plugins[pluginClass.__name__] = plugin +        self.plugins[pluginClass.__name__].instances.append(plugin)          # active the addon in new thread          start_new_thread(plugin.activate, tuple()) -        self.registerEvents() # TODO: BUG: events will be destroyed and not re-registered +        self.registerEvents()      @lock      def deactivateAddon(self, plugin):          if plugin not in self.plugins:              return -        else: -            addon = self.plugins[plugin] +        else: # todo: multiple instances +            addon = self.plugins[plugin].instances[0]          if addon.__internal__: return @@ -140,8 +157,11 @@ class AddonManager:          #remove periodic call          self.log.debug("Removed callback %s" % self.core.scheduler.removeJob(addon.cb)) + +        # todo: only delete instances, meta data is lost otherwise          del self.plugins[addon.__name__] +        # TODO: could be improved          #remove event listener          for f in dir(addon):              if f.startswith("__") or type(getattr(addon, f)) != MethodType: @@ -151,8 +171,9 @@ class AddonManager:      def activateAddons(self):          self.log.info(_("Activating addons..."))          for plugin in self.plugins.itervalues(): -            if plugin.isActivated(): -                self.call(plugin, "activate") +            for inst in plugin.instances: +                if inst.isActivated(): +                    self.call(inst, "activate")          self.registerEvents() @@ -160,7 +181,8 @@ class AddonManager:          """  Called when core is shutting down """          self.log.info(_("Deactivating addons..."))          for plugin in self.plugins.itervalues(): -            self.call(plugin, "deactivate") +            for inst in plugin.instances: +                self.call(inst, "deactivate")      def downloadPreparing(self, pyfile):          self.callInHooks("downloadPreparing", "download:preparing", pyfile) @@ -180,40 +202,40 @@ class AddonManager:      def activePlugins(self):          """ returns all active plugins """ -        return [x for x in self.plugins.itervalues() if x.isActivated()] - -    def getAllInfo(self): -        """returns info stored by addon plugins""" -        info = {} -        for name, plugin in self.plugins.iteritems(): -            if plugin.info: -                #copy and convert so str -                info[name] = dict( -                    [(x, to_string(y)) for x, y in plugin.info.iteritems()]) -        return info +        return [p for x in self.plugins.values() for p in x.instances if p.isActivated()]      def getInfo(self, plugin): -        info = {} -        if plugin in self.plugins and self.plugins[plugin].info: -            info = dict([(x, to_string(y)) -            for x, y in self.plugins[plugin].info.iteritems()]) +        """ Retrieves all info data for a plugin """ -        return info +        data = [] +        # TODO +        if plugin in self.plugins: +            if plugin.instances: +                for attr in dir(plugin.instances[0]): +                    if attr.startswith("__Property"): +                        info = self.info_props[attr] +                        info.value = getattr(plugin.instances[0], attr) +                        data.append(info) +        return data      def addEventListener(self, plugin, func, event):          """ add the event to the list """ -        if plugin not in self.events: -            self.events[plugin] = [] -        self.events[plugin].append((func, event)) +        self.plugins[plugin].events.append((func, event))      def registerEvents(self):          """ actually register all saved events """          for name, plugin in self.plugins.iteritems(): -            if name in self.events: -                for func, event in self.events[name]: -                    self.listenTo(event, getattr(plugin, func)) -                # clean up -                del self.events[name] +            for func, event in plugin.events: +                for inst in plugin.instances: +                    self.listenTo(event, getattr(inst, func)) + +    def addAddonHandler(self, plugin, func, label, desc, args, package, media): +        """ Registers addon service description """ +        self.plugins[plugin].handler[func] = AddonService(func, gettext(label), gettext(desc), args, package, media) + +    def addInfoProperty(self, h, name, desc): +        """  Register property as :class:`AddonInfo` """ +        self.info_props[h] = AddonInfo(name, desc)      def listenTo(self, *args):          self.core.eventManager.listenTo(*args) diff --git a/pyload/api/AddonApi.py b/pyload/api/AddonApi.py index 12d3170d7..ea1e3ce6e 100644 --- a/pyload/api/AddonApi.py +++ b/pyload/api/AddonApi.py @@ -5,25 +5,49 @@ from pyload.Api import Api, RequirePerm, Permission  from ApiComponent import ApiComponent - +# TODO: multi user  class AddonApi(ApiComponent):      """ Methods to interact with addons """ +    @RequirePerm(Permission.Interaction)      def getAllInfo(self):          """Returns all information stored by addon plugins. Values are always strings -        :return: {"plugin": {"name": value } } +        :return:          """ -        return self.core.addonManager.getAllInfo() +        # TODO +    @RequirePerm(Permission.Interaction)      def getInfoByPlugin(self, plugin): -        """Returns information stored by a specific plugin. +        """Returns public information associated with specific plugin. -        :param plugin: pluginname -        :return: dict of attr names mapped to value {"name": value} +        :param plugin: pluginName +        :return: list of :class:`AddonInfo`          """          return self.core.addonManager.getInfo(plugin) +    @RequirePerm(Permission.Interaction) +    def getAddonHandler(self): +        """ Lists all available addon handler + +        :return: dict of plugin name to list of :class:`AddonService` +        """ +        handler = {} +        for name, data in self.core.addonManager.iterAddons(): +            if data.handler: +                handler[name] = data.handler +        return handler + +    @RequirePerm(Permission.Interaction) +    def callAddon(self, plugin, func, arguments): +        """ Calls any function exposed by an addon """ +        pass + +    @RequirePerm(Permission.Interaction) +    def callAddonHandler(self, plugin, func, pid_or_fid): +        """ Calls an addon handler registered to work with packages or files  """ +        pass +  if Api.extend(AddonApi):      del AddonApi
\ No newline at end of file diff --git a/pyload/plugins/Addon.py b/pyload/plugins/Addon.py index c1a297d28..5c27fa983 100644 --- a/pyload/plugins/Addon.py +++ b/pyload/plugins/Addon.py @@ -1,7 +1,5 @@  # -*- coding: utf-8 -*- -from traceback import print_exc -  #from functools import wraps  from pyload.utils import has_method, to_list @@ -27,23 +25,58 @@ def AddEventListener(event):      return _klass -def AddonHandler(desc, media=None): -    """ Register Handler for files, packages, or arbitrary callable methods. -        To let the method work on packages/files, media must be set and the argument named pid or fid. +def AddonHandler(label, desc, package=True, media=-1): +    """ Register Handler for files, packages, or arbitrary callable methods. In case package is True (default) +        The method should only accept a pid as argument. When media is set it will work on files +        and should accept a fileid. Only when both is None the method can be arbitrary. -    :param desc: verbose description -    :param media: if True or bits of media type +    :param label: verbose name +    :param desc: short description +    :param package: True if method works withs packages +    :param media: media type of the file to work with.      """ -    pass +    class _klass(object): +        def __new__(cls, f, *args, **kwargs): +            addonManager.addAddonHandler(class_name(f.__module__), f.func_name, label, desc, +                                         f.func_code.co_varnames[1:], package, media) +            return f + +    return _klass -def AddonInfo(desc): -    """ Called to retrieve information about the current state. -    Decorated method must return anything convertable into string. +def AddonProperty(name, desc, default=None, fire_event=True): +    """ Use this function to declare class variables, that will be exposed as :class:`AddonInfo`. +        It works similar to the @property function. You declare the variable like `state = AddonProperty(...)` +        and use it as any other variable. + +    :param name: display name      :param desc: verbose description +    :param default: the default value +    :param fire_event: Fire `addon:property:change` event, when modified      """ -    pass + +    # generated name for the attribute +    h = "__Property" + str(hash(name) ^ hash(desc)) + +    addonManager.addInfoProperty(h, name, desc) + +    def _get(self): +        if not hasattr(self, h): +            return default + +        return getattr(self, h) + +    def _set(self, value): +        if fire_event: +            self.manager.dispatchEvent("addon:property:change", value) + +        return setattr(self, h, value) + +    def _del(self): +        return delattr(self, h) + +    return property(_get, _set, _del)  def threaded(f): @@ -73,9 +106,6 @@ class Addon(Base):      def __init__(self, core, manager, user=None):          Base.__init__(self, core, user) -        #: Provide information in dict here, usable by API `getInfo` -        self.info = None -          #: Callback of periodical job task, used by addonManager          self.cb = None @@ -130,9 +160,8 @@ class Addon(Base):          try:              if self.isActivated(): self.periodical()          except Exception, e: -            self.core.log.error(_("Error executing addons: %s") % str(e)) -            if self.core.debug: -                print_exc() +            self.core.log.error(_("Error executing addon: %s") % str(e)) +            self.core.print_exc()          if self.cb:              self.cb = self.core.scheduler.addJob(self.interval, self._periodical, threaded=False) diff --git a/pyload/plugins/Hoster.py b/pyload/plugins/Hoster.py index 976918c0d..6bfe47e1f 100644 --- a/pyload/plugins/Hoster.py +++ b/pyload/plugins/Hoster.py @@ -9,7 +9,7 @@ if os.name != "nt":      from grp import getgrnam  from pyload.utils import chunks as _chunks -from pyload.utils.fs import save_join, save_filename, fs_encode, fs_decode, \ +from pyload.utils.fs import save_join, safe_filename, fs_encode, fs_decode, \      remove, makedirs, chmod, stat, exists, join  from Base import Base, Fail, Retry @@ -268,7 +268,7 @@ class Hoster(Base):          # convert back to unicode          location = fs_decode(location) -        name = save_filename(self.pyfile.name) +        name = safe_filename(self.pyfile.name)          filename = join(location, name) diff --git a/pyload/plugins/addons/ExtractArchive.py b/pyload/plugins/addons/ExtractArchive.py index be023301c..67fa5c820 100644 --- a/pyload/plugins/addons/ExtractArchive.py +++ b/pyload/plugins/addons/ExtractArchive.py @@ -49,12 +49,13 @@ if os.name != "nt":      from pwd import getpwnam      from grp import getgrnam -from module.utils import save_join, fs_encode -from module.plugins.Hook import Hook, threaded, Expose -from module.plugins.internal.AbstractExtractor import ArchiveError, CRCError, WrongPassword +from pyload.utils.fs import safe_join as save_join, fs_encode +from pyload.plugins.Addon import Addon, threaded, AddonHandler, AddonProperty +from pyload.plugins.internal.AbstractExtractor import ArchiveError, CRCError, WrongPassword -class ExtractArchive(Hook): + +class ExtractArchive(Addon):      """      Provides: unrarFinished (folder, filename)      """ @@ -77,7 +78,7 @@ class ExtractArchive(Hook):      event_list = ["allDownloadsProcessed"] -    def setup(self): +    def init(self):          self.plugins = []          self.passwords = []          names = [] @@ -111,10 +112,10 @@ class ExtractArchive(Hook):          # queue with package ids          self.queue = [] -    @Expose -    def extractPackage(self, id): +    @AddonHandler(_("Extract package"), _("Scans package for archives and extract them")) +    def extractPackage(self, pid):          """ Extract package with given id""" -        self.manager.startThread(self.extract, [id]) +        self.manager.startThread(self.extract, [pid])      def packageFinished(self, pypack):          if self.getConfig("queue"): @@ -267,25 +268,12 @@ class ExtractArchive(Hook):          return [] -    @Expose +    # TODO: config handler for passwords? +      def getPasswords(self):          """ List of saved passwords """          return self.passwords -    def reloadPasswords(self): -        pwfile = self.getConfig("passwordfile") -        if not exists(pwfile): -            open(pwfile, "wb").close() - -        passwords = [] -        f = open(pwfile, "rb") -        for pw in f.read().splitlines(): -            passwords.append(pw) -        f.close() - -        self.passwords = passwords - -    @Expose      def addPassword(self, pw):          """  Adds a password to saved list"""          pwfile = self.getConfig("passwordfile") @@ -299,6 +287,19 @@ class ExtractArchive(Hook):              f.write(pw + "\n")          f.close() +    def reloadPasswords(self): +        pwfile = self.getConfig("passwordfile") +        if not exists(pwfile): +            open(pwfile, "wb").close() + +        passwords = [] +        f = open(pwfile, "rb") +        for pw in f.read().splitlines(): +            passwords.append(pw) +        f.close() + +        self.passwords = passwords +      def setPermissions(self, files):          for f in files:              if not exists(f): diff --git a/pyload/remote/apitypes.py b/pyload/remote/apitypes.py index 287a5f096..6a7d2f063 100644 --- a/pyload/remote/apitypes.py +++ b/pyload/remote/apitypes.py @@ -114,20 +114,22 @@ class AccountInfo(BaseObject):  		self.config = config  class AddonInfo(BaseObject): -	__slots__ = ['func_name', 'description', 'value'] +	__slots__ = ['name', 'description', 'value'] -	def __init__(self, func_name=None, description=None, value=None): -		self.func_name = func_name +	def __init__(self, name=None, description=None, value=None): +		self.name = name  		self.description = description  		self.value = value  class AddonService(BaseObject): -	__slots__ = ['func_name', 'description', 'arguments', 'media'] +	__slots__ = ['func_name', 'label', 'description', 'arguments', 'pack', 'media'] -	def __init__(self, func_name=None, description=None, arguments=None, media=None): +	def __init__(self, func_name=None, label=None, description=None, arguments=None, pack=None, media=None):  		self.func_name = func_name +		self.label = label  		self.description = description  		self.arguments = arguments +		self.pack = pack  		self.media = media  class ConfigHolder(BaseObject): @@ -419,6 +421,8 @@ class Iface(object):  		pass  	def getAllFiles(self):  		pass +	def getAllInfo(self): +		pass  	def getAllUserData(self):  		pass  	def getAvailablePlugins(self): @@ -437,6 +441,8 @@ class Iface(object):  		pass  	def getFilteredFiles(self, state):  		pass +	def getInfoByPlugin(self, plugin): +		pass  	def getInteractionTasks(self, mode):  		pass  	def getLog(self, offset): @@ -457,8 +463,6 @@ class Iface(object):  		pass  	def getWSAddress(self):  		pass -	def hasAddonHandler(self, plugin, func): -		pass  	def isInteractionWaiting(self, mode):  		pass  	def loadConfig(self, name): diff --git a/pyload/remote/apitypes_debug.py b/pyload/remote/apitypes_debug.py index 74ea8a6a8..14b0cc98e 100644 --- a/pyload/remote/apitypes_debug.py +++ b/pyload/remote/apitypes_debug.py @@ -20,7 +20,7 @@ enums = [  classes = {  	'AccountInfo' : [basestring, basestring, int, bool, int, int, int, bool, bool, bool, (list, ConfigItem)],  	'AddonInfo' : [basestring, basestring, basestring], -	'AddonService' : [basestring, basestring, (list, basestring), (None, int)], +	'AddonService' : [basestring, basestring, basestring, (list, basestring), bool, int],  	'ConfigHolder' : [basestring, basestring, basestring, basestring, (list, ConfigItem), (None, (list, AddonInfo))],  	'ConfigInfo' : [basestring, basestring, basestring, basestring, bool, (None, bool)],  	'ConfigItem' : [basestring, basestring, basestring, Input, basestring], @@ -53,8 +53,8 @@ methods = {  	'addPackageChild': int,  	'addPackageP': int,  	'addUser': UserData, -	'callAddon': None, -	'callAddonHandler': None, +	'callAddon': basestring, +	'callAddonHandler': basestring,  	'checkContainer': OnlineCheck,  	'checkHTML': OnlineCheck,  	'checkLinks': OnlineCheck, @@ -72,6 +72,7 @@ methods = {  	'getAccounts': (list, AccountInfo),  	'getAddonHandler': (dict, basestring, list),  	'getAllFiles': TreeCollection, +	'getAllInfo': (dict, basestring, list),  	'getAllUserData': (dict, int, UserData),  	'getAvailablePlugins': (list, ConfigInfo),  	'getConfig': (dict, basestring, ConfigHolder), @@ -81,6 +82,7 @@ methods = {  	'getFileTree': TreeCollection,  	'getFilteredFileTree': TreeCollection,  	'getFilteredFiles': TreeCollection, +	'getInfoByPlugin': (list, AddonInfo),  	'getInteractionTasks': (list, InteractionTask),  	'getLog': (list, basestring),  	'getPackageContent': TreeCollection, @@ -91,7 +93,6 @@ methods = {  	'getServerVersion': basestring,  	'getUserData': UserData,  	'getWSAddress': basestring, -	'hasAddonHandler': bool,  	'isInteractionWaiting': bool,  	'loadConfig': ConfigHolder,  	'login': bool, diff --git a/pyload/remote/pyload.thrift b/pyload/remote/pyload.thrift index 9bcc2ce89..07782ef42 100644 --- a/pyload/remote/pyload.thrift +++ b/pyload/remote/pyload.thrift @@ -226,13 +226,15 @@ struct InteractionTask {  struct AddonService {    1: string func_name, -  2: string description, -  3: list<string> arguments, -  4: optional i16 media, +  2: string label, +  3: string description, +  4: list<string> arguments, +  5: bool pack, +  6: i16 media,  }  struct AddonInfo { -  1: string func_name, +  1: string name,    2: string description,    3: JSONString value,  } @@ -511,17 +513,16 @@ service Pyload {    // Addon Methods    /////////////////////// -  //map<PluginName, list<AddonInfo>> getAllInfo(), -  //list<AddonInfo> getInfoByPlugin(1: PluginName plugin), +  map<PluginName, list<AddonInfo>> getAllInfo(), +  list<AddonInfo> getInfoByPlugin(1: PluginName plugin),    map<PluginName, list<AddonService>> getAddonHandler(), -  bool hasAddonHandler(1: PluginName plugin, 2: string func), -  void callAddon(1: PluginName plugin, 2: string func, 3: list<JSONString> arguments) +  JSONString callAddon(1: PluginName plugin, 2: string func, 3: list<JSONString> arguments)          throws (1: ServiceDoesNotExists e, 2: ServiceException ex),    // special variant of callAddon that works on the media types, acccepting integer -  void callAddonHandler(1: PluginName plugin, 2: string func, 3: PackageID pid_or_fid) +  JSONString callAddonHandler(1: PluginName plugin, 2: string func, 3: PackageID pid_or_fid)          throws (1: ServiceDoesNotExists e, 2: ServiceException ex), diff --git a/pyload/utils/PluginLoader.py b/pyload/utils/PluginLoader.py index cb1039443..57a899e39 100644 --- a/pyload/utils/PluginLoader.py +++ b/pyload/utils/PluginLoader.py @@ -182,6 +182,8 @@ class PluginLoader:              # save number of of occurred              stack = 0              endpos = m.start(2) - size + +            #TODO: strings must be parsed too, otherwise breaks very easily              for i in xrange(m.end(2), len(content) - size + 1):                  if content[i:i+size] == endchar:                      # closing char seen and match now complete diff --git a/pyload/utils/fs.py b/pyload/utils/fs.py index 05e098e2a..939adb87c 100644 --- a/pyload/utils/fs.py +++ b/pyload/utils/fs.py @@ -48,7 +48,7 @@ def makedirs(path, mode=0755):  def listdir(path):      return [fs_decode(x) for x in os.listdir(fs_encode(path))] -def save_filename(name): +def safe_filename(name):      #remove some chars      if os.name == 'nt':          return remove_chars(name, '/\\?%*:|"<>,') @@ -58,10 +58,13 @@ def save_filename(name):  def stat(name):      return os.stat(fs_encode(name)) -def save_join(*args): +def safe_join(*args):      """ joins a path, encoding aware """      return fs_encode(join(*[x if type(x) == unicode else decode(x) for x in args])) +def save_join(*args): +    return safe_join(*args) +  def free_space(folder):      folder = fs_encode(folder) diff --git a/pyload/web/cnl_app.py b/pyload/web/cnl_app.py index 90aa76d72..d8311d90f 100644 --- a/pyload/web/cnl_app.py +++ b/pyload/web/cnl_app.py @@ -6,7 +6,7 @@ from urllib import unquote  from base64 import standard_b64decode  from binascii import unhexlify -from pyload.utils.fs import save_filename +from pyload.utils.fs import safe_filename  from bottle import route, request, HTTPError  from webinterface import PYLOAD, DL_ROOT, JS @@ -55,7 +55,7 @@ def addcrypted():      package = request.forms.get('referer', 'ClickAndLoad Package')      dlc = request.forms['crypted'].replace(" ", "+") -    dlc_path = join(DL_ROOT, save_filename(package) + ".dlc") +    dlc_path = join(DL_ROOT, safe_filename(package) + ".dlc")      dlc_file = open(dlc_path, "wb")      dlc_file.write(dlc)      dlc_file.close() | 
