diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 934bd6f9..ab258822 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -201,6 +201,55 @@ class UiWebsocketPlugin(object): """.format(**locals())) + + def sidebarRenderOptionalFileStats(self, body, site): + size_total = 0.0 + size_downloaded = 0.0 + for content in site.content_manager.contents.values(): + if "files_optional" not in content: + continue + for file_name, file_details in content["files_optional"].items(): + size_total += file_details["size"] + if site.content_manager.hashfield.hasHash(file_details["sha512"]): + size_downloaded += file_details["size"] + + + if not size_total: + return False + + percent_downloaded = size_downloaded / size_total + + size_formatted_total = size_total / 1024 / 1024 + size_formatted_downloaded = size_downloaded / 1024 / 1024 + + body.append(""" +
  • + + + +
  • + """.format(**locals())) + + return True + + def sidebarRenderOptionalFileSettings(self, body, site): + if self.site.settings.get("autodownloadoptional"): + checked = "checked='checked'" + else: + checked = "" + body.append(""" +
  • + +
    +
  • + """.format(**locals())) + def sidebarRenderDbOptions(self, body, site): if not site.storage.db: return False @@ -232,7 +281,7 @@ class UiWebsocketPlugin(object): checked = "" body.append(""" -

    Owned site settings

    +

    This is my site

    """.format(**locals())) @@ -296,6 +345,9 @@ class UiWebsocketPlugin(object): self.sidebarRenderTransferStats(body, site) self.sidebarRenderFileStats(body, site) self.sidebarRenderSizeLimit(body, site) + has_optional = self.sidebarRenderOptionalFileStats(body, site) + if has_optional: + self.sidebarRenderOptionalFileSettings(body, site) self.sidebarRenderDbOptions(body, site) self.sidebarRenderIdentity(body, site) @@ -405,3 +457,12 @@ class UiWebsocketPlugin(object): if "ADMIN" not in permissions: return self.response(to, "You don't have permission to run this command") self.site.settings["own"] = bool(owned) + + + def actionSiteSetAutodownloadoptional(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["autodownloadoptional"] = bool(owned) + self.site.update() + self.site.worker_manager.removeGoodFileTasks() diff --git a/plugins/Sidebar/media/Scrollable.js b/plugins/Sidebar/media/Scrollable.js index acee6746..689a5719 100644 --- a/plugins/Sidebar/media/Scrollable.js +++ b/plugins/Sidebar/media/Scrollable.js @@ -15,9 +15,9 @@ window.initScrollable = function () { // *Calculation of how tall scroller should be var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight; if (visibleRatio == 1) - scroller.style.display = "none" + scroller.style.display = "none"; else - scroller.style.display = "block" + scroller.style.display = "block"; return visibleRatio * scrollContainer.offsetHeight; } @@ -32,13 +32,13 @@ window.initScrollable = function () { normalizedPosition = evt.pageY; contentPosition = scrollContentWrapper.scrollTop; scrollerBeingDragged = true; - window.addEventListener('mousemove', scrollBarScroll) - return false + window.addEventListener('mousemove', scrollBarScroll); + return false; } function stopDrag(evt) { scrollerBeingDragged = false; - window.removeEventListener('mousemove', scrollBarScroll) + window.removeEventListener('mousemove', scrollBarScroll); } function scrollBarScroll(evt) { @@ -51,7 +51,7 @@ window.initScrollable = function () { } function updateHeight() { - scrollerHeight = calculateScrollerHeight()-10; + scrollerHeight = calculateScrollerHeight() - 10; scroller.style.height = scrollerHeight + 'px'; } @@ -62,9 +62,9 @@ window.initScrollable = function () { scroller.className = 'scroller'; // determine how big scroller should be based on content - scrollerHeight = calculateScrollerHeight()-10; + scrollerHeight = calculateScrollerHeight() - 10; - if (scrollerHeight / scrollContainer.offsetHeight < 1){ + if (scrollerHeight / scrollContainer.offsetHeight < 1) { // *If there is a need to have scroll bar based on content size scroller.style.height = scrollerHeight + 'px'; @@ -87,5 +87,5 @@ window.initScrollable = function () { // *** Listeners *** scrollContentWrapper.addEventListener('scroll', moveScroller); - return updateHeight + return updateHeight; }; \ No newline at end of file diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 869b426f..6b7f20f8 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -220,6 +220,14 @@ class Sidebar extends Class @updateHtmlTag() return false + # Owned checkbox + @tag.find("#checkbox-owned").on "click", => + wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")] + + # Owned checkbox + @tag.find("#checkbox-autodownloadoptional").on "click", => + wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")] + # Change identity button @tag.find("#button-identity").on "click", => wrapper.ws.cmd "certSelect" diff --git a/plugins/Sidebar/media/Sidebar.css b/plugins/Sidebar/media/Sidebar.css index 7710305a..bd7bced2 100644 --- a/plugins/Sidebar/media/Sidebar.css +++ b/plugins/Sidebar/media/Sidebar.css @@ -21,7 +21,10 @@ .sidebar .fields { padding: 0px; list-style-type: none; width: 355px; } .sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px } .sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block } -.sidebar .fields label { font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: block; margin-bottom: 10px; } +.sidebar .fields label { + font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: inline-block; margin-bottom: 10px; + vertical-align: text-bottom; margin-right: 10px; +} .sidebar .fields label small { font-weight: normal; color: white; text-transform: none; } .sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; border-radius: 3px; width: 250px; font-family: Consolas, monospace; } .sidebar .fields .text.long { width: 330px; font-size: 72%; } @@ -52,7 +55,7 @@ /* GRAPH */ .graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; border-radius: 8px; overflow: hidden; position: relative;} -.graph li { height: 100%; position: absolute; } +.graph li { height: 100%; position: absolute; transition: all 0.3s; } .graph-stacked li { position: static; float: left; } .graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; } diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index 2eb760ee..6496d130 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -75,7 +75,10 @@ .sidebar .fields { padding: 0px; list-style-type: none; width: 355px; } .sidebar .fields > li, .sidebar .fields .settings-owned > li { margin-bottom: 30px } .sidebar .fields > li:after, .sidebar .fields .settings-owned > li:after { clear: both; content: ''; display: block } -.sidebar .fields label { font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: block; margin-bottom: 10px; } +.sidebar .fields label { + font-family: Consolas, monospace; text-transform: uppercase; font-size: 13px; color: #ACACAC; display: inline-block; margin-bottom: 10px; + vertical-align: text-bottom; margin-right: 10px; +} .sidebar .fields label small { font-weight: normal; color: white; text-transform: none; } .sidebar .fields .text { background-color: black; border: 0px; padding: 10px; color: white; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; width: 250px; font-family: Consolas, monospace; } .sidebar .fields .text.long { width: 330px; font-size: 72%; } @@ -106,7 +109,7 @@ /* GRAPH */ .graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; -webkit-border-radius: 8px; -moz-border-radius: 8px; -o-border-radius: 8px; -ms-border-radius: 8px; border-radius: 8px ; overflow: hidden; position: relative;} -.graph li { height: 100%; position: absolute; } +.graph li { height: 100%; position: absolute; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; } .graph-stacked li { position: static; float: left; } .graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; } diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 7e86061f..ecc738fb 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -77,9 +77,9 @@ window.initScrollable = function () { // *Calculation of how tall scroller should be var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight; if (visibleRatio == 1) - scroller.style.display = "none" + scroller.style.display = "none"; else - scroller.style.display = "block" + scroller.style.display = "block"; return visibleRatio * scrollContainer.offsetHeight; } @@ -94,13 +94,13 @@ window.initScrollable = function () { normalizedPosition = evt.pageY; contentPosition = scrollContentWrapper.scrollTop; scrollerBeingDragged = true; - window.addEventListener('mousemove', scrollBarScroll) - return false + window.addEventListener('mousemove', scrollBarScroll); + return false; } function stopDrag(evt) { scrollerBeingDragged = false; - window.removeEventListener('mousemove', scrollBarScroll) + window.removeEventListener('mousemove', scrollBarScroll); } function scrollBarScroll(evt) { @@ -113,7 +113,7 @@ window.initScrollable = function () { } function updateHeight() { - scrollerHeight = calculateScrollerHeight()-10; + scrollerHeight = calculateScrollerHeight() - 10; scroller.style.height = scrollerHeight + 'px'; } @@ -124,9 +124,9 @@ window.initScrollable = function () { scroller.className = 'scroller'; // determine how big scroller should be based on content - scrollerHeight = calculateScrollerHeight()-10; + scrollerHeight = calculateScrollerHeight() - 10; - if (scrollerHeight / scrollContainer.offsetHeight < 1){ + if (scrollerHeight / scrollContainer.offsetHeight < 1) { // *If there is a need to have scroll bar based on content size scroller.style.height = scrollerHeight + 'px'; @@ -149,7 +149,7 @@ window.initScrollable = function () { // *** Listeners *** scrollContentWrapper.addEventListener('scroll', moveScroller); - return updateHeight + return updateHeight; }; @@ -398,6 +398,16 @@ window.initScrollable = function () { return false; }; })(this)); + this.tag.find("#checkbox-owned").on("click", (function(_this) { + return function() { + return wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]); + }; + })(this)); + this.tag.find("#checkbox-autodownloadoptional").on("click", (function(_this) { + return function() { + return wrapper.ws.cmd("siteSetAutodownloadoptional", [_this.tag.find("#checkbox-autodownloadoptional").is(":checked")]); + }; + })(this)); this.tag.find("#button-identity").on("click", (function(_this) { return function() { wrapper.ws.cmd("certSelect"); diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 799f3ec5..8ee87133 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -133,7 +133,7 @@ class UiRequestPlugin(object): ("%.0fkB", site.settings.get("bytes_sent", 0) / 1024), ("%.0fkB", site.settings.get("bytes_recv", 0) / 1024), ]) - yield "" % site.address + yield "" % site.address for key, peer in site.peers.items(): if peer.time_found: time_found = int(time.time()-peer.time_found)/60 @@ -143,6 +143,7 @@ class UiRequestPlugin(object): connection_id = peer.connection.id else: connection_id = None + yield "Optional files: %s " % len(peer.hashfield) yield "(#%s, err: %s, found: %s min ago) %22s -
    " % (connection_id, peer.connection_error, time_found, key) yield "
    " yield "" diff --git a/src/Config.py b/src/Config.py index 8d7de4f8..d26dd899 100644 --- a/src/Config.py +++ b/src/Config.py @@ -8,7 +8,7 @@ class Config(object): def __init__(self, argv): self.version = "0.3.2" - self.rev = 562 + self.rev = 571 self.argv = argv self.action = None self.createParser() diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index c9a68921..65e5d1c8 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -62,12 +62,38 @@ class ContentManager(object): if old_hash != new_hash: changed.append(content_inner_dir + relative_path) + # Check changed optional files + for relative_path, info in new_content.get("files_optional", {}).items(): + file_inner_path = content_inner_dir + relative_path + new_hash = info["sha512"] + if old_content and old_content.get("files_optional", {}).get(relative_path): # We have the file in the old content + old_hash = old_content["files_optional"][relative_path].get("sha512") + if old_hash != new_hash and self.site.settings.get("autodownloadoptional"): + changed.append(content_inner_dir + relative_path) # Download new file + elif old_hash != new_hash and not self.site.settings.get("own"): + try: + self.site.storage.delete(file_inner_path) + self.log.debug("Deleted changed optional file: %s" % file_inner_path) + except Exception, err: + self.log.debug("Error deleting file %s: %s" % (file_inner_path, err)) + else: # The file is not in the old content + if self.site.settings.get("autodownloadoptional"): + changed.append(content_inner_dir + relative_path) # Download new file + # Check deleted if old_content: - deleted = [ - content_inner_dir + key for key in old_content.get("files", {}) if key not in new_content.get("files", {}) - ] - if deleted: + old_files = dict( + old_content.get("files", {}), + **old_content.get("files_optional", {}) + ) + + new_files = dict( + new_content.get("files", {}), + **new_content.get("files_optional", {}) + ) + + deleted = [content_inner_dir + key for key in old_files if key not in new_files] + if deleted and not self.site.settings.get("own"): # Deleting files that no longer in content.json for file_inner_path in deleted: try: diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 418e7637..39f0f793 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -72,6 +72,8 @@ class FileRequest(object): self.actionFindHashIds(params) elif cmd == "setHashfield": self.actionSetHashfield(params) + elif cmd == "siteReload": + self.actionSiteReload(params) elif cmd == "ping": self.actionPing() else: @@ -314,6 +316,17 @@ class FileRequest(object): peer.hashfield.replaceFromString(params["hashfield_raw"]) self.response({"ok": "Updated"}) + def actionSiteReload(self, params): + if self.connection.ip != "127.0.0.1" and self.connection.ip != config.ip_external: + self.response({"error": "Only local host allowed"}) + + site = self.sites.get(params["site"]) + site.content_manager.loadContent(params["inner_path"], add_bad_files=False) + site.storage.verifyFiles(quick_check=True) + site.updateWebsocket() + + self.response({"ok": "Reloaded"}) + # Send a simple Pong! answer def actionPing(self): self.response("Pong!") diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 87c58a3b..a543d581 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -1,5 +1,6 @@ import logging import time +import sys import gevent @@ -60,7 +61,11 @@ class Peer(object): self.connection = None try: - self.connection = self.site.connection_server.getConnection(self.ip, self.port) + if self.site: + self.connection = self.site.connection_server.getConnection(self.ip, self.port) + else: + self.connection = sys.modules["main"].file_server.getConnection(self.ip, self.port) + except Exception, err: self.onConnectionError() self.log("Getting connection error: %s (connection_error: %s, hash_failed: %s)" % diff --git a/src/Site/Site.py b/src/Site/Site.py index 8938268d..4acd754a 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -130,6 +130,15 @@ class Site: if res is not True and res is not False: # Need downloading and file is allowed file_threads.append(res) # Append evt + # Optionals files + if self.settings.get("autodownloadoptional"): + for file_relative_path in self.content_manager.contents[inner_path].get("files_optional", {}).keys(): + file_inner_path = content_inner_dir + file_relative_path + # Start download and dont wait for finish, return the event + res = self.needFile(file_inner_path, blocking=False, update=self.bad_files.get(file_inner_path), peer=peer) + if res is not True and res is not False: # Need downloading and file is allowed + file_threads.append(res) # Append evt + # Wait for includes download include_threads = [] for file_relative_path in self.content_manager.contents[inner_path].get("includes", {}).keys(): @@ -212,6 +221,7 @@ class Site: # Wait for peers if not self.peers: + self.announce() for wait in range(10): time.sleep(5+wait) self.log.debug("Waiting for peers...") @@ -258,10 +268,7 @@ class Site: self.log.debug("Fallback to old-style update") self.redownloadContents() - if self.settings["own"]: - self.storage.verifyFiles(quick_check=True) # Check files (need for optional files) - else: - self.storage.checkFiles(quick_check=True) # Quick check and mark bad files based on file size + self.storage.checkFiles(quick_check=True) # Quick check and mark bad files based on file size changed, deleted = self.content_manager.loadContent("content.json") diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index f9a4c3aa..bc276b3d 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -250,7 +250,7 @@ class SiteStorage: return inner_path # Verify all files sha512sum using content.json - def verifyFiles(self, quick_check=False): # Fast = using file size + def verifyFiles(self, quick_check=False, add_optional=False, add_changed=True): bad_files = [] if not self.site.content_manager.contents.get("content.json"): # No content.json, download it first @@ -277,7 +277,8 @@ class SiteStorage: if not ok: self.log.debug("[CHANGED] %s" % file_inner_path) - bad_files.append(file_inner_path) + if add_changed: + bad_files.append(file_inner_path) # Optional files optional_added = 0 @@ -288,6 +289,8 @@ class SiteStorage: file_path = self.getPath(file_inner_path) if not os.path.isfile(file_path): self.site.content_manager.hashfield.removeHash(content["files_optional"][file_relative_path]["sha512"]) + if add_optional: + bad_files.append(file_inner_path) continue if quick_check: @@ -301,6 +304,8 @@ class SiteStorage: else: self.site.content_manager.hashfield.removeHash(content["files_optional"][file_relative_path]["sha512"]) optional_removed += 1 + if add_optional: + bad_files.append(file_inner_path) self.log.debug("[OPTIONAL CHANGED] %s" % file_inner_path) self.log.debug( @@ -313,10 +318,15 @@ class SiteStorage: # Check and try to fix site files integrity def checkFiles(self, quick_check=True): s = time.time() - bad_files = self.verifyFiles(quick_check) + bad_files = self.verifyFiles( + quick_check, + add_optional=self.site.settings.get("autodownloadoptional"), + add_changed=not self.site.settings.get("own") # Don't overwrite changed files if site owned + ) + self.site.bad_files = {} if bad_files: for bad_file in bad_files: - self.site.bad_files[bad_file] = self.site.bad_files.get("bad_file", 0) + 1 + self.site.bad_files[bad_file] = 1 self.log.debug("Checked files in %.2fs... Quick:%s" % (time.time() - s, quick_check)) # Delete site's all file diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 4d762ccd..674de84d 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -138,6 +138,7 @@ class UiRequest(object): headers = [] headers.append(("Version", "HTTP/1.1")) headers.append(("Connection", "Keep-Alive")) + headers.append(("Keep-Alive", "max=25, timeout=30")) headers.append(("Access-Control-Allow-Origin", "*")) # Allow json access if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access @@ -145,11 +146,11 @@ class UiRequest(object): headers.append(("Access-Control-Allow-Credentials", "true")) cacheable_type = ( - content_type == "text/css" or content_type.startswith("image") or + content_type == "text/css" or content_type.startswith("image") or content_type.startswith("video") or self.env["REQUEST_METHOD"] == "OPTIONS" or content_type == "application/javascript" ) - if status == 200 and cacheable_type: # Cache Css, Js, Image files for 10min + if status in (200, 206) and cacheable_type: # Cache Css, Js, Image files for 10min headers.append(("Cache-Control", "public, max-age=600")) # Cache 10 min else: headers.append(("Cache-Control", "no-cache, no-store, private, must-revalidate, max-age=0")) # No caching at all @@ -380,7 +381,7 @@ class UiRequest(object): range_end = int(re.match(".*?-([0-9]+)", range).group(1))+1 else: range_end = file_size - extra_headers["Content-Length"] = range_end - range_start + extra_headers["Content-Length"] = str(range_end - range_start) extra_headers["Content-Range"] = "bytes %s-%s/%s" % (range_start, range_end-1, file_size) if range: status = 206 diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index eb0a50b9..523d762b 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -152,6 +152,7 @@ class UiWebsocket(object): if content: # Remove unnecessary data transfer content = content.copy() content["files"] = len(content.get("files", {})) + content["files_optional"] = len(content.get("files_optional", {})) content["includes"] = len(content.get("includes", {})) if "sign" in content: del(content["sign"]) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index ac7ce935..b969e07b 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -114,6 +114,18 @@ class WorkerManager: continue # No peers found yet for the optional task return task + def removeGoodFileTasks(self): + for task in self.tasks[:]: + if task["inner_path"] not in self.site.bad_files: + self.log.debug("No longer in bad_files, marking as good: %s" % task["inner_path"]) + task["done"] = True + task["evt"].set(True) + self.tasks.remove(task) + if not self.tasks: + self.started_task_num = 0 + self.site.updateWebsocket() + + # New peers added to site def onPeers(self): self.startWorkers() diff --git a/src/main.py b/src/main.py index 045cb676..88be7682 100644 --- a/src/main.py +++ b/src/main.py @@ -226,18 +226,28 @@ class Actions(object): global file_server from Site import SiteManager from File import FileServer # We need fileserver to handle incoming file requests + from Peer import Peer logging.info("Creating FileServer....") file_server = FileServer() file_server_thread = gevent.spawn(file_server.start, check_sites=False) # Dont check every site integrity file_server.openport() + site = SiteManager.site_manager.list()[address] site.settings["serving"] = True # Serving the site even if its disabled + + # Notify local client on new content + if config.ip_external: + logging.info("Sending siteReload") + my_peer = Peer(config.ip_external, config.fileserver_port) + logging.info(my_peer.request("siteReload", {"site": site.address, "inner_path": inner_path})) + if peer_ip: # Announce ip specificed site.addPeer(peer_ip, peer_port) else: # Just ask the tracker logging.info("Gathering peers from tracker") site.announce() # Gather peers + published = site.publish(20, inner_path) # Push to 20 peers if published > 0: time.sleep(3)