diff options
Diffstat (limited to 'pyload')
-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() |