diff --git a/README.md b/README.md index 86d25d36..c5ac0b2f 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ Linux (Debian): - `pip install msgpack-python` - start using `python zeronet.py` +Linux (Without root access): + - `wget https://bootstrap.pypa.io/get-pip.py` + - `python get-pip.py --user pyzmq gevent msgpack-python` + - start using `python zeronet.py` + ## Current limitations - No torrent-like, file splitting big file support @@ -91,4 +96,4 @@ Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX #### Thank you! - More info, help, changelog, zeronet sites: http://www.reddit.com/r/zeronet/ -- Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) +- Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) \ No newline at end of file diff --git a/src/Config.py b/src/Config.py index f44c3758..df512c92 100644 --- a/src/Config.py +++ b/src/Config.py @@ -3,7 +3,7 @@ import ConfigParser class Config(object): def __init__(self): - self.version = "0.1.4" + self.version = "0.1.5" self.parser = self.createArguments() argv = sys.argv[:] # Copy command line arguments argv = self.parseConfig(argv) # Add arguments from config file diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 973b6fca..f70671b8 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -2,6 +2,7 @@ import os, msgpack, shutil from Site import SiteManager from cStringIO import StringIO from Debug import Debug +from Config import config FILE_BUFF = 1024*512 @@ -80,13 +81,17 @@ class FileRequest: self.send({"error": "Unknown site"}) return False try: - file = open(site.getPath(params["inner_path"]), "rb") + file_path = site.getPath(params["inner_path"]) + if config.debug_socket: self.log.debug("Opening file: %s" % file_path) + file = open(file_path, "rb") file.seek(params["location"]) back = {} back["body"] = file.read(FILE_BUFF) back["location"] = file.tell() back["size"] = os.fstat(file.fileno()).st_size + if config.debug_socket: self.log.debug("Sending file %s from position %s to %s" % (file_path, params["location"], back["location"])) self.send(back) + if config.debug_socket: self.log.debug("File %s sent" % file_path) except Exception, err: self.send({"error": "File read error: %s" % Debug.formatException(err)}) return False diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 34ce5d42..4a5e4f86 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -49,10 +49,14 @@ class FileServer: self.log.info("Try to open port using upnpc...") try: exit = os.system("%s -e ZeroNet -r %s tcp" % (config.upnpc, self.port)) - if exit == 0: + if exit == 0: # Success upnpc_success = True - else: - upnpc_success = False + else: # Failed + exit = os.system("%s -r %s tcp" % (config.upnpc, self.port)) # Try without -e option + if exit == 0: + upnpc_success = True + else: + upnpc_success = False except Exception, err: self.log.error("Upnpc run error: %s" % Debug.formatException(err)) upnpc_success = False @@ -122,13 +126,25 @@ class FileServer: # Announce sites every 10 min def announceSites(self): while 1: - time.sleep(10*60) # Announce sites every 10 min + time.sleep(20*60) # Announce sites every 20 min for address, site in self.sites.items(): if site.settings["serving"]: site.announce() # Announce site to tracker time.sleep(2) # Prevent too quick request + # Detects if computer back from wakeup + def wakeupWatcher(self): + last_time = time.time() + while 1: + time.sleep(30) + if time.time()-last_time > 60: # If taken more than 60 second then the computer was in sleep mode + self.log.info("Wakeup detected: time wrap from %s to %s (%s sleep seconds), acting like startup..." % (last_time, time.time(), time.time()-last_time)) + self.port_opened = None # Check if we still has the open port on router + self.checkSites() + last_time = time.time() + + # Bind and start serving sites def start(self, check_sites = True): self.log = logging.getLogger(__name__) @@ -152,6 +168,7 @@ class FileServer: gevent.spawn(self.checkSites) gevent.spawn(self.announceSites) + gevent.spawn(self.wakeupWatcher) while True: try: diff --git a/src/Site/Site.py b/src/Site/Site.py index 44fbe3a7..ed7a1a84 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -267,6 +267,32 @@ class Site: self.bad_files[bad_file] = True + def deleteFiles(self): + self.log.debug("Deleting files from content.json...") + files = self.content["files"].keys() # Make a copy + files.append("content.json") + for inner_path in files: + path = self.getPath(inner_path) + if os.path.isfile(path): os.unlink(path) + + self.log.debug("Deleting empty dirs...") + for root, dirs, files in os.walk(self.directory, topdown=False): + for dir in dirs: + path = os.path.join(root,dir) + if os.path.isdir(path) and os.listdir(path) == []: + os.removedirs(path) + self.log.debug("Removing %s" % path) + if os.path.isdir(self.directory) and os.listdir(self.directory) == []: os.removedirs(self.directory) # Remove sites directory if empty + + if os.path.isdir(self.directory): + self.log.debug("Some unknown file remained in site data dir: %s..." % self.directory) + return False # Some files not deleted + else: + self.log.debug("Site data directory deleted: %s..." % self.directory) + return True # All clean + + + # - Events - # Add event listeners @@ -380,11 +406,10 @@ class Site: else: ok = self.verifyFile(inner_path, open(file_path, "rb")) - if ok: - self.log.debug("[OK] %s" % inner_path) - else: + if not ok: self.log.error("[ERROR] %s" % inner_path) bad_files.append(inner_path) + self.log.debug("Site verified: %s files, quick_check: %s, bad files: %s" % (len(self.content["files"]), quick_check, bad_files)) return bad_files diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 80d5a8e9..9779a639 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -45,12 +45,20 @@ def need(address, all_file=True): from Site import Site if address not in sites: # Site not exits yet if not isAddress(address): raise Exception("Not address: %s" % address) + logging.debug("Added new site: %s" % address) sites[address] = Site(address) + sites[address].settings["serving"] = True # Maybe it was deleted before site = sites[address] if all_file: site.download() return site +def delete(address): + global sites + logging.debug("SiteManager deleted site: %s" % address) + del(sites[address]) + + # Lazy load sites def list(): if sites == None: # Not loaded yet diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 634cd638..44c8e593 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -96,6 +96,8 @@ class UiWebsocket: self.actionSitePause(req["id"], req["params"]) elif cmd == "siteResume" and "ADMIN" in permissions: self.actionSiteResume(req["id"], req["params"]) + elif cmd == "siteDelete" and "ADMIN" in permissions: + self.actionSiteDelete(req["id"], req["params"]) elif cmd == "siteList" and "ADMIN" in permissions: self.actionSiteList(req["id"], req["params"]) elif cmd == "channelJoinAllsite" and "ADMIN" in permissions: @@ -211,6 +213,7 @@ class UiWebsocket: site.settings["serving"] = False site.saveSettings() site.updateWebsocket() + site.worker_manager.stopWorkers() else: self.response(to, {"error": "Unknown site: %s" % address}) @@ -227,3 +230,18 @@ class UiWebsocket: site.updateWebsocket() else: self.response(to, {"error": "Unknown site: %s" % address}) + + + def actionSiteDelete(self, to, params): + address = params.get("address") + site = self.server.sites.get(address) + if site: + site.settings["serving"] = False + site.saveSettings() + site.worker_manager.running = False + site.worker_manager.stopWorkers() + site.deleteFiles() + SiteManager.delete(address) + site.updateWebsocket() + else: + self.response(to, {"error": "Unknown site: %s" % address}) diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index d5190c5a..ec26f425 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -27,10 +27,15 @@ class Notifications $(".notification-icon", elem).html("!") else if type == "done" $(".notification-icon", elem).html("
") + else if type == "ask" + $(".notification-icon", elem).html("?") else $(".notification-icon", elem).html("i") - $(".body", elem).html(body) + if typeof(body) == "string" + $(".body", elem).html(body) + else + $(".body", elem).html("").append(body) elem.appendTo(@elem) @@ -53,7 +58,10 @@ class Notifications @close elem return false - @ + # Close on button click within body (confirm dialog) + $(".button", elem).on "click", => + @close elem + return false close: (elem) -> diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index b99a326e..a43ddbb2 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -55,12 +55,31 @@ class Wrapper if @ws.ws.readyState == 1 and not @wrapperWsInited # If ws already opened @sendInner {"cmd": "wrapperOpenedWebsocket"} @wrapperWsInited = true - else if cmd == "wrapperNotification" + else if cmd == "wrapperNotification" # Display notification + message.params = @toHtmlSafe(message.params) # Escape html @notifications.add("notification-#{message.id}", message.params[0], message.params[1], message.params[2]) + else if cmd == "wrapperConfirm" # Display confirm message + @actionWrapperConfirm(message) else # Send to websocket @ws.send(message) # Pass message to websocket + # - Actions - + + actionWrapperConfirm: (message) -> + message.params = @toHtmlSafe(message.params) # Escape html + if message.params[1] then caption = message.params[1] else caption = "ok" + + body = $(""+message.params[0]+"") + button = $("#{caption}") # Add confirm button + button.on "click", => # Response on button click + @sendInner {"cmd": "response", "to": message.id, "result": "boom"} # Response to confirm + return false + body.append(button) + + @notifications.add("notification-#{message.id}", "ask", body) + + onOpenWebsocket: (e) => @ws.cmd "channelJoin", {"channel": "siteChanged"} # Get info on modifications @log "onOpenWebsocket", @inner_ready, @wrapperWsInited @@ -125,6 +144,7 @@ class Wrapper @loading.printLine("#{site_info.event[1]} downloaded") if site_info.event[1] == window.inner_path # File downloaded we currently on @loading.hideScreen() + if not @site_info then @reloadSiteInfo() if not $(".loadingscreen").length # Loading screen already removed (loaded +2sec) @notifications.add("modified", "info", "New version of this page has just released.
Reload to see the modified content.") # File failed downloading @@ -144,6 +164,10 @@ class Wrapper @site_info = site_info + toHtmlSafe: (unsafe) -> + return unsafe + + log: (args...) -> console.log "[Wrapper]", args... diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index 1f588186..cf50fe81 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -7,6 +7,12 @@ a { color: black } #inner-iframe { width: 100%; height: 100%; position: absolute; border: 0px; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out } #inner-iframe.back { transform: scale(0.95) translate(-300px, 0px); opacity: 0.4 } +.button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; border-radius: 2px; text-decoration: none; transition: all 0.5s; } +.button:hover { background-color: #FFF400; border-bottom: 2px solid #4D4D4C; transition: none } +.button:active { position: relative; top: 1px } + +.button-Delete { background-color: #e74c3c; border-bottom-color: #c0392b; color: white } +.button-Delete:hover { background-color: #FF5442; border-bottom-color: #8E2B21 } /* Fixbutton */ @@ -43,6 +49,7 @@ a { color: black } .notification .close:active, .notification .close:focus { color: #AF3BFF } /* Notification types */ +.notification-ask .notification-icon { background-color: #f39c12; } .notification-info .notification-icon { font-size: 22px; font-weight: bold; background-color: #2980b9; line-height: 48px } .notification-done .notification-icon { font-size: 22px; background-color: #27ae60 } diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index 16df57ea..01e91c31 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -12,6 +12,12 @@ a { color: black } #inner-iframe { width: 100%; height: 100%; position: absolute; border: 0px; -webkit-transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out ; -moz-transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out ; -o-transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out ; -ms-transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out ; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out } #inner-iframe.back { -webkit-transform: scale(0.95) translate(-300px, 0px); -moz-transform: scale(0.95) translate(-300px, 0px); -o-transform: scale(0.95) translate(-300px, 0px); -ms-transform: scale(0.95) translate(-300px, 0px); transform: scale(0.95) translate(-300px, 0px) ; opacity: 0.4 } +.button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; text-decoration: none; -webkit-transition: all 0.5s; -moz-transition: all 0.5s; -o-transition: all 0.5s; -ms-transition: all 0.5s; transition: all 0.5s ; } +.button:hover { background-color: #FFF400; border-bottom: 2px solid #4D4D4C; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } +.button:active { position: relative; top: 1px } + +.button-Delete { background-color: #e74c3c; border-bottom-color: #c0392b; color: white } +.button-Delete:hover { background-color: #FF5442; border-bottom-color: #8E2B21 } /* Fixbutton */ @@ -48,6 +54,7 @@ a { color: black } .notification .close:active, .notification .close:focus { color: #AF3BFF } /* Notification types */ +.notification-ask .notification-icon { background-color: #f39c12; } .notification-info .notification-icon { font-size: 22px; font-weight: bold; background-color: #2980b9; line-height: 48px } .notification-done .notification-icon { font-size: 22px; background-color: #27ae60 } diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 8c18be78..0d6ffc11 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -571,10 +571,16 @@ jQuery.extend( jQuery.easing, $(".notification-icon", elem).html("!"); } else if (type === "done") { $(".notification-icon", elem).html("
"); + } else if (type === "ask") { + $(".notification-icon", elem).html("?"); } else { $(".notification-icon", elem).html("i"); } - $(".body", elem).html(body); + if (typeof body === "string") { + $(".body", elem).html(body); + } else { + $(".body", elem).html("").append(body); + } elem.appendTo(this.elem); if (timeout) { $(".close", elem).remove(); @@ -604,7 +610,12 @@ jQuery.extend( jQuery.easing, return false; }; })(this)); - return this; + return $(".button", elem).on("click", (function(_this) { + return function() { + _this.close(elem); + return false; + }; + })(this)); }; Notifications.prototype.close = function(elem) { @@ -771,12 +782,39 @@ jQuery.extend( jQuery.easing, return this.wrapperWsInited = true; } } else if (cmd === "wrapperNotification") { + message.params = this.toHtmlSafe(message.params); return this.notifications.add("notification-" + message.id, message.params[0], message.params[1], message.params[2]); + } else if (cmd === "wrapperConfirm") { + return this.actionWrapperConfirm(message); } else { return this.ws.send(message); } }; + Wrapper.prototype.actionWrapperConfirm = function(message) { + var body, button, caption; + message.params = this.toHtmlSafe(message.params); + if (message.params[1]) { + caption = message.params[1]; + } else { + caption = "ok"; + } + body = $("" + message.params[0] + ""); + button = $("" + caption + ""); + button.on("click", (function(_this) { + return function() { + _this.sendInner({ + "cmd": "response", + "to": message.id, + "result": "boom" + }); + return false; + }; + })(this)); + body.append(button); + return this.notifications.add("notification-" + message.id, "ask", body); + }; + Wrapper.prototype.onOpenWebsocket = function(e) { this.ws.cmd("channelJoin", { "channel": "siteChanged" @@ -858,6 +896,9 @@ jQuery.extend( jQuery.easing, this.loading.printLine("" + site_info.event[1] + " downloaded"); if (site_info.event[1] === window.inner_path) { this.loading.hideScreen(); + if (!this.site_info) { + this.reloadSiteInfo(); + } if (!$(".loadingscreen").length) { this.notifications.add("modified", "info", "New version of this page has just released.
Reload to see the modified content."); } @@ -880,6 +921,10 @@ jQuery.extend( jQuery.easing, return this.site_info = site_info; }; + Wrapper.prototype.toHtmlSafe = function(unsafe) { + return unsafe; + }; + Wrapper.prototype.log = function() { var args; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index dd2ba7b3..c11887ed 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -9,13 +9,14 @@ class WorkerManager: self.site = site self.workers = {} # Key: ip:port, Value: Worker.Worker self.tasks = [] # {"evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0} + self.running = True self.log = logging.getLogger("WorkerManager:%s" % self.site.address_short) self.process_taskchecker = gevent.spawn(self.checkTasks) # Check expired tasks def checkTasks(self): - while 1: + while self.running: time.sleep(15) # Check every 15 sec # Clean up workers @@ -42,6 +43,7 @@ class WorkerManager: task["peers"] = [] self.startWorkers() break # One reannounce per loop + self.log.debug("checkTasks stopped running") @@ -91,6 +93,16 @@ class WorkerManager: if worker: self.log.debug("Added worker: %s, workers: %s/%s" % (key, len(self.workers), MAX_WORKERS)) + # Stop all worker + def stopWorkers(self): + for worker in self.workers.values(): + worker.stop() + tasks = self.tasks[:] # Copy + for task in tasks: # Mark all current task as failed + self.failTask(task) + + + # Find workers by task def findWorkers(self, task): workers = []