diff options
35 files changed, 458 insertions, 265 deletions
diff --git a/module/Api.py b/module/Api.py index 577c420c3..96b10be9c 100644 --- a/module/Api.py +++ b/module/Api.py @@ -21,15 +21,12 @@ from types import MethodType from remote.apitypes import * -from utils import bits_set +from utils import bits_set, primary_uid # contains function names mapped to their permissions # unlisted functions are for admins only perm_map = {} -# store which methods needs user context -user_context = {} - # decorator only called on init, never initialized, so has no effect on runtime def RequirePerm(bits): class _Dec(object): @@ -39,12 +36,6 @@ def RequirePerm(bits): return _Dec -# TODO: not needed anymore -# decorator to annotate user methods, these methods must have user=None kwarg. -class UserContext(object): - def __new__(cls, f, *args, **kwargs): - user_context[f.__name__] = True - return f urlmatcher = re.compile(r"((https?|ftps?|xdcc|sftp):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+\-=\\\.&]*)", re.IGNORECASE) @@ -93,8 +84,8 @@ class Api(Iface): return None #TODO return default user? @property - def userHandle(self): - return self.user.primary if self.user is not None else None + def primaryUID(self): + return primary_uid(self.user) @classmethod def initComponents(cls): diff --git a/module/FileManager.py b/module/FileManager.py index 74ff2ebeb..082bdb4d4 100644 --- a/module/FileManager.py +++ b/module/FileManager.py @@ -28,9 +28,8 @@ from datatypes.PyPackage import PyPackage, RootPackage # invalidates the cache def invalidate(func): def new(*args): - args[0].filecount = -1 - args[0].downloadcount = -1 - args[0].queuecount = -1 + args[0].downloadstats = {} + args[0].queuestats = {} args[0].jobCache = {} return func(*args) @@ -65,9 +64,8 @@ class FileManager: self.lock = RLock() #self.lock._Verbose__verbose = True - self.filecount = -1 # if an invalid value is set get current value from db - self.downloadcount = -1 # number of downloads - self.queuecount = -1 # number of package to be loaded + self.downloadstats = {} # cached dl stats + self.queuestats = {} # cached queue stats self.db = self.core.db @@ -99,7 +97,7 @@ class FileManager: def addLinks(self, data, package): """Add links, data = (plugin, url) tuple. Internal method should use API.""" self.db.addLinks(data, package, OWNER) - self.evm.dispatchEvent("packageUpdated", package) + self.evm.dispatchEvent("package:updated", package) @invalidate @@ -109,7 +107,7 @@ class FileManager: PackageStatus.Paused if paused else PackageStatus.Ok, OWNER) p = self.db.getPackageInfo(pid) - self.evm.dispatchEvent("packageInserted", pid, p.root, p.packageorder) + self.evm.dispatchEvent("package:inserted", pid, p.root, p.packageorder) return pid @@ -294,28 +292,20 @@ class FileManager: return pyfile - - def getFileCount(self): - """returns number of files""" - - if self.filecount == -1: - self.filecount = self.db.filecount() - - return self.filecount - - def getDownloadCount(self): + #TODO + def getDownloadStats(self, user=None): """ return number of downloads """ - if self.downloadcount == -1: - self.downloadcount = self.db.downloadcount() + if user not in self.downloadstats: + self.downloadstats[user] = self.db.downloadstats() - return self.downloadcount + return self.downloadstats[user] - def getQueueCount(self, force=False): + def getQueueStats(self, user=None, force=False): """number of files that have to be processed""" - if self.queuecount == -1 or force: - self.queuecount = self.db.queuecount() + if user not in self.queuestats or force: + self.queuestats[user] = self.db.queuestats() - return self.queuecount + return self.queuestats[user] def scanDownloadFolder(self): pass @@ -345,7 +335,7 @@ class FileManager: if pack.root == root and pack.packageorder > oldorder: pack.packageorder -= 1 - self.evm.dispatchEvent("packageDeleted", pid) + self.evm.dispatchEvent("package:deleted", pid) @lock @invalidate @@ -370,7 +360,7 @@ class FileManager: if pyfile.packageid == pid and pyfile.fileorder > order: pyfile.fileorder -= 1 - self.evm.dispatchEvent("fileDeleted", fid, pid) + self.evm.dispatchEvent("file:deleted", fid, pid) @lock def releaseFile(self, fid): @@ -387,24 +377,25 @@ class FileManager: def updateFile(self, pyfile): """updates file""" self.db.updateFile(pyfile) - self.evm.dispatchEvent("fileUpdated", pyfile.fid, pyfile.packageid) + self.evm.dispatchEvent("file:updated", pyfile.fid, pyfile.packageid) def updatePackage(self, pypack): """updates a package""" self.db.updatePackage(pypack) - self.evm.dispatchEvent("packageUpdated", pypack.pid) + self.evm.dispatchEvent("package:updated", pypack.pid) @invalidate def updateFileInfo(self, data, pid): """ updates file info (name, size, status,[ hash,] url)""" self.db.updateLinkInfo(data) - self.evm.dispatchEvent("packageUpdated", pid) + self.evm.dispatchEvent("package:updated", pid) def checkAllLinksFinished(self): """checks if all files are finished and dispatch event""" - if not self.getQueueCount(True): - self.core.addonManager.dispatchEvent("allDownloadsFinished") + # TODO: user context? + if not self.getQueueStats(None, True)[0]: + self.core.addonManager.dispatchEvent("downloads:finished") self.core.log.debug("All downloads finished") return True @@ -416,8 +407,9 @@ class FileManager: # reset count so statistic will update (this is called when dl was processed) self.resetCount() + # TODO: user context? if not self.db.processcount(fid): - self.core.addonManager.dispatchEvent("allDownloadsProcessed") + self.core.addonManager.dispatchEvent("downloads:processed") self.core.log.debug("All downloads processed") return True @@ -449,7 +441,7 @@ class FileManager: if pid in self.packages: self.packages[pid].setFinished = False - self.evm.dispatchEvent("packageUpdated", pid) + self.evm.dispatchEvent("package:updated", pid) @lock @invalidate @@ -463,7 +455,7 @@ class FileManager: f.abortDownload() self.db.restartFile(fid) - self.evm.dispatchEvent("fileUpdated", fid) + self.evm.dispatchEvent("file:updated", fid) @lock @@ -486,7 +478,7 @@ class FileManager: self.db.commit() - self.evm.dispatchEvent("packageReordered", pid, position, p.root) + self.evm.dispatchEvent("package:reordered", pid, position, p.root) @lock @invalidate @@ -526,7 +518,7 @@ class FileManager: self.db.commit() - self.evm.dispatchEvent("filesReordered", pid) + self.evm.dispatchEvent("file:reordered", pid) @lock @invalidate @@ -569,6 +561,7 @@ class FileManager: return True + @invalidate def reCheckPackage(self, pid): """ recheck links in package """ data = self.db.getPackageData(pid) diff --git a/module/api/ApiComponent.py b/module/api/ApiComponent.py index c3b8c974b..3948086c2 100644 --- a/module/api/ApiComponent.py +++ b/module/api/ApiComponent.py @@ -18,6 +18,6 @@ class ApiComponent(Iface): self.core = core assert isinstance(user, User) self.user = user - self.userHandle = 0 + self.primaryUID = 0 # No instantiating! raise Exception()
\ No newline at end of file diff --git a/module/api/ConfigApi.py b/module/api/ConfigApi.py index 55e0aa49b..9df9455a2 100644 --- a/module/api/ConfigApi.py +++ b/module/api/ConfigApi.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from module.Api import Api, UserContext, RequirePerm, Permission, ConfigHolder, ConfigItem, ConfigInfo +from module.Api import Api, RequirePerm, Permission, ConfigHolder, ConfigItem, ConfigInfo from module.utils import to_string from ApiComponent import ApiComponent @@ -9,7 +9,6 @@ from ApiComponent import ApiComponent class ConfigApi(ApiComponent): """ Everything related to configuration """ - @UserContext def getConfigValue(self, section, option): """Retrieve config value. @@ -21,7 +20,6 @@ class ConfigApi(ApiComponent): value = self.core.config.get(section, option, self.user) return to_string(value) - @UserContext def setConfigValue(self, section, option, value): """Set new config value. @@ -56,7 +54,6 @@ class ConfigApi(ApiComponent): return [ConfigInfo(section, config.name, config.description, False, False) for section, config, values in self.core.config.iterCoreSections()] - @UserContext @RequirePerm(Permission.Plugins) def getPluginConfig(self): """All plugins and addons the current user has configured @@ -75,7 +72,6 @@ class ConfigApi(ApiComponent): return data - @UserContext @RequirePerm(Permission.Plugins) def getAvailablePlugins(self): """List of all available plugins, that are configurable @@ -88,7 +84,6 @@ class ConfigApi(ApiComponent): self.core.pluginManager.isUserPlugin(name)) for name, config, values in self.core.config.iterSections(self.user)] - @UserContext @RequirePerm(Permission.Plugins) def configurePlugin(self, plugin): """Get complete config options for desired section @@ -99,7 +94,6 @@ class ConfigApi(ApiComponent): pass - @UserContext @RequirePerm(Permission.Plugins) def saveConfig(self, config): """Used to save a configuration, core config can only be saved by admins @@ -108,7 +102,6 @@ class ConfigApi(ApiComponent): """ pass - @UserContext @RequirePerm(Permission.Plugins) def deleteConfig(self, plugin): """Deletes modified config diff --git a/module/api/CoreApi.py b/module/api/CoreApi.py index 4de8c1f96..9338954d0 100644 --- a/module/api/CoreApi.py +++ b/module/api/CoreApi.py @@ -1,12 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from module.Api import Api, RequirePerm, Permission, ServerStatus +from module.Api import Api, RequirePerm, Permission, ServerStatus, PackageStats from module.utils.fs import join, free_space from module.utils import compare_time from ApiComponent import ApiComponent + class CoreApi(ApiComponent): """ This module provides methods for general interaction with the core, like status or progress retrieval """ @@ -18,7 +19,8 @@ class CoreApi(ApiComponent): @RequirePerm(Permission.All) def getWSAddress(self): """Gets and address for the websocket based on configuration""" - # TODO + # TODO SSL (wss) + return "ws://%%s:%d" % self.core.config['remote']['port'] @RequirePerm(Permission.All) def getServerStatus(self): @@ -26,10 +28,17 @@ class CoreApi(ApiComponent): :return: `ServerStatus` """ - serverStatus = ServerStatus(self.core.files.getQueueCount(), self.core.files.getFileCount(), 0, - not self.core.threadManager.pause and self.isTimeDownload(), self.core.threadManager.pause, - self.core.config['reconnect']['activated'] and self.isTimeReconnect()) + queue = self.core.files.getQueueStats(self.primaryUID) + total = self.core.files.getDownloadStats(self.primaryUID) + + serverStatus = ServerStatus(0, + PackageStats(total[0], total[0] - queue[0], total[1], total[1] - queue[1]), + 0, + not self.core.threadManager.pause and self.isTimeDownload(), + self.core.threadManager.pause, + self.core.config['reconnect']['activated'] and self.isTimeReconnect()) + # TODO multi user for pyfile in self.core.threadManager.getActiveDownloads(): serverStatus.speed += pyfile.getSpeed() #bytes/s @@ -117,5 +126,6 @@ class CoreApi(ApiComponent): end = self.core.config['reconnect']['endTime'].split(":") return compare_time(start, end) and self.core.config["reconnect"]["activated"] + if Api.extend(CoreApi): del CoreApi
\ No newline at end of file diff --git a/module/api/FileApi.py b/module/api/FileApi.py index 8f09f3cb7..a5d5a8535 100644 --- a/module/api/FileApi.py +++ b/module/api/FileApi.py @@ -80,7 +80,7 @@ class FileApi(ApiComponent): @RequirePerm(Permission.All) def searchSuggestions(self, pattern): - names = self.core.db.getMatchingFilenames(pattern, self.userHandle) + names = self.core.db.getMatchingFilenames(pattern, self.primaryUID) # TODO: stemming and reducing the names to provide better suggestions return uniqify(names) diff --git a/module/database/FileDatabase.py b/module/database/FileDatabase.py index 557d9c034..632961c2a 100644 --- a/module/database/FileDatabase.py +++ b/module/database/FileDatabase.py @@ -24,30 +24,46 @@ zero_stats = PackageStats(0, 0, 0, 0) class FileMethods(DatabaseMethods): + @queue - def filecount(self, user=None): - """returns number of files""" + def filecount(self): + """returns number of files, currently only used for debugging""" self.c.execute("SELECT COUNT(*) FROM files") return self.c.fetchone()[0] @queue - def downloadcount(self, user=None): - """ number of downloads """ - self.c.execute("SELECT COUNT(*) FROM files WHERE dlstatus != 0") - return self.c.fetchone()[0] + def downloadstats(self, user=None): + """ number of downloads and size """ + if user is None: + self.c.execute("SELECT COUNT(*), SUM(f.size) FROM files f WHERE dlstatus != 0") + else: + self.c.execute( + "SELECT COUNT(*), SUM(f.size) FROM files f, packages p WHERE f.package = p.pid AND dlstatus != 0", + user) + + r = self.c.fetchone() + # sum is None when no elements are added + return (r[0], r[1] if r[1] is not None else 0) if r else (0, 0) @queue - def queuecount(self, user=None): - """ number of files in queue not finished yet""" + def queuestats(self, user=None): + """ number and size of files in queue not finished yet""" # status not in NA, finished, skipped - self.c.execute("SELECT COUNT(*) FROM files WHERE dlstatus NOT IN (0,5,6)") - return self.c.fetchone()[0] + if user is None: + self.c.execute("SELECT COUNT(*), SUM(f.size) FROM files f WHERE dlstatus NOT IN (0,5,6)") + else: + self.c.execute( + "SELECT COUNT(*), SUM(f.size) FROM files f, package p WHERE f.package = p.pid AND p.owner=? AND dlstatus NOT IN (0,5,6)", + user) + + r = self.c.fetchone() + return (r[0], r[1] if r[1] is not None else 0) if r else (0, 0) @queue def processcount(self, fid=-1, user=None): """ number of files which have to be processed """ # status in online, queued, starting, waiting, downloading - self.c.execute("SELECT COUNT(*) FROM files WHERE dlstatus IN (2,3,8,9,10) AND fid != ?", (fid, )) + self.c.execute("SELECT COUNT(*), SUM(size) FROM files WHERE dlstatus IN (2,3,8,9,10) AND fid != ?", (fid, )) return self.c.fetchone()[0] # TODO: think about multiuser side effects on *count methods @@ -184,8 +200,8 @@ class FileMethods(DatabaseMethods): :param tags: optional tag list """ qry = ( - 'SELECT pid, name, folder, root, owner, site, comment, password, added, tags, status, shared, packageorder ' - 'FROM packages%s ORDER BY root, packageorder') + 'SELECT pid, name, folder, root, owner, site, comment, password, added, tags, status, shared, packageorder ' + 'FROM packages%s ORDER BY root, packageorder') if root is None: stats = self.getPackageStats(owner=owner) diff --git a/module/remote/apitypes.py b/module/remote/apitypes.py index bc53f5f7c..aaec2b3ce 100644 --- a/module/remote/apitypes.py +++ b/module/remote/apitypes.py @@ -297,13 +297,13 @@ class ProgressInfo(BaseObject): self.download = download class ServerStatus(BaseObject): - __slots__ = ['queuedDownloads', 'totalDownloads', 'speed', 'pause', 'download', 'reconnect'] + __slots__ = ['speed', 'files', 'notifications', 'paused', 'download', 'reconnect'] - def __init__(self, queuedDownloads=None, totalDownloads=None, speed=None, pause=None, download=None, reconnect=None): - self.queuedDownloads = queuedDownloads - self.totalDownloads = totalDownloads + def __init__(self, speed=None, files=None, notifications=None, paused=None, download=None, reconnect=None): self.speed = speed - self.pause = pause + self.files = files + self.notifications = notifications + self.paused = paused self.download = download self.reconnect = reconnect diff --git a/module/remote/apitypes_debug.py b/module/remote/apitypes_debug.py index 974a68c29..6d30f1da6 100644 --- a/module/remote/apitypes_debug.py +++ b/module/remote/apitypes_debug.py @@ -37,7 +37,7 @@ classes = { 'PackageInfo' : [int, basestring, basestring, int, int, basestring, basestring, basestring, int, (list, basestring), int, bool, int, PackageStats, (list, int), (list, int)], 'PackageStats' : [int, int, int, int], 'ProgressInfo' : [basestring, basestring, basestring, int, int, int, (None, DownloadProgress)], - 'ServerStatus' : [int, int, int, bool, bool, bool], + 'ServerStatus' : [int, PackageStats, int, bool, bool, bool], 'ServiceDoesNotExists' : [basestring, basestring], 'ServiceException' : [basestring], 'TreeCollection' : [PackageInfo, (dict, int, FileInfo), (dict, int, PackageInfo)], diff --git a/module/remote/json_converter.py b/module/remote/json_converter.py index 256674c34..50f0309bd 100644 --- a/module/remote/json_converter.py +++ b/module/remote/json_converter.py @@ -14,7 +14,7 @@ from apitypes import ExceptionObject # compact json separator separators = (',', ':') -# json encoder that accepts TBase objects +# json encoder that accepts api objects class BaseEncoder(json.JSONEncoder): def default(self, o): @@ -26,17 +26,35 @@ class BaseEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) +# more compact representation, only clients with information of the classes can handle it +class BaseEncoderCompact(json.JSONEncoder): + + def default(self, o): + if isinstance(o, BaseObject) or isinstance(o, ExceptionObject): + ret = {"@compact" : [o.__class__.__name__]} + ret["@compact"].extend(getattr(o, attr) for attr in o.__slots__) + return ret + + return json.JSONEncoder.default(self, o) def convert_obj(dct): if '@class' in dct: cls = getattr(apitypes, dct['@class']) del dct['@class'] return cls(**dct) + elif '@compact' in dct: + cls = getattr(apitypes, dct['@compact'][0]) + return cls(*dct['@compact'][1:]) return dct def dumps(*args, **kwargs): - kwargs['cls'] = BaseEncoder + if 'compact' in kwargs: + kwargs['cls'] = BaseEncoderCompact + del kwargs['compact'] + else: + kwargs['cls'] = BaseEncoder + kwargs['separators'] = separators return json.dumps(*args, **kwargs) diff --git a/module/remote/pyload.thrift b/module/remote/pyload.thrift index c66ec20d6..dc6b1c406 100644 --- a/module/remote/pyload.thrift +++ b/module/remote/pyload.thrift @@ -128,15 +128,6 @@ struct ProgressInfo { 7: optional DownloadProgress download } -struct ServerStatus { - 1: i16 queuedDownloads, - 2: i16 totalDownloads, - 3: ByteCount speed, - 4: bool pause, - 5: bool download, - 6: bool reconnect -} - // download info for specific file struct DownloadInfo { 1: string url, @@ -203,6 +194,15 @@ struct LinkStatus { 6: string packagename, } +struct ServerStatus { + 1: ByteCount speed, + 2: PackageStats files, + 3: i16 notifications, + 4: bool paused, + 5: bool download, + 6: bool reconnect, +} + struct InteractionTask { 1: InteractionID iid, 2: Input input, diff --git a/module/remote/wsbackend/AbstractHandler.py b/module/remote/wsbackend/AbstractHandler.py index f843fc278..45fbb134c 100644 --- a/module/remote/wsbackend/AbstractHandler.py +++ b/module/remote/wsbackend/AbstractHandler.py @@ -41,11 +41,38 @@ class AbstractHandler: def do_extra_handshake(self, req): self.log.debug("WS Connected: %s" % req) + req.api = None #when api is set client is logged in + + # allow login via session when webinterface is active + if self.core.config['webinterface']['activated']: + cookie = req.headers_in.getheader('Cookie') + s = self.load_session(cookie) + if s: + uid = s.get('uid', None) + req.api = self.api.withUserContext(uid) + self.log.debug("WS authenticated with cookie: %d" % uid) + self.on_open(req) def on_open(self, req): pass + def load_session(self, cookies): + from Cookie import SimpleCookie + from beaker.session import Session + from module.web.webinterface import session + + cookies = SimpleCookie(cookies) + sid = cookies.get(session.options['key']) + if not sid: + return None + + s = Session({}, use_cookies=False, id=sid.value, **session.options) + if s.is_new: + return None + + return s + def passive_closing_handshake(self, req): self.log.debug("WS Closed: %s" % req) self.on_close(req) @@ -59,8 +86,6 @@ class AbstractHandler: def handle_call(self, msg, req): """ Parses the msg for an argument call. If func is null an response was already sent. - :param msg: - :param req: :return: func, args, kwargs """ try: @@ -70,11 +95,15 @@ class AbstractHandler: self.send_result(req, self.ERROR, "No JSON request") return None, None, None - if type(o) != list and len(o) not in range(1,4): + if not isinstance(o, basestring) and type(o) != list and len(o) not in range(1, 4): self.log.debug("Invalid Api call: %s" % o) self.send_result(req, self.ERROR, "Invalid Api call") return None, None, None - if len(o) == 1: # arguments omitted + + # called only with name, no args + if isinstance(o, basestring): + return o, [], {} + elif len(o) == 1: # arguments omitted return o[0], [], {} elif len(o) == 2: func, args = o @@ -85,5 +114,20 @@ class AbstractHandler: else: return tuple(o) + def do_login(self, req, args, kwargs): + user = self.api.checkAuth(*args, **kwargs) + if user: + req.api = self.api.withUserContext(user.uid) + return self.send_result(req, self.OK, True) + else: + return self.send_result(req, self.FORBIDDEN, "Forbidden") + + def do_logout(self, req): + req.api = None + return self.send_result(req, self.OK, True) + def send_result(self, req, code, result): - return send_message(req, dumps([code, result]))
\ No newline at end of file + return send_message(req, dumps([code, result])) + + def send(self, req, obj): + return send_message(req, dumps(obj))
\ No newline at end of file diff --git a/module/remote/wsbackend/ApiHandler.py b/module/remote/wsbackend/ApiHandler.py index eec546d47..e985e10be 100644 --- a/module/remote/wsbackend/ApiHandler.py +++ b/module/remote/wsbackend/ApiHandler.py @@ -55,18 +55,9 @@ class ApiHandler(AbstractHandler): return # handle_call already sent the result if func == 'login': - user = self.api.checkAuth(*args, **kwargs) - if user: - req.api = self.api.withUserContext(user.uid) - return self.send_result(req, self.OK, True) - - else: - return self.send_result(req, self.OK, False) - + return self.do_login(req, args, kwargs) elif func == 'logout': - req.api = None - return self.send_result(req, self.OK, True) - + return self.do_logout(req) else: if not req.api: return self.send_result(req, self.FORBIDDEN, "Forbidden") diff --git a/module/remote/wsbackend/AsyncHandler.py b/module/remote/wsbackend/AsyncHandler.py index a8382a211..2f9b43ad2 100644 --- a/module/remote/wsbackend/AsyncHandler.py +++ b/module/remote/wsbackend/AsyncHandler.py @@ -16,7 +16,7 @@ # @author: RaNaN ############################################################################### -from Queue import Queue +from Queue import Queue, Empty from threading import Lock from mod_pywebsocket.msgutil import receive_message @@ -34,13 +34,13 @@ class AsyncHandler(AbstractHandler): Progress information are continuous and will be pushed in a fixed interval when available. After connect you have to login and can set the interval by sending the json command ["setInterval", xy]. - To start receiving updates call "start", afterwards no more incoming messages will be accept! + To start receiving updates call "start", afterwards no more incoming messages will be accepted! """ PATH = "/async" COMMAND = "start" - PROGRESS_INTERVAL = 1 + PROGRESS_INTERVAL = 2 STATUS_INTERVAL = 60 def __init__(self, api): @@ -57,7 +57,10 @@ class AsyncHandler(AbstractHandler): @lock def on_close(self, req): - self.clients.remove(req) + try: + self.clients.remove(req) + except ValueError: # ignore when not in list + pass @lock def add_event(self, event): @@ -86,21 +89,15 @@ class AsyncHandler(AbstractHandler): return # Result was already sent if func == 'login': - user = self.api.checkAuth(*args, **kwargs) - if user: - req.api = self.api.withUserContext(user.uid) - return self.send_result(req, self.OK, True) - - else: - return self.send_result(req, self.FORBIDDEN, "Forbidden") + return self.do_login(req, args, kwargs) elif func == 'logout': - req.api = None - return self.send_result(req, self.OK, True) + return self.do_logout(req) else: if not req.api: return self.send_result(req, self.FORBIDDEN, "Forbidden") + if func == "setInterval": req.interval = args[0] elif func == self.COMMAND: @@ -109,4 +106,14 @@ class AsyncHandler(AbstractHandler): def mode_running(self, req): """ Listen for events, closes socket when returning True """ - self.send_result(req, "update", "test")
\ No newline at end of file + try: + ev = req.queue.get(True, req.interval) + self.send(req, ev) + + except Empty: + # TODO: server status is not enough + # modify core api to include progress? think of other needed information to show + # notifications + + self.send(req, self.api.getServerStatus()) + self.send(req, self.api.getProgressInfo())
\ No newline at end of file diff --git a/module/threads/BaseThread.py b/module/threads/BaseThread.py index 3e27eec96..c64678a72 100644 --- a/module/threads/BaseThread.py +++ b/module/threads/BaseThread.py @@ -1,10 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import os -import sys -import locale - from threading import Thread from time import strftime, gmtime from sys import exc_info @@ -12,6 +8,7 @@ from types import MethodType from pprint import pformat from traceback import format_exc +from module.utils import primary_uid from module.utils.fs import listdir, join, save_join, stat, exists class BaseThread(Thread): @@ -24,6 +21,13 @@ class BaseThread(Thread): self.core = manager.core self.log = manager.core.log + #: Owner of the thread, every type should set it + self.owner = None + + @property + def user(self): + return primary_uid(self.owner) + def getProgress(self): """ retrieves progress information about the current running task diff --git a/module/web/api_app.py b/module/web/api_app.py index 75a817c46..52903e92b 100644 --- a/module/web/api_app.py +++ b/module/web/api_app.py @@ -65,6 +65,8 @@ def callApi(api, func, *args, **kwargs): print "Invalid API call", func return HTTPError(404, dumps("Not Found")) + # TODO: accept same payload as WS backends, combine into json_converter + # TODO: arguments as json dictionaries # TODO: encoding result = getattr(api, func)(*[loads(x) for x in args], **dict([(x, loads(y)) for x, y in kwargs.iteritems()])) diff --git a/module/web/pyload_app.py b/module/web/pyload_app.py index f8578fcf0..0c3af103f 100644 --- a/module/web/pyload_app.py +++ b/module/web/pyload_app.py @@ -44,7 +44,8 @@ def pre_processor(): return {"user": user, 'server': status, - 'url': request.url } + 'url': request.url , + 'ws': PYLOAD.getWSAddress()} def base(messages): diff --git a/module/web/static/css/default/style.less b/module/web/static/css/default/style.less index d3f23478f..260f9fa52 100644 --- a/module/web/static/css/default/style.less +++ b/module/web/static/css/default/style.less @@ -422,7 +422,7 @@ footer { // background-color: @greyDark; background: url("../../img/default/bgpatterndark.png") repeat;
color: @grey;
height: @footer-height;
- margin-top: -@footer-height + 10px;
+ margin-top: -@footer-height;
position: relative;
width: 100%;
line-height: 16px;
diff --git a/module/web/static/js/app.js b/module/web/static/js/app.js index b081022af..59ad04fc9 100644 --- a/module/web/static/js/app.js +++ b/module/web/static/js/app.js @@ -28,10 +28,14 @@ define([ // Add Global Helper functions _.extend(Application.prototype, Backbone.Events, { - restartFailed: function(pids, options) { + apiCall: function(method, args, options) { options || (options = {}); - options.url = 'api/restartFailed'; - $.ajax(options); + + + }, + + openWebSocket: function(path) { + return new WebSocket(window.wsAddress.replace('%s', window.location.hostname) + path); } }); diff --git a/module/web/static/js/helpers/formatSize.js b/module/web/static/js/helpers/formatSize.js index a792392b7..a50588bc6 100644 --- a/module/web/static/js/helpers/formatSize.js +++ b/module/web/static/js/helpers/formatSize.js @@ -2,7 +2,7 @@ define('helpers/formatSize', ['handlebars'], function(Handlebars) { var sizes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; function formatSize(bytes, options) { - if (bytes === 0) return '0 B'; + if (!bytes || bytes === 0) return '0 B'; var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); // round to two digits return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; diff --git a/module/web/static/js/helpers/formatTime.js b/module/web/static/js/helpers/formatTime.js new file mode 100644 index 000000000..cb635ede9 --- /dev/null +++ b/module/web/static/js/helpers/formatTime.js @@ -0,0 +1,40 @@ +// Format bytes in human readable format +define('helpers/formatTime', ['handlebars'], function(Handlebars) { + + // TODO: seconds are language dependant + // time could be better formatted + function seconds2time (seconds) { + var hours = Math.floor(seconds / 3600); + var minutes = Math.floor((seconds - (hours * 3600)) / 60); + seconds = seconds - (hours * 3600) - (minutes * 60); + var time = ""; + + if (hours != 0) { + time = hours+":"; + } + if (minutes != 0 || time !== "") { + minutes = (minutes < 10 && time !== "") ? "0"+minutes : String(minutes); + time += minutes+":"; + } + if (time === "") { + time = seconds+"s"; + } + else { + time += (seconds < 10) ? "0"+seconds : String(seconds); + } + return time; + } + + + function formatTime(seconds, options) { + if (seconds === Infinity) + return '∞'; + else if (!seconds || seconds <= 0) + return "-"; + + return seconds2time(seconds); + } + + Handlebars.registerHelper('formatTime', formatTime); + return formatTime; +});
\ No newline at end of file diff --git a/module/web/static/js/models/File.js b/module/web/static/js/models/File.js index 42275a452..fa0945713 100644 --- a/module/web/static/js/models/File.js +++ b/module/web/static/js/models/File.js @@ -31,8 +31,13 @@ define(['jquery', 'backbone', 'underscore', 'utils/apitypes'], function($, Backb }, - destroy: function() { + destroy: function(options) { + options || (options = {}); + // TODO: as post data + options.url = 'api/deleteFiles/[' + this.get('fid') + ']'; + options.type = "post"; + return Backbone.Model.prototype.destroy.call(this, options); }, restart: function(options) { diff --git a/module/web/static/js/models/ServerStatus.js b/module/web/static/js/models/ServerStatus.js index 35257fcb1..2430a9ffd 100644 --- a/module/web/static/js/models/ServerStatus.js +++ b/module/web/static/js/models/ServerStatus.js @@ -1,15 +1,15 @@ -define(['jquery', 'backbone', 'underscore', 'collections/ProgressList'], - function($, Backbone, _, ProgressList) { +define(['jquery', 'backbone', 'underscore'], + function($, Backbone, _) { return Backbone.Model.extend({ defaults: { - queuedDownloads: -1, - totalDownloads: -1, - speed: -1, - pause: false, + speed: 0, + files: null, + notifications: -1, + paused: false, download: false, - reconnect: false, + reconnect: false }, // Model Constructor @@ -24,16 +24,23 @@ define(['jquery', 'backbone', 'underscore', 'collections/ProgressList'], return Backbone.Model.prototype.fetch.call(this, options); }, - parse: function(resp, xhr) { - // Package is loaded from tree collection - if (_.has(resp, 'root')) { - resp.root.files = new FileList(_.values(resp.files)); - // circular dependencies needs to be avoided - var PackageList = require('collections/PackageList'); - resp.root.packs = new PackageList(_.values(resp.packages)); - return resp.root; - } - return Backbone.model.prototype.fetch.call(this, resp, xhr); + toJSON: function(options) { + var obj = Backbone.Model.prototype.toJSON.call(this, options); + + // stats are not available + if (obj.files === null) + return obj; + + obj.files.linksleft = obj.files.linkstotal - obj.files.linksdone; + obj.files.sizeleft = obj.files.sizetotal - obj.files.sizedone; + if (obj.speed && obj.speed > 0) + obj.files.eta = Math.round(obj.files.sizeleft / obj.speed); + else if (obj.files.sizeleft > 0) + obj.files.eta = Infinity; + else + obj.files.eta = 0; + + return obj; } }); diff --git a/module/web/static/js/utils/initHB.js b/module/web/static/js/utils/initHB.js index f3a0955b3..c977f063d 100644 --- a/module/web/static/js/utils/initHB.js +++ b/module/web/static/js/utils/initHB.js @@ -1,6 +1,6 @@ // Loads all helper and set own handlebars rules define(['underscore', 'handlebars', - 'helpers/formatSize', 'helpers/fileHelper'], + 'helpers/formatSize', 'helpers/fileHelper', 'helpers/formatTime'], function(_, Handlebars) { // Replace with own lexer rules compiled from handlebars.l Handlebars.Parser.lexer.rules = [/^(?:[^\x00]*?(?=(<%)))/, /^(?:[^\x00]+)/, /^(?:[^\x00]{2,}?(?=(\{\{|$)))/, /^(?:\{\{>)/, /^(?:<%=)/, /^(?:<%\/)/, /^(?:\{\{\^)/, /^(?:<%\s*else\b)/, /^(?:\{<%%)/, /^(?:\{\{&)/, /^(?:<%![\s\S]*?%>)/, /^(?:<%)/, /^(?:=)/, /^(?:\.(?=[%} ]))/, /^(?:\.\.)/, /^(?:[\/.])/, /^(?:\s+)/, /^(?:%%>)/, /^(?:%>)/, /^(?:"(\\["]|[^"])*")/, /^(?:'(\\[']|[^'])*')/, /^(?:@[a-zA-Z]+)/, /^(?:true(?=[%}\s]))/, /^(?:false(?=[%}\s]))/, /^(?:[0-9]+(?=[%}\s]))/, /^(?:[a-zA-Z0-9_$-]+(?=[=%}\s\/.]))/, /^(?:\[[^\]]*\])/, /^(?:.)/, /^(?:$)/]; diff --git a/module/web/static/js/views/abstract/itemView.js b/module/web/static/js/views/abstract/itemView.js index 75b058874..394044ec4 100644 --- a/module/web/static/js/views/abstract/itemView.js +++ b/module/web/static/js/views/abstract/itemView.js @@ -23,6 +23,13 @@ define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) { this.$el.slideDown(); }, + unrender: function() { + var self = this; + this.$el.slideUp(function() { + self.destroy(); + }); + }, + deleteItem: function(e) { if (e) e.stopPropagation(); diff --git a/module/web/static/js/views/dashboardView.js b/module/web/static/js/views/dashboardView.js index d9ea8d444..d9ff1c5fc 100644 --- a/module/web/static/js/views/dashboardView.js +++ b/module/web/static/js/views/dashboardView.js @@ -1,5 +1,5 @@ define(['jquery', 'backbone', 'underscore', 'app', 'models/TreeCollection', - 'views/packageView', 'views/fileView', 'views/selectionView', 'views/filterView'], + 'views/packageView', 'views/fileView', 'views/selectionView', 'views/filterView', 'select2'], function($, Backbone, _, App, TreeCollection, packageView, fileView, selectionView, filterView) { // Renders whole dashboard @@ -51,6 +51,8 @@ define(['jquery', 'backbone', 'underscore', 'app', 'models/TreeCollection', }); }}); + + this.$('.input').select2({tags: ["a", "b", "sdf"]}); }, render: function() { diff --git a/module/web/static/js/views/fileView.js b/module/web/static/js/views/fileView.js index 17da74de3..2459b6cd6 100644 --- a/module/web/static/js/views/fileView.js +++ b/module/web/static/js/views/fileView.js @@ -9,14 +9,15 @@ define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abst // template: _.template($("#template-file").html()), template: _.compile($("#template-file").html()), events: { - 'click .checkbox': 'select' + 'click .checkbox': 'select', + 'click .iconf-trash': 'deleteItem' }, initialize: function() { this.listenTo(this.model, 'change', this.render); // This will be triggered manually and changed before with silent=true this.listenTo(this.model, 'change:visible', this.visibility_changed); - this.listenTo(this.model, 'remove', this.destroy); + this.listenTo(this.model, 'remove', this.unrender); this.listenTo(App.vent, 'dashboard:destroyContent', this.destroy); }, diff --git a/module/web/static/js/views/headerView.js b/module/web/static/js/views/headerView.js index cfceca6cd..c22f173c4 100644 --- a/module/web/static/js/views/headerView.js +++ b/module/web/static/js/views/headerView.js @@ -1,102 +1,153 @@ -define(['jquery', 'underscore', 'backbone', 'flot'], function($, _, Backbone) { - // Renders the header with all information - return Backbone.View.extend({ - - el: 'header', - - events: { - 'click i.iconf-list': 'toggle_taskList', - 'click .popover .close': 'hide_taskList', - 'click .btn-grabber': 'open_grabber' - }, - - // Will hold the link grabber - grabber: null, - notifications: null, - selections: null, - - initialize: function() { - - this.notifications = this.$('#notification-area').calculateHeight().height(0); - this.selections = this.$('#selection-area').calculateHeight().height(0); - - var totalPoints = 100; - var data = []; - - function getRandomData() { - if (data.length > 0) - data = data.slice(1); - - // do a random walk - while (data.length < totalPoints) { - var prev = data.length > 0 ? data[data.length - 1] : 50; - var y = prev + Math.random() * 10 - 5; - if (y < 0) - y = 0; - if (y > 100) - y = 100; - data.push(y); +define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'flot'], + function($, _, Backbone, App, ServerStatus) { + // Renders the header with all information + return Backbone.View.extend({ + + el: 'header', + + events: { + 'click i.iconf-list': 'toggle_taskList', + 'click .popover .close': 'hide_taskList', + 'click .btn-grabber': 'open_grabber' + }, + + templateStatus: _.compile($('#template-header-status').html()), + + // Will hold the link grabber + grabber: null, + notifications: null, + ws: null, + + // Status model + status: null, + + initialize: function() { + this.notifications = this.$('#notification-area').calculateHeight().height(0); + + this.status = new ServerStatus(); + this.listenTo(this.status, 'change', this.render); + + // TODO: button to start stop refresh + var ws = App.openWebSocket('/async'); + ws.onopen = function() { + ws.send(JSON.stringify('start')); + }; + // TODO compare with polling + ws.onmessage = _.bind(this.onData, this); + + this.ws = ws; + + this.initGraph(); + }, + + initGraph: function() { + var totalPoints = 100; + var data = []; + + function getRandomData() { + if (data.length > 0) + data = data.slice(1); + + // do a random walk + while (data.length < totalPoints) { + var prev = data.length > 0 ? data[data.length - 1] : 50; + var y = prev + Math.random() * 10 - 5; + if (y < 0) + y = 0; + if (y > 100) + y = 100; + data.push(y); + } + + // zip the generated y values with the x values + var res = []; + for (var i = 0; i < data.length; ++i) + res.push([i, data[i]]) + return res; } - // zip the generated y values with the x values - var res = []; - for (var i = 0; i < data.length; ++i) - res.push([i, data[i]]) - return res; - } - - var updateInterval = 1500; - - var speedgraph = $.plot(this.$el.find("#speedgraph"), [getRandomData()], { - series: { - lines: { show: true, lineWidth: 2 }, - shadowSize: 0, - color: "#fee247" - }, - xaxis: { ticks: [], mode: "time" }, - yaxis: { ticks: [], min: 0, autoscaleMargin: 0.1 }, - grid: { - show: true, + var updateInterval = 1500; + + var speedgraph = $.plot(this.$el.find("#speedgraph"), [getRandomData()], { + series: { + lines: { show: true, lineWidth: 2 }, + shadowSize: 0, + color: "#fee247" + }, + xaxis: { ticks: [], mode: "time" }, + yaxis: { ticks: [], min: 0, autoscaleMargin: 0.1 }, + grid: { + show: true, // borderColor: "#757575", - borderColor: "white", - borderWidth: 1, - labelMargin: 0, - axisMargin: 0, - minBorderMargin: 0 + borderColor: "white", + borderWidth: 1, + labelMargin: 0, + axisMargin: 0, + minBorderMargin: 0 + } + }); + + function update() { + speedgraph.setData([ getRandomData() ]); + // since the axes don't change, we don't need to call plot.setupGrid() + speedgraph.draw(); + + setTimeout(update, updateInterval); } - }); - function update() { - speedgraph.setData([ getRandomData() ]); - // since the axes don't change, we don't need to call plot.setupGrid() - speedgraph.draw(); +// update(); - setTimeout(update, updateInterval); - } + }, + + render: function() { +// console.log('Render header'); + + this.$('.status-block').html( + this.templateStatus(this.status.toJSON()) + ); + }, + + toggle_taskList: function() { + this.$('.popover').animate({opacity: 'toggle'}); + }, - update(); + hide_taskList: function() { + this.$('.popover').fadeOut(); + }, - }, + open_grabber: function() { + var self = this; + _.requireOnce(['views/linkGrabberModal'], function(modalView) { + if (self.grabber === null) + self.grabber = new modalView(); - render: function() { - }, + self.grabber.show(); + }); + }, - toggle_taskList: function() { - this.$('.popover').animate({opacity: 'toggle'}); - }, + onData: function(evt) { + var data = JSON.parse(evt.data); + if (data === null) return; - hide_taskList: function() { - this.$('.popover').fadeOut(); - }, + if (data['@class'] === "ServerStatus") { + this.status.set(data); + } + else if (data['@class'] === 'progress') + this.onProgressUpdate(data); + else if (data['@class'] === 'event') + this.onEvent(data); + else + console.log('Unknown Async input'); + + }, + + onProgressUpdate: function(progress) { - open_grabber: function() { - var self = this; - _.requireOnce(['views/linkGrabberModal'], function(modalView) { - if (self.grabber === null) - self.grabber = new modalView(); + }, + + onEvent: function(event) { + + } - self.grabber.show(); - }); - } - }); -});
\ No newline at end of file + }); + });
\ No newline at end of file diff --git a/module/web/static/js/views/packageView.js b/module/web/static/js/views/packageView.js index cfd671611..534fe2ad4 100644 --- a/module/web/static/js/views/packageView.js +++ b/module/web/static/js/views/packageView.js @@ -43,10 +43,7 @@ define(['jquery', 'app', 'views/abstract/itemView', 'underscore'], }, unrender: function() { - var self = this; - this.$el.slideUp(function() { - self.destroy(); - }); + itemView.prototype.unrender.apply(this); // TODO: display other package App.vent.trigger('dashboard:loading', null); diff --git a/module/web/static/js/views/selectionView.js b/module/web/static/js/views/selectionView.js index 2237c5f92..480b7127b 100644 --- a/module/web/static/js/views/selectionView.js +++ b/module/web/static/js/views/selectionView.js @@ -19,6 +19,8 @@ define(['jquery', 'backbone', 'underscore', 'app'], current: 0, initialize: function() { + this.$el.calculateHeight().height(0); + var render = _.bind(this.render, this); App.vent.on('dashboard:updated', render); @@ -69,8 +71,8 @@ define(['jquery', 'backbone', 'underscore', 'app'], this.current = files + packs; }, - // Deselects all items, optional only files - deselect: function(filesOnly) { + // Deselects all items + deselect: function() { this.get_files().map(function(file) { file.set('selected', false); }); @@ -90,6 +92,7 @@ define(['jquery', 'backbone', 'underscore', 'app'], }, trash: function() { + // TODO: delete many at once, check if package is parent this.get_files().map(function(file) { file.destroy(); }); diff --git a/module/web/templates/default/base.html b/module/web/templates/default/base.html index 621059c8c..e8661cbbc 100644 --- a/module/web/templates/default/base.html +++ b/module/web/templates/default/base.html @@ -21,6 +21,9 @@ <script src="/static/js/libs/less-1.3.0.min.js" type="text/javascript"></script>
<script type="text/javascript" data-main="static/js/config" src="/static/js/libs/require-2.1.5.js"></script>
<script>
+ window.wsAddress = "{{ ws }}";
+ window.pathPrefix = ""; // TODO
+
require(['default'], function(App) {
App.init();
{% block require %}
@@ -28,6 +31,13 @@ });
</script>
+ <script type="text/template" id="template-header-status">
+ <span class="pull-right eta"><% formatTime files.eta %></span><br>
+ <span class="pull-right remeaning"><% formatSize files.sizeleft %></span><br>
+ <span class="pull-right"><span
+ style="font-weight:bold;color: #fff !important;"><% files.linksleft %></span> of <% files.linkstotal %></span>
+ </script>
+
{% block head %}
{% endblock %}
</head>
@@ -67,16 +77,11 @@ <div id="speedgraph" class="visible-desktop"></div>
- <div class="header_block right-border">
- <span class="pull-right">8:15:01</span><br>
- <span class="pull-right">Started</span><br>
- <span class="pull-right"><span
- style="font-weight:bold;color: #fff !important;">5</span> of 12</span>
-
+ <div class="header_block right-border status-block">
</div>
<div class="header_block left-border">
- <i class="icon-time icon-white"></i> Remaining:<br>
- <i class="icon-retweet icon-white"></i> Status:<br>
+ <i class="icon-time icon-white"></i> approx. ETA :<br>
+ <i class=" icon-hdd icon-white"></i> Remeaning:<br>
<i class="icon-download-alt icon-white"></i> Downloads: <br>
</div>
diff --git a/module/web/templates/default/dashboard.html b/module/web/templates/default/dashboard.html index 8c20973e4..7fe9c9635 100644 --- a/module/web/templates/default/dashboard.html +++ b/module/web/templates/default/dashboard.html @@ -177,6 +177,7 @@ <div class="sidebar-header">
<i class="iconf-hdd"></i> Local
<div class="pull-right" style="font-size: medium; line-height: normal">
+{# <input type="text" class="input">#}
<i class="iconf-chevron-down" style="font-size: 20px"></i>
</div>
<div class="clearfix"></div>
diff --git a/module/web/webinterface.py b/module/web/webinterface.py index cec0f24a4..f18157cd7 100644 --- a/module/web/webinterface.py +++ b/module/web/webinterface.py @@ -113,7 +113,8 @@ session_opts = { 'session.auto': False
}
-web = StripPathMiddleware(SessionMiddleware(app(), session_opts))
+session = SessionMiddleware(app(), session_opts)
+web = StripPathMiddleware(session)
web = GZipMiddleWare(web)
if PREFIX:
diff --git a/tests/manager/test_filemanager.py b/tests/manager/test_filemanager.py index 81acea4d0..5b9fbb567 100644 --- a/tests/manager/test_filemanager.py +++ b/tests/manager/test_filemanager.py @@ -58,7 +58,7 @@ class TestFileManager(BenchmarkTest): for pid in self.pids: self.m.addLinks([("plugin %d" % i, "url %s" % i) for i in range(self.count)], pid) - count = self.m.getQueueCount() + count = self.m.getQueueStats()[0] files = self.count * len(self.pids) # in test runner files get added twice assert count == files or count == files * 2 @@ -91,7 +91,7 @@ class TestFileManager(BenchmarkTest): finished = self.m.getTree(-1, True, DownloadState.Finished) unfinished = self.m.getTree(-1, True, DownloadState.Unfinished) - assert len(finished.files) + len(unfinished.files) == len(all.files) == self.m.getFileCount() + assert len(finished.files) + len(unfinished.files) == len(all.files) == self.m.db.filecount() def test_get_files_root(self): diff --git a/tests/other/test_filedatabase.py b/tests/other/test_filedatabase.py index 3a63b75d5..9a5b236a8 100644 --- a/tests/other/test_filedatabase.py +++ b/tests/other/test_filedatabase.py @@ -158,9 +158,8 @@ class TestDatabase(BenchmarkTest): def test_count(self): self.db.purgeAll() - assert self.db.filecount() == 0 - assert self.db.downloadcount() == 0 - assert self.db.queuecount() == 0 + assert self.db.downloadstats() == (0,0) + assert self.db.queuestats() == (0,0) assert self.db.processcount() == 0 def test_update(self): |