import re import os import cgi import sys import math import time try: import cStringIO as StringIO except: import StringIO from Config import config from Plugin import PluginManager from Debug import Debug plugin_dir = "plugins/Sidebar" media_dir = plugin_dir + "/media" sys.path.append(plugin_dir) # To able to load geoip lib loc_cache = {} @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): # Inject our resources to end of original file streams def actionUiMedia(self, path): if path == "/uimedia/all.js" or path == "/uimedia/all.css": # First yield the original file and header body_generator = super(UiRequestPlugin, self).actionUiMedia(path) for part in body_generator: yield part # Append our media file to the end ext = re.match(".*(js|css)$", path).group(1) plugin_media_file = "%s/all.%s" % (media_dir, ext) if config.debug: # If debugging merge *.css to all.css and *.js to all.js from Debug import DebugMedia DebugMedia.merge(plugin_media_file) for part in self.actionFile(plugin_media_file, send_header=False): yield part elif path.startswith("/uimedia/globe/"): # Serve WebGL globe files file_name = re.match(".*/(.*)", path).group(1) plugin_media_file = "%s-globe/%s" % (media_dir, file_name) if config.debug and path.endswith("all.js"): # If debugging merge *.css to all.css and *.js to all.js from Debug import DebugMedia DebugMedia.merge(plugin_media_file) for part in self.actionFile(plugin_media_file): yield part else: for part in super(UiRequestPlugin, self).actionUiMedia(path): yield part @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): def sidebarRenderPeerStats(self, body, site): connected = len([peer for peer in site.peers.values() if peer.connection and peer.connection.connected]) connectable = len([peer_id for peer_id in site.peers.keys() if not peer_id.endswith(":0")]) peers_total = len(site.peers) if peers_total: percent_connected = float(connected) / peers_total percent_connectable = float(connectable) / peers_total else: percent_connectable = percent_connected = 0 body.append(""" <li> <label>Peers</label> <ul class='graph'> <li style='width: 100%' class='total back-black' title="Total peers"></li> <li style='width: {percent_connectable:.0%}' class='connectable back-blue' title='Connectable peers'></li> <li style='width: {percent_connected:.0%}' class='connected back-green' title='Connected peers'></li> </ul> <ul class='graph-legend'> <li class='color-green'><span>connected:</span><b>{connected}</b></li> <li class='color-blue'><span>Connectable:</span><b>{connectable}</b></li> <li class='color-black'><span>Total:</span><b>{peers_total}</b></li> </ul> </li> """.format(**locals())) def sidebarRenderTransferStats(self, body, site): recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024 sent = float(site.settings.get("bytes_sent", 0)) / 1024 / 1024 transfer_total = recv + sent if transfer_total: percent_recv = recv / transfer_total percent_sent = sent / transfer_total else: percent_recv = 0.5 percent_sent = 0.5 body.append(""" <li> <label>Data transfer</label> <ul class='graph graph-stacked'> <li style='width: {percent_recv:.0%}' class='received back-yellow' title="Received bytes"></li> <li style='width: {percent_sent:.0%}' class='sent back-green' title="Sent bytes"></li> </ul> <ul class='graph-legend'> <li class='color-yellow'><span>Received:</span><b>{recv:.2f}MB</b></li> <li class='color-green'<span>Sent:</span><b>{sent:.2f}MB</b></li> </ul> </li> """.format(**locals())) def sidebarRenderFileStats(self, body, site): body.append("<li><label>Files</label><ul class='graph graph-stacked'>") extensions = ( ("html", "yellow"), ("css", "orange"), ("js", "purple"), ("image", "green"), ("json", "blue"), ("other", "white"), ("total", "black") ) # Collect stats size_filetypes = {} size_total = 0 for content in site.content_manager.contents.values(): if "files" not in content: continue for file_name, file_details in content["files"].items(): size_total += file_details["size"] ext = file_name.split(".")[-1] size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"] size_other = size_total # Bar for extension, color in extensions: if extension == "total": continue if extension == "other": size = size_other elif extension == "image": size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0) size_other -= size else: size = size_filetypes.get(extension, 0) size_other -= size percent = 100 * (float(size) / size_total) body.append(u"<li style='width: %.2f%%' class='%s back-%s' title='%s'></li>" % (percent, extension, color, extension)) # Legend body.append("</ul><ul class='graph-legend'>") for extension, color in extensions: if extension == "other": size = size_other elif extension == "image": size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0) elif extension == "total": size = size_total else: size = size_filetypes.get(extension, 0) if extension == "js": title = "javascript" else: title = extension if size > 1024 * 1024 * 10: # Format as mB is more than 10mB size_formatted = "%.0fMB" % (size / 1024 / 1024) else: size_formatted = "%.0fkB" % (size / 1024) body.append(u"<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, title, size_formatted)) body.append("</ul></li>") def getFreeSpace(self): free_space = 0 if "statvfs" in dir(os): # Unix statvfs = os.statvfs(config.data_dir) free_space = statvfs.f_frsize * statvfs.f_bavail else: # Windows try: import ctypes free_space_pointer = ctypes.c_ulonglong(0) ctypes.windll.kernel32.GetDiskFreeSpaceExW( ctypes.c_wchar_p(config.data_dir), None, None, ctypes.pointer(free_space_pointer) ) free_space = free_space_pointer.value except Exception, err: self.log.debug("GetFreeSpace error: %s" % err) return free_space def sidebarRenderSizeLimit(self, body, site): free_space = self.getFreeSpace() / 1024 / 1024 size = float(site.settings["size"]) / 1024 / 1024 size_limit = site.getSizeLimit() percent_used = size / size_limit body.append(""" <li> <label>Size limit <small>(limit used: {percent_used:.0%}, free space: {free_space:,d}MB)</small></label> <input type='text' class='text text-num' value='{size_limit}' id='input-sitelimit'/><span class='text-post'>MB</span> <a href='#Set' class='button' id='button-sitelimit'>Set</a> </li> """.format(**locals())) def sidebarRenderDbOptions(self, body, site): if not site.storage.db: return False inner_path = site.storage.getInnerPath(site.storage.db.db_path) size = float(site.storage.getSize(inner_path)) / 1024 body.append(u""" <li> <label>Database <small>({size:.2f}kB)</small></label> <input type='text' class='text disabled' value='{inner_path}' disabled='disabled'/> <a href='#Reindex' class='button' style='display: none'>Reindex</a> </li> """.format(**locals())) def sidebarRenderIdentity(self, body, site): auth_address = self.user.getAuthAddress(self.site.address) body.append(""" <li> <label>Identity address</label> <span class='input text disabled'>{auth_address}</span> <a href='#Change' class='button' id='button-identity'>Change</a> </li> """.format(**locals())) def sidebarRenderOwnedCheckbox(self, body, site): if self.site.settings["own"]: checked = "checked='checked'" else: checked = "" body.append(""" <h2 class='owned-title'>Owned site settings</h2> <input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div> """.format(**locals())) def sidebarRenderOwnSettings(self, body, site): title = cgi.escape(site.content_manager.contents["content.json"]["title"], True) description = cgi.escape(site.content_manager.contents["content.json"]["description"], True) privatekey = cgi.escape(self.user.getSiteData(site.address, create=False).get("privatekey", "")) body.append(u""" <li> <label for='settings-title'>Site title</label> <input type='text' class='text' value="{title}" id='settings-title'/> </li> <li> <label for='settings-description'>Site description</label> <input type='text' class='text' value="{description}" id='settings-description'/> </li> <li style='display: none'> <label>Private key</label> <input type='text' class='text long' value="{privatekey}" placeholder='[Ask on signing]'/> </li> <li> <a href='#Save' class='button' id='button-settings'>Save site settings</a> </li> """.format(**locals())) def sidebarRenderContents(self, body, site): body.append(""" <li> <label>Content publishing</label> <select id='select-contents'> """) for inner_path in sorted(site.content_manager.contents.keys()): body.append(u"<option>%s</option>" % cgi.escape(inner_path, True)) body.append(""" </select> <span class='select-down'>›</span> <a href='#Sign' class='button' id='button-sign'>Sign</a> <a href='#Publish' class='button' id='button-publish'>Publish</a> </li> """) def actionSidebarGetHtmlTag(self, to): site = self.site body = [] body.append("<div>") body.append("<h1>%s</h1>" % site.content_manager.contents["content.json"]["title"]) body.append("<div class='globe loading'></div>") body.append("<ul class='fields'>") self.sidebarRenderPeerStats(body, site) self.sidebarRenderTransferStats(body, site) self.sidebarRenderFileStats(body, site) self.sidebarRenderSizeLimit(body, site) self.sidebarRenderDbOptions(body, site) self.sidebarRenderIdentity(body, site) self.sidebarRenderOwnedCheckbox(body, site) body.append("<div class='settings-owned'>") self.sidebarRenderOwnSettings(body, site) self.sidebarRenderContents(body, site) body.append("</div>") body.append("</ul>") body.append("</div>") self.response(to, "".join(body)) def downloadGeoLiteDb(self, db_path): import urllib import gzip import shutil self.log.info("Downloading GeoLite2 City database...") self.cmd("notification", ["geolite-info", "Downloading GeoLite2 City database (one time only, ~15MB)...", 0]) try: # Download file = urllib.urlopen("http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz") data = StringIO.StringIO() while True: buff = file.read(1024 * 16) if not buff: break data.write(buff) self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell()) data.seek(0) # Unpack with gzip.GzipFile(fileobj=data) as gzip_file: shutil.copyfileobj(gzip_file, open(db_path, "wb")) self.cmd("notification", ["geolite-done", "GeoLite2 City database downloaded!", 5000]) time.sleep(2) # Wait for notify animation except Exception, err: self.cmd("notification", ["geolite-error", "GeoLite2 City database download error: %s!" % err, 0]) raise err def actionSidebarGetPeers(self, to): permissions = self.getPermissions(to) if "ADMIN" not in permissions: return self.response(to, "You don't have permission to run this command") try: import maxminddb db_path = config.data_dir + '/GeoLite2-City.mmdb' if not os.path.isfile(db_path): self.downloadGeoLiteDb(db_path) geodb = maxminddb.open_database(db_path) peers = self.site.peers.values() # Find avg ping ping_times = [ peer.connection.last_ping_delay for peer in peers if peer.connection and peer.connection.last_ping_delay and peer.connection.last_ping_delay ] if ping_times: ping_avg = sum(ping_times) / float(len(ping_times)) else: ping_avg = 0 # Place bars globe_data = [] placed = {} # Already placed bars here for peer in peers: # Height of bar if peer.connection and peer.connection.last_ping_delay: ping = min(0.20, math.log(1 + peer.connection.last_ping_delay / ping_avg, 300)) else: ping = -0.03 # Query and cache location if peer.ip in loc_cache: loc = loc_cache[peer.ip] else: loc = geodb.get(peer.ip) loc_cache[peer.ip] = loc if not loc or "location" not in loc: continue # Create position array lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"]) latlon = "%s,%s" % (lat, lon) if latlon in placed: # Dont place more than 1 bar to same place, fake repos using ip address last two part lat += float(128 - int(peer.ip.split(".")[-2])) / 50 lon += float(128 - int(peer.ip.split(".")[-1])) / 50 latlon = "%s,%s" % (lat, lon) placed[latlon] = True globe_data += (lat, lon, ping) # Append myself loc = geodb.get(config.ip_external) if loc: lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"]) globe_data += (lat, lon, -0.135) self.response(to, globe_data) except Exception, err: self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err)) self.response(to, {"error": err}) def actionSiteSetOwned(self, to, owned): permissions = self.getPermissions(to) if "ADMIN" not in permissions: return self.response(to, "You don't have permission to run this command") self.site.settings["own"] = bool(owned)