version 0.1.5, install as user to readme, more debug on filerequests, if upnpc fail try without -e, announce interval from 10 to 20 min, detect computer wakeup and acts as startup, delete sites files websocket command support, pause stop all current downloads, wrapper confirmation dialog support
This commit is contained in:
parent
3bec738595
commit
a977feec33
13 changed files with 197 additions and 16 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -27,10 +27,15 @@ class Notifications
|
|||
$(".notification-icon", elem).html("!")
|
||||
else if type == "done"
|
||||
$(".notification-icon", elem).html("<div class='icon-success'></div>")
|
||||
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) ->
|
||||
|
|
|
@ -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 = $("<span>"+message.params[0]+"</span>")
|
||||
button = $("<a href='##{caption}' class='button button-#{caption}'>#{caption}</a>") # 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.<br>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...
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -571,10 +571,16 @@ jQuery.extend( jQuery.easing,
|
|||
$(".notification-icon", elem).html("!");
|
||||
} else if (type === "done") {
|
||||
$(".notification-icon", elem).html("<div class='icon-success'></div>");
|
||||
} 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 = $("<span>" + message.params[0] + "</span>");
|
||||
button = $("<a href='#" + caption + "' class='button button-" + caption + "'>" + caption + "</a>");
|
||||
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.<br>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) : [];
|
||||
|
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in a new issue