summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--module/Api.py15
-rw-r--r--module/FileManager.py67
-rw-r--r--module/api/ApiComponent.py2
-rw-r--r--module/api/ConfigApi.py9
-rw-r--r--module/api/CoreApi.py20
-rw-r--r--module/api/FileApi.py2
-rw-r--r--module/database/FileDatabase.py42
-rw-r--r--module/remote/apitypes.py10
-rw-r--r--module/remote/apitypes_debug.py2
-rw-r--r--module/remote/json_converter.py22
-rw-r--r--module/remote/pyload.thrift18
-rw-r--r--module/remote/wsbackend/AbstractHandler.py54
-rw-r--r--module/remote/wsbackend/ApiHandler.py13
-rw-r--r--module/remote/wsbackend/AsyncHandler.py35
-rw-r--r--module/threads/BaseThread.py12
-rw-r--r--module/web/api_app.py2
-rw-r--r--module/web/pyload_app.py3
-rw-r--r--module/web/static/css/default/style.less2
-rw-r--r--module/web/static/js/app.js10
-rw-r--r--module/web/static/js/helpers/formatSize.js2
-rw-r--r--module/web/static/js/helpers/formatTime.js40
-rw-r--r--module/web/static/js/models/File.js7
-rw-r--r--module/web/static/js/models/ServerStatus.js41
-rw-r--r--module/web/static/js/utils/initHB.js2
-rw-r--r--module/web/static/js/views/abstract/itemView.js7
-rw-r--r--module/web/static/js/views/dashboardView.js4
-rw-r--r--module/web/static/js/views/fileView.js5
-rw-r--r--module/web/static/js/views/headerView.js229
-rw-r--r--module/web/static/js/views/packageView.js5
-rw-r--r--module/web/static/js/views/selectionView.js7
-rw-r--r--module/web/templates/default/base.html21
-rw-r--r--module/web/templates/default/dashboard.html1
-rw-r--r--module/web/webinterface.py3
-rw-r--r--tests/manager/test_filemanager.py4
-rw-r--r--tests/other/test_filedatabase.py5
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):