Add files via upload
We recovered Zeronet Enhanced/ZNE from here web.archive.org/https://github.com/zeronet-enhanced/ZeroNet/
This commit is contained in:
parent
1b1a72bc4a
commit
9aa53f848f
|
@ -0,0 +1,193 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Config import config
|
||||||
|
from Debug import Debug
|
||||||
|
|
||||||
|
# Keep archive open for faster reponse times for large sites
|
||||||
|
archive_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def closeArchive(archive_path):
|
||||||
|
if archive_path in archive_cache:
|
||||||
|
del archive_cache[archive_path]
|
||||||
|
|
||||||
|
|
||||||
|
def openArchive(archive_path, file_obj=None):
|
||||||
|
if archive_path not in archive_cache:
|
||||||
|
if archive_path.endswith("tar.gz"):
|
||||||
|
import tarfile
|
||||||
|
archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:gz")
|
||||||
|
else:
|
||||||
|
import zipfile
|
||||||
|
archive_cache[archive_path] = zipfile.ZipFile(file_obj or archive_path)
|
||||||
|
gevent.spawn_later(5, lambda: closeArchive(archive_path)) # Close after 5 sec
|
||||||
|
|
||||||
|
archive = archive_cache[archive_path]
|
||||||
|
return archive
|
||||||
|
|
||||||
|
|
||||||
|
def openArchiveFile(archive_path, path_within, file_obj=None):
|
||||||
|
archive = openArchive(archive_path, file_obj=file_obj)
|
||||||
|
if archive_path.endswith(".zip"):
|
||||||
|
return archive.open(path_within)
|
||||||
|
else:
|
||||||
|
return archive.extractfile(path_within)
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiRequest")
|
||||||
|
class UiRequestPlugin(object):
|
||||||
|
def actionSiteMedia(self, path, **kwargs):
|
||||||
|
if ".zip/" in path or ".tar.gz/" in path:
|
||||||
|
file_obj = None
|
||||||
|
path_parts = self.parsePath(path)
|
||||||
|
file_path = "%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"])
|
||||||
|
match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", file_path)
|
||||||
|
archive_path, path_within = match.groups()
|
||||||
|
if archive_path not in archive_cache:
|
||||||
|
site = self.server.site_manager.get(path_parts["address"])
|
||||||
|
if not site:
|
||||||
|
return self.actionSiteAddPrompt(path)
|
||||||
|
archive_inner_path = site.storage.getInnerPath(archive_path)
|
||||||
|
if not os.path.isfile(archive_path):
|
||||||
|
# Wait until file downloads
|
||||||
|
result = site.needFile(archive_inner_path, priority=10)
|
||||||
|
# Send virutal file path download finished event to remove loading screen
|
||||||
|
site.updateWebsocket(file_done=archive_inner_path)
|
||||||
|
if not result:
|
||||||
|
return self.error404(archive_inner_path)
|
||||||
|
file_obj = site.storage.openBigfile(archive_inner_path)
|
||||||
|
if file_obj == False:
|
||||||
|
file_obj = None
|
||||||
|
|
||||||
|
header_allow_ajax = False
|
||||||
|
if self.get.get("ajax_key"):
|
||||||
|
requester_site = self.server.site_manager.get(path_parts["request_address"])
|
||||||
|
if self.get["ajax_key"] == requester_site.settings["ajax_key"]:
|
||||||
|
header_allow_ajax = True
|
||||||
|
else:
|
||||||
|
return self.error403("Invalid ajax_key")
|
||||||
|
|
||||||
|
try:
|
||||||
|
file = openArchiveFile(archive_path, path_within, file_obj=file_obj)
|
||||||
|
content_type = self.getContentType(file_path)
|
||||||
|
self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False), allow_ajax=header_allow_ajax)
|
||||||
|
return self.streamFile(file)
|
||||||
|
except Exception as err:
|
||||||
|
self.log.debug("Error opening archive file: %s" % Debug.formatException(err))
|
||||||
|
return self.error404(path)
|
||||||
|
|
||||||
|
return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs)
|
||||||
|
|
||||||
|
def streamFile(self, file):
|
||||||
|
for i in range(100): # Read max 6MB
|
||||||
|
try:
|
||||||
|
block = file.read(60 * 1024)
|
||||||
|
if block:
|
||||||
|
yield block
|
||||||
|
else:
|
||||||
|
raise StopIteration
|
||||||
|
except StopIteration:
|
||||||
|
file.close()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("SiteStorage")
|
||||||
|
class SiteStoragePlugin(object):
|
||||||
|
def isFile(self, inner_path):
|
||||||
|
if ".zip/" in inner_path or ".tar.gz/" in inner_path:
|
||||||
|
match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", inner_path)
|
||||||
|
archive_inner_path, path_within = match.groups()
|
||||||
|
return super(SiteStoragePlugin, self).isFile(archive_inner_path)
|
||||||
|
else:
|
||||||
|
return super(SiteStoragePlugin, self).isFile(inner_path)
|
||||||
|
|
||||||
|
def openArchive(self, inner_path):
|
||||||
|
archive_path = self.getPath(inner_path)
|
||||||
|
file_obj = None
|
||||||
|
if archive_path not in archive_cache:
|
||||||
|
if not os.path.isfile(archive_path):
|
||||||
|
result = self.site.needFile(inner_path, priority=10)
|
||||||
|
self.site.updateWebsocket(file_done=inner_path)
|
||||||
|
if not result:
|
||||||
|
raise Exception("Unable to download file")
|
||||||
|
file_obj = self.site.storage.openBigfile(inner_path)
|
||||||
|
if file_obj == False:
|
||||||
|
file_obj = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
archive = openArchive(archive_path, file_obj=file_obj)
|
||||||
|
except Exception as err:
|
||||||
|
raise Exception("Unable to download file: %s" % Debug.formatException(err))
|
||||||
|
|
||||||
|
return archive
|
||||||
|
|
||||||
|
def walk(self, inner_path, *args, **kwags):
|
||||||
|
if ".zip" in inner_path or ".tar.gz" in inner_path:
|
||||||
|
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
|
||||||
|
archive_inner_path, path_within = match.groups()
|
||||||
|
archive = self.openArchive(archive_inner_path)
|
||||||
|
path_within = path_within.lstrip("/")
|
||||||
|
|
||||||
|
if archive_inner_path.endswith(".zip"):
|
||||||
|
namelist = [name for name in archive.namelist() if not name.endswith("/")]
|
||||||
|
else:
|
||||||
|
namelist = [item.name for item in archive.getmembers() if not item.isdir()]
|
||||||
|
|
||||||
|
namelist_relative = []
|
||||||
|
for name in namelist:
|
||||||
|
if not name.startswith(path_within):
|
||||||
|
continue
|
||||||
|
name_relative = name.replace(path_within, "", 1).rstrip("/")
|
||||||
|
namelist_relative.append(name_relative)
|
||||||
|
|
||||||
|
return namelist_relative
|
||||||
|
|
||||||
|
else:
|
||||||
|
return super(SiteStoragePlugin, self).walk(inner_path, *args, **kwags)
|
||||||
|
|
||||||
|
def list(self, inner_path, *args, **kwags):
|
||||||
|
if ".zip" in inner_path or ".tar.gz" in inner_path:
|
||||||
|
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
|
||||||
|
archive_inner_path, path_within = match.groups()
|
||||||
|
archive = self.openArchive(archive_inner_path)
|
||||||
|
path_within = path_within.lstrip("/")
|
||||||
|
|
||||||
|
if archive_inner_path.endswith(".zip"):
|
||||||
|
namelist = [name for name in archive.namelist()]
|
||||||
|
else:
|
||||||
|
namelist = [item.name for item in archive.getmembers()]
|
||||||
|
|
||||||
|
namelist_relative = []
|
||||||
|
for name in namelist:
|
||||||
|
if not name.startswith(path_within):
|
||||||
|
continue
|
||||||
|
name_relative = name.replace(path_within, "", 1).rstrip("/")
|
||||||
|
|
||||||
|
if "/" in name_relative: # File is in sub-directory
|
||||||
|
continue
|
||||||
|
|
||||||
|
namelist_relative.append(name_relative)
|
||||||
|
return namelist_relative
|
||||||
|
|
||||||
|
else:
|
||||||
|
return super(SiteStoragePlugin, self).list(inner_path, *args, **kwags)
|
||||||
|
|
||||||
|
def read(self, inner_path, mode="rb", **kwargs):
|
||||||
|
if ".zip/" in inner_path or ".tar.gz/" in inner_path:
|
||||||
|
match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path)
|
||||||
|
archive_inner_path, path_within = match.groups()
|
||||||
|
archive = self.openArchive(archive_inner_path)
|
||||||
|
path_within = path_within.lstrip("/")
|
||||||
|
|
||||||
|
if archive_inner_path.endswith(".zip"):
|
||||||
|
return archive.open(path_within).read()
|
||||||
|
else:
|
||||||
|
return archive.extractfile(path_within).read()
|
||||||
|
|
||||||
|
else:
|
||||||
|
return super(SiteStoragePlugin, self).read(inner_path, mode, **kwargs)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from . import FilePackPlugin
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "FilePack",
|
||||||
|
"description": "Transparent web access for Zip and Tar.gz files.",
|
||||||
|
"default": "enabled"
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Db.DbQuery import DbQuery
|
||||||
|
from Debug import Debug
|
||||||
|
from util import helper
|
||||||
|
from util.Flag import flag
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiWebsocket")
|
||||||
|
class UiWebsocketPlugin(object):
|
||||||
|
def formatSiteInfo(self, site, create_user=True):
|
||||||
|
site_info = super(UiWebsocketPlugin, self).formatSiteInfo(site, create_user=create_user)
|
||||||
|
feed_following = self.user.sites.get(site.address, {}).get("follow", None)
|
||||||
|
if feed_following == None:
|
||||||
|
site_info["feed_follow_num"] = None
|
||||||
|
else:
|
||||||
|
site_info["feed_follow_num"] = len(feed_following)
|
||||||
|
return site_info
|
||||||
|
|
||||||
|
def actionFeedFollow(self, to, feeds):
|
||||||
|
self.user.setFeedFollow(self.site.address, feeds)
|
||||||
|
self.user.save()
|
||||||
|
self.response(to, "ok")
|
||||||
|
|
||||||
|
def actionFeedListFollow(self, to):
|
||||||
|
feeds = self.user.sites.get(self.site.address, {}).get("follow", {})
|
||||||
|
self.response(to, feeds)
|
||||||
|
|
||||||
|
@flag.admin
|
||||||
|
def actionFeedQuery(self, to, limit=10, day_limit=3):
|
||||||
|
from Site import SiteManager
|
||||||
|
rows = []
|
||||||
|
stats = []
|
||||||
|
|
||||||
|
total_s = time.time()
|
||||||
|
num_sites = 0
|
||||||
|
|
||||||
|
for address, site_data in list(self.user.sites.items()):
|
||||||
|
feeds = site_data.get("follow")
|
||||||
|
if not feeds:
|
||||||
|
continue
|
||||||
|
if type(feeds) is not dict:
|
||||||
|
self.log.debug("Invalid feed for site %s" % address)
|
||||||
|
continue
|
||||||
|
num_sites += 1
|
||||||
|
for name, query_set in feeds.items():
|
||||||
|
site = SiteManager.site_manager.get(address)
|
||||||
|
if not site or not site.storage.has_db:
|
||||||
|
continue
|
||||||
|
|
||||||
|
s = time.time()
|
||||||
|
try:
|
||||||
|
query_raw, params = query_set
|
||||||
|
query_parts = re.split(r"UNION(?:\s+ALL|)", query_raw)
|
||||||
|
for i, query_part in enumerate(query_parts):
|
||||||
|
db_query = DbQuery(query_part)
|
||||||
|
if day_limit:
|
||||||
|
where = " WHERE %s > strftime('%%s', 'now', '-%s day')" % (db_query.fields.get("date_added", "date_added"), day_limit)
|
||||||
|
if "WHERE" in query_part:
|
||||||
|
query_part = re.sub("WHERE (.*?)(?=$| GROUP BY)", where+" AND (\\1)", query_part)
|
||||||
|
else:
|
||||||
|
query_part += where
|
||||||
|
query_parts[i] = query_part
|
||||||
|
query = " UNION ".join(query_parts)
|
||||||
|
|
||||||
|
if ":params" in query:
|
||||||
|
query_params = map(helper.sqlquote, params)
|
||||||
|
query = query.replace(":params", ",".join(query_params))
|
||||||
|
|
||||||
|
res = site.storage.query(query + " ORDER BY date_added DESC LIMIT %s" % limit)
|
||||||
|
|
||||||
|
except Exception as err: # Log error
|
||||||
|
self.log.error("%s feed query %s error: %s" % (address, name, Debug.formatException(err)))
|
||||||
|
stats.append({"site": site.address, "feed_name": name, "error": str(err)})
|
||||||
|
continue
|
||||||
|
|
||||||
|
for row in res:
|
||||||
|
row = dict(row)
|
||||||
|
if not isinstance(row["date_added"], (int, float, complex)):
|
||||||
|
self.log.debug("Invalid date_added from site %s: %r" % (address, row["date_added"]))
|
||||||
|
continue
|
||||||
|
if row["date_added"] > 1000000000000: # Formatted as millseconds
|
||||||
|
row["date_added"] = row["date_added"] / 1000
|
||||||
|
if "date_added" not in row or row["date_added"] > time.time() + 120:
|
||||||
|
self.log.debug("Newsfeed item from the future from from site %s" % address)
|
||||||
|
continue # Feed item is in the future, skip it
|
||||||
|
row["site"] = address
|
||||||
|
row["feed_name"] = name
|
||||||
|
rows.append(row)
|
||||||
|
stats.append({"site": site.address, "feed_name": name, "taken": round(time.time() - s, 3)})
|
||||||
|
time.sleep(0.001)
|
||||||
|
return self.response(to, {"rows": rows, "stats": stats, "num": len(rows), "sites": num_sites, "taken": round(time.time() - total_s, 3)})
|
||||||
|
|
||||||
|
def parseSearch(self, search):
|
||||||
|
parts = re.split("(site|type):", search)
|
||||||
|
if len(parts) > 1: # Found filter
|
||||||
|
search_text = parts[0]
|
||||||
|
parts = [part.strip() for part in parts]
|
||||||
|
filters = dict(zip(parts[1::2], parts[2::2]))
|
||||||
|
else:
|
||||||
|
search_text = search
|
||||||
|
filters = {}
|
||||||
|
return [search_text, filters]
|
||||||
|
|
||||||
|
def actionFeedSearch(self, to, search, limit=30, day_limit=30):
|
||||||
|
if "ADMIN" not in self.site.settings["permissions"]:
|
||||||
|
return self.response(to, "FeedSearch not allowed")
|
||||||
|
|
||||||
|
from Site import SiteManager
|
||||||
|
rows = []
|
||||||
|
stats = []
|
||||||
|
num_sites = 0
|
||||||
|
total_s = time.time()
|
||||||
|
|
||||||
|
search_text, filters = self.parseSearch(search)
|
||||||
|
|
||||||
|
for address, site in SiteManager.site_manager.list().items():
|
||||||
|
if not site.storage.has_db:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "site" in filters:
|
||||||
|
if filters["site"].lower() not in [site.address, site.content_manager.contents["content.json"].get("title").lower()]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if site.storage.db: # Database loaded
|
||||||
|
feeds = site.storage.db.schema.get("feeds")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
feeds = site.storage.loadJson("dbschema.json").get("feeds")
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not feeds:
|
||||||
|
continue
|
||||||
|
|
||||||
|
num_sites += 1
|
||||||
|
|
||||||
|
for name, query in feeds.items():
|
||||||
|
s = time.time()
|
||||||
|
try:
|
||||||
|
db_query = DbQuery(query)
|
||||||
|
|
||||||
|
params = []
|
||||||
|
# Filters
|
||||||
|
if search_text:
|
||||||
|
db_query.wheres.append("(%s LIKE ? OR %s LIKE ?)" % (db_query.fields["body"], db_query.fields["title"]))
|
||||||
|
search_like = "%" + search_text.replace(" ", "%") + "%"
|
||||||
|
params.append(search_like)
|
||||||
|
params.append(search_like)
|
||||||
|
if filters.get("type") and filters["type"] not in query:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if day_limit:
|
||||||
|
db_query.wheres.append(
|
||||||
|
"%s > strftime('%%s', 'now', '-%s day')" % (db_query.fields.get("date_added", "date_added"), day_limit)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Order
|
||||||
|
db_query.parts["ORDER BY"] = "date_added DESC"
|
||||||
|
db_query.parts["LIMIT"] = str(limit)
|
||||||
|
|
||||||
|
res = site.storage.query(str(db_query), params)
|
||||||
|
except Exception as err:
|
||||||
|
self.log.error("%s feed query %s error: %s" % (address, name, Debug.formatException(err)))
|
||||||
|
stats.append({"site": site.address, "feed_name": name, "error": str(err), "query": query})
|
||||||
|
continue
|
||||||
|
for row in res:
|
||||||
|
row = dict(row)
|
||||||
|
if not row["date_added"] or row["date_added"] > time.time() + 120:
|
||||||
|
continue # Feed item is in the future, skip it
|
||||||
|
row["site"] = address
|
||||||
|
row["feed_name"] = name
|
||||||
|
rows.append(row)
|
||||||
|
stats.append({"site": site.address, "feed_name": name, "taken": round(time.time() - s, 3)})
|
||||||
|
return self.response(to, {"rows": rows, "num": len(rows), "sites": num_sites, "taken": round(time.time() - total_s, 3), "stats": stats})
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("User")
|
||||||
|
class UserPlugin(object):
|
||||||
|
# Set queries that user follows
|
||||||
|
def setFeedFollow(self, address, feeds):
|
||||||
|
site_data = self.getSiteData(address)
|
||||||
|
site_data["follow"] = feeds
|
||||||
|
self.save()
|
||||||
|
return site_data
|
|
@ -0,0 +1 @@
|
||||||
|
from . import NewsfeedPlugin
|
|
@ -0,0 +1,414 @@
|
||||||
|
import time
|
||||||
|
import collections
|
||||||
|
import itertools
|
||||||
|
import re
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
|
||||||
|
from util import helper
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Config import config
|
||||||
|
from Debug import Debug
|
||||||
|
|
||||||
|
if "content_db" not in locals().keys(): # To keep between module reloads
|
||||||
|
content_db = None
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("ContentDb")
|
||||||
|
class ContentDbPlugin(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
global content_db
|
||||||
|
content_db = self
|
||||||
|
self.filled = {} # Site addresses that already filled from content.json
|
||||||
|
self.need_filling = False # file_optional table just created, fill data from content.json files
|
||||||
|
self.time_peer_numbers_updated = 0
|
||||||
|
self.my_optional_files = {} # Last 50 site_address/inner_path called by fileWrite (auto-pinning these files)
|
||||||
|
self.optional_files = collections.defaultdict(dict)
|
||||||
|
self.optional_files_loaded = False
|
||||||
|
self.timer_check_optional = helper.timer(60 * 5, self.checkOptionalLimit)
|
||||||
|
super(ContentDbPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def getSchema(self):
|
||||||
|
schema = super(ContentDbPlugin, self).getSchema()
|
||||||
|
|
||||||
|
# Need file_optional table
|
||||||
|
schema["tables"]["file_optional"] = {
|
||||||
|
"cols": [
|
||||||
|
["file_id", "INTEGER PRIMARY KEY UNIQUE NOT NULL"],
|
||||||
|
["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
|
||||||
|
["inner_path", "TEXT"],
|
||||||
|
["hash_id", "INTEGER"],
|
||||||
|
["size", "INTEGER"],
|
||||||
|
["peer", "INTEGER DEFAULT 0"],
|
||||||
|
["uploaded", "INTEGER DEFAULT 0"],
|
||||||
|
["is_downloaded", "INTEGER DEFAULT 0"],
|
||||||
|
["is_pinned", "INTEGER DEFAULT 0"],
|
||||||
|
["time_added", "INTEGER DEFAULT 0"],
|
||||||
|
["time_downloaded", "INTEGER DEFAULT 0"],
|
||||||
|
["time_accessed", "INTEGER DEFAULT 0"]
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX file_optional_key ON file_optional (site_id, inner_path)",
|
||||||
|
"CREATE INDEX is_downloaded ON file_optional (is_downloaded)"
|
||||||
|
],
|
||||||
|
"schema_changed": 11
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def initSite(self, site):
|
||||||
|
super(ContentDbPlugin, self).initSite(site)
|
||||||
|
if self.need_filling:
|
||||||
|
self.fillTableFileOptional(site)
|
||||||
|
|
||||||
|
def checkTables(self):
|
||||||
|
changed_tables = super(ContentDbPlugin, self).checkTables()
|
||||||
|
if "file_optional" in changed_tables:
|
||||||
|
self.need_filling = True
|
||||||
|
return changed_tables
|
||||||
|
|
||||||
|
# Load optional files ending
|
||||||
|
def loadFilesOptional(self):
|
||||||
|
s = time.time()
|
||||||
|
num = 0
|
||||||
|
total = 0
|
||||||
|
total_downloaded = 0
|
||||||
|
res = content_db.execute("SELECT site_id, inner_path, size, is_downloaded FROM file_optional")
|
||||||
|
site_sizes = collections.defaultdict(lambda: collections.defaultdict(int))
|
||||||
|
for row in res:
|
||||||
|
self.optional_files[row["site_id"]][row["inner_path"][-8:]] = 1
|
||||||
|
num += 1
|
||||||
|
|
||||||
|
# Update site size stats
|
||||||
|
site_sizes[row["site_id"]]["size_optional"] += row["size"]
|
||||||
|
if row["is_downloaded"]:
|
||||||
|
site_sizes[row["site_id"]]["optional_downloaded"] += row["size"]
|
||||||
|
|
||||||
|
# Site site size stats to sites.json settings
|
||||||
|
site_ids_reverse = {val: key for key, val in self.site_ids.items()}
|
||||||
|
for site_id, stats in site_sizes.items():
|
||||||
|
site_address = site_ids_reverse.get(site_id)
|
||||||
|
if not site_address or site_address not in self.sites:
|
||||||
|
self.log.error("Not found site_id: %s" % site_id)
|
||||||
|
continue
|
||||||
|
site = self.sites[site_address]
|
||||||
|
site.settings["size_optional"] = stats["size_optional"]
|
||||||
|
site.settings["optional_downloaded"] = stats["optional_downloaded"]
|
||||||
|
total += stats["size_optional"]
|
||||||
|
total_downloaded += stats["optional_downloaded"]
|
||||||
|
|
||||||
|
self.log.info(
|
||||||
|
"Loaded %s optional files: %.2fMB, downloaded: %.2fMB in %.3fs" %
|
||||||
|
(num, float(total) / 1024 / 1024, float(total_downloaded) / 1024 / 1024, time.time() - s)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.need_filling and self.getOptionalLimitBytes() >= 0 and self.getOptionalLimitBytes() < total_downloaded:
|
||||||
|
limit_bytes = self.getOptionalLimitBytes()
|
||||||
|
limit_new = round((float(total_downloaded) / 1024 / 1024 / 1024) * 1.1, 2) # Current limit + 10%
|
||||||
|
self.log.info(
|
||||||
|
"First startup after update and limit is smaller than downloaded files size (%.2fGB), increasing it from %.2fGB to %.2fGB" %
|
||||||
|
(float(total_downloaded) / 1024 / 1024 / 1024, float(limit_bytes) / 1024 / 1024 / 1024, limit_new)
|
||||||
|
)
|
||||||
|
config.saveValue("optional_limit", limit_new)
|
||||||
|
config.optional_limit = str(limit_new)
|
||||||
|
|
||||||
|
# Predicts if the file is optional
|
||||||
|
def isOptionalFile(self, site_id, inner_path):
|
||||||
|
return self.optional_files[site_id].get(inner_path[-8:])
|
||||||
|
|
||||||
|
# Fill file_optional table with optional files found in sites
|
||||||
|
def fillTableFileOptional(self, site):
|
||||||
|
s = time.time()
|
||||||
|
site_id = self.site_ids.get(site.address)
|
||||||
|
if not site_id:
|
||||||
|
return False
|
||||||
|
cur = self.getCursor()
|
||||||
|
res = cur.execute("SELECT * FROM content WHERE size_files_optional > 0 AND site_id = %s" % site_id)
|
||||||
|
num = 0
|
||||||
|
for row in res.fetchall():
|
||||||
|
content = site.content_manager.contents[row["inner_path"]]
|
||||||
|
try:
|
||||||
|
num += self.setContentFilesOptional(site, row["inner_path"], content, cur=cur)
|
||||||
|
except Exception as err:
|
||||||
|
self.log.error("Error loading %s into file_optional: %s" % (row["inner_path"], err))
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
# Set my files to pinned
|
||||||
|
from User import UserManager
|
||||||
|
user = UserManager.user_manager.get()
|
||||||
|
if not user:
|
||||||
|
user = UserManager.user_manager.create()
|
||||||
|
auth_address = user.getAuthAddress(site.address)
|
||||||
|
res = self.execute(
|
||||||
|
"UPDATE file_optional SET is_pinned = 1 WHERE site_id = :site_id AND inner_path LIKE :inner_path",
|
||||||
|
{"site_id": site_id, "inner_path": "%%/%s/%%" % auth_address}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log.debug(
|
||||||
|
"Filled file_optional table for %s in %.3fs (loaded: %s, is_pinned: %s)" %
|
||||||
|
(site.address, time.time() - s, num, res.rowcount)
|
||||||
|
)
|
||||||
|
self.filled[site.address] = True
|
||||||
|
|
||||||
|
def setContentFilesOptional(self, site, content_inner_path, content, cur=None):
|
||||||
|
if not cur:
|
||||||
|
cur = self
|
||||||
|
|
||||||
|
num = 0
|
||||||
|
site_id = self.site_ids[site.address]
|
||||||
|
content_inner_dir = helper.getDirname(content_inner_path)
|
||||||
|
for relative_inner_path, file in content.get("files_optional", {}).items():
|
||||||
|
file_inner_path = content_inner_dir + relative_inner_path
|
||||||
|
hash_id = int(file["sha512"][0:4], 16)
|
||||||
|
if hash_id in site.content_manager.hashfield:
|
||||||
|
is_downloaded = 1
|
||||||
|
else:
|
||||||
|
is_downloaded = 0
|
||||||
|
if site.address + "/" + content_inner_dir in self.my_optional_files:
|
||||||
|
is_pinned = 1
|
||||||
|
else:
|
||||||
|
is_pinned = 0
|
||||||
|
cur.insertOrUpdate("file_optional", {
|
||||||
|
"hash_id": hash_id,
|
||||||
|
"size": int(file["size"])
|
||||||
|
}, {
|
||||||
|
"site_id": site_id,
|
||||||
|
"inner_path": file_inner_path
|
||||||
|
}, oninsert={
|
||||||
|
"time_added": int(time.time()),
|
||||||
|
"time_downloaded": int(time.time()) if is_downloaded else 0,
|
||||||
|
"is_downloaded": is_downloaded,
|
||||||
|
"peer": is_downloaded,
|
||||||
|
"is_pinned": is_pinned
|
||||||
|
})
|
||||||
|
self.optional_files[site_id][file_inner_path[-8:]] = 1
|
||||||
|
num += 1
|
||||||
|
|
||||||
|
return num
|
||||||
|
|
||||||
|
def setContent(self, site, inner_path, content, size=0):
|
||||||
|
super(ContentDbPlugin, self).setContent(site, inner_path, content, size=size)
|
||||||
|
old_content = site.content_manager.contents.get(inner_path, {})
|
||||||
|
if (not self.need_filling or self.filled.get(site.address)) and ("files_optional" in content or "files_optional" in old_content):
|
||||||
|
self.setContentFilesOptional(site, inner_path, content)
|
||||||
|
# Check deleted files
|
||||||
|
if old_content:
|
||||||
|
old_files = old_content.get("files_optional", {}).keys()
|
||||||
|
new_files = content.get("files_optional", {}).keys()
|
||||||
|
content_inner_dir = helper.getDirname(inner_path)
|
||||||
|
deleted = [content_inner_dir + key for key in old_files if key not in new_files]
|
||||||
|
if deleted:
|
||||||
|
site_id = self.site_ids[site.address]
|
||||||
|
self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": deleted})
|
||||||
|
|
||||||
|
def deleteContent(self, site, inner_path):
|
||||||
|
content = site.content_manager.contents.get(inner_path)
|
||||||
|
if content and "files_optional" in content:
|
||||||
|
site_id = self.site_ids[site.address]
|
||||||
|
content_inner_dir = helper.getDirname(inner_path)
|
||||||
|
optional_inner_paths = [
|
||||||
|
content_inner_dir + relative_inner_path
|
||||||
|
for relative_inner_path in content.get("files_optional", {}).keys()
|
||||||
|
]
|
||||||
|
self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": optional_inner_paths})
|
||||||
|
super(ContentDbPlugin, self).deleteContent(site, inner_path)
|
||||||
|
|
||||||
|
def updatePeerNumbers(self):
|
||||||
|
s = time.time()
|
||||||
|
num_file = 0
|
||||||
|
num_updated = 0
|
||||||
|
num_site = 0
|
||||||
|
for site in list(self.sites.values()):
|
||||||
|
if not site.content_manager.has_optional_files:
|
||||||
|
continue
|
||||||
|
if not site.isServing():
|
||||||
|
continue
|
||||||
|
has_updated_hashfield = next((
|
||||||
|
peer
|
||||||
|
for peer in site.peers.values()
|
||||||
|
if peer.has_hashfield and peer.hashfield.time_changed > self.time_peer_numbers_updated
|
||||||
|
), None)
|
||||||
|
|
||||||
|
if not has_updated_hashfield and site.content_manager.hashfield.time_changed < self.time_peer_numbers_updated:
|
||||||
|
continue
|
||||||
|
|
||||||
|
hashfield_peers = itertools.chain.from_iterable(
|
||||||
|
peer.hashfield.storage
|
||||||
|
for peer in site.peers.values()
|
||||||
|
if peer.has_hashfield
|
||||||
|
)
|
||||||
|
peer_nums = collections.Counter(
|
||||||
|
itertools.chain(
|
||||||
|
hashfield_peers,
|
||||||
|
site.content_manager.hashfield
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
site_id = self.site_ids[site.address]
|
||||||
|
if not site_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
res = self.execute("SELECT file_id, hash_id, peer FROM file_optional WHERE ?", {"site_id": site_id})
|
||||||
|
updates = {}
|
||||||
|
for row in res:
|
||||||
|
peer_num = peer_nums.get(row["hash_id"], 0)
|
||||||
|
if peer_num != row["peer"]:
|
||||||
|
updates[row["file_id"]] = peer_num
|
||||||
|
|
||||||
|
for file_id, peer_num in updates.items():
|
||||||
|
self.execute("UPDATE file_optional SET peer = ? WHERE file_id = ?", (peer_num, file_id))
|
||||||
|
|
||||||
|
num_updated += len(updates)
|
||||||
|
num_file += len(peer_nums)
|
||||||
|
num_site += 1
|
||||||
|
|
||||||
|
self.time_peer_numbers_updated = time.time()
|
||||||
|
self.log.debug("%s/%s peer number for %s site updated in %.3fs" % (num_updated, num_file, num_site, time.time() - s))
|
||||||
|
|
||||||
|
def queryDeletableFiles(self):
|
||||||
|
# First return the files with atleast 10 seeder and not accessed in last week
|
||||||
|
query = """
|
||||||
|
SELECT * FROM file_optional
|
||||||
|
WHERE peer > 10 AND %s
|
||||||
|
ORDER BY time_accessed < %s DESC, uploaded / size
|
||||||
|
""" % (self.getOptionalUsedWhere(), int(time.time() - 60 * 60 * 7))
|
||||||
|
limit_start = 0
|
||||||
|
while 1:
|
||||||
|
num = 0
|
||||||
|
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
|
||||||
|
for row in res:
|
||||||
|
yield row
|
||||||
|
num += 1
|
||||||
|
if num < 50:
|
||||||
|
break
|
||||||
|
limit_start += 50
|
||||||
|
|
||||||
|
self.log.debug("queryDeletableFiles returning less-seeded files")
|
||||||
|
|
||||||
|
# Then return files less seeder but still not accessed in last week
|
||||||
|
query = """
|
||||||
|
SELECT * FROM file_optional
|
||||||
|
WHERE peer <= 10 AND %s
|
||||||
|
ORDER BY peer DESC, time_accessed < %s DESC, uploaded / size
|
||||||
|
""" % (self.getOptionalUsedWhere(), int(time.time() - 60 * 60 * 7))
|
||||||
|
limit_start = 0
|
||||||
|
while 1:
|
||||||
|
num = 0
|
||||||
|
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
|
||||||
|
for row in res:
|
||||||
|
yield row
|
||||||
|
num += 1
|
||||||
|
if num < 50:
|
||||||
|
break
|
||||||
|
limit_start += 50
|
||||||
|
|
||||||
|
self.log.debug("queryDeletableFiles returning everyting")
|
||||||
|
|
||||||
|
# At the end return all files
|
||||||
|
query = """
|
||||||
|
SELECT * FROM file_optional
|
||||||
|
WHERE peer <= 10 AND %s
|
||||||
|
ORDER BY peer DESC, time_accessed, uploaded / size
|
||||||
|
""" % self.getOptionalUsedWhere()
|
||||||
|
limit_start = 0
|
||||||
|
while 1:
|
||||||
|
num = 0
|
||||||
|
res = self.execute("%s LIMIT %s, 50" % (query, limit_start))
|
||||||
|
for row in res:
|
||||||
|
yield row
|
||||||
|
num += 1
|
||||||
|
if num < 50:
|
||||||
|
break
|
||||||
|
limit_start += 50
|
||||||
|
|
||||||
|
def getOptionalLimitBytes(self):
|
||||||
|
if config.optional_limit.endswith("%"):
|
||||||
|
limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit))
|
||||||
|
limit_bytes = helper.getFreeSpace() * (limit_percent / 100)
|
||||||
|
else:
|
||||||
|
limit_bytes = float(re.sub("[^0-9.]", "", config.optional_limit)) * 1024 * 1024 * 1024
|
||||||
|
return limit_bytes
|
||||||
|
|
||||||
|
def getOptionalUsedWhere(self):
|
||||||
|
maxsize = config.optional_limit_exclude_minsize * 1024 * 1024
|
||||||
|
query = "is_downloaded = 1 AND is_pinned = 0 AND size < %s" % maxsize
|
||||||
|
|
||||||
|
# Don't delete optional files from owned sites
|
||||||
|
my_site_ids = []
|
||||||
|
for address, site in self.sites.items():
|
||||||
|
if site.settings["own"]:
|
||||||
|
my_site_ids.append(str(self.site_ids[address]))
|
||||||
|
|
||||||
|
if my_site_ids:
|
||||||
|
query += " AND site_id NOT IN (%s)" % ", ".join(my_site_ids)
|
||||||
|
return query
|
||||||
|
|
||||||
|
def getOptionalUsedBytes(self):
|
||||||
|
size = self.execute("SELECT SUM(size) FROM file_optional WHERE %s" % self.getOptionalUsedWhere()).fetchone()[0]
|
||||||
|
if not size:
|
||||||
|
size = 0
|
||||||
|
return size
|
||||||
|
|
||||||
|
def getOptionalNeedDelete(self, size):
|
||||||
|
if config.optional_limit.endswith("%"):
|
||||||
|
limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit))
|
||||||
|
need_delete = size - ((helper.getFreeSpace() + size) * (limit_percent / 100))
|
||||||
|
else:
|
||||||
|
need_delete = size - self.getOptionalLimitBytes()
|
||||||
|
return need_delete
|
||||||
|
|
||||||
|
def checkOptionalLimit(self, limit=None):
|
||||||
|
if not limit:
|
||||||
|
limit = self.getOptionalLimitBytes()
|
||||||
|
|
||||||
|
if limit < 0:
|
||||||
|
self.log.debug("Invalid limit for optional files: %s" % limit)
|
||||||
|
return False
|
||||||
|
|
||||||
|
size = self.getOptionalUsedBytes()
|
||||||
|
|
||||||
|
need_delete = self.getOptionalNeedDelete(size)
|
||||||
|
|
||||||
|
self.log.debug(
|
||||||
|
"Optional size: %.1fMB/%.1fMB, Need delete: %.1fMB" %
|
||||||
|
(float(size) / 1024 / 1024, float(limit) / 1024 / 1024, float(need_delete) / 1024 / 1024)
|
||||||
|
)
|
||||||
|
if need_delete <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.updatePeerNumbers()
|
||||||
|
|
||||||
|
site_ids_reverse = {val: key for key, val in self.site_ids.items()}
|
||||||
|
deleted_file_ids = []
|
||||||
|
for row in self.queryDeletableFiles():
|
||||||
|
site_address = site_ids_reverse.get(row["site_id"])
|
||||||
|
site = self.sites.get(site_address)
|
||||||
|
if not site:
|
||||||
|
self.log.error("No site found for id: %s" % row["site_id"])
|
||||||
|
continue
|
||||||
|
site.log.debug("Deleting %s %.3f MB left" % (row["inner_path"], float(need_delete) / 1024 / 1024))
|
||||||
|
deleted_file_ids.append(row["file_id"])
|
||||||
|
try:
|
||||||
|
site.content_manager.optionalRemoved(row["inner_path"], row["hash_id"], row["size"])
|
||||||
|
site.storage.delete(row["inner_path"])
|
||||||
|
need_delete -= row["size"]
|
||||||
|
except Exception as err:
|
||||||
|
site.log.error("Error deleting %s: %s" % (row["inner_path"], err))
|
||||||
|
|
||||||
|
if need_delete <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
cur = self.getCursor()
|
||||||
|
for file_id in deleted_file_ids:
|
||||||
|
cur.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"file_id": file_id})
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("SiteManager")
|
||||||
|
class SiteManagerPlugin(object):
|
||||||
|
def load(self, *args, **kwargs):
|
||||||
|
back = super(SiteManagerPlugin, self).load(*args, **kwargs)
|
||||||
|
if self.sites and not content_db.optional_files_loaded and content_db.conn:
|
||||||
|
content_db.optional_files_loaded = True
|
||||||
|
content_db.loadFilesOptional()
|
||||||
|
return back
|
|
@ -0,0 +1,253 @@
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import collections
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
|
||||||
|
from util import helper
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from . import ContentDbPlugin
|
||||||
|
|
||||||
|
|
||||||
|
# We can only import plugin host clases after the plugins are loaded
|
||||||
|
@PluginManager.afterLoad
|
||||||
|
def importPluginnedClasses():
|
||||||
|
global config
|
||||||
|
from Config import config
|
||||||
|
|
||||||
|
|
||||||
|
def processAccessLog():
|
||||||
|
global access_log
|
||||||
|
if access_log:
|
||||||
|
content_db = ContentDbPlugin.content_db
|
||||||
|
if not content_db.conn:
|
||||||
|
return False
|
||||||
|
|
||||||
|
s = time.time()
|
||||||
|
access_log_prev = access_log
|
||||||
|
access_log = collections.defaultdict(dict)
|
||||||
|
now = int(time.time())
|
||||||
|
num = 0
|
||||||
|
for site_id in access_log_prev:
|
||||||
|
content_db.execute(
|
||||||
|
"UPDATE file_optional SET time_accessed = %s WHERE ?" % now,
|
||||||
|
{"site_id": site_id, "inner_path": list(access_log_prev[site_id].keys())}
|
||||||
|
)
|
||||||
|
num += len(access_log_prev[site_id])
|
||||||
|
|
||||||
|
content_db.log.debug("Inserted %s web request stat in %.3fs" % (num, time.time() - s))
|
||||||
|
|
||||||
|
|
||||||
|
def processRequestLog():
|
||||||
|
global request_log
|
||||||
|
if request_log:
|
||||||
|
content_db = ContentDbPlugin.content_db
|
||||||
|
if not content_db.conn:
|
||||||
|
return False
|
||||||
|
|
||||||
|
s = time.time()
|
||||||
|
request_log_prev = request_log
|
||||||
|
request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}}
|
||||||
|
num = 0
|
||||||
|
for site_id in request_log_prev:
|
||||||
|
for inner_path, uploaded in request_log_prev[site_id].items():
|
||||||
|
content_db.execute(
|
||||||
|
"UPDATE file_optional SET uploaded = uploaded + %s WHERE ?" % uploaded,
|
||||||
|
{"site_id": site_id, "inner_path": inner_path}
|
||||||
|
)
|
||||||
|
num += 1
|
||||||
|
content_db.log.debug("Inserted %s file request stat in %.3fs" % (num, time.time() - s))
|
||||||
|
|
||||||
|
|
||||||
|
if "access_log" not in locals().keys(): # To keep between module reloads
|
||||||
|
access_log = collections.defaultdict(dict) # {site_id: {inner_path1: 1, inner_path2: 1...}}
|
||||||
|
request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}}
|
||||||
|
helper.timer(61, processAccessLog)
|
||||||
|
helper.timer(60, processRequestLog)
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("ContentManager")
|
||||||
|
class ContentManagerPlugin(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.cache_is_pinned = {}
|
||||||
|
super(ContentManagerPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def optionalDownloaded(self, inner_path, hash_id, size=None, own=False):
|
||||||
|
if "|" in inner_path: # Big file piece
|
||||||
|
file_inner_path, file_range = inner_path.split("|")
|
||||||
|
else:
|
||||||
|
file_inner_path = inner_path
|
||||||
|
|
||||||
|
self.contents.db.executeDelayed(
|
||||||
|
"UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 0",
|
||||||
|
{"now": int(time.time()), "site_id": self.contents.db.site_ids[self.site.address], "inner_path": file_inner_path}
|
||||||
|
)
|
||||||
|
|
||||||
|
return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own)
|
||||||
|
|
||||||
|
def optionalRemoved(self, inner_path, hash_id, size=None):
|
||||||
|
res = self.contents.db.execute(
|
||||||
|
"UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 1",
|
||||||
|
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.rowcount > 0:
|
||||||
|
back = super(ContentManagerPlugin, self).optionalRemoved(inner_path, hash_id, size)
|
||||||
|
# Re-add to hashfield if we have other file with the same hash_id
|
||||||
|
if self.isDownloaded(hash_id=hash_id, force_check_db=True):
|
||||||
|
self.hashfield.appendHashId(hash_id)
|
||||||
|
else:
|
||||||
|
back = False
|
||||||
|
self.cache_is_pinned = {}
|
||||||
|
return back
|
||||||
|
|
||||||
|
def optionalRenamed(self, inner_path_old, inner_path_new):
|
||||||
|
back = super(ContentManagerPlugin, self).optionalRenamed(inner_path_old, inner_path_new)
|
||||||
|
self.cache_is_pinned = {}
|
||||||
|
self.contents.db.execute(
|
||||||
|
"UPDATE file_optional SET inner_path = :inner_path_new WHERE site_id = :site_id AND inner_path = :inner_path_old",
|
||||||
|
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path_old": inner_path_old, "inner_path_new": inner_path_new}
|
||||||
|
)
|
||||||
|
return back
|
||||||
|
|
||||||
|
def isDownloaded(self, inner_path=None, hash_id=None, force_check_db=False):
|
||||||
|
if hash_id and not force_check_db and hash_id not in self.hashfield:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if inner_path:
|
||||||
|
res = self.contents.db.execute(
|
||||||
|
"SELECT is_downloaded FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1",
|
||||||
|
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
res = self.contents.db.execute(
|
||||||
|
"SELECT is_downloaded FROM file_optional WHERE site_id = :site_id AND hash_id = :hash_id AND is_downloaded = 1 LIMIT 1",
|
||||||
|
{"site_id": self.contents.db.site_ids[self.site.address], "hash_id": hash_id}
|
||||||
|
)
|
||||||
|
row = res.fetchone()
|
||||||
|
if row and row["is_downloaded"]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isPinned(self, inner_path):
|
||||||
|
if inner_path in self.cache_is_pinned:
|
||||||
|
self.site.log.debug("Cached is pinned: %s" % inner_path)
|
||||||
|
return self.cache_is_pinned[inner_path]
|
||||||
|
|
||||||
|
res = self.contents.db.execute(
|
||||||
|
"SELECT is_pinned FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1",
|
||||||
|
{"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path}
|
||||||
|
)
|
||||||
|
row = res.fetchone()
|
||||||
|
|
||||||
|
if row and row[0]:
|
||||||
|
is_pinned = True
|
||||||
|
else:
|
||||||
|
is_pinned = False
|
||||||
|
|
||||||
|
self.cache_is_pinned[inner_path] = is_pinned
|
||||||
|
self.site.log.debug("Cache set is pinned: %s %s" % (inner_path, is_pinned))
|
||||||
|
|
||||||
|
return is_pinned
|
||||||
|
|
||||||
|
def setPin(self, inner_path, is_pinned):
|
||||||
|
content_db = self.contents.db
|
||||||
|
site_id = content_db.site_ids[self.site.address]
|
||||||
|
content_db.execute("UPDATE file_optional SET is_pinned = %d WHERE ?" % is_pinned, {"site_id": site_id, "inner_path": inner_path})
|
||||||
|
self.cache_is_pinned = {}
|
||||||
|
|
||||||
|
def optionalDelete(self, inner_path):
|
||||||
|
if self.isPinned(inner_path):
|
||||||
|
self.site.log.debug("Skip deleting pinned optional file: %s" % inner_path)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return super(ContentManagerPlugin, self).optionalDelete(inner_path)
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("WorkerManager")
|
||||||
|
class WorkerManagerPlugin(object):
|
||||||
|
def doneTask(self, task):
|
||||||
|
super(WorkerManagerPlugin, self).doneTask(task)
|
||||||
|
|
||||||
|
if task["optional_hash_id"] and not self.tasks: # Execute delayed queries immedietly after tasks finished
|
||||||
|
ContentDbPlugin.content_db.processDelayed()
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiRequest")
|
||||||
|
class UiRequestPlugin(object):
|
||||||
|
def parsePath(self, path):
|
||||||
|
global access_log
|
||||||
|
path_parts = super(UiRequestPlugin, self).parsePath(path)
|
||||||
|
if path_parts:
|
||||||
|
site_id = ContentDbPlugin.content_db.site_ids.get(path_parts["request_address"])
|
||||||
|
if site_id:
|
||||||
|
if ContentDbPlugin.content_db.isOptionalFile(site_id, path_parts["inner_path"]):
|
||||||
|
access_log[site_id][path_parts["inner_path"]] = 1
|
||||||
|
return path_parts
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("FileRequest")
|
||||||
|
class FileRequestPlugin(object):
|
||||||
|
def actionGetFile(self, params):
|
||||||
|
stats = super(FileRequestPlugin, self).actionGetFile(params)
|
||||||
|
self.recordFileRequest(params["site"], params["inner_path"], stats)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def actionStreamFile(self, params):
|
||||||
|
stats = super(FileRequestPlugin, self).actionStreamFile(params)
|
||||||
|
self.recordFileRequest(params["site"], params["inner_path"], stats)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def recordFileRequest(self, site_address, inner_path, stats):
|
||||||
|
if not stats:
|
||||||
|
# Only track the last request of files
|
||||||
|
return False
|
||||||
|
site_id = ContentDbPlugin.content_db.site_ids[site_address]
|
||||||
|
if site_id and ContentDbPlugin.content_db.isOptionalFile(site_id, inner_path):
|
||||||
|
request_log[site_id][inner_path] += stats["bytes_sent"]
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("Site")
|
||||||
|
class SitePlugin(object):
|
||||||
|
def isDownloadable(self, inner_path):
|
||||||
|
is_downloadable = super(SitePlugin, self).isDownloadable(inner_path)
|
||||||
|
if is_downloadable:
|
||||||
|
return is_downloadable
|
||||||
|
|
||||||
|
for path in self.settings.get("optional_help", {}).keys():
|
||||||
|
if inner_path.startswith(path):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fileForgot(self, inner_path):
|
||||||
|
if "|" in inner_path and self.content_manager.isPinned(re.sub(r"\|.*", "", inner_path)):
|
||||||
|
self.log.debug("File %s is pinned, no fileForgot" % inner_path)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return super(SitePlugin, self).fileForgot(inner_path)
|
||||||
|
|
||||||
|
def fileDone(self, inner_path):
|
||||||
|
if "|" in inner_path and self.bad_files.get(inner_path, 0) > 5: # Idle optional file done
|
||||||
|
inner_path_file = re.sub(r"\|.*", "", inner_path)
|
||||||
|
num_changed = 0
|
||||||
|
for key, val in self.bad_files.items():
|
||||||
|
if key.startswith(inner_path_file) and val > 1:
|
||||||
|
self.bad_files[key] = 1
|
||||||
|
num_changed += 1
|
||||||
|
self.log.debug("Idle optional file piece done, changed retry number of %s pieces." % num_changed)
|
||||||
|
if num_changed:
|
||||||
|
gevent.spawn(self.retryBadFiles)
|
||||||
|
|
||||||
|
return super(SitePlugin, self).fileDone(inner_path)
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("ConfigPlugin")
|
||||||
|
class ConfigPlugin(object):
|
||||||
|
def createArguments(self):
|
||||||
|
group = self.parser.add_argument_group("OptionalManager plugin")
|
||||||
|
group.add_argument('--optional_limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %")
|
||||||
|
group.add_argument('--optional_limit_exclude_minsize', help='Exclude files larger than this limit from optional size limit calculation', default=20, metavar="MB", type=int)
|
||||||
|
|
||||||
|
return super(ConfigPlugin, self).createArguments()
|
|
@ -0,0 +1,158 @@
|
||||||
|
import copy
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("resetSettings")
|
||||||
|
class TestOptionalManager:
|
||||||
|
def testDbFill(self, site):
|
||||||
|
contents = site.content_manager.contents
|
||||||
|
assert len(site.content_manager.hashfield) > 0
|
||||||
|
assert contents.db.execute("SELECT COUNT(*) FROM file_optional WHERE is_downloaded = 1").fetchone()[0] == len(site.content_manager.hashfield)
|
||||||
|
|
||||||
|
def testSetContent(self, site):
|
||||||
|
contents = site.content_manager.contents
|
||||||
|
|
||||||
|
# Add new file
|
||||||
|
new_content = copy.deepcopy(contents["content.json"])
|
||||||
|
new_content["files_optional"]["testfile"] = {
|
||||||
|
"size": 1234,
|
||||||
|
"sha512": "aaaabbbbcccc"
|
||||||
|
}
|
||||||
|
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
|
||||||
|
contents["content.json"] = new_content
|
||||||
|
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] > num_optional_files_before
|
||||||
|
|
||||||
|
# Remove file
|
||||||
|
new_content = copy.deepcopy(contents["content.json"])
|
||||||
|
del new_content["files_optional"]["testfile"]
|
||||||
|
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
|
||||||
|
contents["content.json"] = new_content
|
||||||
|
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before
|
||||||
|
|
||||||
|
def testDeleteContent(self, site):
|
||||||
|
contents = site.content_manager.contents
|
||||||
|
num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0]
|
||||||
|
del contents["content.json"]
|
||||||
|
assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before
|
||||||
|
|
||||||
|
def testVerifyFiles(self, site):
|
||||||
|
contents = site.content_manager.contents
|
||||||
|
|
||||||
|
# Add new file
|
||||||
|
new_content = copy.deepcopy(contents["content.json"])
|
||||||
|
new_content["files_optional"]["testfile"] = {
|
||||||
|
"size": 1234,
|
||||||
|
"sha512": "aaaabbbbcccc"
|
||||||
|
}
|
||||||
|
contents["content.json"] = new_content
|
||||||
|
file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone()
|
||||||
|
assert not file_row["is_downloaded"]
|
||||||
|
|
||||||
|
# Write file from outside of ZeroNet
|
||||||
|
site.storage.open("testfile", "wb").write(b"A" * 1234) # For quick check hash does not matter only file size
|
||||||
|
|
||||||
|
hashfield_len_before = len(site.content_manager.hashfield)
|
||||||
|
site.storage.verifyFiles(quick_check=True)
|
||||||
|
assert len(site.content_manager.hashfield) == hashfield_len_before + 1
|
||||||
|
|
||||||
|
file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone()
|
||||||
|
assert file_row["is_downloaded"]
|
||||||
|
|
||||||
|
# Delete file outside of ZeroNet
|
||||||
|
site.storage.delete("testfile")
|
||||||
|
site.storage.verifyFiles(quick_check=True)
|
||||||
|
file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone()
|
||||||
|
assert not file_row["is_downloaded"]
|
||||||
|
|
||||||
|
def testVerifyFilesSameHashId(self, site):
|
||||||
|
contents = site.content_manager.contents
|
||||||
|
|
||||||
|
new_content = copy.deepcopy(contents["content.json"])
|
||||||
|
|
||||||
|
# Add two files with same hashid (first 4 character)
|
||||||
|
new_content["files_optional"]["testfile1"] = {
|
||||||
|
"size": 1234,
|
||||||
|
"sha512": "aaaabbbbcccc"
|
||||||
|
}
|
||||||
|
new_content["files_optional"]["testfile2"] = {
|
||||||
|
"size": 2345,
|
||||||
|
"sha512": "aaaabbbbdddd"
|
||||||
|
}
|
||||||
|
contents["content.json"] = new_content
|
||||||
|
|
||||||
|
assert site.content_manager.hashfield.getHashId("aaaabbbbcccc") == site.content_manager.hashfield.getHashId("aaaabbbbdddd")
|
||||||
|
|
||||||
|
# Write files from outside of ZeroNet (For quick check hash does not matter only file size)
|
||||||
|
site.storage.open("testfile1", "wb").write(b"A" * 1234)
|
||||||
|
site.storage.open("testfile2", "wb").write(b"B" * 2345)
|
||||||
|
|
||||||
|
site.storage.verifyFiles(quick_check=True)
|
||||||
|
|
||||||
|
# Make sure that both is downloaded
|
||||||
|
assert site.content_manager.isDownloaded("testfile1")
|
||||||
|
assert site.content_manager.isDownloaded("testfile2")
|
||||||
|
assert site.content_manager.hashfield.getHashId("aaaabbbbcccc") in site.content_manager.hashfield
|
||||||
|
|
||||||
|
# Delete one of the files
|
||||||
|
site.storage.delete("testfile1")
|
||||||
|
site.storage.verifyFiles(quick_check=True)
|
||||||
|
assert not site.content_manager.isDownloaded("testfile1")
|
||||||
|
assert site.content_manager.isDownloaded("testfile2")
|
||||||
|
assert site.content_manager.hashfield.getHashId("aaaabbbbdddd") in site.content_manager.hashfield
|
||||||
|
|
||||||
|
def testIsPinned(self, site):
|
||||||
|
assert not site.content_manager.isPinned("data/img/zerotalk-upvote.png")
|
||||||
|
site.content_manager.setPin("data/img/zerotalk-upvote.png", True)
|
||||||
|
assert site.content_manager.isPinned("data/img/zerotalk-upvote.png")
|
||||||
|
|
||||||
|
assert len(site.content_manager.cache_is_pinned) == 1
|
||||||
|
site.content_manager.cache_is_pinned = {}
|
||||||
|
assert site.content_manager.isPinned("data/img/zerotalk-upvote.png")
|
||||||
|
|
||||||
|
def testBigfilePieceReset(self, site):
|
||||||
|
site.bad_files = {
|
||||||
|
"data/fake_bigfile.mp4|0-1024": 10,
|
||||||
|
"data/fake_bigfile.mp4|1024-2048": 10,
|
||||||
|
"data/fake_bigfile.mp4|2048-3064": 10
|
||||||
|
}
|
||||||
|
site.onFileDone("data/fake_bigfile.mp4|0-1024")
|
||||||
|
assert site.bad_files["data/fake_bigfile.mp4|1024-2048"] == 1
|
||||||
|
assert site.bad_files["data/fake_bigfile.mp4|2048-3064"] == 1
|
||||||
|
|
||||||
|
def testOptionalDelete(self, site):
|
||||||
|
contents = site.content_manager.contents
|
||||||
|
|
||||||
|
site.content_manager.setPin("data/img/zerotalk-upvote.png", True)
|
||||||
|
site.content_manager.setPin("data/img/zeroid.png", False)
|
||||||
|
new_content = copy.deepcopy(contents["content.json"])
|
||||||
|
del new_content["files_optional"]["data/img/zerotalk-upvote.png"]
|
||||||
|
del new_content["files_optional"]["data/img/zeroid.png"]
|
||||||
|
|
||||||
|
assert site.storage.isFile("data/img/zerotalk-upvote.png")
|
||||||
|
assert site.storage.isFile("data/img/zeroid.png")
|
||||||
|
|
||||||
|
site.storage.writeJson("content.json", new_content)
|
||||||
|
site.content_manager.loadContent("content.json", force=True)
|
||||||
|
|
||||||
|
assert not site.storage.isFile("data/img/zeroid.png")
|
||||||
|
assert site.storage.isFile("data/img/zerotalk-upvote.png")
|
||||||
|
|
||||||
|
def testOptionalRename(self, site):
|
||||||
|
contents = site.content_manager.contents
|
||||||
|
|
||||||
|
site.content_manager.setPin("data/img/zerotalk-upvote.png", True)
|
||||||
|
new_content = copy.deepcopy(contents["content.json"])
|
||||||
|
new_content["files_optional"]["data/img/zerotalk-upvote-new.png"] = new_content["files_optional"]["data/img/zerotalk-upvote.png"]
|
||||||
|
del new_content["files_optional"]["data/img/zerotalk-upvote.png"]
|
||||||
|
|
||||||
|
assert site.storage.isFile("data/img/zerotalk-upvote.png")
|
||||||
|
assert site.content_manager.isPinned("data/img/zerotalk-upvote.png")
|
||||||
|
|
||||||
|
site.storage.writeJson("content.json", new_content)
|
||||||
|
site.content_manager.loadContent("content.json", force=True)
|
||||||
|
|
||||||
|
assert not site.storage.isFile("data/img/zerotalk-upvote.png")
|
||||||
|
assert not site.content_manager.isPinned("data/img/zerotalk-upvote.png")
|
||||||
|
assert site.content_manager.isPinned("data/img/zerotalk-upvote-new.png")
|
||||||
|
assert site.storage.isFile("data/img/zerotalk-upvote-new.png")
|
|
@ -0,0 +1 @@
|
||||||
|
from src.Test.conftest import *
|
|
@ -0,0 +1,5 @@
|
||||||
|
[pytest]
|
||||||
|
python_files = Test*.py
|
||||||
|
addopts = -rsxX -v --durations=6
|
||||||
|
markers =
|
||||||
|
webtest: mark a test as a webtest.
|
|
@ -0,0 +1,396 @@
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import html
|
||||||
|
import os
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Config import config
|
||||||
|
from util import helper
|
||||||
|
from util.Flag import flag
|
||||||
|
from Translate import Translate
|
||||||
|
|
||||||
|
|
||||||
|
plugin_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
if "_" not in locals():
|
||||||
|
_ = Translate(plugin_dir + "/languages/")
|
||||||
|
|
||||||
|
bigfile_sha512_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiWebsocket")
|
||||||
|
class UiWebsocketPlugin(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.time_peer_numbers_updated = 0
|
||||||
|
super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
|
||||||
|
# Add file to content.db and set it as pinned
|
||||||
|
content_db = self.site.content_manager.contents.db
|
||||||
|
content_inner_dir = helper.getDirname(inner_path)
|
||||||
|
content_db.my_optional_files[self.site.address + "/" + content_inner_dir] = time.time()
|
||||||
|
if len(content_db.my_optional_files) > 50: # Keep only last 50
|
||||||
|
oldest_key = min(
|
||||||
|
iter(content_db.my_optional_files.keys()),
|
||||||
|
key=(lambda key: content_db.my_optional_files[key])
|
||||||
|
)
|
||||||
|
del content_db.my_optional_files[oldest_key]
|
||||||
|
|
||||||
|
return super(UiWebsocketPlugin, self).actionSiteSign(to, privatekey, inner_path, *args, **kwargs)
|
||||||
|
|
||||||
|
def updatePeerNumbers(self):
|
||||||
|
self.site.updateHashfield()
|
||||||
|
content_db = self.site.content_manager.contents.db
|
||||||
|
content_db.updatePeerNumbers()
|
||||||
|
self.site.updateWebsocket(peernumber_updated=True)
|
||||||
|
|
||||||
|
def addBigfileInfo(self, row):
|
||||||
|
global bigfile_sha512_cache
|
||||||
|
|
||||||
|
content_db = self.site.content_manager.contents.db
|
||||||
|
site = content_db.sites[row["address"]]
|
||||||
|
if not site.settings.get("has_bigfile"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
file_key = row["address"] + "/" + row["inner_path"]
|
||||||
|
sha512 = bigfile_sha512_cache.get(file_key)
|
||||||
|
file_info = None
|
||||||
|
if not sha512:
|
||||||
|
file_info = site.content_manager.getFileInfo(row["inner_path"])
|
||||||
|
if not file_info or not file_info.get("piece_size"):
|
||||||
|
return False
|
||||||
|
sha512 = file_info["sha512"]
|
||||||
|
bigfile_sha512_cache[file_key] = sha512
|
||||||
|
|
||||||
|
if sha512 in site.storage.piecefields:
|
||||||
|
piecefield = site.storage.piecefields[sha512].tobytes()
|
||||||
|
else:
|
||||||
|
piecefield = None
|
||||||
|
|
||||||
|
if piecefield:
|
||||||
|
row["pieces"] = len(piecefield)
|
||||||
|
row["pieces_downloaded"] = piecefield.count(b"\x01")
|
||||||
|
row["downloaded_percent"] = 100 * row["pieces_downloaded"] / row["pieces"]
|
||||||
|
if row["pieces_downloaded"]:
|
||||||
|
if row["pieces"] == row["pieces_downloaded"]:
|
||||||
|
row["bytes_downloaded"] = row["size"]
|
||||||
|
else:
|
||||||
|
if not file_info:
|
||||||
|
file_info = site.content_manager.getFileInfo(row["inner_path"])
|
||||||
|
row["bytes_downloaded"] = row["pieces_downloaded"] * file_info.get("piece_size", 0)
|
||||||
|
else:
|
||||||
|
row["bytes_downloaded"] = 0
|
||||||
|
|
||||||
|
row["is_downloading"] = bool(next((inner_path for inner_path in site.bad_files if inner_path.startswith(row["inner_path"])), False))
|
||||||
|
|
||||||
|
# Add leech / seed stats
|
||||||
|
row["peer_seed"] = 0
|
||||||
|
row["peer_leech"] = 0
|
||||||
|
for peer in site.peers.values():
|
||||||
|
if not peer.time_piecefields_updated or sha512 not in peer.piecefields:
|
||||||
|
continue
|
||||||
|
peer_piecefield = peer.piecefields[sha512].tobytes()
|
||||||
|
if not peer_piecefield:
|
||||||
|
continue
|
||||||
|
if peer_piecefield == b"\x01" * len(peer_piecefield):
|
||||||
|
row["peer_seed"] += 1
|
||||||
|
else:
|
||||||
|
row["peer_leech"] += 1
|
||||||
|
|
||||||
|
# Add myself
|
||||||
|
if piecefield:
|
||||||
|
if row["pieces_downloaded"] == row["pieces"]:
|
||||||
|
row["peer_seed"] += 1
|
||||||
|
else:
|
||||||
|
row["peer_leech"] += 1
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Optional file functions
|
||||||
|
|
||||||
|
def actionOptionalFileList(self, to, address=None, orderby="time_downloaded DESC", limit=10, filter="downloaded", filter_inner_path=None):
|
||||||
|
if not address:
|
||||||
|
address = self.site.address
|
||||||
|
|
||||||
|
# Update peer numbers if necessary
|
||||||
|
content_db = self.site.content_manager.contents.db
|
||||||
|
if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5:
|
||||||
|
# Start in new thread to avoid blocking
|
||||||
|
self.time_peer_numbers_updated = time.time()
|
||||||
|
gevent.spawn(self.updatePeerNumbers)
|
||||||
|
|
||||||
|
if address == "all" and "ADMIN" not in self.permissions:
|
||||||
|
return self.response(to, {"error": "Forbidden"})
|
||||||
|
|
||||||
|
if not self.hasSitePermission(address):
|
||||||
|
return self.response(to, {"error": "Forbidden"})
|
||||||
|
|
||||||
|
if not all([re.match("^[a-z_*/+-]+( DESC| ASC|)$", part.strip()) for part in orderby.split(",")]):
|
||||||
|
return self.response(to, "Invalid order_by")
|
||||||
|
|
||||||
|
if type(limit) != int:
|
||||||
|
return self.response(to, "Invalid limit")
|
||||||
|
|
||||||
|
back = []
|
||||||
|
content_db = self.site.content_manager.contents.db
|
||||||
|
|
||||||
|
wheres = {}
|
||||||
|
wheres_raw = []
|
||||||
|
if "bigfile" in filter:
|
||||||
|
wheres["size >"] = 1024 * 1024 * 1
|
||||||
|
if "downloaded" in filter:
|
||||||
|
wheres_raw.append("(is_downloaded = 1 OR is_pinned = 1)")
|
||||||
|
if "pinned" in filter:
|
||||||
|
wheres["is_pinned"] = 1
|
||||||
|
if filter_inner_path:
|
||||||
|
wheres["inner_path__like"] = filter_inner_path
|
||||||
|
|
||||||
|
if address == "all":
|
||||||
|
join = "LEFT JOIN site USING (site_id)"
|
||||||
|
else:
|
||||||
|
wheres["site_id"] = content_db.site_ids[address]
|
||||||
|
join = ""
|
||||||
|
|
||||||
|
if wheres_raw:
|
||||||
|
query_wheres_raw = "AND" + " AND ".join(wheres_raw)
|
||||||
|
else:
|
||||||
|
query_wheres_raw = ""
|
||||||
|
|
||||||
|
query = "SELECT * FROM file_optional %s WHERE ? %s ORDER BY %s LIMIT %s" % (join, query_wheres_raw, orderby, limit)
|
||||||
|
|
||||||
|
for row in content_db.execute(query, wheres):
|
||||||
|
row = dict(row)
|
||||||
|
if address != "all":
|
||||||
|
row["address"] = address
|
||||||
|
|
||||||
|
if row["size"] > 1024 * 1024:
|
||||||
|
has_bigfile_info = self.addBigfileInfo(row)
|
||||||
|
else:
|
||||||
|
has_bigfile_info = False
|
||||||
|
|
||||||
|
if not has_bigfile_info and "bigfile" in filter:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not has_bigfile_info:
|
||||||
|
if row["is_downloaded"]:
|
||||||
|
row["bytes_downloaded"] = row["size"]
|
||||||
|
row["downloaded_percent"] = 100
|
||||||
|
else:
|
||||||
|
row["bytes_downloaded"] = 0
|
||||||
|
row["downloaded_percent"] = 0
|
||||||
|
|
||||||
|
back.append(row)
|
||||||
|
self.response(to, back)
|
||||||
|
|
||||||
|
def actionOptionalFileInfo(self, to, inner_path):
|
||||||
|
content_db = self.site.content_manager.contents.db
|
||||||
|
site_id = content_db.site_ids[self.site.address]
|
||||||
|
|
||||||
|
# Update peer numbers if necessary
|
||||||
|
if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5:
|
||||||
|
# Start in new thread to avoid blocking
|
||||||
|
self.time_peer_numbers_updated = time.time()
|
||||||
|
gevent.spawn(self.updatePeerNumbers)
|
||||||
|
|
||||||
|
query = "SELECT * FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1"
|
||||||
|
res = content_db.execute(query, {"site_id": site_id, "inner_path": inner_path})
|
||||||
|
row = next(res, None)
|
||||||
|
if row:
|
||||||
|
row = dict(row)
|
||||||
|
if row["size"] > 1024 * 1024:
|
||||||
|
row["address"] = self.site.address
|
||||||
|
self.addBigfileInfo(row)
|
||||||
|
self.response(to, row)
|
||||||
|
else:
|
||||||
|
self.response(to, None)
|
||||||
|
|
||||||
|
def setPin(self, inner_path, is_pinned, address=None):
|
||||||
|
if not address:
|
||||||
|
address = self.site.address
|
||||||
|
|
||||||
|
if not self.hasSitePermission(address):
|
||||||
|
return {"error": "Forbidden"}
|
||||||
|
|
||||||
|
site = self.server.sites[address]
|
||||||
|
site.content_manager.setPin(inner_path, is_pinned)
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionOptionalFilePin(self, to, inner_path, address=None):
|
||||||
|
if type(inner_path) is not list:
|
||||||
|
inner_path = [inner_path]
|
||||||
|
back = self.setPin(inner_path, 1, address)
|
||||||
|
num_file = len(inner_path)
|
||||||
|
if back == "ok":
|
||||||
|
if num_file == 1:
|
||||||
|
self.cmd("notification", ["done", _["Pinned %s"] % html.escape(helper.getFilename(inner_path[0])), 5000])
|
||||||
|
else:
|
||||||
|
self.cmd("notification", ["done", _["Pinned %s files"] % num_file, 5000])
|
||||||
|
self.response(to, back)
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionOptionalFileUnpin(self, to, inner_path, address=None):
|
||||||
|
if type(inner_path) is not list:
|
||||||
|
inner_path = [inner_path]
|
||||||
|
back = self.setPin(inner_path, 0, address)
|
||||||
|
num_file = len(inner_path)
|
||||||
|
if back == "ok":
|
||||||
|
if num_file == 1:
|
||||||
|
self.cmd("notification", ["done", _["Removed pin from %s"] % html.escape(helper.getFilename(inner_path[0])), 5000])
|
||||||
|
else:
|
||||||
|
self.cmd("notification", ["done", _["Removed pin from %s files"] % num_file, 5000])
|
||||||
|
self.response(to, back)
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionOptionalFileDelete(self, to, inner_path, address=None):
|
||||||
|
if not address:
|
||||||
|
address = self.site.address
|
||||||
|
|
||||||
|
if not self.hasSitePermission(address):
|
||||||
|
return self.response(to, {"error": "Forbidden"})
|
||||||
|
|
||||||
|
site = self.server.sites[address]
|
||||||
|
|
||||||
|
content_db = site.content_manager.contents.db
|
||||||
|
site_id = content_db.site_ids[site.address]
|
||||||
|
|
||||||
|
res = content_db.execute("SELECT * FROM file_optional WHERE ? LIMIT 1", {"site_id": site_id, "inner_path": inner_path, "is_downloaded": 1})
|
||||||
|
row = next(res, None)
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return self.response(to, {"error": "Not found in content.db"})
|
||||||
|
|
||||||
|
removed = site.content_manager.optionalRemoved(inner_path, row["hash_id"], row["size"])
|
||||||
|
# if not removed:
|
||||||
|
# return self.response(to, {"error": "Not found in hash_id: %s" % row["hash_id"]})
|
||||||
|
|
||||||
|
content_db.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"site_id": site_id, "inner_path": inner_path})
|
||||||
|
|
||||||
|
try:
|
||||||
|
site.storage.delete(inner_path)
|
||||||
|
except Exception as err:
|
||||||
|
return self.response(to, {"error": "File delete error: %s" % err})
|
||||||
|
site.updateWebsocket(file_delete=inner_path)
|
||||||
|
|
||||||
|
if inner_path in site.content_manager.cache_is_pinned:
|
||||||
|
site.content_manager.cache_is_pinned = {}
|
||||||
|
|
||||||
|
self.response(to, "ok")
|
||||||
|
|
||||||
|
# Limit functions
|
||||||
|
|
||||||
|
@flag.admin
|
||||||
|
def actionOptionalLimitStats(self, to):
|
||||||
|
back = {}
|
||||||
|
back["limit"] = config.optional_limit
|
||||||
|
back["used"] = self.site.content_manager.contents.db.getOptionalUsedBytes()
|
||||||
|
back["free"] = helper.getFreeSpace()
|
||||||
|
|
||||||
|
self.response(to, back)
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
@flag.admin
|
||||||
|
def actionOptionalLimitSet(self, to, limit):
|
||||||
|
config.optional_limit = re.sub(r"\.0+$", "", limit) # Remove unnecessary digits from end
|
||||||
|
config.saveValue("optional_limit", limit)
|
||||||
|
self.response(to, "ok")
|
||||||
|
|
||||||
|
# Distribute help functions
|
||||||
|
|
||||||
|
def actionOptionalHelpList(self, to, address=None):
|
||||||
|
if not address:
|
||||||
|
address = self.site.address
|
||||||
|
|
||||||
|
if not self.hasSitePermission(address):
|
||||||
|
return self.response(to, {"error": "Forbidden"})
|
||||||
|
|
||||||
|
site = self.server.sites[address]
|
||||||
|
|
||||||
|
self.response(to, site.settings.get("optional_help", {}))
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionOptionalHelp(self, to, directory, title, address=None):
|
||||||
|
if not address:
|
||||||
|
address = self.site.address
|
||||||
|
|
||||||
|
if not self.hasSitePermission(address):
|
||||||
|
return self.response(to, {"error": "Forbidden"})
|
||||||
|
|
||||||
|
site = self.server.sites[address]
|
||||||
|
content_db = site.content_manager.contents.db
|
||||||
|
site_id = content_db.site_ids[address]
|
||||||
|
|
||||||
|
if "optional_help" not in site.settings:
|
||||||
|
site.settings["optional_help"] = {}
|
||||||
|
|
||||||
|
stats = content_db.execute(
|
||||||
|
"SELECT COUNT(*) AS num, SUM(size) AS size FROM file_optional WHERE site_id = :site_id AND inner_path LIKE :inner_path",
|
||||||
|
{"site_id": site_id, "inner_path": directory + "%"}
|
||||||
|
).fetchone()
|
||||||
|
stats = dict(stats)
|
||||||
|
|
||||||
|
if not stats["size"]:
|
||||||
|
stats["size"] = 0
|
||||||
|
if not stats["num"]:
|
||||||
|
stats["num"] = 0
|
||||||
|
|
||||||
|
self.cmd("notification", [
|
||||||
|
"done",
|
||||||
|
_["You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>"] %
|
||||||
|
(html.escape(title), html.escape(directory)),
|
||||||
|
10000
|
||||||
|
])
|
||||||
|
|
||||||
|
site.settings["optional_help"][directory] = title
|
||||||
|
|
||||||
|
self.response(to, dict(stats))
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionOptionalHelpRemove(self, to, directory, address=None):
|
||||||
|
if not address:
|
||||||
|
address = self.site.address
|
||||||
|
|
||||||
|
if not self.hasSitePermission(address):
|
||||||
|
return self.response(to, {"error": "Forbidden"})
|
||||||
|
|
||||||
|
site = self.server.sites[address]
|
||||||
|
|
||||||
|
try:
|
||||||
|
del site.settings["optional_help"][directory]
|
||||||
|
self.response(to, "ok")
|
||||||
|
except Exception:
|
||||||
|
self.response(to, {"error": "Not found"})
|
||||||
|
|
||||||
|
def cbOptionalHelpAll(self, to, site, value):
|
||||||
|
site.settings["autodownloadoptional"] = value
|
||||||
|
self.response(to, value)
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionOptionalHelpAll(self, to, value, address=None):
|
||||||
|
if not address:
|
||||||
|
address = self.site.address
|
||||||
|
|
||||||
|
if not self.hasSitePermission(address):
|
||||||
|
return self.response(to, {"error": "Forbidden"})
|
||||||
|
|
||||||
|
site = self.server.sites[address]
|
||||||
|
|
||||||
|
if value:
|
||||||
|
if "ADMIN" in self.site.settings["permissions"]:
|
||||||
|
self.cbOptionalHelpAll(to, site, True)
|
||||||
|
else:
|
||||||
|
site_title = site.content_manager.contents["content.json"].get("title", address)
|
||||||
|
self.cmd(
|
||||||
|
"confirm",
|
||||||
|
[
|
||||||
|
_["Help distribute all new optional files on site <b>%s</b>"] % html.escape(site_title),
|
||||||
|
_["Yes, I want to help!"]
|
||||||
|
],
|
||||||
|
lambda res: self.cbOptionalHelpAll(to, site, True)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
site.settings["autodownloadoptional"] = False
|
||||||
|
self.response(to, False)
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import OptionalManagerPlugin
|
||||||
|
from . import UiWebsocketPlugin
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Pinned %s files": "Archivos %s fijados",
|
||||||
|
"Removed pin from %s files": "Archivos %s que no estan fijados",
|
||||||
|
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Tu empezaste a ayudar a distribuir <b>%s</b>.<br><small>Directorio: %s</small>",
|
||||||
|
"Help distribute all new optional files on site <b>%s</b>": "Ayude a distribuir todos los archivos opcionales en el sitio <b>%s</b>",
|
||||||
|
"Yes, I want to help!": "¡Si, yo quiero ayudar!"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Pinned %s files": "Fichiers %s épinglés",
|
||||||
|
"Removed pin from %s files": "Fichiers %s ne sont plus épinglés",
|
||||||
|
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Vous avez commencé à aider à distribuer <b>%s</b>.<br><small>Dossier : %s</small>",
|
||||||
|
"Help distribute all new optional files on site <b>%s</b>": "Aider à distribuer tous les fichiers optionnels du site <b>%s</b>",
|
||||||
|
"Yes, I want to help!": "Oui, je veux aider !"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Pinned %s files": "%s fájl rögzítve",
|
||||||
|
"Removed pin from %s files": "%s fájl rögzítés eltávolítva",
|
||||||
|
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Új segítség a terjesztésben: <b>%s</b>.<br><small>Könyvtár: %s</small>",
|
||||||
|
"Help distribute all new optional files on site <b>%s</b>": "Segítség az összes új opcionális fájl terjesztésében az <b>%s</b> oldalon",
|
||||||
|
"Yes, I want to help!": "Igen, segíteni akarok!"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Pinned %s files": "%s 件のファイルを固定",
|
||||||
|
"Removed pin from %s files": "%s 件のファイルの固定を解除",
|
||||||
|
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "あなたはサイト: <b>%s</b> の配布の援助を開始しました。<br><small>ディレクトリ: %s</small>",
|
||||||
|
"Help distribute all new optional files on site <b>%s</b>": "サイト: <b>%s</b> のすべての新しいオプションファイルの配布を援助しますか?",
|
||||||
|
"Yes, I want to help!": "はい、やります!"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Pinned %s files": "Arquivos %s fixados",
|
||||||
|
"Removed pin from %s files": "Arquivos %s não estão fixados",
|
||||||
|
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "Você começou a ajudar a distribuir <b>%s</b>.<br><small>Pasta: %s</small>",
|
||||||
|
"Help distribute all new optional files on site <b>%s</b>": "Ajude a distribuir todos os novos arquivos opcionais no site <b>%s</b>",
|
||||||
|
"Yes, I want to help!": "Sim, eu quero ajudar!"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Pinned %s files": "已固定 %s 個檔",
|
||||||
|
"Removed pin from %s files": "已解除固定 %s 個檔",
|
||||||
|
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "你已經開始幫助分發 <b>%s</b> 。<br><small>目錄:%s</small>",
|
||||||
|
"Help distribute all new optional files on site <b>%s</b>": "你想要幫助分發 <b>%s</b> 網站的所有檔嗎?",
|
||||||
|
"Yes, I want to help!": "是,我想要幫助!"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Pinned %s files": "已固定 %s 个文件",
|
||||||
|
"Removed pin from %s files": "已解除固定 %s 个文件",
|
||||||
|
"You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>": "您已经开始帮助分发 <b>%s</b> 。<br><small>目录:%s</small>",
|
||||||
|
"Help distribute all new optional files on site <b>%s</b>": "您想要帮助分发 <b>%s</b> 站点的所有文件吗?",
|
||||||
|
"Yes, I want to help!": "是,我想要帮助!"
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
import random
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
from Plugin import PluginManager
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("ContentDb")
|
||||||
|
class ContentDbPlugin(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
atexit.register(self.saveAllPeers)
|
||||||
|
super(ContentDbPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def getSchema(self):
|
||||||
|
schema = super(ContentDbPlugin, self).getSchema()
|
||||||
|
|
||||||
|
schema["tables"]["peer"] = {
|
||||||
|
"cols": [
|
||||||
|
["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
|
||||||
|
["address", "TEXT NOT NULL"],
|
||||||
|
["port", "INTEGER NOT NULL"],
|
||||||
|
["hashfield", "BLOB"],
|
||||||
|
["reputation", "INTEGER NOT NULL"],
|
||||||
|
["time_added", "INTEGER NOT NULL"],
|
||||||
|
["time_found", "INTEGER NOT NULL"],
|
||||||
|
["time_response", "INTEGER NOT NULL"],
|
||||||
|
["connection_error", "INTEGER NOT NULL"]
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX peer_key ON peer (site_id, address, port)"
|
||||||
|
],
|
||||||
|
"schema_changed": 3
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def loadPeers(self, site):
|
||||||
|
s = time.time()
|
||||||
|
site_id = self.site_ids.get(site.address)
|
||||||
|
res = self.execute("SELECT * FROM peer WHERE site_id = :site_id", {"site_id": site_id})
|
||||||
|
num = 0
|
||||||
|
num_hashfield = 0
|
||||||
|
for row in res:
|
||||||
|
peer = site.addPeer(str(row["address"]), row["port"])
|
||||||
|
if not peer: # Already exist
|
||||||
|
continue
|
||||||
|
if row["hashfield"]:
|
||||||
|
peer.hashfield.replaceFromBytes(row["hashfield"])
|
||||||
|
num_hashfield += 1
|
||||||
|
peer.time_added = row["time_added"]
|
||||||
|
peer.time_found = row["time_found"]
|
||||||
|
peer.time_found = row["time_found"]
|
||||||
|
peer.time_response = row["time_response"]
|
||||||
|
peer.connection_error = row["connection_error"]
|
||||||
|
if row["address"].endswith(".onion"):
|
||||||
|
# Onion peers less likely working
|
||||||
|
if peer.reputation > 0:
|
||||||
|
peer.reputation = peer.reputation / 2
|
||||||
|
else:
|
||||||
|
peer.reputation -= 1
|
||||||
|
num += 1
|
||||||
|
if num_hashfield:
|
||||||
|
site.content_manager.has_optional_files = True
|
||||||
|
site.log.debug("%s peers (%s with hashfield) loaded in %.3fs" % (num, num_hashfield, time.time() - s))
|
||||||
|
|
||||||
|
def iteratePeers(self, site):
|
||||||
|
site_id = self.site_ids.get(site.address)
|
||||||
|
for key, peer in list(site.peers.items()):
|
||||||
|
address, port = key.rsplit(":", 1)
|
||||||
|
if peer.has_hashfield:
|
||||||
|
hashfield = sqlite3.Binary(peer.hashfield.tobytes())
|
||||||
|
else:
|
||||||
|
hashfield = ""
|
||||||
|
yield (site_id, address, port, hashfield, peer.reputation, int(peer.time_added), int(peer.time_found), int(peer.time_response), int(peer.connection_error))
|
||||||
|
|
||||||
|
def savePeers(self, site, spawn=False):
|
||||||
|
if spawn:
|
||||||
|
# Save peers every hour (+random some secs to not update very site at same time)
|
||||||
|
site.greenlet_manager.spawnLater(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True)
|
||||||
|
if not site.peers:
|
||||||
|
site.log.debug("Peers not saved: No peers found")
|
||||||
|
return
|
||||||
|
s = time.time()
|
||||||
|
site_id = self.site_ids.get(site.address)
|
||||||
|
cur = self.getCursor()
|
||||||
|
try:
|
||||||
|
cur.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id})
|
||||||
|
cur.executemany(
|
||||||
|
"INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found, time_response, connection_error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
self.iteratePeers(site)
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
site.log.error("Save peer error: %s" % err)
|
||||||
|
site.log.debug("Peers saved in %.3fs" % (time.time() - s))
|
||||||
|
|
||||||
|
def initSite(self, site):
|
||||||
|
super(ContentDbPlugin, self).initSite(site)
|
||||||
|
site.greenlet_manager.spawnLater(0.5, self.loadPeers, site)
|
||||||
|
site.greenlet_manager.spawnLater(60*60, self.savePeers, site, spawn=True)
|
||||||
|
|
||||||
|
def saveAllPeers(self):
|
||||||
|
for site in list(self.sites.values()):
|
||||||
|
try:
|
||||||
|
self.savePeers(site)
|
||||||
|
except Exception as err:
|
||||||
|
site.log.error("Save peer error: %s" % err)
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import PeerDbPlugin
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "PeerDb",
|
||||||
|
"description": "Save/restore peer list on client restart.",
|
||||||
|
"default": "enabled"
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Config import config
|
||||||
|
from Debug import Debug
|
||||||
|
from util import SafeRe
|
||||||
|
from util.Flag import flag
|
||||||
|
|
||||||
|
|
||||||
|
class WsLogStreamer(logging.StreamHandler):
|
||||||
|
def __init__(self, stream_id, ui_websocket, filter):
|
||||||
|
self.stream_id = stream_id
|
||||||
|
self.ui_websocket = ui_websocket
|
||||||
|
|
||||||
|
if filter:
|
||||||
|
if not SafeRe.isSafePattern(filter):
|
||||||
|
raise Exception("Not a safe prex pattern")
|
||||||
|
self.filter_re = re.compile(".*" + filter)
|
||||||
|
else:
|
||||||
|
self.filter_re = None
|
||||||
|
return super(WsLogStreamer, self).__init__()
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
if self.ui_websocket.ws.closed:
|
||||||
|
self.stop()
|
||||||
|
return
|
||||||
|
line = self.format(record)
|
||||||
|
if self.filter_re and not self.filter_re.match(line):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.ui_websocket.cmd("logLineAdd", {"stream_id": self.stream_id, "lines": [line]})
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
logging.getLogger('').removeHandler(self)
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiWebsocket")
|
||||||
|
class UiWebsocketPlugin(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.log_streamers = {}
|
||||||
|
return super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
@flag.admin
|
||||||
|
def actionConsoleLogRead(self, to, filter=None, read_size=32 * 1024, limit=500):
|
||||||
|
log_file_path = "%s/debug.log" % config.log_dir
|
||||||
|
log_file = open(log_file_path, encoding="utf-8")
|
||||||
|
log_file.seek(0, 2)
|
||||||
|
end_pos = log_file.tell()
|
||||||
|
log_file.seek(max(0, end_pos - read_size))
|
||||||
|
if log_file.tell() != 0:
|
||||||
|
log_file.readline() # Partial line junk
|
||||||
|
|
||||||
|
pos_start = log_file.tell()
|
||||||
|
lines = []
|
||||||
|
if filter:
|
||||||
|
assert SafeRe.isSafePattern(filter)
|
||||||
|
filter_re = re.compile(".*" + filter)
|
||||||
|
|
||||||
|
last_match = False
|
||||||
|
for line in log_file:
|
||||||
|
if not line.startswith("[") and last_match: # Multi-line log entry
|
||||||
|
lines.append(line.replace(" ", " "))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if filter and not filter_re.match(line):
|
||||||
|
last_match = False
|
||||||
|
continue
|
||||||
|
last_match = True
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
num_found = len(lines)
|
||||||
|
lines = lines[-limit:]
|
||||||
|
|
||||||
|
return {"lines": lines, "pos_end": log_file.tell(), "pos_start": pos_start, "num_found": num_found}
|
||||||
|
|
||||||
|
def addLogStreamer(self, stream_id, filter=None):
|
||||||
|
logger = WsLogStreamer(stream_id, self, filter)
|
||||||
|
logger.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)-8s %(name)s %(message)s'))
|
||||||
|
logger.setLevel(logging.getLevelName("DEBUG"))
|
||||||
|
|
||||||
|
logging.getLogger('').addHandler(logger)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
@flag.admin
|
||||||
|
def actionConsoleLogStream(self, to, filter=None):
|
||||||
|
stream_id = to
|
||||||
|
self.log_streamers[stream_id] = self.addLogStreamer(stream_id, filter)
|
||||||
|
self.response(to, {"stream_id": stream_id})
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
@flag.admin
|
||||||
|
def actionConsoleLogStreamRemove(self, to, stream_id):
|
||||||
|
try:
|
||||||
|
self.log_streamers[stream_id].stop()
|
||||||
|
del self.log_streamers[stream_id]
|
||||||
|
return "ok"
|
||||||
|
except Exception as err:
|
||||||
|
return {"error": Debug.formatException(err)}
|
|
@ -0,0 +1,892 @@
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import html
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
import urllib
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
|
||||||
|
import util
|
||||||
|
from Config import config
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Debug import Debug
|
||||||
|
from Translate import Translate
|
||||||
|
from util import helper
|
||||||
|
from util.Flag import flag
|
||||||
|
from .ZipStream import ZipStream
|
||||||
|
|
||||||
|
plugin_dir = os.path.dirname(__file__)
|
||||||
|
media_dir = plugin_dir + "/media"
|
||||||
|
|
||||||
|
loc_cache = {}
|
||||||
|
if "_" not in locals():
|
||||||
|
_ = Translate(plugin_dir + "/languages/")
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
if ext == "js":
|
||||||
|
yield _.translateData(open(plugin_media_file).read()).encode("utf8")
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
|
||||||
|
def actionZip(self):
|
||||||
|
address = self.get["address"]
|
||||||
|
site = self.server.site_manager.get(address)
|
||||||
|
if not site:
|
||||||
|
return self.error404("Site not found")
|
||||||
|
|
||||||
|
title = site.content_manager.contents.get("content.json", {}).get("title", "")
|
||||||
|
filename = "%s-backup-%s.zip" % (title, time.strftime("%Y-%m-%d_%H_%M"))
|
||||||
|
filename_quoted = urllib.parse.quote(filename)
|
||||||
|
self.sendHeader(content_type="application/zip", extra_headers={'Content-Disposition': 'attachment; filename="%s"' % filename_quoted})
|
||||||
|
|
||||||
|
return self.streamZip(site.storage.getPath("."))
|
||||||
|
|
||||||
|
def streamZip(self, dir_path):
|
||||||
|
zs = ZipStream(dir_path)
|
||||||
|
while 1:
|
||||||
|
data = zs.read()
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
yield data
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiWebsocket")
|
||||||
|
class UiWebsocketPlugin(object):
|
||||||
|
def sidebarRenderPeerStats(self, body, site):
|
||||||
|
# Peers by status
|
||||||
|
peers_total = len(site.peers)
|
||||||
|
peers_reachable = 0
|
||||||
|
peers_connectable = 0
|
||||||
|
peers_connected = 0
|
||||||
|
peers_failed = 0
|
||||||
|
# Peers by type
|
||||||
|
peers_by_type = {}
|
||||||
|
|
||||||
|
type_proper_names = {
|
||||||
|
'ipv4': 'IPv4',
|
||||||
|
'ipv6': 'IPv6',
|
||||||
|
'onion': 'Onion',
|
||||||
|
'unknown': 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
type_defs = {
|
||||||
|
'local-ipv4': {
|
||||||
|
'order' : -21,
|
||||||
|
'color' : 'yellow'
|
||||||
|
},
|
||||||
|
'local-ipv6': {
|
||||||
|
'order' : -20,
|
||||||
|
'color' : 'orange'
|
||||||
|
},
|
||||||
|
'ipv4': {
|
||||||
|
'order' : -11,
|
||||||
|
'color' : 'blue'
|
||||||
|
},
|
||||||
|
'ipv6': {
|
||||||
|
'order' : -10,
|
||||||
|
'color' : 'darkblue'
|
||||||
|
},
|
||||||
|
'unknown': {
|
||||||
|
'order' : 10,
|
||||||
|
'color' : 'red'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for peer in list(site.peers.values()):
|
||||||
|
# Peers by status
|
||||||
|
if peer.isConnected():
|
||||||
|
peers_connected += 1
|
||||||
|
elif peer.isConnectable():
|
||||||
|
peers_connectable += 1
|
||||||
|
elif peer.isReachable() and not peer.connection_error:
|
||||||
|
peers_reachable += 1
|
||||||
|
elif peer.isReachable() and peer.connection_error:
|
||||||
|
peers_failed += 1
|
||||||
|
# Peers by type
|
||||||
|
peer_type = peer.getIpType()
|
||||||
|
peer_readable_type = type_proper_names.get(peer_type, peer_type)
|
||||||
|
if helper.isPrivateIp(peer.ip):
|
||||||
|
peer_type = 'local-' + peer.getIpType()
|
||||||
|
peer_readable_type = 'Local ' + peer_readable_type
|
||||||
|
peers_by_type[peer_type] = peers_by_type.get(peer_type, {
|
||||||
|
'type': peer_type,
|
||||||
|
'readable_type': _[peer_readable_type],
|
||||||
|
'order': type_defs.get(peer_type, {}).get('order', 0),
|
||||||
|
'color': type_defs.get(peer_type, {}).get('color', 'purple'),
|
||||||
|
'count': 0
|
||||||
|
})
|
||||||
|
peers_by_type[peer_type]['count'] += 1
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
if peers_total:
|
||||||
|
percent_connected = float(peers_connected) / peers_total
|
||||||
|
percent_connectable = float(peers_connectable) / peers_total
|
||||||
|
percent_reachable = float(peers_reachable) / peers_total
|
||||||
|
percent_failed = float(peers_failed) / peers_total
|
||||||
|
percent_other = min(0.0, 1.0 - percent_connected - percent_connectable - percent_reachable - percent_failed)
|
||||||
|
else:
|
||||||
|
percent_connected = 0
|
||||||
|
percent_reachable = 0
|
||||||
|
percent_connectable = 0
|
||||||
|
percent_failed = 0
|
||||||
|
percent_other = 0
|
||||||
|
|
||||||
|
peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)]
|
||||||
|
peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip)
|
||||||
|
copy_link = "http://127.0.0.1:43110/%s/?zeronet_peers=%s" % (
|
||||||
|
site.content_manager.contents.get("content.json", {}).get("domain", site.address),
|
||||||
|
",".join(peer_ips)
|
||||||
|
)
|
||||||
|
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
{_[Peers]}
|
||||||
|
<small class="label-right"><a href='{copy_link}' id='link-copypeers' class='link-right'>{_[Copy to clipboard]}</a></small>
|
||||||
|
</label>
|
||||||
|
<ul class='graph graph-stacked'>
|
||||||
|
<li style='width: {percent_connected:.0%}' class='connected back-green' title='{_[Connected peers]}'></li>
|
||||||
|
<li style='width: {percent_connectable:.0%}' class='connectable back-blue' title='{_[Peers that were recently connected succefully]}'></li>
|
||||||
|
<li style='width: {percent_reachable:.0%}' class='reachable back-yellow' title='{_[Peers available for connection]}'></li>
|
||||||
|
<li style='width: {percent_failed:.0%}' class='connectable back-red' title='{_[Peers recently failed to connect]}'></li>
|
||||||
|
<li style='width: {percent_other:.0%}' class='connectable back-black' title='{_[Peers with wrong, unknown or invalid network address]}'></li>
|
||||||
|
</ul>
|
||||||
|
<ul class='graph-legend'>
|
||||||
|
<li class='color-green' title='{_[Connected peers]}'><span>{_[Connected]}:</span><b>{peers_connected}</b></li>
|
||||||
|
<li class='color-blue' title='{_[Peers that were recently connected succefully]}'><span>{_[Connectable]}:</span><b>{peers_connectable}</b></li>
|
||||||
|
<li class='color-yellow' title='{_[Peers available for connection]}'><span>{_[Reachable]}:</span><b>{peers_reachable}</b></li>
|
||||||
|
<li class='color-red' title='{_[Peers recently failed to connect]}'><span>{_[Failed]}:</span><b>{peers_failed}</b></li>
|
||||||
|
<li class='color-black'><span>{_[Total peers]}:</span><b>{peers_total}</b></li>
|
||||||
|
</ul>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
peers_by_type = sorted(
|
||||||
|
peers_by_type.values(),
|
||||||
|
key=lambda t: t['order'],
|
||||||
|
)
|
||||||
|
|
||||||
|
peers_by_type_graph = ''
|
||||||
|
peers_by_type_legend = ''
|
||||||
|
|
||||||
|
if peers_total:
|
||||||
|
for t in peers_by_type:
|
||||||
|
peers_by_type_graph += """
|
||||||
|
<li style='width: %.2f%%' class='%s back-%s' title='%s'></li>
|
||||||
|
""" % (float(t['count']) * 100.0 / peers_total, t['type'], t["color"], t['readable_type'])
|
||||||
|
peers_by_type_legend += """
|
||||||
|
<li class='color-%s'><span>%s:</span><b>%s</b></li>
|
||||||
|
""" % (t["color"], t['readable_type'], t['count'])
|
||||||
|
|
||||||
|
if peers_by_type_legend != '':
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
{_[Peer types]}
|
||||||
|
</label>
|
||||||
|
<ul class='graph graph-stacked'>
|
||||||
|
%s
|
||||||
|
</ul>
|
||||||
|
<ul class='graph-legend'>
|
||||||
|
%s
|
||||||
|
</ul>
|
||||||
|
<li>
|
||||||
|
""" % (peers_by_type_graph, peers_by_type_legend)))
|
||||||
|
|
||||||
|
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>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
def sidebarRenderFileStats(self, body, site):
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
{_[Files]}
|
||||||
|
<a href='/list/{site.address}' class='link-right link-outline' id="browse-files">{_[Browse files]}</a>
|
||||||
|
<small class="label-right">
|
||||||
|
<a href='/ZeroNet-Internal/Zip?address={site.address}' id='link-zip' class='link-right' download='site.zip'>{_[Save as .zip]}</a>
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
<ul class='graph graph-stacked'>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
extensions = (
|
||||||
|
("html", "yellow"),
|
||||||
|
("css", "orange"),
|
||||||
|
("js", "purple"),
|
||||||
|
("Image", "green"),
|
||||||
|
("json", "darkblue"),
|
||||||
|
("User data", "blue"),
|
||||||
|
("Other", "white"),
|
||||||
|
("Total", "black")
|
||||||
|
)
|
||||||
|
# Collect stats
|
||||||
|
size_filetypes = {}
|
||||||
|
size_total = 0
|
||||||
|
contents = site.content_manager.listContents() # Without user files
|
||||||
|
for inner_path in contents:
|
||||||
|
content = site.content_manager.contents[inner_path]
|
||||||
|
if "files" not in content or content["files"] is None:
|
||||||
|
continue
|
||||||
|
for file_name, file_details in list(content["files"].items()):
|
||||||
|
size_total += file_details["size"]
|
||||||
|
ext = file_name.split(".")[-1]
|
||||||
|
size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"]
|
||||||
|
|
||||||
|
# Get user file sizes
|
||||||
|
size_user_content = site.content_manager.contents.execute(
|
||||||
|
"SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?",
|
||||||
|
{"not__inner_path": contents}
|
||||||
|
).fetchone()["size"]
|
||||||
|
if not size_user_content:
|
||||||
|
size_user_content = 0
|
||||||
|
size_filetypes["User data"] = size_user_content
|
||||||
|
size_total += size_user_content
|
||||||
|
|
||||||
|
# The missing difference is content.json sizes
|
||||||
|
if "json" in size_filetypes:
|
||||||
|
size_filetypes["json"] += max(0, site.settings["size"] - size_total)
|
||||||
|
size_total = size_other = site.settings["size"]
|
||||||
|
|
||||||
|
# Bar
|
||||||
|
for extension, color in extensions:
|
||||||
|
if extension == "Total":
|
||||||
|
continue
|
||||||
|
if extension == "Other":
|
||||||
|
size = max(0, 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
|
||||||
|
if size_total == 0:
|
||||||
|
percent = 0
|
||||||
|
else:
|
||||||
|
percent = 100 * (float(size) / size_total)
|
||||||
|
percent = math.floor(percent * 100) / 100 # Floor to 2 digits
|
||||||
|
body.append(
|
||||||
|
"""<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 = max(0, 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("<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, _[title], size_formatted))
|
||||||
|
|
||||||
|
body.append("</ul></li>")
|
||||||
|
|
||||||
|
def sidebarRenderSizeLimit(self, body, site):
|
||||||
|
free_space = helper.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:,.0f}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>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
def sidebarRenderOptionalFileStats(self, body, site):
|
||||||
|
size_total = float(site.settings["size_optional"])
|
||||||
|
size_downloaded = float(site.settings["optional_downloaded"])
|
||||||
|
|
||||||
|
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(_("""
|
||||||
|
<li>
|
||||||
|
<label>{_[Optional files]}</label>
|
||||||
|
<ul class='graph'>
|
||||||
|
<li style='width: 100%' class='total back-black' title="{_[Total size]}"></li>
|
||||||
|
<li style='width: {percent_downloaded:.0%}' class='connected back-green' title='{_[Downloaded files]}'></li>
|
||||||
|
</ul>
|
||||||
|
<ul class='graph-legend'>
|
||||||
|
<li class='color-green'><span>{_[Downloaded]}:</span><b>{size_formatted_downloaded:.2f}MB</b></li>
|
||||||
|
<li class='color-black'><span>{_[Total]}:</span><b>{size_formatted_total:.2f}MB</b></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def sidebarRenderOptionalFileSettings(self, body, site):
|
||||||
|
if self.site.settings.get("autodownloadoptional"):
|
||||||
|
checked = "checked='checked'"
|
||||||
|
else:
|
||||||
|
checked = ""
|
||||||
|
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>{_[Help distribute added optional files]}</label>
|
||||||
|
<input type="checkbox" class="checkbox" id="checkbox-autodownloadoptional" {checked}/><div class="checkbox-skin"></div>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
if hasattr(config, "autodownload_bigfile_size_limit"):
|
||||||
|
autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit))
|
||||||
|
body.append(_("""
|
||||||
|
<div class='settings-autodownloadoptional'>
|
||||||
|
<label>{_[Auto download big file size limit]}</label>
|
||||||
|
<input type='text' class='text text-num' value="{autodownload_bigfile_size_limit}" id='input-autodownload_bigfile_size_limit'/><span class='text-post'>MB</span>
|
||||||
|
<a href='#Set' class='button' id='button-autodownload_bigfile_size_limit'>{_[Set]}</a>
|
||||||
|
<a href='#Download+previous' class='button' id='button-autodownload_previous'>{_[Download previous files]}</a>
|
||||||
|
</div>
|
||||||
|
"""))
|
||||||
|
body.append("</li>")
|
||||||
|
|
||||||
|
def sidebarRenderBadFiles(self, body, site):
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>{_[Needs to be updated]}:</label>
|
||||||
|
<ul class='filelist'>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for bad_file, tries in site.bad_files.items():
|
||||||
|
i += 1
|
||||||
|
body.append(_("""<li class='color-red' title="{bad_file_path} ({tries})">{bad_filename}</li>""", {
|
||||||
|
"bad_file_path": bad_file,
|
||||||
|
"bad_filename": helper.getFilename(bad_file),
|
||||||
|
"tries": _.pluralize(tries, "{} try", "{} tries")
|
||||||
|
}))
|
||||||
|
if i > 30:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(site.bad_files) > 30:
|
||||||
|
num_bad_files = len(site.bad_files) - 30
|
||||||
|
body.append(_("""<li class='color-red'>{_[+ {num_bad_files} more]}</li>""", nested=True))
|
||||||
|
|
||||||
|
body.append("""
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
""")
|
||||||
|
|
||||||
|
def sidebarRenderDbOptions(self, body, site):
|
||||||
|
if site.storage.db:
|
||||||
|
inner_path = site.storage.getInnerPath(site.storage.db.db_path)
|
||||||
|
size = float(site.storage.getSize(inner_path)) / 1024
|
||||||
|
feeds = len(site.storage.db.schema.get("feeds", {}))
|
||||||
|
else:
|
||||||
|
inner_path = _["No database found"]
|
||||||
|
size = 0.0
|
||||||
|
feeds = 0
|
||||||
|
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>{_[Database]} <small>({size:.2f}kB, {_[search feeds]}: {_[{feeds} query]})</small></label>
|
||||||
|
<div class='flex'>
|
||||||
|
<input type='text' class='text disabled' value="{inner_path}" disabled='disabled'/>
|
||||||
|
<a href='#Reload' id="button-dbreload" class='button'>{_[Reload]}</a>
|
||||||
|
<a href='#Rebuild' id="button-dbrebuild" class='button'>{_[Rebuild]}</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
""", nested=True))
|
||||||
|
|
||||||
|
def sidebarRenderIdentity(self, body, site):
|
||||||
|
auth_address = self.user.getAuthAddress(self.site.address, create=False)
|
||||||
|
rules = self.site.content_manager.getRules("data/users/%s/content.json" % auth_address)
|
||||||
|
if rules and rules.get("max_size"):
|
||||||
|
quota = rules["max_size"] / 1024
|
||||||
|
try:
|
||||||
|
content = site.content_manager.contents["data/users/%s/content.json" % auth_address]
|
||||||
|
used = len(json.dumps(content)) + sum([file["size"] for file in list(content["files"].values())])
|
||||||
|
except:
|
||||||
|
used = 0
|
||||||
|
used = used / 1024
|
||||||
|
else:
|
||||||
|
quota = used = 0
|
||||||
|
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>{_[Identity address]} <small>({_[limit used]}: {used:.2f}kB / {quota:.2f}kB)</small></label>
|
||||||
|
<div class='flex'>
|
||||||
|
<span class='input text disabled'>{auth_address}</span>
|
||||||
|
<a href='#Change' class='button' id='button-identity'>{_[Change]}</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
def sidebarRenderControls(self, body, site):
|
||||||
|
auth_address = self.user.getAuthAddress(self.site.address, create=False)
|
||||||
|
if self.site.settings["serving"]:
|
||||||
|
class_pause = ""
|
||||||
|
class_resume = "hidden"
|
||||||
|
else:
|
||||||
|
class_pause = "hidden"
|
||||||
|
class_resume = ""
|
||||||
|
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>{_[Site control]}</label>
|
||||||
|
<a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
|
||||||
|
<a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
|
||||||
|
<a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
|
||||||
|
<a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
|
||||||
|
</li>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True)
|
||||||
|
site_address = self.site.address
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>{_[Site address]}</label><br>
|
||||||
|
<div class='flex'>
|
||||||
|
<span class='input text disabled'>{site_address}</span>
|
||||||
|
"""))
|
||||||
|
if donate_key == False or donate_key == "":
|
||||||
|
pass
|
||||||
|
elif (type(donate_key) == str or type(donate_key) == str) and len(donate_key) > 0:
|
||||||
|
body.append(_("""
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label>{_[Donate]}</label><br>
|
||||||
|
<div class='flex'>
|
||||||
|
{donate_key}
|
||||||
|
"""))
|
||||||
|
else:
|
||||||
|
body.append(_("""
|
||||||
|
<a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a>
|
||||||
|
"""))
|
||||||
|
body.append(_("""
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
def sidebarRenderOwnedCheckbox(self, body, site):
|
||||||
|
if self.site.settings["own"]:
|
||||||
|
checked = "checked='checked'"
|
||||||
|
else:
|
||||||
|
checked = ""
|
||||||
|
|
||||||
|
body.append(_("""
|
||||||
|
<h2 class='owned-title'>{_[This is my site]}</h2>
|
||||||
|
<input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
def sidebarRenderOwnSettings(self, body, site):
|
||||||
|
title = site.content_manager.contents.get("content.json", {}).get("title", "")
|
||||||
|
description = site.content_manager.contents.get("content.json", {}).get("description", "")
|
||||||
|
|
||||||
|
body.append(_("""
|
||||||
|
<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>
|
||||||
|
<a href='#Save' class='button' id='button-settings'>{_[Save site settings]}</a>
|
||||||
|
</li>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
def sidebarRenderContents(self, body, site):
|
||||||
|
has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey"))
|
||||||
|
if has_privatekey:
|
||||||
|
tag_privatekey = _("{_[Private key saved.]} <a href='#Forget+private+key' id='privatekey-forget' class='link-right'>{_[Forget]}</a>")
|
||||||
|
else:
|
||||||
|
tag_privatekey = _("<a href='#Add+private+key' id='privatekey-add' class='link-right'>{_[Add saved private key]}</a>")
|
||||||
|
|
||||||
|
body.append(_("""
|
||||||
|
<li>
|
||||||
|
<label>{_[Content publishing]} <small class='label-right'>{tag_privatekey}</small></label>
|
||||||
|
""".replace("{tag_privatekey}", tag_privatekey)))
|
||||||
|
|
||||||
|
# Choose content you want to sign
|
||||||
|
body.append(_("""
|
||||||
|
<div class='flex'>
|
||||||
|
<input type='text' class='text' value="content.json" id='input-contents'/>
|
||||||
|
<a href='#Sign-and-Publish' id='button-sign-publish' class='button'>{_[Sign and publish]}</a>
|
||||||
|
<a href='#Sign-or-Publish' id='menu-sign-publish'>\u22EE</a>
|
||||||
|
</div>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
contents = ["content.json"]
|
||||||
|
contents += list(site.content_manager.contents.get("content.json", {}).get("includes", {}).keys())
|
||||||
|
body.append(_("<div class='contents'>{_[Choose]}: "))
|
||||||
|
for content in contents:
|
||||||
|
body.append(_("<a href='{content}' class='contents-content'>{content}</a> "))
|
||||||
|
body.append("</div>")
|
||||||
|
body.append("</li>")
|
||||||
|
|
||||||
|
@flag.admin
|
||||||
|
def actionSidebarGetHtmlTag(self, to):
|
||||||
|
site = self.site
|
||||||
|
|
||||||
|
body = []
|
||||||
|
|
||||||
|
body.append("<div>")
|
||||||
|
body.append("<a href='#Close' class='close'>×</a>")
|
||||||
|
body.append("<h1>%s</h1>" % html.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True))
|
||||||
|
|
||||||
|
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)
|
||||||
|
has_optional = self.sidebarRenderOptionalFileStats(body, site)
|
||||||
|
if has_optional:
|
||||||
|
self.sidebarRenderOptionalFileSettings(body, site)
|
||||||
|
self.sidebarRenderDbOptions(body, site)
|
||||||
|
self.sidebarRenderIdentity(body, site)
|
||||||
|
self.sidebarRenderControls(body, site)
|
||||||
|
if site.bad_files:
|
||||||
|
self.sidebarRenderBadFiles(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>")
|
||||||
|
|
||||||
|
body.append("<div class='menu template'>")
|
||||||
|
body.append("<a href='#'' class='menu-item template'>Template</a>")
|
||||||
|
body.append("</div>")
|
||||||
|
|
||||||
|
self.response(to, "".join(body))
|
||||||
|
|
||||||
|
def downloadGeoLiteDb(self, db_path):
|
||||||
|
import gzip
|
||||||
|
import shutil
|
||||||
|
from util import helper
|
||||||
|
|
||||||
|
if config.offline:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info("Downloading GeoLite2 City database...")
|
||||||
|
self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
|
||||||
|
db_urls = [
|
||||||
|
"https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz",
|
||||||
|
"https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
|
||||||
|
]
|
||||||
|
for db_url in db_urls:
|
||||||
|
downloadl_err = None
|
||||||
|
try:
|
||||||
|
# Download
|
||||||
|
response = helper.httpRequest(db_url)
|
||||||
|
data_size = response.getheader('content-length')
|
||||||
|
data_recv = 0
|
||||||
|
data = io.BytesIO()
|
||||||
|
while True:
|
||||||
|
buff = response.read(1024 * 512)
|
||||||
|
if not buff:
|
||||||
|
break
|
||||||
|
data.write(buff)
|
||||||
|
data_recv += 1024 * 512
|
||||||
|
if data_size:
|
||||||
|
progress = int(float(data_recv) / int(data_size) * 100)
|
||||||
|
self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
|
||||||
|
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("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100])
|
||||||
|
time.sleep(2) # Wait for notify animation
|
||||||
|
self.log.info("GeoLite2 City database is ready at: %s" % db_path)
|
||||||
|
return True
|
||||||
|
except Exception as err:
|
||||||
|
download_err = err
|
||||||
|
self.log.error("Error downloading %s: %s" % (db_url, err))
|
||||||
|
pass
|
||||||
|
self.cmd("progress", [
|
||||||
|
"geolite-info",
|
||||||
|
_["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(download_err, db_urls[0]),
|
||||||
|
-100
|
||||||
|
])
|
||||||
|
|
||||||
|
def getLoc(self, geodb, ip):
|
||||||
|
global loc_cache
|
||||||
|
|
||||||
|
if ip in loc_cache:
|
||||||
|
return loc_cache[ip]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
loc_data = geodb.get(ip)
|
||||||
|
except:
|
||||||
|
loc_data = None
|
||||||
|
|
||||||
|
if not loc_data or "location" not in loc_data:
|
||||||
|
loc_cache[ip] = None
|
||||||
|
return None
|
||||||
|
|
||||||
|
loc = {
|
||||||
|
"lat": loc_data["location"]["latitude"],
|
||||||
|
"lon": loc_data["location"]["longitude"],
|
||||||
|
}
|
||||||
|
if "city" in loc_data:
|
||||||
|
loc["city"] = loc_data["city"]["names"]["en"]
|
||||||
|
|
||||||
|
if "country" in loc_data:
|
||||||
|
loc["country"] = loc_data["country"]["names"]["en"]
|
||||||
|
|
||||||
|
loc_cache[ip] = loc
|
||||||
|
return loc
|
||||||
|
|
||||||
|
@util.Noparallel()
|
||||||
|
def getGeoipDb(self):
|
||||||
|
db_name = 'GeoLite2-City.mmdb'
|
||||||
|
|
||||||
|
sys_db_paths = []
|
||||||
|
if sys.platform == "linux":
|
||||||
|
sys_db_paths += ['/usr/share/GeoIP/' + db_name]
|
||||||
|
|
||||||
|
data_dir_db_path = os.path.join(config.data_dir, db_name)
|
||||||
|
|
||||||
|
db_paths = sys_db_paths + [data_dir_db_path]
|
||||||
|
|
||||||
|
for path in db_paths:
|
||||||
|
if os.path.isfile(path) and os.path.getsize(path) > 0:
|
||||||
|
return path
|
||||||
|
|
||||||
|
self.log.info("GeoIP database not found at [%s]. Downloading to: %s",
|
||||||
|
" ".join(db_paths), data_dir_db_path)
|
||||||
|
if self.downloadGeoLiteDb(data_dir_db_path):
|
||||||
|
return data_dir_db_path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getPeerLocations(self, peers):
|
||||||
|
import maxminddb
|
||||||
|
|
||||||
|
db_path = self.getGeoipDb()
|
||||||
|
if not db_path:
|
||||||
|
self.log.debug("Not showing peer locations: no GeoIP database")
|
||||||
|
return False
|
||||||
|
|
||||||
|
geodb = maxminddb.open_database(db_path)
|
||||||
|
|
||||||
|
peers = list(peers.values())
|
||||||
|
# Place bars
|
||||||
|
peer_locations = []
|
||||||
|
placed = {} # Already placed bars here
|
||||||
|
for peer in peers:
|
||||||
|
# Height of bar
|
||||||
|
if peer.connection and peer.connection.last_ping_delay:
|
||||||
|
ping = round(peer.connection.last_ping_delay * 1000)
|
||||||
|
else:
|
||||||
|
ping = None
|
||||||
|
loc = self.getLoc(geodb, peer.ip)
|
||||||
|
|
||||||
|
if not loc:
|
||||||
|
continue
|
||||||
|
# Create position array
|
||||||
|
lat, lon = loc["lat"], loc["lon"]
|
||||||
|
latlon = "%s,%s" % (lat, lon)
|
||||||
|
if latlon in placed and peer.getIpType() == "ipv4": # 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
|
||||||
|
peer_location = {}
|
||||||
|
peer_location.update(loc)
|
||||||
|
peer_location["lat"] = lat
|
||||||
|
peer_location["lon"] = lon
|
||||||
|
peer_location["ping"] = ping
|
||||||
|
|
||||||
|
peer_locations.append(peer_location)
|
||||||
|
|
||||||
|
# Append myself
|
||||||
|
for ip in self.site.connection_server.ip_external_list:
|
||||||
|
my_loc = self.getLoc(geodb, ip)
|
||||||
|
if my_loc:
|
||||||
|
my_loc["ping"] = 0
|
||||||
|
peer_locations.append(my_loc)
|
||||||
|
|
||||||
|
return peer_locations
|
||||||
|
|
||||||
|
@flag.admin
|
||||||
|
@flag.async_run
|
||||||
|
def actionSidebarGetPeers(self, to):
|
||||||
|
try:
|
||||||
|
peer_locations = self.getPeerLocations(self.site.peers)
|
||||||
|
globe_data = []
|
||||||
|
ping_times = [
|
||||||
|
peer_location["ping"]
|
||||||
|
for peer_location in peer_locations
|
||||||
|
if peer_location["ping"]
|
||||||
|
]
|
||||||
|
if ping_times:
|
||||||
|
ping_avg = sum(ping_times) / float(len(ping_times))
|
||||||
|
else:
|
||||||
|
ping_avg = 0
|
||||||
|
|
||||||
|
for peer_location in peer_locations:
|
||||||
|
if peer_location["ping"] == 0: # Me
|
||||||
|
height = -0.135
|
||||||
|
elif peer_location["ping"]:
|
||||||
|
height = min(0.20, math.log(1 + peer_location["ping"] / ping_avg, 300))
|
||||||
|
else:
|
||||||
|
height = -0.03
|
||||||
|
|
||||||
|
globe_data += [peer_location["lat"], peer_location["lon"], height]
|
||||||
|
|
||||||
|
self.response(to, globe_data)
|
||||||
|
except Exception as err:
|
||||||
|
self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
|
||||||
|
self.response(to, {"error": str(err)})
|
||||||
|
|
||||||
|
@flag.admin
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionSiteSetOwned(self, to, owned):
|
||||||
|
if self.site.address == config.updatesite:
|
||||||
|
return {"error": "You can't change the ownership of the updater site"}
|
||||||
|
|
||||||
|
self.site.settings["own"] = bool(owned)
|
||||||
|
self.site.updateWebsocket(owned=owned)
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
@flag.admin
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionSiteRecoverPrivatekey(self, to):
|
||||||
|
from Crypt import CryptBitcoin
|
||||||
|
|
||||||
|
site_data = self.user.sites[self.site.address]
|
||||||
|
if site_data.get("privatekey"):
|
||||||
|
return {"error": "This site already has saved privated key"}
|
||||||
|
|
||||||
|
address_index = self.site.content_manager.contents.get("content.json", {}).get("address_index")
|
||||||
|
if not address_index:
|
||||||
|
return {"error": "No address_index in content.json"}
|
||||||
|
|
||||||
|
privatekey = CryptBitcoin.hdPrivatekey(self.user.master_seed, address_index)
|
||||||
|
privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey)
|
||||||
|
|
||||||
|
if privatekey_address == self.site.address:
|
||||||
|
site_data["privatekey"] = privatekey
|
||||||
|
self.user.save()
|
||||||
|
self.site.updateWebsocket(recover_privatekey=True)
|
||||||
|
return "ok"
|
||||||
|
else:
|
||||||
|
return {"error": "Unable to deliver private key for this site from current user's master_seed"}
|
||||||
|
|
||||||
|
@flag.admin
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionUserSetSitePrivatekey(self, to, privatekey):
|
||||||
|
site_data = self.user.sites[self.site.address]
|
||||||
|
site_data["privatekey"] = privatekey
|
||||||
|
self.site.updateWebsocket(set_privatekey=bool(privatekey))
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
@flag.admin
|
||||||
|
@flag.no_multiuser
|
||||||
|
def actionSiteSetAutodownloadoptional(self, to, owned):
|
||||||
|
self.site.settings["autodownloadoptional"] = bool(owned)
|
||||||
|
self.site.worker_manager.removeSolvedFileTasks()
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
@flag.admin
|
||||||
|
def actionDbReload(self, to):
|
||||||
|
self.site.storage.closeDb()
|
||||||
|
self.site.storage.getDb()
|
||||||
|
|
||||||
|
return self.response(to, "ok")
|
||||||
|
|
||||||
|
@flag.no_multiuser
|
||||||
|
@flag.admin
|
||||||
|
def actionDbRebuild(self, to):
|
||||||
|
try:
|
||||||
|
self.site.storage.rebuildDb()
|
||||||
|
except Exception as err:
|
||||||
|
return self.response(to, {"error": str(err)})
|
||||||
|
|
||||||
|
|
||||||
|
return self.response(to, "ok")
|
|
@ -0,0 +1,59 @@
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
class ZipStream(object):
|
||||||
|
def __init__(self, dir_path):
|
||||||
|
self.dir_path = dir_path
|
||||||
|
self.pos = 0
|
||||||
|
self.buff_pos = 0
|
||||||
|
self.zf = zipfile.ZipFile(self, 'w', zipfile.ZIP_DEFLATED, allowZip64=True)
|
||||||
|
self.buff = io.BytesIO()
|
||||||
|
self.file_list = self.getFileList()
|
||||||
|
|
||||||
|
def getFileList(self):
|
||||||
|
for root, dirs, files in os.walk(self.dir_path):
|
||||||
|
for file in files:
|
||||||
|
file_path = root + "/" + file
|
||||||
|
relative_path = os.path.join(os.path.relpath(root, self.dir_path), file)
|
||||||
|
yield file_path, relative_path
|
||||||
|
self.zf.close()
|
||||||
|
|
||||||
|
def read(self, size=60 * 1024):
|
||||||
|
for file_path, relative_path in self.file_list:
|
||||||
|
self.zf.write(file_path, relative_path)
|
||||||
|
if self.buff.tell() >= size:
|
||||||
|
break
|
||||||
|
self.buff.seek(0)
|
||||||
|
back = self.buff.read()
|
||||||
|
self.buff.truncate(0)
|
||||||
|
self.buff.seek(0)
|
||||||
|
self.buff_pos += len(back)
|
||||||
|
return back
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.pos += len(data)
|
||||||
|
self.buff.write(data)
|
||||||
|
|
||||||
|
def tell(self):
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
def seek(self, pos, whence=0):
|
||||||
|
if pos >= self.buff_pos:
|
||||||
|
self.buff.seek(pos - self.buff_pos, whence)
|
||||||
|
self.pos = pos
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
zs = ZipStream(".")
|
||||||
|
out = open("out.zip", "wb")
|
||||||
|
while 1:
|
||||||
|
data = zs.read()
|
||||||
|
print("Write %s" % len(data))
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
out.write(data)
|
||||||
|
out.close()
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import SidebarPlugin
|
||||||
|
from . import ConsolePlugin
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"Peers": "Klienter",
|
||||||
|
"Connected": "Forbundet",
|
||||||
|
"Connectable": "Mulige",
|
||||||
|
"Connectable peers": "Mulige klienter",
|
||||||
|
|
||||||
|
"Data transfer": "Data overførsel",
|
||||||
|
"Received": "Modtaget",
|
||||||
|
"Received bytes": "Bytes modtaget",
|
||||||
|
"Sent": "Sendt",
|
||||||
|
"Sent bytes": "Bytes sendt",
|
||||||
|
|
||||||
|
"Files": "Filer",
|
||||||
|
"Total": "I alt",
|
||||||
|
"Image": "Image",
|
||||||
|
"Other": "Andet",
|
||||||
|
"User data": "Bruger data",
|
||||||
|
|
||||||
|
"Size limit": "Side max størrelse",
|
||||||
|
"limit used": "brugt",
|
||||||
|
"free space": "fri",
|
||||||
|
"Set": "Opdater",
|
||||||
|
|
||||||
|
"Optional files": "Valgfri filer",
|
||||||
|
"Downloaded": "Downloadet",
|
||||||
|
"Download and help distribute all files": "Download og hjælp med at dele filer",
|
||||||
|
"Total size": "Størrelse i alt",
|
||||||
|
"Downloaded files": "Filer downloadet",
|
||||||
|
|
||||||
|
"Database": "Database",
|
||||||
|
"search feeds": "søgninger",
|
||||||
|
"{feeds} query": "{feeds} søgninger",
|
||||||
|
"Reload": "Genindlæs",
|
||||||
|
"Rebuild": "Genopbyg",
|
||||||
|
"No database found": "Ingen database fundet",
|
||||||
|
|
||||||
|
"Identity address": "Autorisations ID",
|
||||||
|
"Change": "Skift",
|
||||||
|
|
||||||
|
"Update": "Opdater",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Resume": "Aktiv",
|
||||||
|
"Delete": "Slet",
|
||||||
|
"Are you sure?": "Er du sikker?",
|
||||||
|
|
||||||
|
"Site address": "Side addresse",
|
||||||
|
"Donate": "Doner penge",
|
||||||
|
|
||||||
|
"Missing files": "Manglende filer",
|
||||||
|
"{} try": "{} forsøg",
|
||||||
|
"{} tries": "{} forsøg",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} mere",
|
||||||
|
|
||||||
|
"This is my site": "Dette er min side",
|
||||||
|
"Site title": "Side navn",
|
||||||
|
"Site description": "Side beskrivelse",
|
||||||
|
"Save site settings": "Gem side opsætning",
|
||||||
|
|
||||||
|
"Content publishing": "Indhold offentliggøres",
|
||||||
|
"Choose": "Vælg",
|
||||||
|
"Sign": "Signer",
|
||||||
|
"Publish": "Offentliggør",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "Denne funktion er slået fra på denne ZeroNet proxyEz a funkció ki van kapcsolva ezen a proxy-n",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 City database kunne ikke downloades: {}!<br>Download venligst databasen manuelt og udpak i data folder:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "GeoLite2 City database downloadet!",
|
||||||
|
|
||||||
|
"Are you sure?": "Er du sikker?",
|
||||||
|
"Site storage limit modified!": "Side max størrelse ændret!",
|
||||||
|
"Database schema reloaded!": "Database definition genindlæst!",
|
||||||
|
"Database rebuilding....": "Genopbygger database...",
|
||||||
|
"Database rebuilt!": "Database genopbygget!",
|
||||||
|
"Site updated!": "Side opdateret!",
|
||||||
|
"Delete this site": "Slet denne side",
|
||||||
|
"File write error: ": "Fejl ved skrivning af fil: ",
|
||||||
|
"Site settings saved!": "Side opsætning gemt!",
|
||||||
|
"Enter your private key:": "Indtast din private nøgle:",
|
||||||
|
" Signed!": " Signeret!",
|
||||||
|
"WebGL not supported": "WebGL er ikke supporteret"
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"Peers": "Peers",
|
||||||
|
"Connected": "Verbunden",
|
||||||
|
"Connectable": "Verbindbar",
|
||||||
|
"Connectable peers": "Verbindbare Peers",
|
||||||
|
|
||||||
|
"Data transfer": "Datei Transfer",
|
||||||
|
"Received": "Empfangen",
|
||||||
|
"Received bytes": "Empfangene Bytes",
|
||||||
|
"Sent": "Gesendet",
|
||||||
|
"Sent bytes": "Gesendete Bytes",
|
||||||
|
|
||||||
|
"Files": "Dateien",
|
||||||
|
"Total": "Gesamt",
|
||||||
|
"Image": "Bilder",
|
||||||
|
"Other": "Sonstiges",
|
||||||
|
"User data": "Nutzer Daten",
|
||||||
|
|
||||||
|
"Size limit": "Speicher Limit",
|
||||||
|
"limit used": "Limit benutzt",
|
||||||
|
"free space": "freier Speicher",
|
||||||
|
"Set": "Setzten",
|
||||||
|
|
||||||
|
"Optional files": "Optionale Dateien",
|
||||||
|
"Downloaded": "Heruntergeladen",
|
||||||
|
"Download and help distribute all files": "Herunterladen und helfen alle Dateien zu verteilen",
|
||||||
|
"Total size": "Gesamte Größe",
|
||||||
|
"Downloaded files": "Heruntergeladene Dateien",
|
||||||
|
|
||||||
|
"Database": "Datenbank",
|
||||||
|
"search feeds": "Feeds durchsuchen",
|
||||||
|
"{feeds} query": "{feeds} Abfrage",
|
||||||
|
"Reload": "Neu laden",
|
||||||
|
"Rebuild": "Neu bauen",
|
||||||
|
"No database found": "Keine Datenbank gefunden",
|
||||||
|
|
||||||
|
"Identity address": "Identitäts Adresse",
|
||||||
|
"Change": "Ändern",
|
||||||
|
|
||||||
|
"Update": "Aktualisieren",
|
||||||
|
"Pause": "Pausieren",
|
||||||
|
"Resume": "Fortsetzen",
|
||||||
|
"Delete": "Löschen",
|
||||||
|
"Are you sure?": "Bist du sicher?",
|
||||||
|
|
||||||
|
"Site address": "Seiten Adresse",
|
||||||
|
"Donate": "Spenden",
|
||||||
|
|
||||||
|
"Missing files": "Fehlende Dateien",
|
||||||
|
"{} try": "{} versuch",
|
||||||
|
"{} tries": "{} versuche",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} mehr",
|
||||||
|
|
||||||
|
"This is my site": "Das ist meine Seite",
|
||||||
|
"Site title": "Seiten Titel",
|
||||||
|
"Site description": "Seiten Beschreibung",
|
||||||
|
"Save site settings": "Einstellungen der Seite speichern",
|
||||||
|
|
||||||
|
"Content publishing": "Inhaltsveröffentlichung",
|
||||||
|
"Choose": "Wähle",
|
||||||
|
"Sign": "Signieren",
|
||||||
|
"Publish": "Veröffentlichen",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "Diese Funktion ist auf dieser Proxy deaktiviert",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 City Datenbank Download Fehler: {}!<br>Bitte manuell herunterladen und die Datei in das Datei Verzeichnis extrahieren:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Herunterladen der GeoLite2 City Datenbank (einmalig, ~20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "GeoLite2 City Datenbank heruntergeladen!",
|
||||||
|
|
||||||
|
"Are you sure?": "Bist du sicher?",
|
||||||
|
"Site storage limit modified!": "Speicher Limit der Seite modifiziert!",
|
||||||
|
"Database schema reloaded!": "Datebank Schema neu geladen!",
|
||||||
|
"Database rebuilding....": "Datenbank neu bauen...",
|
||||||
|
"Database rebuilt!": "Datenbank neu gebaut!",
|
||||||
|
"Site updated!": "Seite aktualisiert!",
|
||||||
|
"Delete this site": "Diese Seite löschen",
|
||||||
|
"File write error: ": "Datei schreib fehler:",
|
||||||
|
"Site settings saved!": "Seiten Einstellungen gespeichert!",
|
||||||
|
"Enter your private key:": "Gib deinen privaten Schlüssel ein:",
|
||||||
|
" Signed!": " Signiert!",
|
||||||
|
"WebGL not supported": "WebGL nicht unterstützt"
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"Peers": "Pares",
|
||||||
|
"Connected": "Conectados",
|
||||||
|
"Connectable": "Conectables",
|
||||||
|
"Connectable peers": "Pares conectables",
|
||||||
|
|
||||||
|
"Data transfer": "Transferencia de datos",
|
||||||
|
"Received": "Recibidos",
|
||||||
|
"Received bytes": "Bytes recibidos",
|
||||||
|
"Sent": "Enviados",
|
||||||
|
"Sent bytes": "Bytes envidados",
|
||||||
|
|
||||||
|
"Files": "Ficheros",
|
||||||
|
"Total": "Total",
|
||||||
|
"Image": "Imagen",
|
||||||
|
"Other": "Otro",
|
||||||
|
"User data": "Datos del usuario",
|
||||||
|
|
||||||
|
"Size limit": "Límite de tamaño",
|
||||||
|
"limit used": "Límite utilizado",
|
||||||
|
"free space": "Espacio libre",
|
||||||
|
"Set": "Establecer",
|
||||||
|
|
||||||
|
"Optional files": "Ficheros opcionales",
|
||||||
|
"Downloaded": "Descargado",
|
||||||
|
"Download and help distribute all files": "Descargar y ayudar a distribuir todos los ficheros",
|
||||||
|
"Total size": "Tamaño total",
|
||||||
|
"Downloaded files": "Ficheros descargados",
|
||||||
|
|
||||||
|
"Database": "Base de datos",
|
||||||
|
"search feeds": "Fuentes de búsqueda",
|
||||||
|
"{feeds} query": "{feeds} consulta",
|
||||||
|
"Reload": "Recargar",
|
||||||
|
"Rebuild": "Reconstruir",
|
||||||
|
"No database found": "No se ha encontrado la base de datos",
|
||||||
|
|
||||||
|
"Identity address": "Dirección de la identidad",
|
||||||
|
"Change": "Cambiar",
|
||||||
|
|
||||||
|
"Update": "Actualizar",
|
||||||
|
"Pause": "Pausar",
|
||||||
|
"Resume": "Reanudar",
|
||||||
|
"Delete": "Borrar",
|
||||||
|
|
||||||
|
"Site address": "Dirección del sitio",
|
||||||
|
"Donate": "Donar",
|
||||||
|
|
||||||
|
"Missing files": "Ficheros perdidos",
|
||||||
|
"{} try": "{} intento",
|
||||||
|
"{} tries": "{} intentos",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} más",
|
||||||
|
|
||||||
|
"This is my site": "Este es mi sitio",
|
||||||
|
"Site title": "Título del sitio",
|
||||||
|
"Site description": "Descripción del sitio",
|
||||||
|
"Save site settings": "Guardar la configuración del sitio",
|
||||||
|
|
||||||
|
"Content publishing": "Publicación del contenido",
|
||||||
|
"Choose": "Elegir",
|
||||||
|
"Sign": "Firmar",
|
||||||
|
"Publish": "Publicar",
|
||||||
|
"This function is disabled on this proxy": "Esta función está deshabilitada en este proxy",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "¡Error de la base de datos GeoLite2: {}!<br>Por favor, descárgalo manualmente y descomprime al directorio de datos:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Descargando la base de datos de GeoLite2 (una única vez, ~20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "¡Base de datos de GeoLite2 descargada!",
|
||||||
|
|
||||||
|
"Are you sure?": "¿Estás seguro?",
|
||||||
|
"Site storage limit modified!": "¡Límite de almacenamiento del sitio modificado!",
|
||||||
|
"Database schema reloaded!": "¡Esquema de la base de datos recargado!",
|
||||||
|
"Database rebuilding....": "Reconstruyendo la base de datos...",
|
||||||
|
"Database rebuilt!": "¡Base de datos reconstruida!",
|
||||||
|
"Site updated!": "¡Sitio actualizado!",
|
||||||
|
"Delete this site": "Borrar este sitio",
|
||||||
|
"File write error: ": "Error de escritura de fichero:",
|
||||||
|
"Site settings saved!": "¡Configuración del sitio guardada!",
|
||||||
|
"Enter your private key:": "Introduce tu clave privada:",
|
||||||
|
" Signed!": " ¡firmado!",
|
||||||
|
"WebGL not supported": "WebGL no está soportado"
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"Peers": "Pairs",
|
||||||
|
"Connected": "Connectés",
|
||||||
|
"Connectable": "Accessibles",
|
||||||
|
"Connectable peers": "Pairs accessibles",
|
||||||
|
|
||||||
|
"Data transfer": "Données transférées",
|
||||||
|
"Received": "Reçues",
|
||||||
|
"Received bytes": "Bytes reçus",
|
||||||
|
"Sent": "Envoyées",
|
||||||
|
"Sent bytes": "Bytes envoyés",
|
||||||
|
|
||||||
|
"Files": "Fichiers",
|
||||||
|
"Total": "Total",
|
||||||
|
"Image": "Image",
|
||||||
|
"Other": "Autre",
|
||||||
|
"User data": "Utilisateurs",
|
||||||
|
|
||||||
|
"Size limit": "Taille maximale",
|
||||||
|
"limit used": "utlisé",
|
||||||
|
"free space": "libre",
|
||||||
|
"Set": "Modifier",
|
||||||
|
|
||||||
|
"Optional files": "Fichiers optionnels",
|
||||||
|
"Downloaded": "Téléchargé",
|
||||||
|
"Download and help distribute all files": "Télécharger et distribuer tous les fichiers",
|
||||||
|
"Total size": "Taille totale",
|
||||||
|
"Downloaded files": "Fichiers téléchargés",
|
||||||
|
|
||||||
|
"Database": "Base de données",
|
||||||
|
"search feeds": "recherche",
|
||||||
|
"{feeds} query": "{feeds} requête",
|
||||||
|
"Reload": "Recharger",
|
||||||
|
"Rebuild": "Reconstruire",
|
||||||
|
"No database found": "Aucune base de données trouvée",
|
||||||
|
|
||||||
|
"Identity address": "Adresse d'identité",
|
||||||
|
"Change": "Modifier",
|
||||||
|
|
||||||
|
"Site control": "Opérations",
|
||||||
|
"Update": "Mettre à jour",
|
||||||
|
"Pause": "Suspendre",
|
||||||
|
"Resume": "Reprendre",
|
||||||
|
"Delete": "Supprimer",
|
||||||
|
"Are you sure?": "Êtes-vous certain?",
|
||||||
|
|
||||||
|
"Site address": "Adresse du site",
|
||||||
|
"Donate": "Faire un don",
|
||||||
|
|
||||||
|
"Missing files": "Fichiers manquants",
|
||||||
|
"{} try": "{} essai",
|
||||||
|
"{} tries": "{} essais",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} manquants",
|
||||||
|
|
||||||
|
"This is my site": "Ce site m'appartient",
|
||||||
|
"Site title": "Nom du site",
|
||||||
|
"Site description": "Description du site",
|
||||||
|
"Save site settings": "Enregistrer les paramètres",
|
||||||
|
|
||||||
|
"Content publishing": "Publication du contenu",
|
||||||
|
"Choose": "Sélectionner",
|
||||||
|
"Sign": "Signer",
|
||||||
|
"Publish": "Publier",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "Cette fonction est désactivé sur ce proxy",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Erreur au téléchargement de la base de données GeoLite2: {}!<br>Téléchargez et décompressez dans le dossier data:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Téléchargement de la base de données GeoLite2 (une seule fois, ~20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "Base de données GeoLite2 téléchargée!",
|
||||||
|
|
||||||
|
"Are you sure?": "Êtes-vous certain?",
|
||||||
|
"Site storage limit modified!": "Taille maximale modifiée!",
|
||||||
|
"Database schema reloaded!": "Base de données rechargée!",
|
||||||
|
"Database rebuilding....": "Reconstruction de la base de données...",
|
||||||
|
"Database rebuilt!": "Base de données reconstruite!",
|
||||||
|
"Site updated!": "Site mis à jour!",
|
||||||
|
"Delete this site": "Supprimer ce site",
|
||||||
|
"File write error: ": "Erreur à l'écriture du fichier: ",
|
||||||
|
"Site settings saved!": "Paramètres du site enregistrés!",
|
||||||
|
"Enter your private key:": "Entrez votre clé privée:",
|
||||||
|
" Signed!": " Signé!",
|
||||||
|
"WebGL not supported": "WebGL n'est pas supporté"
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"Peers": "Csatlakozási pontok",
|
||||||
|
"Connected": "Csaltakozva",
|
||||||
|
"Connectable": "Csatlakozható",
|
||||||
|
"Connectable peers": "Csatlakozható peer-ek",
|
||||||
|
|
||||||
|
"Data transfer": "Adatátvitel",
|
||||||
|
"Received": "Fogadott",
|
||||||
|
"Received bytes": "Fogadott byte-ok",
|
||||||
|
"Sent": "Küldött",
|
||||||
|
"Sent bytes": "Küldött byte-ok",
|
||||||
|
|
||||||
|
"Files": "Fájlok",
|
||||||
|
"Total": "Összesen",
|
||||||
|
"Image": "Kép",
|
||||||
|
"Other": "Egyéb",
|
||||||
|
"User data": "Felh. adat",
|
||||||
|
|
||||||
|
"Size limit": "Méret korlát",
|
||||||
|
"limit used": "felhasznált",
|
||||||
|
"free space": "szabad hely",
|
||||||
|
"Set": "Beállít",
|
||||||
|
|
||||||
|
"Optional files": "Opcionális fájlok",
|
||||||
|
"Downloaded": "Letöltött",
|
||||||
|
"Download and help distribute all files": "Minden opcionális fájl letöltése",
|
||||||
|
"Total size": "Teljes méret",
|
||||||
|
"Downloaded files": "Letöltve",
|
||||||
|
|
||||||
|
"Database": "Adatbázis",
|
||||||
|
"search feeds": "Keresés források",
|
||||||
|
"{feeds} query": "{feeds} lekérdezés",
|
||||||
|
"Reload": "Újratöltés",
|
||||||
|
"Rebuild": "Újraépítés",
|
||||||
|
"No database found": "Adatbázis nem található",
|
||||||
|
|
||||||
|
"Identity address": "Azonosító cím",
|
||||||
|
"Change": "Módosít",
|
||||||
|
|
||||||
|
"Site control": "Oldal műveletek",
|
||||||
|
"Update": "Frissít",
|
||||||
|
"Pause": "Szünteltet",
|
||||||
|
"Resume": "Folytat",
|
||||||
|
"Delete": "Töröl",
|
||||||
|
"Are you sure?": "Biztos vagy benne?",
|
||||||
|
|
||||||
|
"Site address": "Oldal címe",
|
||||||
|
"Donate": "Támogatás",
|
||||||
|
|
||||||
|
"Missing files": "Hiányzó fájlok",
|
||||||
|
"{} try": "{} próbálkozás",
|
||||||
|
"{} tries": "{} próbálkozás",
|
||||||
|
"+ {num_bad_files} more": "+ még {num_bad_files} darab",
|
||||||
|
|
||||||
|
"This is my site": "Ez az én oldalam",
|
||||||
|
"Site title": "Oldal neve",
|
||||||
|
"Site description": "Oldal leírása",
|
||||||
|
"Save site settings": "Oldal beállítások mentése",
|
||||||
|
|
||||||
|
"Content publishing": "Tartalom publikálás",
|
||||||
|
"Choose": "Válassz",
|
||||||
|
"Sign": "Aláírás",
|
||||||
|
"Publish": "Publikálás",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "Ez a funkció ki van kapcsolva ezen a proxy-n",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 város adatbázis letöltési hiba: {}!<br>A térképhez töltsd le és csomagold ki a data könyvtárba:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "GeoLite2 város adatbázis letöltve!",
|
||||||
|
|
||||||
|
"Are you sure?": "Biztos vagy benne?",
|
||||||
|
"Site storage limit modified!": "Az oldalt méret korlát módosítva!",
|
||||||
|
"Database schema reloaded!": "Adatbázis séma újratöltve!",
|
||||||
|
"Database rebuilding....": "Adatbázis újraépítés...",
|
||||||
|
"Database rebuilt!": "Adatbázis újraépítve!",
|
||||||
|
"Site updated!": "Az oldal frissítve!",
|
||||||
|
"Delete this site": "Az oldal törlése",
|
||||||
|
"File write error: ": "Fájl írási hiba: ",
|
||||||
|
"Site settings saved!": "Az oldal beállításai elmentve!",
|
||||||
|
"Enter your private key:": "Add meg a privát kulcsod:",
|
||||||
|
" Signed!": " Aláírva!",
|
||||||
|
"WebGL not supported": "WebGL nem támogatott"
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"Peers": "Peer",
|
||||||
|
"Connected": "Connessi",
|
||||||
|
"Connectable": "Collegabili",
|
||||||
|
"Connectable peers": "Peer collegabili",
|
||||||
|
|
||||||
|
"Data transfer": "Trasferimento dati",
|
||||||
|
"Received": "Ricevuti",
|
||||||
|
"Received bytes": "Byte ricevuti",
|
||||||
|
"Sent": "Inviati",
|
||||||
|
"Sent bytes": "Byte inviati",
|
||||||
|
|
||||||
|
"Files": "File",
|
||||||
|
"Total": "Totale",
|
||||||
|
"Image": "Imagine",
|
||||||
|
"Other": "Altro",
|
||||||
|
"User data": "Dati utente",
|
||||||
|
|
||||||
|
"Size limit": "Limite dimensione",
|
||||||
|
"limit used": "limite usato",
|
||||||
|
"free space": "spazio libero",
|
||||||
|
"Set": "Imposta",
|
||||||
|
|
||||||
|
"Optional files": "File facoltativi",
|
||||||
|
"Downloaded": "Scaricati",
|
||||||
|
"Download and help distribute all files": "Scarica e aiuta a distribuire tutti i file",
|
||||||
|
"Total size": "Dimensione totale",
|
||||||
|
"Downloaded files": "File scaricati",
|
||||||
|
|
||||||
|
"Database": "Database",
|
||||||
|
"search feeds": "ricerca di feed",
|
||||||
|
"{feeds} query": "{feeds} interrogazione",
|
||||||
|
"Reload": "Ricaricare",
|
||||||
|
"Rebuild": "Ricostruire",
|
||||||
|
"No database found": "Nessun database trovato",
|
||||||
|
|
||||||
|
"Identity address": "Indirizzo di identità",
|
||||||
|
"Change": "Cambia",
|
||||||
|
|
||||||
|
"Update": "Aggiorna",
|
||||||
|
"Pause": "Sospendi",
|
||||||
|
"Resume": "Riprendi",
|
||||||
|
"Delete": "Cancella",
|
||||||
|
"Are you sure?": "Sei sicuro?",
|
||||||
|
|
||||||
|
"Site address": "Indirizzo sito",
|
||||||
|
"Donate": "Dona",
|
||||||
|
|
||||||
|
"Missing files": "File mancanti",
|
||||||
|
"{} try": "{} tenta",
|
||||||
|
"{} tries": "{} prova",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} altri",
|
||||||
|
|
||||||
|
"This is my site": "Questo è il mio sito",
|
||||||
|
"Site title": "Titolo sito",
|
||||||
|
"Site description": "Descrizione sito",
|
||||||
|
"Save site settings": "Salva impostazioni sito",
|
||||||
|
|
||||||
|
"Content publishing": "Pubblicazione contenuto",
|
||||||
|
"Choose": "Scegli",
|
||||||
|
"Sign": "Firma",
|
||||||
|
"Publish": "Pubblica",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "Questa funzione è disabilitata su questo proxy",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Errore scaricamento database GeoLite2 City: {}!<br>Si prega di scaricarlo manualmente e spacchetarlo nella cartella dir:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Scaricamento database GeoLite2 City (solo una volta, ~20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "Database GeoLite2 City scaricato!",
|
||||||
|
|
||||||
|
"Are you sure?": "Sei sicuro?",
|
||||||
|
"Site storage limit modified!": "Limite di archiviazione del sito modificato!",
|
||||||
|
"Database schema reloaded!": "Schema database ricaricato!",
|
||||||
|
"Database rebuilding....": "Ricostruzione database...",
|
||||||
|
"Database rebuilt!": "Database ricostruito!",
|
||||||
|
"Site updated!": "Sito aggiornato!",
|
||||||
|
"Delete this site": "Cancella questo sito",
|
||||||
|
"File write error: ": "Errore scrittura file:",
|
||||||
|
"Site settings saved!": "Impostazioni sito salvate!",
|
||||||
|
"Enter your private key:": "Inserisci la tua chiave privata:",
|
||||||
|
" Signed!": " Firmato!",
|
||||||
|
"WebGL not supported": "WebGL non supportato"
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"Copy to clipboard": "クリップボードにコピー",
|
||||||
|
"Peers": "ピア",
|
||||||
|
"Connected": "接続済み",
|
||||||
|
"Connectable": "利用可能",
|
||||||
|
"Connectable peers": "ピアに接続可能",
|
||||||
|
"Onion": "Onion",
|
||||||
|
"Local": "ローカル",
|
||||||
|
|
||||||
|
"Data transfer": "データ転送",
|
||||||
|
"Received": "受信",
|
||||||
|
"Received bytes": "受信バイト数",
|
||||||
|
"Sent": "送信",
|
||||||
|
"Sent bytes": "送信バイト数",
|
||||||
|
|
||||||
|
"Files": "ファイル",
|
||||||
|
"Browse files": "ファイルを見る",
|
||||||
|
"Save as .zip": "ZIP形式で保存",
|
||||||
|
"Total": "合計",
|
||||||
|
"Image": "画像",
|
||||||
|
"Other": "その他",
|
||||||
|
"User data": "ユーザーデータ",
|
||||||
|
|
||||||
|
"Size limit": "サイズ制限",
|
||||||
|
"limit used": "使用上限",
|
||||||
|
"free space": "フリースペース",
|
||||||
|
"Set": "セット",
|
||||||
|
|
||||||
|
"Optional files": "オプション ファイル",
|
||||||
|
"Downloaded": "ダウンロード済み",
|
||||||
|
"Help distribute added optional files": "オプションファイルの配布を支援する",
|
||||||
|
"Auto download big file size limit": "大きなファイルの自動ダウンロードのサイズ制限",
|
||||||
|
"Download previous files": "以前のファイルのダウンロード",
|
||||||
|
"Optional files download started": "オプションファイルのダウンロードを開始",
|
||||||
|
"Optional files downloaded": "オプションファイルのダウンロードが完了しました",
|
||||||
|
"Download and help distribute all files": "ダウンロードしてすべてのファイルの配布を支援する",
|
||||||
|
"Total size": "合計サイズ",
|
||||||
|
"Downloaded files": "ダウンロードされたファイル",
|
||||||
|
|
||||||
|
"Database": "データベース",
|
||||||
|
"search feeds": "フィードを検索する",
|
||||||
|
"{feeds} query": "{feeds} お問い合わせ",
|
||||||
|
"Reload": "再読込",
|
||||||
|
"Rebuild": "再ビルド",
|
||||||
|
"No database found": "データベースが見つかりません",
|
||||||
|
|
||||||
|
"Identity address": "あなたの識別アドレス",
|
||||||
|
"Change": "編集",
|
||||||
|
|
||||||
|
"Site control": "サイト管理",
|
||||||
|
"Update": "更新",
|
||||||
|
"Pause": "一時停止",
|
||||||
|
"Resume": "再開",
|
||||||
|
"Delete": "削除",
|
||||||
|
"Are you sure?": "本当によろしいですか?",
|
||||||
|
|
||||||
|
"Site address": "サイトアドレス",
|
||||||
|
"Donate": "寄付する",
|
||||||
|
|
||||||
|
"Missing files": "ファイルがありません",
|
||||||
|
"{} try": "{} 試す",
|
||||||
|
"{} tries": "{} 試行",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} more",
|
||||||
|
|
||||||
|
"This is my site": "これは私のサイトです",
|
||||||
|
"Site title": "サイトタイトル",
|
||||||
|
"Site description": "サイトの説明",
|
||||||
|
"Save site settings": "サイトの設定を保存する",
|
||||||
|
"Open site directory": "サイトのディレクトリを開く",
|
||||||
|
|
||||||
|
"Content publishing": "コンテンツを公開する",
|
||||||
|
"Add saved private key": "秘密鍵の追加と保存",
|
||||||
|
"Save": "保存",
|
||||||
|
"Private key saved.": "秘密鍵が保存されています",
|
||||||
|
"Private key saved for site signing": "サイトに署名するための秘密鍵を保存",
|
||||||
|
"Forgot": "わすれる",
|
||||||
|
"Saved private key removed": "保存された秘密鍵を削除しました",
|
||||||
|
"Choose": "選択",
|
||||||
|
"Sign": "署名",
|
||||||
|
"Publish": "公開する",
|
||||||
|
"Sign and publish": "署名して公開",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "この機能はこのプロキシで無効になっています",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 Cityデータベースのダウンロードエラー: {}!<br>手動でダウンロードして、フォルダに解凍してください。:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Cityデータベースの読み込み (これは一度だけ行われます, ~20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "GeoLite2 Cityデータベースがダウンロードされました!",
|
||||||
|
|
||||||
|
"Are you sure?": "本当によろしいですか?",
|
||||||
|
"Site storage limit modified!": "サイトの保存容量の制限が変更されました!",
|
||||||
|
"Database schema reloaded!": "データベーススキーマがリロードされました!",
|
||||||
|
"Database rebuilding....": "データベースの再構築中....",
|
||||||
|
"Database rebuilt!": "データベースが再構築されました!",
|
||||||
|
"Site updated!": "サイトが更新されました!",
|
||||||
|
"Delete this site": "このサイトを削除する",
|
||||||
|
"Blacklist": "NG",
|
||||||
|
"Blacklist this site": "NGリストに入れる",
|
||||||
|
"Reason": "理由",
|
||||||
|
"Delete and Blacklist": "削除してNG",
|
||||||
|
"File write error: ": "ファイル書き込みエラー:",
|
||||||
|
"Site settings saved!": "サイト設定が保存されました!",
|
||||||
|
"Enter your private key:": "秘密鍵を入力してください:",
|
||||||
|
" Signed!": " 署名しました!",
|
||||||
|
"WebGL not supported": "WebGLはサポートされていません"
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"Peers": "Użytkownicy równorzędni",
|
||||||
|
"Connected": "Połączony",
|
||||||
|
"Connectable": "Możliwy do podłączenia",
|
||||||
|
"Connectable peers": "Połączeni użytkownicy równorzędni",
|
||||||
|
|
||||||
|
"Data transfer": "Transfer danych",
|
||||||
|
"Received": "Odebrane",
|
||||||
|
"Received bytes": "Odebrany bajty",
|
||||||
|
"Sent": "Wysłane",
|
||||||
|
"Sent bytes": "Wysłane bajty",
|
||||||
|
|
||||||
|
"Files": "Pliki",
|
||||||
|
"Total": "Sumarycznie",
|
||||||
|
"Image": "Obraz",
|
||||||
|
"Other": "Inne",
|
||||||
|
"User data": "Dane użytkownika",
|
||||||
|
|
||||||
|
"Size limit": "Rozmiar limitu",
|
||||||
|
"limit used": "zużyty limit",
|
||||||
|
"free space": "wolna przestrzeń",
|
||||||
|
"Set": "Ustaw",
|
||||||
|
|
||||||
|
"Optional files": "Pliki opcjonalne",
|
||||||
|
"Downloaded": "Ściągnięte",
|
||||||
|
"Download and help distribute all files": "Ściągnij i pomóż rozpowszechniać wszystkie pliki",
|
||||||
|
"Total size": "Rozmiar sumaryczny",
|
||||||
|
"Downloaded files": "Ściągnięte pliki",
|
||||||
|
|
||||||
|
"Database": "Baza danych",
|
||||||
|
"search feeds": "przeszukaj zasoby",
|
||||||
|
"{feeds} query": "{feeds} pytanie",
|
||||||
|
"Reload": "Odśwież",
|
||||||
|
"Rebuild": "Odbuduj",
|
||||||
|
"No database found": "Nie odnaleziono bazy danych",
|
||||||
|
|
||||||
|
"Identity address": "Adres identyfikacyjny",
|
||||||
|
"Change": "Zmień",
|
||||||
|
|
||||||
|
"Site control": "Kontrola strony",
|
||||||
|
"Update": "Zaktualizuj",
|
||||||
|
"Pause": "Wstrzymaj",
|
||||||
|
"Resume": "Wznów",
|
||||||
|
"Delete": "Skasuj",
|
||||||
|
"Are you sure?": "Jesteś pewien?",
|
||||||
|
|
||||||
|
"Site address": "Adres strony",
|
||||||
|
"Donate": "Wspomóż",
|
||||||
|
|
||||||
|
"Missing files": "Brakujące pliki",
|
||||||
|
"{} try": "{} próba",
|
||||||
|
"{} tries": "{} próby",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} więcej",
|
||||||
|
|
||||||
|
"This is my site": "To moja strona",
|
||||||
|
"Site title": "Tytuł strony",
|
||||||
|
"Site description": "Opis strony",
|
||||||
|
"Save site settings": "Zapisz ustawienia strony",
|
||||||
|
|
||||||
|
"Content publishing": "Publikowanie treści",
|
||||||
|
"Choose": "Wybierz",
|
||||||
|
"Sign": "Podpisz",
|
||||||
|
"Publish": "Opublikuj",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Błąd ściągania bazy danych GeoLite2 City: {}!<br>Proszę ściągnąć ją recznie i wypakować do katalogu danych:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Ściąganie bazy danych GeoLite2 City (tylko jednorazowo, ok. 20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "Baza danych GeoLite2 City ściagnięta!",
|
||||||
|
|
||||||
|
"Are you sure?": "Jesteś pewien?",
|
||||||
|
"Site storage limit modified!": "Limit pamięci strony zmodyfikowany!",
|
||||||
|
"Database schema reloaded!": "Schemat bazy danych załadowany ponownie!",
|
||||||
|
"Database rebuilding....": "Przebudowywanie bazy danych...",
|
||||||
|
"Database rebuilt!": "Baza danych przebudowana!",
|
||||||
|
"Site updated!": "Strona zaktualizowana!",
|
||||||
|
"Delete this site": "Usuń tę stronę",
|
||||||
|
"File write error: ": "Błąd zapisu pliku: ",
|
||||||
|
"Site settings saved!": "Ustawienia strony zapisane!",
|
||||||
|
"Enter your private key:": "Wpisz swój prywatny klucz:",
|
||||||
|
" Signed!": " Podpisane!",
|
||||||
|
"WebGL not supported": "WebGL nie jest obsługiwany"
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"Copy to clipboard": "Copiar para área de transferência (clipboard)",
|
||||||
|
"Peers": "Peers",
|
||||||
|
"Connected": "Ligados",
|
||||||
|
"Connectable": "Disponíveis",
|
||||||
|
"Onion": "Onion",
|
||||||
|
"Local": "Locais",
|
||||||
|
"Connectable peers": "Peers disponíveis",
|
||||||
|
|
||||||
|
"Data transfer": "Transferência de dados",
|
||||||
|
"Received": "Recebidos",
|
||||||
|
"Received bytes": "Bytes recebidos",
|
||||||
|
"Sent": "Enviados",
|
||||||
|
"Sent bytes": "Bytes enviados",
|
||||||
|
|
||||||
|
"Files": "Arquivos",
|
||||||
|
"Save as .zip": "Salvar como .zip",
|
||||||
|
"Total": "Total",
|
||||||
|
"Image": "Imagem",
|
||||||
|
"Other": "Outros",
|
||||||
|
"User data": "Dados do usuário",
|
||||||
|
|
||||||
|
"Size limit": "Limite de tamanho",
|
||||||
|
"limit used": "limite utilizado",
|
||||||
|
"free space": "espaço livre",
|
||||||
|
"Set": "Definir",
|
||||||
|
|
||||||
|
"Optional files": "Arquivos opcionais",
|
||||||
|
"Downloaded": "Baixados",
|
||||||
|
"Download and help distribute all files": "Baixar e ajudar a distribuir todos os arquivos",
|
||||||
|
"Total size": "Tamanho total",
|
||||||
|
"Downloaded files": "Arquivos baixados",
|
||||||
|
|
||||||
|
"Database": "Banco de dados",
|
||||||
|
"search feeds": "pesquisar feeds",
|
||||||
|
"{feeds} query": "consulta de {feeds}",
|
||||||
|
"Reload": "Recarregar",
|
||||||
|
"Rebuild": "Reconstruir",
|
||||||
|
"No database found": "Base de dados não encontrada",
|
||||||
|
|
||||||
|
"Identity address": "Endereço de identidade",
|
||||||
|
"Change": "Alterar",
|
||||||
|
|
||||||
|
"Site control": "Controle do site",
|
||||||
|
"Update": "Atualizar",
|
||||||
|
"Pause": "Suspender",
|
||||||
|
"Resume": "Continuar",
|
||||||
|
"Delete": "Remover",
|
||||||
|
"Are you sure?": "Tem certeza?",
|
||||||
|
|
||||||
|
"Site address": "Endereço do site",
|
||||||
|
"Donate": "Doar",
|
||||||
|
|
||||||
|
"Needs to be updated": "Necessitam ser atualizados",
|
||||||
|
"{} try": "{} tentativa",
|
||||||
|
"{} tries": "{} tentativas",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} adicionais",
|
||||||
|
|
||||||
|
"This is my site": "Este é o meu site",
|
||||||
|
"Site title": "Título do site",
|
||||||
|
"Site description": "Descrição do site",
|
||||||
|
"Save site settings": "Salvar definições do site",
|
||||||
|
"Open site directory": "Abrir diretório do site",
|
||||||
|
|
||||||
|
"Content publishing": "Publicação do conteúdo",
|
||||||
|
"Choose": "Escolher",
|
||||||
|
"Sign": "Assinar",
|
||||||
|
"Publish": "Publicar",
|
||||||
|
"Sign and publish": "Assinar e publicar",
|
||||||
|
"add saved private key": "adicionar privatekey (chave privada) para salvar",
|
||||||
|
"Private key saved for site signing": "Privatekey foi salva para assinar o site",
|
||||||
|
"Private key saved.": "Privatekey salva.",
|
||||||
|
"forgot": "esquecer",
|
||||||
|
"Saved private key removed": "Privatekey salva foi removida",
|
||||||
|
"This function is disabled on this proxy": "Esta função encontra-se desativada neste proxy",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Erro ao baixar a base de dados GeoLite2 City: {}!<br>Por favor baixe manualmente e descompacte os dados para a seguinte pasta:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Baixando a base de dados GeoLite2 City (uma única vez, ~20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "A base de dados GeoLite2 City foi baixada!",
|
||||||
|
|
||||||
|
"Are you sure?": "Tem certeza?",
|
||||||
|
"Site storage limit modified!": "O limite de armazenamento do site foi modificado!",
|
||||||
|
"Database schema reloaded!": "O esquema da base de dados foi atualizado!",
|
||||||
|
"Database rebuilding....": "Reconstruindo base de dados...",
|
||||||
|
"Database rebuilt!": "Base de dados reconstruída!",
|
||||||
|
"Site updated!": "Site atualizado!",
|
||||||
|
"Delete this site": "Remover este site",
|
||||||
|
"Blacklist": "Blacklist",
|
||||||
|
"Blacklist this site": "Blacklistar este site",
|
||||||
|
"Reason": "Motivo",
|
||||||
|
"Delete and Blacklist": "Deletar e blacklistar",
|
||||||
|
"File write error: ": "Erro de escrita de arquivo: ",
|
||||||
|
"Site settings saved!": "Definições do site salvas!",
|
||||||
|
"Enter your private key:": "Digite sua chave privada:",
|
||||||
|
" Signed!": " Assinado!",
|
||||||
|
"WebGL not supported": "WebGL não é suportado",
|
||||||
|
"Save as .zip": "Salvar como .zip"
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"Peers": "Пиры",
|
||||||
|
"Connected": "Подключенные",
|
||||||
|
"Connectable": "Доступные",
|
||||||
|
"Connectable peers": "Пиры доступны для подключения",
|
||||||
|
|
||||||
|
"Data transfer": "Передача данных",
|
||||||
|
"Received": "Получено",
|
||||||
|
"Received bytes": "Получено байн",
|
||||||
|
"Sent": "Отправлено",
|
||||||
|
"Sent bytes": "Отправлено байт",
|
||||||
|
|
||||||
|
"Files": "Файлы",
|
||||||
|
"Total": "Всего",
|
||||||
|
"Image": "Изображений",
|
||||||
|
"Other": "Другое",
|
||||||
|
"User data": "Ваш контент",
|
||||||
|
|
||||||
|
"Size limit": "Ограничение по размеру",
|
||||||
|
"limit used": "Использовано",
|
||||||
|
"free space": "Доступно",
|
||||||
|
"Set": "Установить",
|
||||||
|
|
||||||
|
"Optional files": "Опциональные файлы",
|
||||||
|
"Downloaded": "Загружено",
|
||||||
|
"Download and help distribute all files": "Загрузить опциональные файлы для помощи сайту",
|
||||||
|
"Total size": "Объём",
|
||||||
|
"Downloaded files": "Загруженные файлы",
|
||||||
|
|
||||||
|
"Database": "База данных",
|
||||||
|
"search feeds": "поиск подписок",
|
||||||
|
"{feeds} query": "{feeds} запрос",
|
||||||
|
"Reload": "Перезагрузить",
|
||||||
|
"Rebuild": "Перестроить",
|
||||||
|
"No database found": "База данных не найдена",
|
||||||
|
|
||||||
|
"Identity address": "Уникальный адрес",
|
||||||
|
"Change": "Изменить",
|
||||||
|
|
||||||
|
"Site control": "Управление сайтом",
|
||||||
|
"Update": "Обновить",
|
||||||
|
"Pause": "Пауза",
|
||||||
|
"Resume": "Продолжить",
|
||||||
|
"Delete": "Удалить",
|
||||||
|
"Are you sure?": "Вы уверены?",
|
||||||
|
|
||||||
|
"Site address": "Адрес сайта",
|
||||||
|
"Donate": "Пожертвовать",
|
||||||
|
|
||||||
|
"Missing files": "Отсутствующие файлы",
|
||||||
|
"{} try": "{} попробовать",
|
||||||
|
"{} tries": "{} попыток",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} ещё",
|
||||||
|
|
||||||
|
"This is my site": "Это мой сайт",
|
||||||
|
"Site title": "Название сайта",
|
||||||
|
"Site description": "Описание сайта",
|
||||||
|
"Save site settings": "Сохранить настройки сайта",
|
||||||
|
|
||||||
|
"Content publishing": "Публикация контента",
|
||||||
|
"Choose": "Выбрать",
|
||||||
|
"Sign": "Подписать",
|
||||||
|
"Publish": "Опубликовать",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "Эта функция отключена на этом прокси",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "Ошибка загрузки базы городов GeoLite2: {}!<br>Пожалуйста, загрузите её вручную и распакуйте в папку:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "Загрузка базы городов GeoLite2 (это делается только 1 раз, ~20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "База GeoLite2 успешно загружена!",
|
||||||
|
|
||||||
|
"Are you sure?": "Вы уверены?",
|
||||||
|
"Site storage limit modified!": "Лимит хранилища для сайта изменен!",
|
||||||
|
"Database schema reloaded!": "Схема базы данных перезагружена!",
|
||||||
|
"Database rebuilding....": "Перестройка базы данных...",
|
||||||
|
"Database rebuilt!": "База данных перестроена!",
|
||||||
|
"Site updated!": "Сайт обновлён!",
|
||||||
|
"Delete this site": "Удалить этот сайт",
|
||||||
|
"File write error: ": "Ошибка записи файла:",
|
||||||
|
"Site settings saved!": "Настройки сайта сохранены!",
|
||||||
|
"Enter your private key:": "Введите свой приватный ключ:",
|
||||||
|
" Signed!": " Подписано!",
|
||||||
|
"WebGL not supported": "WebGL не поддерживается"
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"Peers": "Eşler",
|
||||||
|
"Connected": "Bağlı",
|
||||||
|
"Connectable": "Erişilebilir",
|
||||||
|
"Connectable peers": "Bağlanılabilir eşler",
|
||||||
|
|
||||||
|
"Data transfer": "Veri aktarımı",
|
||||||
|
"Received": "Alınan",
|
||||||
|
"Received bytes": "Bayt alındı",
|
||||||
|
"Sent": "Gönderilen",
|
||||||
|
"Sent bytes": "Bayt gönderildi",
|
||||||
|
|
||||||
|
"Files": "Dosyalar",
|
||||||
|
"Total": "Toplam",
|
||||||
|
"Image": "Resim",
|
||||||
|
"Other": "Diğer",
|
||||||
|
"User data": "Kullanıcı verisi",
|
||||||
|
|
||||||
|
"Size limit": "Boyut sınırı",
|
||||||
|
"limit used": "kullanılan",
|
||||||
|
"free space": "boş",
|
||||||
|
"Set": "Ayarla",
|
||||||
|
|
||||||
|
"Optional files": "İsteğe bağlı dosyalar",
|
||||||
|
"Downloaded": "İndirilen",
|
||||||
|
"Download and help distribute all files": "Tüm dosyaları indir ve yayılmalarına yardım et",
|
||||||
|
"Total size": "Toplam boyut",
|
||||||
|
"Downloaded files": "İndirilen dosyalar",
|
||||||
|
|
||||||
|
"Database": "Veritabanı",
|
||||||
|
"search feeds": "kaynak ara",
|
||||||
|
"{feeds} query": "{feeds} sorgu",
|
||||||
|
"Reload": "Yenile",
|
||||||
|
"Rebuild": "Yapılandır",
|
||||||
|
"No database found": "Veritabanı yok",
|
||||||
|
|
||||||
|
"Identity address": "Kimlik adresi",
|
||||||
|
"Change": "Değiştir",
|
||||||
|
|
||||||
|
"Site control": "Site kontrolü",
|
||||||
|
"Update": "Güncelle",
|
||||||
|
"Pause": "Duraklat",
|
||||||
|
"Resume": "Sürdür",
|
||||||
|
"Delete": "Sil",
|
||||||
|
"Are you sure?": "Emin misin?",
|
||||||
|
|
||||||
|
"Site address": "Site adresi",
|
||||||
|
"Donate": "Bağış yap",
|
||||||
|
|
||||||
|
"Missing files": "Eksik dosyalar",
|
||||||
|
"{} try": "{} deneme",
|
||||||
|
"{} tries": "{} deneme",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} tane daha",
|
||||||
|
|
||||||
|
"This is my site": "Bu benim sitem",
|
||||||
|
"Site title": "Site başlığı",
|
||||||
|
"Site description": "Site açıklaması",
|
||||||
|
"Save site settings": "Site ayarlarını kaydet",
|
||||||
|
|
||||||
|
"Content publishing": "İçerik yayımlanıyor",
|
||||||
|
"Choose": "Seç",
|
||||||
|
"Sign": "İmzala",
|
||||||
|
"Publish": "Yayımla",
|
||||||
|
|
||||||
|
"This function is disabled on this proxy": "Bu özellik bu vekilde kullanılamaz",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 Şehir veritabanı indirme hatası: {}!<br>Lütfen kendiniz indirip aşağıdaki konuma açınınız:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Şehir veritabanı indiriliyor (sadece bir kere, ~20MB)...",
|
||||||
|
"GeoLite2 City database downloaded!": "GeoLite2 Şehir veritabanı indirildi!",
|
||||||
|
|
||||||
|
"Are you sure?": "Emin misiniz?",
|
||||||
|
"Site storage limit modified!": "Site saklama sınırı değiştirildi!",
|
||||||
|
"Database schema reloaded!": "Veritabanı şeması yeniden yüklendi!",
|
||||||
|
"Database rebuilding....": "Veritabanı yeniden inşa ediliyor...",
|
||||||
|
"Database rebuilt!": "Veritabanı yeniden inşa edildi!",
|
||||||
|
"Site updated!": "Site güncellendi!",
|
||||||
|
"Delete this site": "Bu siteyi sil",
|
||||||
|
"File write error: ": "Dosya yazma hatası: ",
|
||||||
|
"Site settings saved!": "Site ayarları kaydedildi!",
|
||||||
|
"Enter your private key:": "Özel anahtarınızı giriniz:",
|
||||||
|
" Signed!": " İmzala!",
|
||||||
|
"WebGL not supported": "WebGL desteklenmiyor"
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
{
|
||||||
|
"Peers": "節點數",
|
||||||
|
"Connected": "已連線",
|
||||||
|
"Connectable": "可連線",
|
||||||
|
"Connectable peers": "可連線節點",
|
||||||
|
|
||||||
|
"Data transfer": "數據傳輸",
|
||||||
|
"Received": "已接收",
|
||||||
|
"Received bytes": "已接收位元組",
|
||||||
|
"Sent": "已傳送",
|
||||||
|
"Sent bytes": "已傳送位元組",
|
||||||
|
|
||||||
|
"Files": "檔案",
|
||||||
|
"Total": "共計",
|
||||||
|
"Image": "圖片",
|
||||||
|
"Other": "其他",
|
||||||
|
"User data": "使用者數據",
|
||||||
|
|
||||||
|
"Size limit": "大小限制",
|
||||||
|
"limit used": "已使用",
|
||||||
|
"free space": "可用空間",
|
||||||
|
"Set": "偏好設定",
|
||||||
|
|
||||||
|
"Optional files": "可選檔案",
|
||||||
|
"Downloaded": "已下載",
|
||||||
|
"Download and help distribute all files": "下載並幫助分發所有檔案",
|
||||||
|
"Total size": "總大小",
|
||||||
|
"Downloaded files": "下載的檔案",
|
||||||
|
|
||||||
|
"Database": "資料庫",
|
||||||
|
"search feeds": "搜尋供稿",
|
||||||
|
"{feeds} query": "{feeds} 查詢 ",
|
||||||
|
"Reload": "重新整理",
|
||||||
|
"Rebuild": "重建",
|
||||||
|
"No database found": "未找到資料庫",
|
||||||
|
|
||||||
|
"Identity address": "身分位址",
|
||||||
|
"Change": "變更",
|
||||||
|
|
||||||
|
"Site control": "網站控制",
|
||||||
|
"Update": "更新",
|
||||||
|
"Pause": "暫停",
|
||||||
|
"Resume": "恢復",
|
||||||
|
"Delete": "刪除",
|
||||||
|
"Are you sure?": "你確定?",
|
||||||
|
|
||||||
|
"Site address": "網站位址",
|
||||||
|
"Donate": "捐贈",
|
||||||
|
|
||||||
|
"Missing files": "缺少的檔案",
|
||||||
|
"{} try": "{} 嘗試",
|
||||||
|
"{} tries": "{} 已嘗試",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} 更多",
|
||||||
|
|
||||||
|
"This is my site": "這是我的網站",
|
||||||
|
"Site title": "網站標題",
|
||||||
|
"Site description": "網站描述",
|
||||||
|
"Save site settings": "存儲網站設定",
|
||||||
|
"Open site directory": "打開所在資料夾",
|
||||||
|
|
||||||
|
"Content publishing": "內容發布",
|
||||||
|
"Choose": "選擇",
|
||||||
|
"Sign": "簽署",
|
||||||
|
"Publish": "發布",
|
||||||
|
"Sign and publish": "簽名並發布",
|
||||||
|
"This function is disabled on this proxy": "此代理上禁用此功能",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 地理位置資料庫下載錯誤:{}!<br>請手動下載並解壓到數據目錄:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下載 GeoLite2 地理位置資料庫 (僅一次,約 20MB )...",
|
||||||
|
"GeoLite2 City database downloaded!": "GeoLite2 地理位置資料庫已下載!",
|
||||||
|
|
||||||
|
"Are you sure?": "你確定?",
|
||||||
|
"Site storage limit modified!": "網站存儲限制已變更!",
|
||||||
|
"Database schema reloaded!": "資料庫架構重新加載!",
|
||||||
|
"Database rebuilding....": "資料庫重建中...",
|
||||||
|
"Database rebuilt!": "資料庫已重建!",
|
||||||
|
"Site updated!": "網站已更新!",
|
||||||
|
"Delete this site": "刪除此網站",
|
||||||
|
"File write error: ": "檔案寫入錯誤:",
|
||||||
|
"Site settings saved!": "網站設置已保存!",
|
||||||
|
"Enter your private key:": "輸入您的私鑰:",
|
||||||
|
" Signed!": " 已簽署!",
|
||||||
|
"WebGL not supported": "不支援 WebGL"
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
{
|
||||||
|
"Copy to clipboard": "复制到剪切板",
|
||||||
|
"Peers": "节点数",
|
||||||
|
"Connected": "已连接",
|
||||||
|
"Connectable": "可连接",
|
||||||
|
"Onion": "洋葱点",
|
||||||
|
"Local": "局域网",
|
||||||
|
"Connectable peers": "可连接节点",
|
||||||
|
|
||||||
|
"Data transfer": "数据传输",
|
||||||
|
"Received": "已接收",
|
||||||
|
"Received bytes": "已接收字节",
|
||||||
|
"Sent": "已发送",
|
||||||
|
"Sent bytes": "已发送字节",
|
||||||
|
|
||||||
|
"Files": "文件",
|
||||||
|
"Save as .zip": "打包成zip文件",
|
||||||
|
"Total": "总计",
|
||||||
|
"Image": "图像",
|
||||||
|
"Other": "其他",
|
||||||
|
"User data": "用户数据",
|
||||||
|
|
||||||
|
"Size limit": "大小限制",
|
||||||
|
"limit used": "限额",
|
||||||
|
"free space": "剩余空间",
|
||||||
|
"Set": "设置",
|
||||||
|
|
||||||
|
"Optional files": "可选文件",
|
||||||
|
"Downloaded": "已下载",
|
||||||
|
"Help distribute added optional files": "帮助分发新的可选文件",
|
||||||
|
"Auto download big file size limit": "自动下载大文件大小限制",
|
||||||
|
"Download previous files": "下载之前的文件",
|
||||||
|
"Optional files download started": "可选文件下载启动",
|
||||||
|
"Optional files downloaded": "可选文件下载完成",
|
||||||
|
"Total size": "总大小",
|
||||||
|
"Downloaded files": "已下载文件",
|
||||||
|
|
||||||
|
"Database": "数据库",
|
||||||
|
"search feeds": "搜索数据源",
|
||||||
|
"{feeds} query": "{feeds} 请求",
|
||||||
|
"Reload": "重载",
|
||||||
|
"Rebuild": "重建",
|
||||||
|
"No database found": "没有找到数据库",
|
||||||
|
|
||||||
|
"Identity address": "身份地址",
|
||||||
|
"Change": "更改",
|
||||||
|
|
||||||
|
"Site control": "站点控制",
|
||||||
|
"Update": "更新",
|
||||||
|
"Pause": "暂停",
|
||||||
|
"Resume": "恢复",
|
||||||
|
"Delete": "删除",
|
||||||
|
"Are you sure?": "您确定吗?",
|
||||||
|
|
||||||
|
"Site address": "站点地址",
|
||||||
|
"Donate": "捐赠",
|
||||||
|
|
||||||
|
"Needs to be updated": "需要更新",
|
||||||
|
"{} try": "{} 尝试",
|
||||||
|
"{} tries": "{} 已尝试",
|
||||||
|
"+ {num_bad_files} more": "+ {num_bad_files} 更多",
|
||||||
|
|
||||||
|
"This is my site": "这是我的站点",
|
||||||
|
"Site title": "站点标题",
|
||||||
|
"Site description": "站点描述",
|
||||||
|
"Save site settings": "保存站点设置",
|
||||||
|
"Open site directory": "打开所在文件夹",
|
||||||
|
|
||||||
|
"Content publishing": "内容发布",
|
||||||
|
"Add saved private key": "添加并保存私钥",
|
||||||
|
"Save": "保存",
|
||||||
|
"Private key saved.": "私钥已保存",
|
||||||
|
"Private key saved for site signing": "已保存用于站点签名的私钥",
|
||||||
|
"Forgot": "删除私钥",
|
||||||
|
"Saved private key removed": "保存的私钥已删除",
|
||||||
|
"Choose": "选择",
|
||||||
|
"Sign": "签名",
|
||||||
|
"Publish": "发布",
|
||||||
|
"Sign and publish": "签名并发布",
|
||||||
|
"This function is disabled on this proxy": "此功能在代理上被禁用",
|
||||||
|
"GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}": "GeoLite2 地理位置数据库下载错误:{}!<br>请手动下载并解压在数据目录:<br>{}",
|
||||||
|
"Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下载 GeoLite2 地理位置数据库 (仅需一次,约 20MB )...",
|
||||||
|
"GeoLite2 City database downloaded!": "GeoLite2 地理位置数据库已下载!",
|
||||||
|
|
||||||
|
"Are you sure?": "您确定吗?",
|
||||||
|
"Site storage limit modified!": "站点存储限制已更改!",
|
||||||
|
"Database schema reloaded!": "数据库模式已重新加载!",
|
||||||
|
"Database rebuilding....": "数据库重建中...",
|
||||||
|
"Database rebuilt!": "数据库已重建!",
|
||||||
|
"Site updated!": "站点已更新!",
|
||||||
|
"Delete this site": "删除此站点",
|
||||||
|
"Blacklist": "黑名单",
|
||||||
|
"Blacklist this site": "拉黑此站点",
|
||||||
|
"Reason": "原因",
|
||||||
|
"Delete and Blacklist": "删除并拉黑",
|
||||||
|
"File write error: ": "文件写入错误:",
|
||||||
|
"Site settings saved!": "站点设置已保存!",
|
||||||
|
"Enter your private key:": "输入您的私钥:",
|
||||||
|
" Signed!": " 已签名!",
|
||||||
|
"WebGL not supported": "不支持 WebGL"
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
class Class
|
||||||
|
trace: true
|
||||||
|
|
||||||
|
log: (args...) ->
|
||||||
|
return unless @trace
|
||||||
|
return if typeof console is 'undefined'
|
||||||
|
args.unshift("[#{@.constructor.name}]")
|
||||||
|
console.log(args...)
|
||||||
|
@
|
||||||
|
|
||||||
|
logStart: (name, args...) ->
|
||||||
|
return unless @trace
|
||||||
|
@logtimers or= {}
|
||||||
|
@logtimers[name] = +(new Date)
|
||||||
|
@log "#{name}", args..., "(started)" if args.length > 0
|
||||||
|
@
|
||||||
|
|
||||||
|
logEnd: (name, args...) ->
|
||||||
|
ms = +(new Date)-@logtimers[name]
|
||||||
|
@log "#{name}", args..., "(Done in #{ms}ms)"
|
||||||
|
@
|
||||||
|
|
||||||
|
window.Class = Class
|
|
@ -0,0 +1,201 @@
|
||||||
|
class Console extends Class
|
||||||
|
constructor: (@sidebar) ->
|
||||||
|
@tag = null
|
||||||
|
@opened = false
|
||||||
|
@filter = null
|
||||||
|
@tab_types = [
|
||||||
|
{title: "All", filter: ""},
|
||||||
|
{title: "Info", filter: "INFO"},
|
||||||
|
{title: "Warning", filter: "WARNING"},
|
||||||
|
{title: "Error", filter: "ERROR"}
|
||||||
|
]
|
||||||
|
@read_size = 32 * 1024
|
||||||
|
@tab_active = ""
|
||||||
|
#@filter = @sidebar.wrapper.site_info.address_short
|
||||||
|
handleMessageWebsocket_original = @sidebar.wrapper.handleMessageWebsocket
|
||||||
|
@sidebar.wrapper.handleMessageWebsocket = (message) =>
|
||||||
|
if message.cmd == "logLineAdd" and message.params.stream_id == @stream_id
|
||||||
|
@addLines(message.params.lines)
|
||||||
|
else
|
||||||
|
handleMessageWebsocket_original(message)
|
||||||
|
|
||||||
|
$(window).on "hashchange", =>
|
||||||
|
if window.top.location.hash.startsWith("#ZeroNet:Console")
|
||||||
|
@open()
|
||||||
|
|
||||||
|
if window.top.location.hash.startsWith("#ZeroNet:Console")
|
||||||
|
setTimeout (=> @open()), 10
|
||||||
|
|
||||||
|
createHtmltag: ->
|
||||||
|
if not @container
|
||||||
|
@container = $("""
|
||||||
|
<div class="console-container">
|
||||||
|
<div class="console">
|
||||||
|
<div class="console-top">
|
||||||
|
<div class="console-tabs"></div>
|
||||||
|
<div class="console-text">Loading...</div>
|
||||||
|
</div>
|
||||||
|
<div class="console-middle">
|
||||||
|
<div class="mynode"></div>
|
||||||
|
<div class="peers">
|
||||||
|
<div class="peer"><div class="line"></div><a href="#" class="icon">\u25BD</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
@text = @container.find(".console-text")
|
||||||
|
@text_elem = @text[0]
|
||||||
|
@tabs = @container.find(".console-tabs")
|
||||||
|
|
||||||
|
@text.on "mousewheel", (e) => # Stop animation on manual scrolling
|
||||||
|
if e.originalEvent.deltaY < 0
|
||||||
|
@text.stop()
|
||||||
|
RateLimit 300, @checkTextIsBottom
|
||||||
|
|
||||||
|
@text.is_bottom = true
|
||||||
|
|
||||||
|
@container.appendTo(document.body)
|
||||||
|
@tag = @container.find(".console")
|
||||||
|
for tab_type in @tab_types
|
||||||
|
tab = $("<a></a>", {href: "#", "data-filter": tab_type.filter, "data-title": tab_type.title}).text(tab_type.title)
|
||||||
|
if tab_type.filter == @tab_active
|
||||||
|
tab.addClass("active")
|
||||||
|
tab.on("click", @handleTabClick)
|
||||||
|
if window.top.location.hash.endsWith(tab_type.title)
|
||||||
|
@log "Triggering click on", tab
|
||||||
|
tab.trigger("click")
|
||||||
|
@tabs.append(tab)
|
||||||
|
|
||||||
|
@container.on "mousedown touchend touchcancel", (e) =>
|
||||||
|
if e.target != e.currentTarget
|
||||||
|
return true
|
||||||
|
@log "closing"
|
||||||
|
if $(document.body).hasClass("body-console")
|
||||||
|
@close()
|
||||||
|
return true
|
||||||
|
|
||||||
|
@loadConsoleText()
|
||||||
|
|
||||||
|
checkTextIsBottom: =>
|
||||||
|
@text.is_bottom = Math.round(@text_elem.scrollTop + @text_elem.clientHeight) >= @text_elem.scrollHeight - 15
|
||||||
|
|
||||||
|
toColor: (text, saturation=60, lightness=70) ->
|
||||||
|
hash = 0
|
||||||
|
for i in [0..text.length-1]
|
||||||
|
hash += text.charCodeAt(i)*i
|
||||||
|
hash = hash % 1777
|
||||||
|
return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)";
|
||||||
|
|
||||||
|
formatLine: (line) =>
|
||||||
|
match = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/)
|
||||||
|
if not match
|
||||||
|
return line.replace(/\</g, "<").replace(/\>/g, ">")
|
||||||
|
|
||||||
|
[line, added, level, module, text] = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/)
|
||||||
|
added = "<span style='color: #dfd0fa'>#{added}</span>"
|
||||||
|
level = "<span style='color: #{@toColor(level, 100)};'>#{level}</span>"
|
||||||
|
module = "<span style='color: #{@toColor(module, 60)}; font-weight: bold;'>#{module}</span>"
|
||||||
|
|
||||||
|
text = text.replace(/(Site:[A-Za-z0-9\.]+)/g, "<span style='color: #AAAAFF'>$1</span>")
|
||||||
|
text = text.replace(/\</g, "<").replace(/\>/g, ">")
|
||||||
|
#text = text.replace(/( [0-9\.]+(|s|ms))/g, "<span style='color: #FFF;'>$1</span>")
|
||||||
|
return "#{added} #{level} #{module} #{text}"
|
||||||
|
|
||||||
|
|
||||||
|
addLines: (lines, animate=true) =>
|
||||||
|
html_lines = []
|
||||||
|
@logStart "formatting"
|
||||||
|
for line in lines
|
||||||
|
html_lines.push @formatLine(line)
|
||||||
|
@logEnd "formatting"
|
||||||
|
@logStart "adding"
|
||||||
|
@text.append(html_lines.join("<br>") + "<br>")
|
||||||
|
@logEnd "adding"
|
||||||
|
if @text.is_bottom and animate
|
||||||
|
@text.stop().animate({scrollTop: @text_elem.scrollHeight - @text_elem.clientHeight + 1}, 600, 'easeInOutCubic')
|
||||||
|
|
||||||
|
|
||||||
|
loadConsoleText: =>
|
||||||
|
@sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter, read_size: @read_size}, (res) =>
|
||||||
|
@text.html("")
|
||||||
|
pos_diff = res["pos_end"] - res["pos_start"]
|
||||||
|
size_read = Math.round(pos_diff/1024)
|
||||||
|
size_total = Math.round(res['pos_end']/1024)
|
||||||
|
@text.append("<br><br>")
|
||||||
|
@text.append("Displaying #{res.lines.length} of #{res.num_found} lines found in the last #{size_read}kB of the log file. (#{size_total}kB total)<br>")
|
||||||
|
@addLines res.lines, false
|
||||||
|
@text_elem.scrollTop = @text_elem.scrollHeight
|
||||||
|
if @stream_id
|
||||||
|
@sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id}
|
||||||
|
@sidebar.wrapper.ws.cmd "consoleLogStream", {filter: @filter}, (res) =>
|
||||||
|
@stream_id = res.stream_id
|
||||||
|
|
||||||
|
close: =>
|
||||||
|
window.top.location.hash = ""
|
||||||
|
@sidebar.move_lock = "y"
|
||||||
|
@sidebar.startDrag()
|
||||||
|
@sidebar.stopDrag()
|
||||||
|
|
||||||
|
open: =>
|
||||||
|
@sidebar.startDrag()
|
||||||
|
@sidebar.moved("y")
|
||||||
|
@sidebar.fixbutton_targety = @sidebar.page_height - @sidebar.fixbutton_inity - 50
|
||||||
|
@sidebar.stopDrag()
|
||||||
|
|
||||||
|
onOpened: =>
|
||||||
|
@sidebar.onClosed()
|
||||||
|
@log "onOpened"
|
||||||
|
|
||||||
|
onClosed: =>
|
||||||
|
$(document.body).removeClass("body-console")
|
||||||
|
if @stream_id
|
||||||
|
@sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id}
|
||||||
|
|
||||||
|
cleanup: =>
|
||||||
|
if @container
|
||||||
|
@container.remove()
|
||||||
|
@container = null
|
||||||
|
|
||||||
|
stopDragY: =>
|
||||||
|
# Animate sidebar and iframe
|
||||||
|
if @sidebar.fixbutton_targety == @sidebar.fixbutton_inity
|
||||||
|
# Closed
|
||||||
|
targety = 0
|
||||||
|
@opened = false
|
||||||
|
else
|
||||||
|
# Opened
|
||||||
|
targety = @sidebar.fixbutton_targety - @sidebar.fixbutton_inity
|
||||||
|
@onOpened()
|
||||||
|
@opened = true
|
||||||
|
|
||||||
|
# Revent sidebar transitions
|
||||||
|
if @tag
|
||||||
|
@tag.css("transition", "0.5s ease-out")
|
||||||
|
@tag.css("transform", "translateY(#{targety}px)").one transitionEnd, =>
|
||||||
|
@tag.css("transition", "")
|
||||||
|
if not @opened
|
||||||
|
@cleanup()
|
||||||
|
# Revert body transformations
|
||||||
|
@log "stopDragY", "opened:", @opened, targety
|
||||||
|
if not @opened
|
||||||
|
@onClosed()
|
||||||
|
|
||||||
|
changeFilter: (filter) =>
|
||||||
|
@filter = filter
|
||||||
|
if @filter == ""
|
||||||
|
@read_size = 32 * 1024
|
||||||
|
else
|
||||||
|
@read_size = 5 * 1024 * 1024
|
||||||
|
@loadConsoleText()
|
||||||
|
|
||||||
|
handleTabClick: (e) =>
|
||||||
|
elem = $(e.currentTarget)
|
||||||
|
@tab_active = elem.data("filter")
|
||||||
|
$("a", @tabs).removeClass("active")
|
||||||
|
elem.addClass("active")
|
||||||
|
@changeFilter(@tab_active)
|
||||||
|
window.top.location.hash = "#ZeroNet:Console:" + elem.data("title")
|
||||||
|
return false
|
||||||
|
|
||||||
|
window.Console = Console
|
|
@ -0,0 +1,31 @@
|
||||||
|
.console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; }
|
||||||
|
.console-container .console { background-color: #212121; height: 100vh; transform: translateY(0px); padding-top: 80px; box-sizing: border-box; }
|
||||||
|
|
||||||
|
.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;}
|
||||||
|
.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; }
|
||||||
|
.console-tabs {
|
||||||
|
background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/
|
||||||
|
box-shadow: -30px 0px 45px #7d2463; background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473;
|
||||||
|
}
|
||||||
|
.console-tabs a {
|
||||||
|
margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA;
|
||||||
|
font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666;
|
||||||
|
border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.console-tabs a:hover { color: #FFF }
|
||||||
|
.console-tabs a.active { background-color: #46223c; color: #FFF }
|
||||||
|
.console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; }
|
||||||
|
|
||||||
|
.console .mynode {
|
||||||
|
border: 0.5px solid #aaa; width: 50px; height: 50px; transform: rotateZ(45deg); margin-top: -25px; margin-left: -25px;
|
||||||
|
opacity: 1; display: inline-block; background-color: #EEE; z-index: 9; position: absolute; outline: 5px solid #EEE;
|
||||||
|
}
|
||||||
|
.console .peers { width: 0px; height: 0px; position: absolute; left: -20px; top: -20px; text-align: center; }
|
||||||
|
.console .peer { left: 0px; top: 0px; position: absolute; }
|
||||||
|
.console .peer .icon { width: 20px; height: 20px; padding: 10px; display: inline-block; text-decoration: none; left: 200px; position: absolute; color: #666; }
|
||||||
|
.console .peer .icon:before { content: "\25BC"; position: absolute; margin-top: 3px; margin-left: -1px; opacity: 0; transition: all 0.3s }
|
||||||
|
.console .peer .icon:hover:before { opacity: 1; transition: none }
|
||||||
|
.console .peer .line {
|
||||||
|
width: 187px; border-top: 1px solid #CCC; position: absolute; top: 20px; left: 20px;
|
||||||
|
transform: rotateZ(334deg); transform-origin: bottom left;
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
class Menu
|
||||||
|
constructor: (@button) ->
|
||||||
|
@elem = $(".menu.template").clone().removeClass("template")
|
||||||
|
@elem.appendTo("body")
|
||||||
|
@items = []
|
||||||
|
|
||||||
|
show: ->
|
||||||
|
if window.visible_menu and window.visible_menu.button[0] == @button[0] # Same menu visible then hide it
|
||||||
|
window.visible_menu.hide()
|
||||||
|
@hide()
|
||||||
|
else
|
||||||
|
button_pos = @button.offset()
|
||||||
|
left = button_pos.left
|
||||||
|
@elem.css({"top": button_pos.top+@button.outerHeight(), "left": left})
|
||||||
|
@button.addClass("menu-active")
|
||||||
|
@elem.addClass("visible")
|
||||||
|
if @elem.position().left + @elem.width() + 20 > window.innerWidth
|
||||||
|
@elem.css("left", window.innerWidth - @elem.width() - 20)
|
||||||
|
if window.visible_menu then window.visible_menu.hide()
|
||||||
|
window.visible_menu = @
|
||||||
|
|
||||||
|
|
||||||
|
hide: ->
|
||||||
|
@elem.removeClass("visible")
|
||||||
|
@button.removeClass("menu-active")
|
||||||
|
window.visible_menu = null
|
||||||
|
|
||||||
|
|
||||||
|
addItem: (title, cb) ->
|
||||||
|
item = $(".menu-item.template", @elem).clone().removeClass("template")
|
||||||
|
item.html(title)
|
||||||
|
item.on "click", =>
|
||||||
|
if not cb(item)
|
||||||
|
@hide()
|
||||||
|
return false
|
||||||
|
item.appendTo(@elem)
|
||||||
|
@items.push item
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
log: (args...) ->
|
||||||
|
console.log "[Menu]", args...
|
||||||
|
|
||||||
|
window.Menu = Menu
|
||||||
|
|
||||||
|
# Hide menu on outside click
|
||||||
|
$("body").on "click", (e) ->
|
||||||
|
if window.visible_menu and e.target != window.visible_menu.button[0] and $(e.target).parent()[0] != window.visible_menu.elem[0]
|
||||||
|
window.visible_menu.hide()
|
|
@ -0,0 +1,19 @@
|
||||||
|
.menu {
|
||||||
|
background-color: white; padding: 10px 0px; position: absolute; top: 0px; left: 0px; max-height: 0px; overflow: hidden; transform: translate(0px, -30px); pointer-events: none;
|
||||||
|
box-shadow: 0px 2px 8px rgba(0,0,0,0.3); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.menu.visible { opacity: 1; max-height: 350px; transform: translate(0px, 0px); transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; pointer-events: all }
|
||||||
|
|
||||||
|
.menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal; padding-left: 30px; }
|
||||||
|
.menu-item-separator { margin-top: 5px; border-top: 1px solid #eee }
|
||||||
|
|
||||||
|
.menu-item:hover { background-color: #F6F6F6; transition: none; color: inherit; border: none }
|
||||||
|
.menu-item:active, .menu-item:focus { background-color: #AF3BFF; color: white; transition: none }
|
||||||
|
.menu-item.selected:before {
|
||||||
|
content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1);
|
||||||
|
font-weight: bold; position: absolute; margin-left: -17px; font-size: 12px; margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.menu, .menu.visible { position: absolute; left: unset !important; right: 20px; }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
String::startsWith = (s) -> @[...s.length] is s
|
||||||
|
String::endsWith = (s) -> s is '' or @[-s.length..] is s
|
||||||
|
String::capitalize = -> if @.length then @[0].toUpperCase() + @.slice(1) else ""
|
||||||
|
String::repeat = (count) -> new Array( count + 1 ).join(@)
|
||||||
|
|
||||||
|
window.isEmpty = (obj) ->
|
||||||
|
for key of obj
|
||||||
|
return false
|
||||||
|
return true
|
|
@ -0,0 +1,14 @@
|
||||||
|
limits = {}
|
||||||
|
call_after_interval = {}
|
||||||
|
window.RateLimit = (interval, fn) ->
|
||||||
|
if not limits[fn]
|
||||||
|
call_after_interval[fn] = false
|
||||||
|
fn() # First call is not delayed
|
||||||
|
limits[fn] = setTimeout (->
|
||||||
|
if call_after_interval[fn]
|
||||||
|
fn()
|
||||||
|
delete limits[fn]
|
||||||
|
delete call_after_interval[fn]
|
||||||
|
), interval
|
||||||
|
else # Called within iterval, delay the call
|
||||||
|
call_after_interval[fn] = true
|
|
@ -0,0 +1,91 @@
|
||||||
|
/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
|
||||||
|
|
||||||
|
window.initScrollable = function () {
|
||||||
|
|
||||||
|
var scrollContainer = document.querySelector('.scrollable'),
|
||||||
|
scrollContentWrapper = document.querySelector('.scrollable .content-wrapper'),
|
||||||
|
scrollContent = document.querySelector('.scrollable .content'),
|
||||||
|
contentPosition = 0,
|
||||||
|
scrollerBeingDragged = false,
|
||||||
|
scroller,
|
||||||
|
topPosition,
|
||||||
|
scrollerHeight;
|
||||||
|
|
||||||
|
function calculateScrollerHeight() {
|
||||||
|
// *Calculation of how tall scroller should be
|
||||||
|
var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight;
|
||||||
|
if (visibleRatio == 1)
|
||||||
|
scroller.style.display = "none";
|
||||||
|
else
|
||||||
|
scroller.style.display = "block";
|
||||||
|
return visibleRatio * scrollContainer.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveScroller(evt) {
|
||||||
|
// Move Scroll bar to top offset
|
||||||
|
var scrollPercentage = evt.target.scrollTop / scrollContentWrapper.scrollHeight;
|
||||||
|
topPosition = scrollPercentage * (scrollContainer.offsetHeight - 5); // 5px arbitrary offset so scroll bar doesn't move too far beyond content wrapper bounding box
|
||||||
|
scroller.style.top = topPosition + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrag(evt) {
|
||||||
|
normalizedPosition = evt.pageY;
|
||||||
|
contentPosition = scrollContentWrapper.scrollTop;
|
||||||
|
scrollerBeingDragged = true;
|
||||||
|
window.addEventListener('mousemove', scrollBarScroll);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrag(evt) {
|
||||||
|
scrollerBeingDragged = false;
|
||||||
|
window.removeEventListener('mousemove', scrollBarScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollBarScroll(evt) {
|
||||||
|
if (scrollerBeingDragged === true) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var mouseDifferential = evt.pageY - normalizedPosition;
|
||||||
|
var scrollEquivalent = mouseDifferential * (scrollContentWrapper.scrollHeight / scrollContainer.offsetHeight);
|
||||||
|
scrollContentWrapper.scrollTop = contentPosition + scrollEquivalent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHeight() {
|
||||||
|
scrollerHeight = calculateScrollerHeight() - 10;
|
||||||
|
scroller.style.height = scrollerHeight + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScroller() {
|
||||||
|
// *Creates scroller element and appends to '.scrollable' div
|
||||||
|
// create scroller element
|
||||||
|
scroller = document.createElement("div");
|
||||||
|
scroller.className = 'scroller';
|
||||||
|
|
||||||
|
// determine how big scroller should be based on content
|
||||||
|
scrollerHeight = calculateScrollerHeight() - 10;
|
||||||
|
|
||||||
|
if (scrollerHeight / scrollContainer.offsetHeight < 1) {
|
||||||
|
// *If there is a need to have scroll bar based on content size
|
||||||
|
scroller.style.height = scrollerHeight + 'px';
|
||||||
|
|
||||||
|
// append scroller to scrollContainer div
|
||||||
|
scrollContainer.appendChild(scroller);
|
||||||
|
|
||||||
|
// show scroll path divot
|
||||||
|
scrollContainer.className += ' showScroll';
|
||||||
|
|
||||||
|
// attach related draggable listeners
|
||||||
|
scroller.addEventListener('mousedown', startDrag);
|
||||||
|
window.addEventListener('mouseup', stopDrag);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createScroller();
|
||||||
|
|
||||||
|
|
||||||
|
// *** Listeners ***
|
||||||
|
scrollContentWrapper.addEventListener('scroll', moveScroller);
|
||||||
|
|
||||||
|
return updateHeight;
|
||||||
|
};
|
|
@ -0,0 +1,44 @@
|
||||||
|
.scrollable {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable.showScroll::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
top: 5%;
|
||||||
|
right: 7px;
|
||||||
|
height: 90%;
|
||||||
|
width: 3px;
|
||||||
|
background: rgba(224, 224, 255, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable .content-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-right: 50%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
.scroller {
|
||||||
|
margin-top: 5px;
|
||||||
|
z-index: 5;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
width: 7px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #3A3A3A;
|
||||||
|
top: 0px;
|
||||||
|
left: 395px;
|
||||||
|
-webkit-transition: top .08s;
|
||||||
|
-moz-transition: top .08s;
|
||||||
|
-ms-transition: top .08s;
|
||||||
|
-o-transition: top .08s;
|
||||||
|
transition: top .08s;
|
||||||
|
}
|
||||||
|
.scroller {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
|
@ -0,0 +1,644 @@
|
||||||
|
class Sidebar extends Class
|
||||||
|
constructor: (@wrapper) ->
|
||||||
|
@tag = null
|
||||||
|
@container = null
|
||||||
|
@opened = false
|
||||||
|
@width = 410
|
||||||
|
@console = new Console(@)
|
||||||
|
@fixbutton = $(".fixbutton")
|
||||||
|
@fixbutton_addx = 0
|
||||||
|
@fixbutton_addy = 0
|
||||||
|
@fixbutton_initx = 0
|
||||||
|
@fixbutton_inity = 15
|
||||||
|
@fixbutton_targetx = 0
|
||||||
|
@move_lock = null
|
||||||
|
@page_width = $(window).width()
|
||||||
|
@page_height = $(window).height()
|
||||||
|
@frame = $("#inner-iframe")
|
||||||
|
@initFixbutton()
|
||||||
|
@dragStarted = 0
|
||||||
|
@globe = null
|
||||||
|
@preload_html = null
|
||||||
|
|
||||||
|
@original_set_site_info = @wrapper.setSiteInfo # We going to override this, save the original
|
||||||
|
|
||||||
|
# Start in opened state for debugging
|
||||||
|
if window.top.location.hash == "#ZeroNet:OpenSidebar"
|
||||||
|
@startDrag()
|
||||||
|
@moved("x")
|
||||||
|
@fixbutton_targetx = @fixbutton_initx - @width
|
||||||
|
@stopDrag()
|
||||||
|
|
||||||
|
|
||||||
|
initFixbutton: ->
|
||||||
|
|
||||||
|
# Detect dragging
|
||||||
|
@fixbutton.on "mousedown touchstart", (e) =>
|
||||||
|
if e.button > 0 # Right or middle click
|
||||||
|
return
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
# Disable previous listeners
|
||||||
|
@fixbutton.off "click touchend touchcancel"
|
||||||
|
|
||||||
|
# Make sure its not a click
|
||||||
|
@dragStarted = (+ new Date)
|
||||||
|
|
||||||
|
# Fullscreen drag bg to capture mouse events over iframe
|
||||||
|
$(".drag-bg").remove()
|
||||||
|
$("<div class='drag-bg'></div>").appendTo(document.body)
|
||||||
|
|
||||||
|
$("body").one "mousemove touchmove", (e) =>
|
||||||
|
mousex = e.pageX
|
||||||
|
mousey = e.pageY
|
||||||
|
if not mousex
|
||||||
|
mousex = e.originalEvent.touches[0].pageX
|
||||||
|
mousey = e.originalEvent.touches[0].pageY
|
||||||
|
|
||||||
|
@fixbutton_addx = @fixbutton.offset().left - mousex
|
||||||
|
@fixbutton_addy = @fixbutton.offset().top - mousey
|
||||||
|
@startDrag()
|
||||||
|
@fixbutton.parent().on "click touchend touchcancel", (e) =>
|
||||||
|
if (+ new Date) - @dragStarted < 100
|
||||||
|
window.top.location = @fixbutton.find(".fixbutton-bg").attr("href")
|
||||||
|
@stopDrag()
|
||||||
|
@resized()
|
||||||
|
$(window).on "resize", @resized
|
||||||
|
|
||||||
|
resized: =>
|
||||||
|
@page_width = $(window).width()
|
||||||
|
@page_height = $(window).height()
|
||||||
|
@fixbutton_initx = @page_width - 75 # Initial x position
|
||||||
|
if @opened
|
||||||
|
@fixbutton.css
|
||||||
|
left: @fixbutton_initx - @width
|
||||||
|
else
|
||||||
|
@fixbutton.css
|
||||||
|
left: @fixbutton_initx
|
||||||
|
|
||||||
|
# Start dragging the fixbutton
|
||||||
|
startDrag: ->
|
||||||
|
#@move_lock = "x" # Temporary until internals not finished
|
||||||
|
@log "startDrag", @fixbutton_initx, @fixbutton_inity
|
||||||
|
@fixbutton_targetx = @fixbutton_initx # Fallback x position
|
||||||
|
@fixbutton_targety = @fixbutton_inity # Fallback y position
|
||||||
|
|
||||||
|
@fixbutton.addClass("dragging")
|
||||||
|
|
||||||
|
# IE position wrap fix
|
||||||
|
if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0
|
||||||
|
@fixbutton.css("pointer-events", "none")
|
||||||
|
|
||||||
|
# Don't go to homepage
|
||||||
|
@fixbutton.one "click", (e) =>
|
||||||
|
@stopDrag()
|
||||||
|
@fixbutton.removeClass("dragging")
|
||||||
|
moved_x = Math.abs(@fixbutton.offset().left - @fixbutton_initx)
|
||||||
|
moved_y = Math.abs(@fixbutton.offset().top - @fixbutton_inity)
|
||||||
|
if moved_x > 5 or moved_y > 10
|
||||||
|
# If moved more than some pixel the button then don't go to homepage
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
# Animate drag
|
||||||
|
@fixbutton.parents().on "mousemove touchmove", @animDrag
|
||||||
|
@fixbutton.parents().on "mousemove touchmove" ,@waitMove
|
||||||
|
|
||||||
|
# Stop dragging listener
|
||||||
|
@fixbutton.parents().one "mouseup touchend touchcancel", (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
@stopDrag()
|
||||||
|
|
||||||
|
|
||||||
|
# Wait for moving the fixbutton
|
||||||
|
waitMove: (e) =>
|
||||||
|
document.body.style.perspective = "1000px"
|
||||||
|
document.body.style.height = "100%"
|
||||||
|
document.body.style.willChange = "perspective"
|
||||||
|
document.documentElement.style.height = "100%"
|
||||||
|
#$(document.body).css("backface-visibility", "hidden").css("perspective", "1000px").css("height", "900px")
|
||||||
|
# $("iframe").css("backface-visibility", "hidden")
|
||||||
|
|
||||||
|
moved_x = Math.abs(parseInt(@fixbutton[0].style.left) - @fixbutton_targetx)
|
||||||
|
moved_y = Math.abs(parseInt(@fixbutton[0].style.top) - @fixbutton_targety)
|
||||||
|
if moved_x > 5 and (+ new Date) - @dragStarted + moved_x > 50
|
||||||
|
@moved("x")
|
||||||
|
@fixbutton.stop().animate {"top": @fixbutton_inity}, 1000
|
||||||
|
@fixbutton.parents().off "mousemove touchmove" ,@waitMove
|
||||||
|
|
||||||
|
else if moved_y > 5 and (+ new Date) - @dragStarted + moved_y > 50
|
||||||
|
@moved("y")
|
||||||
|
@fixbutton.parents().off "mousemove touchmove" ,@waitMove
|
||||||
|
|
||||||
|
moved: (direction) ->
|
||||||
|
@log "Moved", direction
|
||||||
|
@move_lock = direction
|
||||||
|
if direction == "y"
|
||||||
|
$(document.body).addClass("body-console")
|
||||||
|
return @console.createHtmltag()
|
||||||
|
@createHtmltag()
|
||||||
|
$(document.body).addClass("body-sidebar")
|
||||||
|
@container.on "mousedown touchend touchcancel", (e) =>
|
||||||
|
if e.target != e.currentTarget
|
||||||
|
return true
|
||||||
|
@log "closing"
|
||||||
|
if $(document.body).hasClass("body-sidebar")
|
||||||
|
@close()
|
||||||
|
return true
|
||||||
|
|
||||||
|
$(window).off "resize"
|
||||||
|
$(window).on "resize", =>
|
||||||
|
$(document.body).css "height", $(window).height()
|
||||||
|
@scrollable()
|
||||||
|
@resized()
|
||||||
|
|
||||||
|
# Override setsiteinfo to catch changes
|
||||||
|
@wrapper.setSiteInfo = (site_info) =>
|
||||||
|
@setSiteInfo(site_info)
|
||||||
|
@original_set_site_info.apply(@wrapper, arguments)
|
||||||
|
|
||||||
|
# Preload world.jpg
|
||||||
|
img = new Image();
|
||||||
|
img.src = "/uimedia/globe/world.jpg";
|
||||||
|
|
||||||
|
setSiteInfo: (site_info) ->
|
||||||
|
RateLimit 1500, =>
|
||||||
|
@updateHtmlTag()
|
||||||
|
RateLimit 30000, =>
|
||||||
|
@displayGlobe()
|
||||||
|
|
||||||
|
# Create the sidebar html tag
|
||||||
|
createHtmltag: ->
|
||||||
|
@when_loaded = $.Deferred()
|
||||||
|
if not @container
|
||||||
|
@container = $("""
|
||||||
|
<div class="sidebar-container"><div class="sidebar scrollable"><div class="content-wrapper"><div class="content">
|
||||||
|
</div></div></div></div>
|
||||||
|
""")
|
||||||
|
@container.appendTo(document.body)
|
||||||
|
@tag = @container.find(".sidebar")
|
||||||
|
@updateHtmlTag()
|
||||||
|
@scrollable = window.initScrollable()
|
||||||
|
|
||||||
|
|
||||||
|
updateHtmlTag: ->
|
||||||
|
if @preload_html
|
||||||
|
@setHtmlTag(@preload_html)
|
||||||
|
@preload_html = null
|
||||||
|
else
|
||||||
|
@wrapper.ws.cmd "sidebarGetHtmlTag", {}, @setHtmlTag
|
||||||
|
|
||||||
|
setHtmlTag: (res) =>
|
||||||
|
if @tag.find(".content").children().length == 0 # First update
|
||||||
|
@log "Creating content"
|
||||||
|
@container.addClass("loaded")
|
||||||
|
morphdom(@tag.find(".content")[0], '<div class="content">'+res+'</div>')
|
||||||
|
# @scrollable()
|
||||||
|
@when_loaded.resolve()
|
||||||
|
|
||||||
|
else # Not first update, patch the html to keep unchanged dom elements
|
||||||
|
morphdom @tag.find(".content")[0], '<div class="content">'+res+'</div>', {
|
||||||
|
onBeforeMorphEl: (from_el, to_el) -> # Ignore globe loaded state
|
||||||
|
if from_el.className == "globe" or from_el.className.indexOf("noupdate") >= 0
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save and forget privatekey for site signing
|
||||||
|
@tag.find("#privatekey-add").off("click, touchend").on "click touchend", (e) =>
|
||||||
|
@wrapper.displayPrompt "Enter your private key:", "password", "Save", "", (privatekey) =>
|
||||||
|
@wrapper.ws.cmd "userSetSitePrivatekey", [privatekey], (res) =>
|
||||||
|
@wrapper.notifications.add "privatekey", "done", "Private key saved for site signing", 5000
|
||||||
|
return false
|
||||||
|
|
||||||
|
@tag.find("#privatekey-forget").off("click, touchend").on "click touchend", (e) =>
|
||||||
|
@wrapper.displayConfirm "Remove saved private key for this site?", "Forget", (res) =>
|
||||||
|
if not res
|
||||||
|
return false
|
||||||
|
@wrapper.ws.cmd "userSetSitePrivatekey", [""], (res) =>
|
||||||
|
@wrapper.notifications.add "privatekey", "done", "Saved private key removed", 5000
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Use requested address for browse files urls
|
||||||
|
@tag.find("#browse-files").attr("href", document.location.pathname.replace(/(\/.*?(\/|$)).*$/, "/list$1"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
animDrag: (e) =>
|
||||||
|
mousex = e.pageX
|
||||||
|
mousey = e.pageY
|
||||||
|
if not mousex and e.originalEvent.touches
|
||||||
|
mousex = e.originalEvent.touches[0].pageX
|
||||||
|
mousey = e.originalEvent.touches[0].pageY
|
||||||
|
|
||||||
|
overdrag = @fixbutton_initx - @width - mousex
|
||||||
|
if overdrag > 0 # Overdragged
|
||||||
|
overdrag_percent = 1 + overdrag/300
|
||||||
|
mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent)
|
||||||
|
targetx = @fixbutton_initx - mousex - @fixbutton_addx
|
||||||
|
targety = @fixbutton_inity - mousey - @fixbutton_addy
|
||||||
|
|
||||||
|
if @move_lock == "x"
|
||||||
|
targety = @fixbutton_inity
|
||||||
|
else if @move_lock == "y"
|
||||||
|
targetx = @fixbutton_initx
|
||||||
|
|
||||||
|
if not @move_lock or @move_lock == "x"
|
||||||
|
@fixbutton[0].style.left = (mousex + @fixbutton_addx) + "px"
|
||||||
|
if @tag
|
||||||
|
@tag[0].style.transform = "translateX(#{0 - targetx}px)"
|
||||||
|
|
||||||
|
if not @move_lock or @move_lock == "y"
|
||||||
|
@fixbutton[0].style.top = (mousey + @fixbutton_addy) + "px"
|
||||||
|
if @console.tag
|
||||||
|
@console.tag[0].style.transform = "translateY(#{0 - targety}px)"
|
||||||
|
|
||||||
|
#if @move_lock == "x"
|
||||||
|
# @fixbutton[0].style.left = "#{@fixbutton_targetx} px"
|
||||||
|
#@fixbutton[0].style.top = "#{@fixbutton_inity}px"
|
||||||
|
#if @move_lock == "y"
|
||||||
|
# @fixbutton[0].style.top = "#{@fixbutton_targety} px"
|
||||||
|
|
||||||
|
# Check if opened
|
||||||
|
if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9)
|
||||||
|
@fixbutton_targetx = @fixbutton_initx - @width # Make it opened
|
||||||
|
else
|
||||||
|
@fixbutton_targetx = @fixbutton_initx
|
||||||
|
|
||||||
|
if (not @console.opened and 0 - targety > @page_height/10) or (@console.opened and 0 - targety > @page_height*0.8)
|
||||||
|
@fixbutton_targety = @page_height - @fixbutton_inity - 50
|
||||||
|
else
|
||||||
|
@fixbutton_targety = @fixbutton_inity
|
||||||
|
|
||||||
|
|
||||||
|
# Stop dragging the fixbutton
|
||||||
|
stopDrag: ->
|
||||||
|
@fixbutton.parents().off "mousemove touchmove"
|
||||||
|
@fixbutton.off "mousemove touchmove"
|
||||||
|
@fixbutton.css("pointer-events", "")
|
||||||
|
$(".drag-bg").remove()
|
||||||
|
if not @fixbutton.hasClass("dragging")
|
||||||
|
return
|
||||||
|
@fixbutton.removeClass("dragging")
|
||||||
|
|
||||||
|
# Move back to initial position
|
||||||
|
if @fixbutton_targetx != @fixbutton.offset().left or @fixbutton_targety != @fixbutton.offset().top
|
||||||
|
# Animate fixbutton
|
||||||
|
if @move_lock == "y"
|
||||||
|
top = @fixbutton_targety
|
||||||
|
left = @fixbutton_initx
|
||||||
|
if @move_lock == "x"
|
||||||
|
top = @fixbutton_inity
|
||||||
|
left = @fixbutton_targetx
|
||||||
|
@fixbutton.stop().animate {"left": left, "top": top}, 500, "easeOutBack", =>
|
||||||
|
# Switch back to auto align
|
||||||
|
if @fixbutton_targetx == @fixbutton_initx # Closed
|
||||||
|
@fixbutton.css("left", "auto")
|
||||||
|
else # Opened
|
||||||
|
@fixbutton.css("left", left)
|
||||||
|
|
||||||
|
$(".fixbutton-bg").trigger "mouseout" # Switch fixbutton back to normal status
|
||||||
|
|
||||||
|
@stopDragX()
|
||||||
|
@console.stopDragY()
|
||||||
|
@move_lock = null
|
||||||
|
|
||||||
|
stopDragX: ->
|
||||||
|
# Animate sidebar and iframe
|
||||||
|
if @fixbutton_targetx == @fixbutton_initx or @move_lock == "y"
|
||||||
|
# Closed
|
||||||
|
targetx = 0
|
||||||
|
@opened = false
|
||||||
|
else
|
||||||
|
# Opened
|
||||||
|
targetx = @width
|
||||||
|
if @opened
|
||||||
|
@onOpened()
|
||||||
|
else
|
||||||
|
@when_loaded.done =>
|
||||||
|
@onOpened()
|
||||||
|
@opened = true
|
||||||
|
|
||||||
|
# Revent sidebar transitions
|
||||||
|
if @tag
|
||||||
|
@tag.css("transition", "0.4s ease-out")
|
||||||
|
@tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, =>
|
||||||
|
@tag.css("transition", "")
|
||||||
|
if not @opened
|
||||||
|
@container.remove()
|
||||||
|
@container = null
|
||||||
|
if @tag
|
||||||
|
@tag.remove()
|
||||||
|
@tag = null
|
||||||
|
|
||||||
|
# Revert body transformations
|
||||||
|
@log "stopdrag", "opened:", @opened
|
||||||
|
if not @opened
|
||||||
|
@onClosed()
|
||||||
|
|
||||||
|
sign: (inner_path, privatekey) ->
|
||||||
|
@wrapper.displayProgress("sign", "Signing: #{inner_path}...", 0)
|
||||||
|
@wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) =>
|
||||||
|
if res == "ok"
|
||||||
|
@wrapper.displayProgress("sign", "#{inner_path} signed!", 100)
|
||||||
|
else
|
||||||
|
@wrapper.displayProgress("sign", "Error signing #{inner_path}", -1)
|
||||||
|
|
||||||
|
publish: (inner_path, privatekey) ->
|
||||||
|
@wrapper.ws.cmd "sitePublish", {privatekey: privatekey, inner_path: inner_path, sign: true, update_changed_files: true}, (res) =>
|
||||||
|
if res == "ok"
|
||||||
|
@wrapper.notifications.add "sign", "done", "#{inner_path} Signed and published!", 5000
|
||||||
|
|
||||||
|
handleSiteDeleteClick: ->
|
||||||
|
if @wrapper.site_info.privatekey
|
||||||
|
question = "Are you sure?<br>This site has a saved private key"
|
||||||
|
options = ["Forget private key and delete site"]
|
||||||
|
else
|
||||||
|
question = "Are you sure?"
|
||||||
|
options = ["Delete this site", "Blacklist"]
|
||||||
|
@wrapper.displayConfirm question, options, (confirmed) =>
|
||||||
|
if confirmed == 1
|
||||||
|
@tag.find("#button-delete").addClass("loading")
|
||||||
|
@wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
|
||||||
|
document.location = $(".fixbutton-bg").attr("href")
|
||||||
|
else if confirmed == 2
|
||||||
|
@wrapper.displayPrompt "Blacklist this site", "text", "Delete and Blacklist", "Reason", (reason) =>
|
||||||
|
@tag.find("#button-delete").addClass("loading")
|
||||||
|
@wrapper.ws.cmd "siteblockAdd", [@wrapper.site_info.address, reason]
|
||||||
|
@wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, ->
|
||||||
|
document.location = $(".fixbutton-bg").attr("href")
|
||||||
|
|
||||||
|
onOpened: ->
|
||||||
|
@log "Opened"
|
||||||
|
@scrollable()
|
||||||
|
|
||||||
|
# Re-calculate height when site admin opened or closed
|
||||||
|
@tag.find("#checkbox-owned, #checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
|
||||||
|
setTimeout (=>
|
||||||
|
@scrollable()
|
||||||
|
), 300
|
||||||
|
|
||||||
|
# Site limit button
|
||||||
|
@tag.find("#button-sitelimit").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), (res) =>
|
||||||
|
if res == "ok"
|
||||||
|
@wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000
|
||||||
|
@updateHtmlTag()
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Site autodownload limit button
|
||||||
|
@tag.find("#button-autodownload_bigfile_size_limit").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.ws.cmd "siteSetAutodownloadBigfileLimit", $("#input-autodownload_bigfile_size_limit").val(), (res) =>
|
||||||
|
if res == "ok"
|
||||||
|
@wrapper.notifications.add "done-bigfilelimit", "done", "Site bigfile auto download limit modified!", 5000
|
||||||
|
@updateHtmlTag()
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Site start download optional files
|
||||||
|
@tag.find("#button-autodownload_previous").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.ws.cmd "siteUpdate", {"address": @wrapper.site_info.address, "check_files": true}, =>
|
||||||
|
@wrapper.notifications.add "done-download_optional", "done", "Optional files downloaded", 5000
|
||||||
|
|
||||||
|
@wrapper.notifications.add "start-download_optional", "info", "Optional files download started", 5000
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Database reload
|
||||||
|
@tag.find("#button-dbreload").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.ws.cmd "dbReload", [], =>
|
||||||
|
@wrapper.notifications.add "done-dbreload", "done", "Database schema reloaded!", 5000
|
||||||
|
@updateHtmlTag()
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Database rebuild
|
||||||
|
@tag.find("#button-dbrebuild").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.notifications.add "done-dbrebuild", "info", "Database rebuilding...."
|
||||||
|
@wrapper.ws.cmd "dbRebuild", [], =>
|
||||||
|
@wrapper.notifications.add "done-dbrebuild", "done", "Database rebuilt!", 5000
|
||||||
|
@updateHtmlTag()
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Update site
|
||||||
|
@tag.find("#button-update").off("click touchend").on "click touchend", =>
|
||||||
|
@tag.find("#button-update").addClass("loading")
|
||||||
|
@wrapper.ws.cmd "siteUpdate", @wrapper.site_info.address, =>
|
||||||
|
@wrapper.notifications.add "done-updated", "done", "Site updated!", 5000
|
||||||
|
@tag.find("#button-update").removeClass("loading")
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Pause site
|
||||||
|
@tag.find("#button-pause").off("click touchend").on "click touchend", =>
|
||||||
|
@tag.find("#button-pause").addClass("hidden")
|
||||||
|
@wrapper.ws.cmd "sitePause", @wrapper.site_info.address
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Resume site
|
||||||
|
@tag.find("#button-resume").off("click touchend").on "click touchend", =>
|
||||||
|
@tag.find("#button-resume").addClass("hidden")
|
||||||
|
@wrapper.ws.cmd "siteResume", @wrapper.site_info.address
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Delete site
|
||||||
|
@tag.find("#button-delete").off("click touchend").on "click touchend", =>
|
||||||
|
@handleSiteDeleteClick()
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Owned checkbox
|
||||||
|
@tag.find("#checkbox-owned").off("click touchend").on "click touchend", =>
|
||||||
|
owned = @tag.find("#checkbox-owned").is(":checked")
|
||||||
|
@wrapper.ws.cmd "siteSetOwned", [owned], (res_set_owned) =>
|
||||||
|
@log "Owned", owned
|
||||||
|
if owned
|
||||||
|
@wrapper.ws.cmd "siteRecoverPrivatekey", [], (res_recover) =>
|
||||||
|
if res_recover == "ok"
|
||||||
|
@wrapper.notifications.add("recover", "done", "Private key recovered from master seed", 5000)
|
||||||
|
else
|
||||||
|
@log "Unable to recover private key: #{res_recover.error}"
|
||||||
|
|
||||||
|
|
||||||
|
# Owned auto download checkbox
|
||||||
|
@tag.find("#checkbox-autodownloadoptional").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")]
|
||||||
|
|
||||||
|
# Change identity button
|
||||||
|
@tag.find("#button-identity").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.ws.cmd "certSelect"
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Save settings
|
||||||
|
@tag.find("#button-settings").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.ws.cmd "fileGet", "content.json", (res) =>
|
||||||
|
data = JSON.parse(res)
|
||||||
|
data["title"] = $("#settings-title").val()
|
||||||
|
data["description"] = $("#settings-description").val()
|
||||||
|
json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
|
||||||
|
@wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) =>
|
||||||
|
if res != "ok" # fileWrite failed
|
||||||
|
@wrapper.notifications.add "file-write", "error", "File write error: #{res}"
|
||||||
|
else
|
||||||
|
@wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000
|
||||||
|
if @wrapper.site_info.privatekey
|
||||||
|
@wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: "content.json", update_changed_files: true}
|
||||||
|
@updateHtmlTag()
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
# Open site directory
|
||||||
|
@tag.find("#link-directory").off("click touchend").on "click touchend", =>
|
||||||
|
@wrapper.ws.cmd "serverShowdirectory", ["site", @wrapper.site_info.address]
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Copy site with peers
|
||||||
|
@tag.find("#link-copypeers").off("click touchend").on "click touchend", (e) =>
|
||||||
|
copy_text = e.currentTarget.href
|
||||||
|
handler = (e) =>
|
||||||
|
e.clipboardData.setData('text/plain', copy_text)
|
||||||
|
e.preventDefault()
|
||||||
|
@wrapper.notifications.add "copy", "done", "Site address with peers copied to your clipboard", 5000
|
||||||
|
document.removeEventListener('copy', handler, true)
|
||||||
|
|
||||||
|
document.addEventListener('copy', handler, true)
|
||||||
|
document.execCommand('copy')
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Sign and publish content.json
|
||||||
|
$(document).on "click touchend", =>
|
||||||
|
@tag?.find("#button-sign-publish-menu").removeClass("visible")
|
||||||
|
@tag?.find(".contents + .flex").removeClass("sign-publish-flex")
|
||||||
|
|
||||||
|
@tag.find(".contents-content").off("click touchend").on "click touchend", (e) =>
|
||||||
|
$("#input-contents").val(e.currentTarget.innerText);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
menu = new Menu(@tag.find("#menu-sign-publish"))
|
||||||
|
menu.elem.css("margin-top", "-130px") # Open upwards
|
||||||
|
menu.addItem "Sign", =>
|
||||||
|
inner_path = @tag.find("#input-contents").val()
|
||||||
|
|
||||||
|
@wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) =>
|
||||||
|
if @wrapper.site_info.auth_address in rules.signers
|
||||||
|
# ZeroID or other ID provider
|
||||||
|
@sign(inner_path)
|
||||||
|
else if @wrapper.site_info.privatekey
|
||||||
|
# Privatekey stored in users.json
|
||||||
|
@sign(inner_path, "stored")
|
||||||
|
else
|
||||||
|
# Ask the user for privatekey
|
||||||
|
@wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
|
||||||
|
@sign(inner_path, privatekey)
|
||||||
|
|
||||||
|
@tag.find(".contents + .flex").removeClass "active"
|
||||||
|
menu.hide()
|
||||||
|
|
||||||
|
menu.addItem "Publish", =>
|
||||||
|
inner_path = @tag.find("#input-contents").val()
|
||||||
|
@wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}
|
||||||
|
|
||||||
|
@tag.find(".contents + .flex").removeClass "active"
|
||||||
|
menu.hide()
|
||||||
|
|
||||||
|
@tag.find("#menu-sign-publish").off("click touchend").on "click touchend", =>
|
||||||
|
if window.visible_menu == menu
|
||||||
|
@tag.find(".contents + .flex").removeClass "active"
|
||||||
|
menu.hide()
|
||||||
|
else
|
||||||
|
@tag.find(".contents + .flex").addClass "active"
|
||||||
|
@tag.find(".content-wrapper").prop "scrollTop", 10000
|
||||||
|
menu.show()
|
||||||
|
return false
|
||||||
|
|
||||||
|
$("body").on "click", =>
|
||||||
|
if @tag
|
||||||
|
@tag.find(".contents + .flex").removeClass "active"
|
||||||
|
|
||||||
|
@tag.find("#button-sign-publish").off("click touchend").on "click touchend", =>
|
||||||
|
inner_path = @tag.find("#input-contents").val()
|
||||||
|
|
||||||
|
@wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) =>
|
||||||
|
if @wrapper.site_info.auth_address in rules.signers
|
||||||
|
# ZeroID or other ID provider
|
||||||
|
@publish(inner_path, null)
|
||||||
|
else if @wrapper.site_info.privatekey
|
||||||
|
# Privatekey stored in users.json
|
||||||
|
@publish(inner_path, "stored")
|
||||||
|
else
|
||||||
|
# Ask the user for privatekey
|
||||||
|
@wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key
|
||||||
|
@publish(inner_path, privatekey)
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Close
|
||||||
|
@tag.find(".close").off("click touchend").on "click touchend", (e) =>
|
||||||
|
@close()
|
||||||
|
return false
|
||||||
|
|
||||||
|
@loadGlobe()
|
||||||
|
|
||||||
|
close: ->
|
||||||
|
@move_lock = "x"
|
||||||
|
@startDrag()
|
||||||
|
@stopDrag()
|
||||||
|
|
||||||
|
|
||||||
|
onClosed: ->
|
||||||
|
$(window).off "resize"
|
||||||
|
$(window).on "resize", @resized
|
||||||
|
$(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) =>
|
||||||
|
if e.target == document.body and not $(document.body).hasClass("body-sidebar") and not $(document.body).hasClass("body-console")
|
||||||
|
$(document.body).css("height", "auto").css("perspective", "").css("will-change", "").css("transition", "").off transitionEnd
|
||||||
|
@unloadGlobe()
|
||||||
|
|
||||||
|
# We dont need site info anymore
|
||||||
|
@wrapper.setSiteInfo = @original_set_site_info
|
||||||
|
|
||||||
|
|
||||||
|
loadGlobe: =>
|
||||||
|
if @tag.find(".globe").hasClass("loading")
|
||||||
|
setTimeout (=>
|
||||||
|
if typeof(DAT) == "undefined" # Globe script not loaded, do it first
|
||||||
|
script_tag = $("<script>")
|
||||||
|
script_tag.attr("nonce", @wrapper.script_nonce)
|
||||||
|
script_tag.attr("src", "/uimedia/globe/all.js")
|
||||||
|
script_tag.on("load", @displayGlobe)
|
||||||
|
document.head.appendChild(script_tag[0])
|
||||||
|
else
|
||||||
|
@displayGlobe()
|
||||||
|
), 600
|
||||||
|
|
||||||
|
|
||||||
|
displayGlobe: =>
|
||||||
|
img = new Image();
|
||||||
|
img.src = "/uimedia/globe/world.jpg";
|
||||||
|
img.onload = =>
|
||||||
|
@wrapper.ws.cmd "sidebarGetPeers", [], (globe_data) =>
|
||||||
|
if @globe
|
||||||
|
@globe.scene.remove(@globe.points)
|
||||||
|
@globe.addData( globe_data, {format: 'magnitude', name: "hello", animated: false} )
|
||||||
|
@globe.createPoints()
|
||||||
|
@tag?.find(".globe").removeClass("loading")
|
||||||
|
else if typeof(DAT) != "undefined"
|
||||||
|
try
|
||||||
|
@globe = new DAT.Globe( @tag.find(".globe")[0], {"imgDir": "/uimedia/globe/"} )
|
||||||
|
@globe.addData( globe_data, {format: 'magnitude', name: "hello"} )
|
||||||
|
@globe.createPoints()
|
||||||
|
@globe.animate()
|
||||||
|
catch e
|
||||||
|
console.log "WebGL error", e
|
||||||
|
@tag?.find(".globe").addClass("error").text("WebGL not supported")
|
||||||
|
@tag?.find(".globe").removeClass("loading")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
unloadGlobe: =>
|
||||||
|
if not @globe
|
||||||
|
return false
|
||||||
|
@globe.unload()
|
||||||
|
@globe = null
|
||||||
|
|
||||||
|
|
||||||
|
wrapper = window.wrapper
|
||||||
|
setTimeout ( ->
|
||||||
|
window.sidebar = new Sidebar(wrapper)
|
||||||
|
), 500
|
||||||
|
|
||||||
|
|
||||||
|
window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend'
|
|
@ -0,0 +1,169 @@
|
||||||
|
.menu {
|
||||||
|
font-family: Roboto, 'Segoe UI', 'Helvetica Neue'; z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-bg { width: 100%; height: 100%; position: fixed; }
|
||||||
|
.fixbutton.dragging { cursor: -webkit-grabbing; }
|
||||||
|
.fixbutton-bg:active { cursor: -webkit-grabbing; }
|
||||||
|
|
||||||
|
|
||||||
|
.body-sidebar, .body-console { background-color: #666 !important; }
|
||||||
|
#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left bottom; }
|
||||||
|
.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none; outline: 1px solid transparent }
|
||||||
|
.body-console iframe { transform: rotateX(5deg); opacity: 0.8; pointer-events: none; outline: 1px solid transparent }
|
||||||
|
|
||||||
|
.sidebar .label-right { float: right; margin-right: 7px; margin-top: 1px; float: right; }
|
||||||
|
.sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; }
|
||||||
|
.sidebar .link-right:hover { border-color: #CCC; }
|
||||||
|
.sidebar .link-right:active { background-color: #444 }
|
||||||
|
.sidebar .link-outline { outline: 1px solid #eee6; padding: 2px 13px; border-bottom: none; font-size: 80%; }
|
||||||
|
/* SIDEBAR */
|
||||||
|
|
||||||
|
.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; top: 0px; z-index: 2;}
|
||||||
|
.sidebar { background-color: #212121; position: fixed; backface-visibility: hidden; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
|
||||||
|
.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; transition: all 1s; opacity: 0 }
|
||||||
|
.sidebar-container.loaded .content { opacity: 1; transform: none }
|
||||||
|
.sidebar h1, .sidebar h2 { font-weight: lighter; }
|
||||||
|
.sidebar .close { color: #999; float: right; text-decoration: none; margin-top: -5px; padding: 0px 5px; font-size: 33px; margin-right: 20px; display: none }
|
||||||
|
.sidebar .button { margin: 0px; display: inline-block; transition: all 0.3s; box-sizing: border-box; max-width: 260px }
|
||||||
|
.sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none }
|
||||||
|
.sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px }
|
||||||
|
.sidebar #button-delete:hover { border: 1px solid #666; color: white }
|
||||||
|
|
||||||
|
.sidebar .flex { display: flex }
|
||||||
|
.sidebar .flex .input.text, .sidebar .flex input.text { width: 100%; }
|
||||||
|
.sidebar .flex .button { margin-left: 4px; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* FIELDS */
|
||||||
|
|
||||||
|
.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: inline-block; margin-bottom: 10px;
|
||||||
|
vertical-align: text-bottom; margin-right: 10px; width: 100%
|
||||||
|
}
|
||||||
|
.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: 260px; font-family: Consolas, monospace; }
|
||||||
|
.sidebar .fields .text.long { width: 330px; font-size: 72%; }
|
||||||
|
.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
|
||||||
|
.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
|
||||||
|
.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
|
||||||
|
|
||||||
|
/* Select */
|
||||||
|
.sidebar .fields select {
|
||||||
|
width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; appearance: none;
|
||||||
|
padding: 5px; padding-right: 25px; border: 0px; border-radius: 3px; height: 35px; vertical-align: 1px; box-shadow: 0px 1px 2px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; transform: rotateZ(90deg); height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
|
||||||
|
.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; margin-left: -59px; }
|
||||||
|
.sidebar .fields .checkbox-skin:before {
|
||||||
|
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
|
||||||
|
transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
|
||||||
|
.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
|
||||||
|
|
||||||
|
/* Fake input */
|
||||||
|
.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
|
||||||
|
|
||||||
|
/* GRAPH */
|
||||||
|
|
||||||
|
.graph { padding: 0px; list-style-type: none; width: 351px; background-color: black; height: 10px; border-radius: 8px; overflow: hidden; position: relative; font-size: 0 }
|
||||||
|
.graph li { height: 100%; position: absolute; transition: all 0.3s; }
|
||||||
|
.graph-stacked { white-space: nowrap; }
|
||||||
|
.graph-stacked li { position: static; display: inline-block; height: 20px }
|
||||||
|
|
||||||
|
.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
|
||||||
|
.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
|
||||||
|
.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
|
||||||
|
.graph-legend span { position: absolute; }
|
||||||
|
.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
|
||||||
|
.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
|
||||||
|
|
||||||
|
.filelist { font-size: 12px; font-family: monospace; margin: 0px; padding: 0px; list-style-type: none; line-height: 1.5em; }
|
||||||
|
.filelist li:before { content: '\2022'; font-size: 11px; line-height: 0px; vertical-align: 0px; margin-right: 5px; color: #FFBE00; }
|
||||||
|
.filelist li { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
|
/* COLORS */
|
||||||
|
|
||||||
|
.back-green { background-color: #2ECC71 }
|
||||||
|
.color-green:before { color: #2ECC71 }
|
||||||
|
.back-blue { background-color: #3BAFDA }
|
||||||
|
.color-blue:before { color: #3BAFDA }
|
||||||
|
.back-darkblue { background-color: #156fb7 }
|
||||||
|
.color-darkblue:before { color: #156fb7 }
|
||||||
|
.back-purple { background-color: #B10DC9 }
|
||||||
|
.color-purple:before { color: #B10DC9 }
|
||||||
|
.back-yellow { background-color: #FFDC00 }
|
||||||
|
.color-yellow:before { color: #FFDC00 }
|
||||||
|
.back-orange { background-color: #FF9800 }
|
||||||
|
.color-orange:before { color: #FF9800 }
|
||||||
|
.back-gray { background-color: #ECF0F1 }
|
||||||
|
.color-gray:before { color: #ECF0F1 }
|
||||||
|
.back-black { background-color: #34495E }
|
||||||
|
.color-black:before { color: #34495E }
|
||||||
|
.back-red { background-color: #5E4934 }
|
||||||
|
.color-red:before { color: #5E4934 }
|
||||||
|
.back-gray { background-color: #9e9e9e }
|
||||||
|
.color-gray:before { color: #9e9e9e }
|
||||||
|
.back-white { background-color: #EEE }
|
||||||
|
.color-white:before { color: #EEE }
|
||||||
|
.back-red { background-color: #E91E63 }
|
||||||
|
.color-red:before { color: #E91E63 }
|
||||||
|
|
||||||
|
|
||||||
|
/* Settings owned */
|
||||||
|
|
||||||
|
.owned-title { float: left }
|
||||||
|
#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
|
||||||
|
.settings-owned { clear: both }
|
||||||
|
#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; transition: all 0.3s linear; overflow: hidden }
|
||||||
|
#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 420px }
|
||||||
|
|
||||||
|
/* Settings autodownload */
|
||||||
|
|
||||||
|
.settings-autodownloadoptional { clear: both; box-sizing: border-box; padding-top: 0px; }
|
||||||
|
#checkbox-autodownloadoptional ~ .settings-autodownloadoptional { opacity: 0; max-height: 0px; transition: all 0.3s ease-in-out; overflow: hidden; }
|
||||||
|
#checkbox-autodownloadoptional:checked ~ .settings-autodownloadoptional { opacity: 1; max-height: 120px; padding-top: 30px; }
|
||||||
|
|
||||||
|
/* Globe */
|
||||||
|
.globe { width: 360px; height: 360px }
|
||||||
|
.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }
|
||||||
|
.globe.error { text-align: center; padding-top: 156px; box-sizing: border-box; opacity: 0.2; }
|
||||||
|
|
||||||
|
/* Sign publish */
|
||||||
|
.contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; margin-top: 10px }
|
||||||
|
.contents a { color: white }
|
||||||
|
.contents a:active { background-color: #6B6B6B }
|
||||||
|
|
||||||
|
.contents + .flex.active {
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
#wrapper-sign-publish {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#button-sign-publish, #menu-sign-publish {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px 10px;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
#button-sign-publish {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
#menu-sign-publish {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #AAA;
|
||||||
|
padding: 7px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
#menu-sign-publish:hover { color: white }
|
||||||
|
|
||||||
|
/* Small screen */
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.sidebar .close { display: block }
|
||||||
|
}
|
|
@ -0,0 +1,281 @@
|
||||||
|
|
||||||
|
/* ---- Console.css ---- */
|
||||||
|
|
||||||
|
|
||||||
|
.console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; }
|
||||||
|
.console-container .console { background-color: #212121; height: 100vh; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -o-transform: translateY(0px); -ms-transform: translateY(0px); transform: translateY(0px) ; padding-top: 80px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; }
|
||||||
|
|
||||||
|
.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; letter-spacing: 0.5px;}
|
||||||
|
.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; }
|
||||||
|
.console-tabs {
|
||||||
|
background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/
|
||||||
|
-webkit-box-shadow: -30px 0px 45px #7d2463; -moz-box-shadow: -30px 0px 45px #7d2463; -o-box-shadow: -30px 0px 45px #7d2463; -ms-box-shadow: -30px 0px 45px #7d2463; box-shadow: -30px 0px 45px #7d2463 ; background: -webkit-linear-gradient(-75deg, #591a48ed, #70305e66);background: -moz-linear-gradient(-75deg, #591a48ed, #70305e66);background: -o-linear-gradient(-75deg, #591a48ed, #70305e66);background: -ms-linear-gradient(-75deg, #591a48ed, #70305e66);background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473;
|
||||||
|
}
|
||||||
|
.console-tabs a {
|
||||||
|
margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA;
|
||||||
|
font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666;
|
||||||
|
border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.console-tabs a:hover { color: #FFF }
|
||||||
|
.console-tabs a.active { background-color: #46223c; color: #FFF }
|
||||||
|
.console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; }
|
||||||
|
|
||||||
|
.console .mynode {
|
||||||
|
border: 0.5px solid #aaa; width: 50px; height: 50px; -webkit-transform: rotateZ(45deg); -moz-transform: rotateZ(45deg); -o-transform: rotateZ(45deg); -ms-transform: rotateZ(45deg); transform: rotateZ(45deg) ; margin-top: -25px; margin-left: -25px;
|
||||||
|
opacity: 1; display: inline-block; background-color: #EEE; z-index: 9; position: absolute; outline: 5px solid #EEE;
|
||||||
|
}
|
||||||
|
.console .peers { width: 0px; height: 0px; position: absolute; left: -20px; top: -20px; text-align: center; }
|
||||||
|
.console .peer { left: 0px; top: 0px; position: absolute; }
|
||||||
|
.console .peer .icon { width: 20px; height: 20px; padding: 10px; display: inline-block; text-decoration: none; left: 200px; position: absolute; color: #666; }
|
||||||
|
.console .peer .icon:before { content: "\25BC"; position: absolute; margin-top: 3px; margin-left: -1px; opacity: 0; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s }
|
||||||
|
.console .peer .icon:hover:before { opacity: 1; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none }
|
||||||
|
.console .peer .line {
|
||||||
|
width: 187px; border-top: 1px solid #CCC; position: absolute; top: 20px; left: 20px;
|
||||||
|
-webkit-transform: rotateZ(334deg); -moz-transform: rotateZ(334deg); -o-transform: rotateZ(334deg); -ms-transform: rotateZ(334deg); transform: rotateZ(334deg) ; transform-origin: bottom left;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ---- Menu.css ---- */
|
||||||
|
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
background-color: white; padding: 10px 0px; position: absolute; top: 0px; left: 0px; max-height: 0px; overflow: hidden; -webkit-transform: translate(0px, -30px); -moz-transform: translate(0px, -30px); -o-transform: translate(0px, -30px); -ms-transform: translate(0px, -30px); transform: translate(0px, -30px) ; pointer-events: none;
|
||||||
|
-webkit-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -moz-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -o-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -ms-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); box-shadow: 0px 2px 8px rgba(0,0,0,0.3) ; -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; opacity: 0; -webkit-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -moz-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -o-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -ms-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out ;
|
||||||
|
}
|
||||||
|
.menu.visible { opacity: 1; max-height: 350px; -webkit-transform: translate(0px, 0px); -moz-transform: translate(0px, 0px); -o-transform: translate(0px, 0px); -ms-transform: translate(0px, 0px); transform: translate(0px, 0px) ; -webkit-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; -moz-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; -o-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; -ms-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out ; pointer-events: all }
|
||||||
|
|
||||||
|
.menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; -webkit-transition: all 0.2s; -moz-transition: all 0.2s; -o-transition: all 0.2s; -ms-transition: all 0.2s; transition: all 0.2s ; border-bottom: none; font-weight: normal; padding-left: 30px; }
|
||||||
|
.menu-item-separator { margin-top: 5px; border-top: 1px solid #eee }
|
||||||
|
|
||||||
|
.menu-item:hover { background-color: #F6F6F6; -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; color: inherit; border: none }
|
||||||
|
.menu-item:active, .menu-item:focus { background-color: #AF3BFF; color: white; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none }
|
||||||
|
.menu-item.selected:before {
|
||||||
|
content: "L"; display: inline-block; -webkit-transform: rotateZ(45deg) scaleX(-1); -moz-transform: rotateZ(45deg) scaleX(-1); -o-transform: rotateZ(45deg) scaleX(-1); -ms-transform: rotateZ(45deg) scaleX(-1); transform: rotateZ(45deg) scaleX(-1) ;
|
||||||
|
font-weight: bold; position: absolute; margin-left: -17px; font-size: 12px; margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.menu, .menu.visible { position: absolute; left: unset !important; right: 20px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Scrollbable.css ---- */
|
||||||
|
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable.showScroll::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
top: 5%;
|
||||||
|
right: 7px;
|
||||||
|
height: 90%;
|
||||||
|
width: 3px;
|
||||||
|
background: rgba(224, 224, 255, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable .content-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-right: 50%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
.scroller {
|
||||||
|
margin-top: 5px;
|
||||||
|
z-index: 5;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
width: 7px;
|
||||||
|
-webkit-border-radius: 5px; -moz-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; border-radius: 5px ;
|
||||||
|
background: #3A3A3A;
|
||||||
|
top: 0px;
|
||||||
|
left: 395px;
|
||||||
|
-webkit-transition: top .08s;
|
||||||
|
-moz-transition: top .08s;
|
||||||
|
-ms-transition: top .08s;
|
||||||
|
-o-transition: top .08s;
|
||||||
|
-webkit-transition: top .08s; -moz-transition: top .08s; -o-transition: top .08s; -ms-transition: top .08s; transition: top .08s ;
|
||||||
|
}
|
||||||
|
.scroller {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ---- Sidebar.css ---- */
|
||||||
|
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
font-family: Roboto, 'Segoe UI', 'Helvetica Neue'; z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-bg { width: 100%; height: 100%; position: fixed; }
|
||||||
|
.fixbutton.dragging { cursor: -webkit-grabbing; }
|
||||||
|
.fixbutton-bg:active { cursor: -webkit-grabbing; }
|
||||||
|
|
||||||
|
|
||||||
|
.body-sidebar, .body-console { background-color: #666 !important; }
|
||||||
|
#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left bottom; }
|
||||||
|
.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none; outline: 1px solid transparent }
|
||||||
|
.body-console iframe { -webkit-transform: rotateX(5deg); -moz-transform: rotateX(5deg); -o-transform: rotateX(5deg); -ms-transform: rotateX(5deg); transform: rotateX(5deg) ; opacity: 0.8; pointer-events: none; outline: 1px solid transparent }
|
||||||
|
|
||||||
|
.sidebar .label-right { float: right; margin-right: 7px; margin-top: 1px; float: right; }
|
||||||
|
.sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; }
|
||||||
|
.sidebar .link-right:hover { border-color: #CCC; }
|
||||||
|
.sidebar .link-right:active { background-color: #444 }
|
||||||
|
.sidebar .link-outline { outline: 1px solid #eee6; padding: 2px 13px; border-bottom: none; font-size: 80%; }
|
||||||
|
/* SIDEBAR */
|
||||||
|
|
||||||
|
.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; top: 0px; z-index: 2;}
|
||||||
|
.sidebar { background-color: #212121; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/
|
||||||
|
.sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; -webkit-transition: all 1s; -moz-transition: all 1s; -o-transition: all 1s; -ms-transition: all 1s; transition: all 1s ; opacity: 0 }
|
||||||
|
.sidebar-container.loaded .content { opacity: 1; -webkit-transform: none ; -moz-transform: none ; -o-transform: none ; -ms-transform: none ; transform: none }
|
||||||
|
.sidebar h1, .sidebar h2 { font-weight: lighter; }
|
||||||
|
.sidebar .close { color: #999; float: right; text-decoration: none; margin-top: -5px; padding: 0px 5px; font-size: 33px; margin-right: 20px; display: none }
|
||||||
|
.sidebar .button { margin: 0px; display: inline-block; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; max-width: 260px }
|
||||||
|
.sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none }
|
||||||
|
.sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px }
|
||||||
|
.sidebar #button-delete:hover { border: 1px solid #666; color: white }
|
||||||
|
|
||||||
|
.sidebar .flex { display: flex }
|
||||||
|
.sidebar .flex .input.text, .sidebar .flex input.text { width: 100%; }
|
||||||
|
.sidebar .flex .button { margin-left: 4px; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* FIELDS */
|
||||||
|
|
||||||
|
.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: inline-block; margin-bottom: 10px;
|
||||||
|
vertical-align: text-bottom; margin-right: 10px; width: 100%
|
||||||
|
}
|
||||||
|
.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: 260px; font-family: Consolas, monospace; }
|
||||||
|
.sidebar .fields .text.long { width: 330px; font-size: 72%; }
|
||||||
|
.sidebar .fields .disabled { color: #AAA; background-color: #3B3B3B; }
|
||||||
|
.sidebar .fields .text-num { width: 30px; text-align: right; padding-right: 30px; }
|
||||||
|
.sidebar .fields .text-post { color: white; font-family: Consolas, monospace; display: inline-block; font-size: 13px; margin-left: -25px; width: 25px; }
|
||||||
|
|
||||||
|
/* Select */
|
||||||
|
.sidebar .fields select {
|
||||||
|
width: 225px; background-color: #3B3B3B; color: white; font-family: Consolas, monospace; -webkit-appearance: none; -moz-appearance: none; -o-appearance: none; -ms-appearance: none; appearance: none ;
|
||||||
|
padding: 5px; padding-right: 25px; border: 0px; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; height: 35px; vertical-align: 1px; -webkit-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -moz-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -o-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); -ms-box-shadow: 0px 1px 2px rgba(0,0,0,0.5); box-shadow: 0px 1px 2px rgba(0,0,0,0.5) ;
|
||||||
|
}
|
||||||
|
.sidebar .fields .select-down { margin-left: -39px; width: 34px; display: inline-block; -webkit-transform: rotateZ(90deg); -moz-transform: rotateZ(90deg); -o-transform: rotateZ(90deg); -ms-transform: rotateZ(90deg); transform: rotateZ(90deg) ; height: 35px; vertical-align: -8px; pointer-events: none; font-weight: bold }
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.sidebar .fields .checkbox { width: 50px; height: 24px; position: relative; z-index: 999; opacity: 0; }
|
||||||
|
.sidebar .fields .checkbox-skin { background-color: #CCC; width: 50px; height: 24px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; display: inline-block; margin-left: -59px; }
|
||||||
|
.sidebar .fields .checkbox-skin:before {
|
||||||
|
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; -webkit-border-radius: 100%; -moz-border-radius: 100%; -o-border-radius: 100%; -ms-border-radius: 100%; border-radius: 100% ; margin-top: 2px; margin-left: 2px;
|
||||||
|
-webkit-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -moz-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -o-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -ms-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86) ;
|
||||||
|
}
|
||||||
|
.sidebar .fields .checkbox:checked ~ .checkbox-skin:before { margin-left: 27px; }
|
||||||
|
.sidebar .fields .checkbox:checked ~ .checkbox-skin { background-color: #2ECC71; }
|
||||||
|
|
||||||
|
/* Fake input */
|
||||||
|
.sidebar .input { font-size: 13px; width: 250px; display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: top }
|
||||||
|
|
||||||
|
/* 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; font-size: 0 }
|
||||||
|
.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 { white-space: nowrap; }
|
||||||
|
.graph-stacked li { position: static; display: inline-block; height: 20px }
|
||||||
|
|
||||||
|
.graph-legend { padding: 0px; list-style-type: none; margin-top: 13px; font-family: Consolas, "Andale Mono", monospace; font-size: 13px; text-transform: capitalize; }
|
||||||
|
.sidebar .graph-legend li { margin: 0px; margin-top: 5px; margin-left: 0px; width: 160px; float: left; position: relative; }
|
||||||
|
.sidebar .graph-legend li:nth-child(odd) { margin-right: 29px }
|
||||||
|
.graph-legend span { position: absolute; }
|
||||||
|
.graph-legend b { text-align: right; display: inline-block; width: 50px; float: right; font-weight: normal; }
|
||||||
|
.graph-legend li:before { content: '\2022'; font-size: 23px; line-height: 0px; vertical-align: -3px; margin-right: 5px; }
|
||||||
|
|
||||||
|
.filelist { font-size: 12px; font-family: monospace; margin: 0px; padding: 0px; list-style-type: none; line-height: 1.5em; }
|
||||||
|
.filelist li:before { content: '\2022'; font-size: 11px; line-height: 0px; vertical-align: 0px; margin-right: 5px; color: #FFBE00; }
|
||||||
|
.filelist li { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
|
/* COLORS */
|
||||||
|
|
||||||
|
.back-green { background-color: #2ECC71 }
|
||||||
|
.color-green:before { color: #2ECC71 }
|
||||||
|
.back-blue { background-color: #3BAFDA }
|
||||||
|
.color-blue:before { color: #3BAFDA }
|
||||||
|
.back-darkblue { background-color: #156fb7 }
|
||||||
|
.color-darkblue:before { color: #156fb7 }
|
||||||
|
.back-purple { background-color: #B10DC9 }
|
||||||
|
.color-purple:before { color: #B10DC9 }
|
||||||
|
.back-yellow { background-color: #FFDC00 }
|
||||||
|
.color-yellow:before { color: #FFDC00 }
|
||||||
|
.back-orange { background-color: #FF9800 }
|
||||||
|
.color-orange:before { color: #FF9800 }
|
||||||
|
.back-gray { background-color: #ECF0F1 }
|
||||||
|
.color-gray:before { color: #ECF0F1 }
|
||||||
|
.back-black { background-color: #34495E }
|
||||||
|
.color-black:before { color: #34495E }
|
||||||
|
.back-red { background-color: #5E4934 }
|
||||||
|
.color-red:before { color: #5E4934 }
|
||||||
|
.back-gray { background-color: #9e9e9e }
|
||||||
|
.color-gray:before { color: #9e9e9e }
|
||||||
|
.back-white { background-color: #EEE }
|
||||||
|
.color-white:before { color: #EEE }
|
||||||
|
.back-red { background-color: #E91E63 }
|
||||||
|
.color-red:before { color: #E91E63 }
|
||||||
|
|
||||||
|
|
||||||
|
/* Settings owned */
|
||||||
|
|
||||||
|
.owned-title { float: left }
|
||||||
|
#checkbox-owned { margin-bottom: 25px; margin-top: 26px; margin-left: 11px; }
|
||||||
|
.settings-owned { clear: both }
|
||||||
|
#checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; -webkit-transition: all 0.3s linear; -moz-transition: all 0.3s linear; -o-transition: all 0.3s linear; -ms-transition: all 0.3s linear; transition: all 0.3s linear ; overflow: hidden }
|
||||||
|
#checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 420px }
|
||||||
|
|
||||||
|
/* Settings autodownload */
|
||||||
|
|
||||||
|
.settings-autodownloadoptional { clear: both; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; padding-top: 0px; }
|
||||||
|
#checkbox-autodownloadoptional ~ .settings-autodownloadoptional { opacity: 0; max-height: 0px; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; overflow: hidden; }
|
||||||
|
#checkbox-autodownloadoptional:checked ~ .settings-autodownloadoptional { opacity: 1; max-height: 120px; padding-top: 30px; }
|
||||||
|
|
||||||
|
/* Globe */
|
||||||
|
.globe { width: 360px; height: 360px }
|
||||||
|
.globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat }
|
||||||
|
.globe.error { text-align: center; padding-top: 156px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; opacity: 0.2; }
|
||||||
|
|
||||||
|
/* Sign publish */
|
||||||
|
.contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; margin-top: 10px }
|
||||||
|
.contents a { color: white }
|
||||||
|
.contents a:active { background-color: #6B6B6B }
|
||||||
|
|
||||||
|
.contents + .flex.active {
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
#wrapper-sign-publish {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#button-sign-publish, #menu-sign-publish {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px 10px;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
#button-sign-publish {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
#menu-sign-publish {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #AAA;
|
||||||
|
padding: 7px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
#menu-sign-publish:hover { color: white }
|
||||||
|
|
||||||
|
/* Small screen */
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.sidebar .close { display: block }
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,340 @@
|
||||||
|
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||||
|
var specialElHandlers = {
|
||||||
|
/**
|
||||||
|
* Needed for IE. Apparently IE doesn't think
|
||||||
|
* that "selected" is an attribute when reading
|
||||||
|
* over the attributes using selectEl.attributes
|
||||||
|
*/
|
||||||
|
OPTION: function(fromEl, toEl) {
|
||||||
|
if ((fromEl.selected = toEl.selected)) {
|
||||||
|
fromEl.setAttribute('selected', '');
|
||||||
|
} else {
|
||||||
|
fromEl.removeAttribute('selected', '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The "value" attribute is special for the <input> element
|
||||||
|
* since it sets the initial value. Changing the "value"
|
||||||
|
* attribute without changing the "value" property will have
|
||||||
|
* no effect since it is only used to the set the initial value.
|
||||||
|
* Similar for the "checked" attribute.
|
||||||
|
*/
|
||||||
|
/*INPUT: function(fromEl, toEl) {
|
||||||
|
fromEl.checked = toEl.checked;
|
||||||
|
fromEl.value = toEl.value;
|
||||||
|
|
||||||
|
if (!toEl.hasAttribute('checked')) {
|
||||||
|
fromEl.removeAttribute('checked');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toEl.hasAttribute('value')) {
|
||||||
|
fromEl.removeAttribute('value');
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
};
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop over all of the attributes on the target node and make sure the
|
||||||
|
* original DOM node has the same attributes. If an attribute
|
||||||
|
* found on the original node is not on the new node then remove it from
|
||||||
|
* the original node
|
||||||
|
* @param {HTMLElement} fromNode
|
||||||
|
* @param {HTMLElement} toNode
|
||||||
|
*/
|
||||||
|
function morphAttrs(fromNode, toNode) {
|
||||||
|
var attrs = toNode.attributes;
|
||||||
|
var i;
|
||||||
|
var attr;
|
||||||
|
var attrName;
|
||||||
|
var attrValue;
|
||||||
|
var foundAttrs = {};
|
||||||
|
|
||||||
|
for (i=attrs.length-1; i>=0; i--) {
|
||||||
|
attr = attrs[i];
|
||||||
|
if (attr.specified !== false) {
|
||||||
|
attrName = attr.name;
|
||||||
|
attrValue = attr.value;
|
||||||
|
foundAttrs[attrName] = true;
|
||||||
|
|
||||||
|
if (fromNode.getAttribute(attrName) !== attrValue) {
|
||||||
|
fromNode.setAttribute(attrName, attrValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete any extra attributes found on the original DOM element that weren't
|
||||||
|
// found on the target element.
|
||||||
|
attrs = fromNode.attributes;
|
||||||
|
|
||||||
|
for (i=attrs.length-1; i>=0; i--) {
|
||||||
|
attr = attrs[i];
|
||||||
|
if (attr.specified !== false) {
|
||||||
|
attrName = attr.name;
|
||||||
|
if (!foundAttrs.hasOwnProperty(attrName)) {
|
||||||
|
fromNode.removeAttribute(attrName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the children of one DOM element to another DOM element
|
||||||
|
*/
|
||||||
|
function moveChildren(from, to) {
|
||||||
|
var curChild = from.firstChild;
|
||||||
|
while(curChild) {
|
||||||
|
var nextChild = curChild.nextSibling;
|
||||||
|
to.appendChild(curChild);
|
||||||
|
curChild = nextChild;
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
|
||||||
|
function morphdom(fromNode, toNode, options) {
|
||||||
|
if (!options) {
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof toNode === 'string') {
|
||||||
|
var newBodyEl = document.createElement('body');
|
||||||
|
newBodyEl.innerHTML = toNode;
|
||||||
|
toNode = newBodyEl.childNodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedEls = {}; // Used to save off DOM elements with IDs
|
||||||
|
var unmatchedEls = {};
|
||||||
|
var onNodeDiscarded = options.onNodeDiscarded || noop;
|
||||||
|
var onBeforeMorphEl = options.onBeforeMorphEl || noop;
|
||||||
|
var onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop;
|
||||||
|
|
||||||
|
function removeNodeHelper(node, nestedInSavedEl) {
|
||||||
|
var id = node.id;
|
||||||
|
// If the node has an ID then save it off since we will want
|
||||||
|
// to reuse it in case the target DOM tree has a DOM element
|
||||||
|
// with the same ID
|
||||||
|
if (id) {
|
||||||
|
savedEls[id] = node;
|
||||||
|
} else if (!nestedInSavedEl) {
|
||||||
|
// If we are not nested in a saved element then we know that this node has been
|
||||||
|
// completely discarded and will not exist in the final DOM.
|
||||||
|
onNodeDiscarded(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.nodeType === 1) {
|
||||||
|
var curChild = node.firstChild;
|
||||||
|
while(curChild) {
|
||||||
|
removeNodeHelper(curChild, nestedInSavedEl || id);
|
||||||
|
curChild = curChild.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkDiscardedChildNodes(node) {
|
||||||
|
if (node.nodeType === 1) {
|
||||||
|
var curChild = node.firstChild;
|
||||||
|
while(curChild) {
|
||||||
|
|
||||||
|
|
||||||
|
if (!curChild.id) {
|
||||||
|
// We only want to handle nodes that don't have an ID to avoid double
|
||||||
|
// walking the same saved element.
|
||||||
|
|
||||||
|
onNodeDiscarded(curChild);
|
||||||
|
|
||||||
|
// Walk recursively
|
||||||
|
walkDiscardedChildNodes(curChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
curChild = curChild.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNode(node, parentNode, alreadyVisited) {
|
||||||
|
parentNode.removeChild(node);
|
||||||
|
|
||||||
|
if (alreadyVisited) {
|
||||||
|
if (!node.id) {
|
||||||
|
onNodeDiscarded(node);
|
||||||
|
walkDiscardedChildNodes(node);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removeNodeHelper(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function morphEl(fromNode, toNode, alreadyVisited) {
|
||||||
|
if (toNode.id) {
|
||||||
|
// If an element with an ID is being morphed then it is will be in the final
|
||||||
|
// DOM so clear it out of the saved elements collection
|
||||||
|
delete savedEls[toNode.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onBeforeMorphEl(fromNode, toNode) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
morphAttrs(fromNode, toNode);
|
||||||
|
|
||||||
|
if (onBeforeMorphElChildren(fromNode, toNode) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var curToNodeChild = toNode.firstChild;
|
||||||
|
var curFromNodeChild = fromNode.firstChild;
|
||||||
|
var curToNodeId;
|
||||||
|
|
||||||
|
var fromNextSibling;
|
||||||
|
var toNextSibling;
|
||||||
|
var savedEl;
|
||||||
|
var unmatchedEl;
|
||||||
|
|
||||||
|
outer: while(curToNodeChild) {
|
||||||
|
toNextSibling = curToNodeChild.nextSibling;
|
||||||
|
curToNodeId = curToNodeChild.id;
|
||||||
|
|
||||||
|
while(curFromNodeChild) {
|
||||||
|
var curFromNodeId = curFromNodeChild.id;
|
||||||
|
fromNextSibling = curFromNodeChild.nextSibling;
|
||||||
|
|
||||||
|
if (!alreadyVisited) {
|
||||||
|
if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) {
|
||||||
|
unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl);
|
||||||
|
morphEl(curFromNodeChild, unmatchedEl, alreadyVisited);
|
||||||
|
curFromNodeChild = fromNextSibling;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var curFromNodeType = curFromNodeChild.nodeType;
|
||||||
|
|
||||||
|
if (curFromNodeType === curToNodeChild.nodeType) {
|
||||||
|
var isCompatible = false;
|
||||||
|
|
||||||
|
if (curFromNodeType === 1) { // Both nodes being compared are Element nodes
|
||||||
|
if (curFromNodeChild.tagName === curToNodeChild.tagName) {
|
||||||
|
// We have compatible DOM elements
|
||||||
|
if (curFromNodeId || curToNodeId) {
|
||||||
|
// If either DOM element has an ID then we handle
|
||||||
|
// those differently since we want to match up
|
||||||
|
// by ID
|
||||||
|
if (curToNodeId === curFromNodeId) {
|
||||||
|
isCompatible = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isCompatible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCompatible) {
|
||||||
|
// We found compatible DOM elements so add a
|
||||||
|
// task to morph the compatible DOM elements
|
||||||
|
morphEl(curFromNodeChild, curToNodeChild, alreadyVisited);
|
||||||
|
}
|
||||||
|
} else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes
|
||||||
|
isCompatible = true;
|
||||||
|
curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCompatible) {
|
||||||
|
curToNodeChild = toNextSibling;
|
||||||
|
curFromNodeChild = fromNextSibling;
|
||||||
|
continue outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No compatible match so remove the old node from the DOM
|
||||||
|
removeNode(curFromNodeChild, fromNode, alreadyVisited);
|
||||||
|
|
||||||
|
curFromNodeChild = fromNextSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curToNodeId) {
|
||||||
|
if ((savedEl = savedEls[curToNodeId])) {
|
||||||
|
morphEl(savedEl, curToNodeChild, true);
|
||||||
|
curToNodeChild = savedEl; // We want to append the saved element instead
|
||||||
|
} else {
|
||||||
|
// The current DOM element in the target tree has an ID
|
||||||
|
// but we did not find a match in any of the corresponding
|
||||||
|
// siblings. We just put the target element in the old DOM tree
|
||||||
|
// but if we later find an element in the old DOM tree that has
|
||||||
|
// a matching ID then we will replace the target element
|
||||||
|
// with the corresponding old element and morph the old element
|
||||||
|
unmatchedEls[curToNodeId] = curToNodeChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got this far then we did not find a candidate match for our "to node"
|
||||||
|
// and we exhausted all of the children "from" nodes. Therefore, we will just
|
||||||
|
// append the current "to node" to the end
|
||||||
|
fromNode.appendChild(curToNodeChild);
|
||||||
|
|
||||||
|
curToNodeChild = toNextSibling;
|
||||||
|
curFromNodeChild = fromNextSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have processed all of the "to nodes". If curFromNodeChild is non-null then
|
||||||
|
// we still have some from nodes left over that need to be removed
|
||||||
|
while(curFromNodeChild) {
|
||||||
|
fromNextSibling = curFromNodeChild.nextSibling;
|
||||||
|
removeNode(curFromNodeChild, fromNode, alreadyVisited);
|
||||||
|
curFromNodeChild = fromNextSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
var specialElHandler = specialElHandlers[fromNode.tagName];
|
||||||
|
if (specialElHandler) {
|
||||||
|
specialElHandler(fromNode, toNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var morphedNode = fromNode;
|
||||||
|
var morphedNodeType = morphedNode.nodeType;
|
||||||
|
var toNodeType = toNode.nodeType;
|
||||||
|
|
||||||
|
// Handle the case where we are given two DOM nodes that are not
|
||||||
|
// compatible (e.g. <div> --> <span> or <div> --> TEXT)
|
||||||
|
if (morphedNodeType === 1) {
|
||||||
|
if (toNodeType === 1) {
|
||||||
|
if (morphedNode.tagName !== toNode.tagName) {
|
||||||
|
onNodeDiscarded(fromNode);
|
||||||
|
morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Going from an element node to a text node
|
||||||
|
return toNode;
|
||||||
|
}
|
||||||
|
} else if (morphedNodeType === 3) { // Text node
|
||||||
|
if (toNodeType === 3) {
|
||||||
|
morphedNode.nodeValue = toNode.nodeValue;
|
||||||
|
return morphedNode;
|
||||||
|
} else {
|
||||||
|
onNodeDiscarded(fromNode);
|
||||||
|
// Text node to something else
|
||||||
|
return toNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
morphEl(morphedNode, toNode, false);
|
||||||
|
|
||||||
|
// Fire the "onNodeDiscarded" event for any saved elements
|
||||||
|
// that never found a new home in the morphed DOM
|
||||||
|
for (var savedElId in savedEls) {
|
||||||
|
if (savedEls.hasOwnProperty(savedElId)) {
|
||||||
|
var savedEl = savedEls[savedElId];
|
||||||
|
onNodeDiscarded(savedEl);
|
||||||
|
walkDiscardedChildNodes(savedEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (morphedNode !== fromNode && fromNode.parentNode) {
|
||||||
|
fromNode.parentNode.replaceChild(morphedNode, fromNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return morphedNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = morphdom;
|
||||||
|
},{}]},{},[1])(1)
|
||||||
|
});
|
|
@ -0,0 +1,60 @@
|
||||||
|
/**
|
||||||
|
* @author alteredq / http://alteredqualia.com/
|
||||||
|
* @author mr.doob / http://mrdoob.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
Detector = {
|
||||||
|
|
||||||
|
canvas : !! window.CanvasRenderingContext2D,
|
||||||
|
webgl : ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(),
|
||||||
|
workers : !! window.Worker,
|
||||||
|
fileapi : window.File && window.FileReader && window.FileList && window.Blob,
|
||||||
|
|
||||||
|
getWebGLErrorMessage : function () {
|
||||||
|
|
||||||
|
var domElement = document.createElement( 'div' );
|
||||||
|
|
||||||
|
domElement.style.fontFamily = 'monospace';
|
||||||
|
domElement.style.fontSize = '13px';
|
||||||
|
domElement.style.textAlign = 'center';
|
||||||
|
domElement.style.background = '#eee';
|
||||||
|
domElement.style.color = '#000';
|
||||||
|
domElement.style.padding = '1em';
|
||||||
|
domElement.style.width = '475px';
|
||||||
|
domElement.style.margin = '5em auto 0';
|
||||||
|
|
||||||
|
if ( ! this.webgl ) {
|
||||||
|
|
||||||
|
domElement.innerHTML = window.WebGLRenderingContext ? [
|
||||||
|
'Sorry, your graphics card doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a>'
|
||||||
|
].join( '\n' ) : [
|
||||||
|
'Sorry, your browser doesn\'t support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">WebGL</a><br/>',
|
||||||
|
'Please try with',
|
||||||
|
'<a href="http://www.google.com/chrome">Chrome</a>, ',
|
||||||
|
'<a href="http://www.mozilla.com/en-US/firefox/new/">Firefox 4</a> or',
|
||||||
|
'<a href="http://nightly.webkit.org/">Webkit Nightly (Mac)</a>'
|
||||||
|
].join( '\n' );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return domElement;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
addGetWebGLMessage : function ( parameters ) {
|
||||||
|
|
||||||
|
var parent, id, domElement;
|
||||||
|
|
||||||
|
parameters = parameters || {};
|
||||||
|
|
||||||
|
parent = parameters.parent !== undefined ? parameters.parent : document.body;
|
||||||
|
id = parameters.id !== undefined ? parameters.id : 'oldie';
|
||||||
|
|
||||||
|
domElement = Detector.getWebGLErrorMessage();
|
||||||
|
domElement.id = id;
|
||||||
|
|
||||||
|
parent.appendChild( domElement );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Tween.js - http://github.com/sole/tween.js
|
||||||
|
var TWEEN=TWEEN||function(){var a,e,c,d,f=[];return{start:function(g){c=setInterval(this.update,1E3/(g||60))},stop:function(){clearInterval(c)},add:function(g){f.push(g)},remove:function(g){a=f.indexOf(g);a!==-1&&f.splice(a,1)},update:function(){a=0;e=f.length;for(d=(new Date).getTime();a<e;)if(f[a].update(d))a++;else{f.splice(a,1);e--}}}}();
|
||||||
|
TWEEN.Tween=function(a){var e={},c={},d={},f=1E3,g=0,j=null,n=TWEEN.Easing.Linear.EaseNone,k=null,l=null,m=null;this.to=function(b,h){if(h!==null)f=h;for(var i in b)if(a[i]!==null)d[i]=b[i];return this};this.start=function(){TWEEN.add(this);j=(new Date).getTime()+g;for(var b in d)if(a[b]!==null){e[b]=a[b];c[b]=d[b]-a[b]}return this};this.stop=function(){TWEEN.remove(this);return this};this.delay=function(b){g=b;return this};this.easing=function(b){n=b;return this};this.chain=function(b){k=b};this.onUpdate=
|
||||||
|
function(b){l=b;return this};this.onComplete=function(b){m=b;return this};this.update=function(b){var h,i;if(b<j)return true;b=(b-j)/f;b=b>1?1:b;i=n(b);for(h in c)a[h]=e[h]+c[h]*i;l!==null&&l.call(a,i);if(b==1){m!==null&&m.call(a);k!==null&&k.start();return false}return true}};TWEEN.Easing={Linear:{},Quadratic:{},Cubic:{},Quartic:{},Quintic:{},Sinusoidal:{},Exponential:{},Circular:{},Elastic:{},Back:{},Bounce:{}};TWEEN.Easing.Linear.EaseNone=function(a){return a};
|
||||||
|
TWEEN.Easing.Quadratic.EaseIn=function(a){return a*a};TWEEN.Easing.Quadratic.EaseOut=function(a){return-a*(a-2)};TWEEN.Easing.Quadratic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a;return-0.5*(--a*(a-2)-1)};TWEEN.Easing.Cubic.EaseIn=function(a){return a*a*a};TWEEN.Easing.Cubic.EaseOut=function(a){return--a*a*a+1};TWEEN.Easing.Cubic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a;return 0.5*((a-=2)*a*a+2)};TWEEN.Easing.Quartic.EaseIn=function(a){return a*a*a*a};
|
||||||
|
TWEEN.Easing.Quartic.EaseOut=function(a){return-(--a*a*a*a-1)};TWEEN.Easing.Quartic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a;return-0.5*((a-=2)*a*a*a-2)};TWEEN.Easing.Quintic.EaseIn=function(a){return a*a*a*a*a};TWEEN.Easing.Quintic.EaseOut=function(a){return(a-=1)*a*a*a*a+1};TWEEN.Easing.Quintic.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*a*a*a;return 0.5*((a-=2)*a*a*a*a+2)};TWEEN.Easing.Sinusoidal.EaseIn=function(a){return-Math.cos(a*Math.PI/2)+1};
|
||||||
|
TWEEN.Easing.Sinusoidal.EaseOut=function(a){return Math.sin(a*Math.PI/2)};TWEEN.Easing.Sinusoidal.EaseInOut=function(a){return-0.5*(Math.cos(Math.PI*a)-1)};TWEEN.Easing.Exponential.EaseIn=function(a){return a==0?0:Math.pow(2,10*(a-1))};TWEEN.Easing.Exponential.EaseOut=function(a){return a==1?1:-Math.pow(2,-10*a)+1};TWEEN.Easing.Exponential.EaseInOut=function(a){if(a==0)return 0;if(a==1)return 1;if((a*=2)<1)return 0.5*Math.pow(2,10*(a-1));return 0.5*(-Math.pow(2,-10*(a-1))+2)};
|
||||||
|
TWEEN.Easing.Circular.EaseIn=function(a){return-(Math.sqrt(1-a*a)-1)};TWEEN.Easing.Circular.EaseOut=function(a){return Math.sqrt(1- --a*a)};TWEEN.Easing.Circular.EaseInOut=function(a){if((a/=0.5)<1)return-0.5*(Math.sqrt(1-a*a)-1);return 0.5*(Math.sqrt(1-(a-=2)*a)+1)};TWEEN.Easing.Elastic.EaseIn=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return-(c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d))};
|
||||||
|
TWEEN.Easing.Elastic.EaseOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);return c*Math.pow(2,-10*a)*Math.sin((a-e)*2*Math.PI/d)+1};
|
||||||
|
TWEEN.Easing.Elastic.EaseInOut=function(a){var e,c=0.1,d=0.4;if(a==0)return 0;if(a==1)return 1;d||(d=0.3);if(!c||c<1){c=1;e=d/4}else e=d/(2*Math.PI)*Math.asin(1/c);if((a*=2)<1)return-0.5*c*Math.pow(2,10*(a-=1))*Math.sin((a-e)*2*Math.PI/d);return c*Math.pow(2,-10*(a-=1))*Math.sin((a-e)*2*Math.PI/d)*0.5+1};TWEEN.Easing.Back.EaseIn=function(a){return a*a*(2.70158*a-1.70158)};TWEEN.Easing.Back.EaseOut=function(a){return(a-=1)*a*(2.70158*a+1.70158)+1};
|
||||||
|
TWEEN.Easing.Back.EaseInOut=function(a){if((a*=2)<1)return 0.5*a*a*(3.5949095*a-2.5949095);return 0.5*((a-=2)*a*(3.5949095*a+2.5949095)+2)};TWEEN.Easing.Bounce.EaseIn=function(a){return 1-TWEEN.Easing.Bounce.EaseOut(1-a)};TWEEN.Easing.Bounce.EaseOut=function(a){return(a/=1)<1/2.75?7.5625*a*a:a<2/2.75?7.5625*(a-=1.5/2.75)*a+0.75:a<2.5/2.75?7.5625*(a-=2.25/2.75)*a+0.9375:7.5625*(a-=2.625/2.75)*a+0.984375};
|
||||||
|
TWEEN.Easing.Bounce.EaseInOut=function(a){if(a<0.5)return TWEEN.Easing.Bounce.EaseIn(a*2)*0.5;return TWEEN.Easing.Bounce.EaseOut(a*2-1)*0.5+0.5};
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,436 @@
|
||||||
|
/**
|
||||||
|
* dat.globe Javascript WebGL Globe Toolkit
|
||||||
|
* http://dataarts.github.com/dat.globe
|
||||||
|
*
|
||||||
|
* Copyright 2011 Data Arts Team, Google Creative Lab
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
var DAT = DAT || {};
|
||||||
|
|
||||||
|
DAT.Globe = function(container, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
var colorFn = opts.colorFn || function(x) {
|
||||||
|
var c = new THREE.Color();
|
||||||
|
c.setHSL( ( 0.5 - (x * 2) ), Math.max(0.8, 1.0 - (x * 3)), 0.5 );
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
var imgDir = opts.imgDir || '/globe/';
|
||||||
|
|
||||||
|
var Shaders = {
|
||||||
|
'earth' : {
|
||||||
|
uniforms: {
|
||||||
|
'texture': { type: 't', value: null }
|
||||||
|
},
|
||||||
|
vertexShader: [
|
||||||
|
'varying vec3 vNormal;',
|
||||||
|
'varying vec2 vUv;',
|
||||||
|
'void main() {',
|
||||||
|
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
|
||||||
|
'vNormal = normalize( normalMatrix * normal );',
|
||||||
|
'vUv = uv;',
|
||||||
|
'}'
|
||||||
|
].join('\n'),
|
||||||
|
fragmentShader: [
|
||||||
|
'uniform sampler2D texture;',
|
||||||
|
'varying vec3 vNormal;',
|
||||||
|
'varying vec2 vUv;',
|
||||||
|
'void main() {',
|
||||||
|
'vec3 diffuse = texture2D( texture, vUv ).xyz;',
|
||||||
|
'float intensity = 1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) );',
|
||||||
|
'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * pow( intensity, 3.0 );',
|
||||||
|
'gl_FragColor = vec4( diffuse + atmosphere, 1.0 );',
|
||||||
|
'}'
|
||||||
|
].join('\n')
|
||||||
|
},
|
||||||
|
'atmosphere' : {
|
||||||
|
uniforms: {},
|
||||||
|
vertexShader: [
|
||||||
|
'varying vec3 vNormal;',
|
||||||
|
'void main() {',
|
||||||
|
'vNormal = normalize( normalMatrix * normal );',
|
||||||
|
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
|
||||||
|
'}'
|
||||||
|
].join('\n'),
|
||||||
|
fragmentShader: [
|
||||||
|
'varying vec3 vNormal;',
|
||||||
|
'void main() {',
|
||||||
|
'float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 12.0 );',
|
||||||
|
'gl_FragColor = vec4( 1.0, 1.0, 1.0, 1.0 ) * intensity;',
|
||||||
|
'}'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var camera, scene, renderer, w, h;
|
||||||
|
var mesh, atmosphere, point, running;
|
||||||
|
|
||||||
|
var overRenderer;
|
||||||
|
var running = true;
|
||||||
|
|
||||||
|
var curZoomSpeed = 0;
|
||||||
|
var zoomSpeed = 50;
|
||||||
|
|
||||||
|
var mouse = { x: 0, y: 0 }, mouseOnDown = { x: 0, y: 0 };
|
||||||
|
var rotation = { x: 0, y: 0 },
|
||||||
|
target = { x: Math.PI*3/2, y: Math.PI / 6.0 },
|
||||||
|
targetOnDown = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
var distance = 100000, distanceTarget = 100000;
|
||||||
|
var padding = 10;
|
||||||
|
var PI_HALF = Math.PI / 2;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
|
||||||
|
container.style.color = '#fff';
|
||||||
|
container.style.font = '13px/20px Arial, sans-serif';
|
||||||
|
|
||||||
|
var shader, uniforms, material;
|
||||||
|
w = container.offsetWidth || window.innerWidth;
|
||||||
|
h = container.offsetHeight || window.innerHeight;
|
||||||
|
|
||||||
|
camera = new THREE.PerspectiveCamera(30, w / h, 1, 10000);
|
||||||
|
camera.position.z = distance;
|
||||||
|
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
|
||||||
|
var geometry = new THREE.SphereGeometry(200, 40, 30);
|
||||||
|
|
||||||
|
shader = Shaders['earth'];
|
||||||
|
uniforms = THREE.UniformsUtils.clone(shader.uniforms);
|
||||||
|
|
||||||
|
uniforms['texture'].value = THREE.ImageUtils.loadTexture(imgDir+'world.jpg');
|
||||||
|
|
||||||
|
material = new THREE.ShaderMaterial({
|
||||||
|
|
||||||
|
uniforms: uniforms,
|
||||||
|
vertexShader: shader.vertexShader,
|
||||||
|
fragmentShader: shader.fragmentShader
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
mesh = new THREE.Mesh(geometry, material);
|
||||||
|
mesh.rotation.y = Math.PI;
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
shader = Shaders['atmosphere'];
|
||||||
|
uniforms = THREE.UniformsUtils.clone(shader.uniforms);
|
||||||
|
|
||||||
|
material = new THREE.ShaderMaterial({
|
||||||
|
|
||||||
|
uniforms: uniforms,
|
||||||
|
vertexShader: shader.vertexShader,
|
||||||
|
fragmentShader: shader.fragmentShader,
|
||||||
|
side: THREE.BackSide,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
transparent: true
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
mesh = new THREE.Mesh(geometry, material);
|
||||||
|
mesh.scale.set( 1.1, 1.1, 1.1 );
|
||||||
|
scene.add(mesh);
|
||||||
|
|
||||||
|
geometry = new THREE.BoxGeometry(2.75, 2.75, 1);
|
||||||
|
geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,0,-0.5));
|
||||||
|
|
||||||
|
point = new THREE.Mesh(geometry);
|
||||||
|
|
||||||
|
renderer = new THREE.WebGLRenderer({antialias: true});
|
||||||
|
renderer.setSize(w, h);
|
||||||
|
renderer.setClearColor( 0x212121, 1 );
|
||||||
|
|
||||||
|
renderer.domElement.style.position = 'relative';
|
||||||
|
|
||||||
|
container.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
container.addEventListener('mousedown', onMouseDown, false);
|
||||||
|
|
||||||
|
if ('onwheel' in document) {
|
||||||
|
container.addEventListener('wheel', onMouseWheel, false);
|
||||||
|
} else {
|
||||||
|
container.addEventListener('mousewheel', onMouseWheel, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', onDocumentKeyDown, false);
|
||||||
|
|
||||||
|
window.addEventListener('resize', onWindowResize, false);
|
||||||
|
|
||||||
|
container.addEventListener('mouseover', function() {
|
||||||
|
overRenderer = true;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
container.addEventListener('mouseout', function() {
|
||||||
|
overRenderer = false;
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addData(data, opts) {
|
||||||
|
var lat, lng, size, color, i, step, colorFnWrapper;
|
||||||
|
|
||||||
|
opts.animated = opts.animated || false;
|
||||||
|
this.is_animated = opts.animated;
|
||||||
|
opts.format = opts.format || 'magnitude'; // other option is 'legend'
|
||||||
|
if (opts.format === 'magnitude') {
|
||||||
|
step = 3;
|
||||||
|
colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
|
||||||
|
} else if (opts.format === 'legend') {
|
||||||
|
step = 4;
|
||||||
|
colorFnWrapper = function(data, i) { return colorFn(data[i+3]); }
|
||||||
|
} else if (opts.format === 'peer') {
|
||||||
|
colorFnWrapper = function(data, i) { return colorFn(data[i+2]); }
|
||||||
|
} else {
|
||||||
|
throw('error: format not supported: '+opts.format);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.animated) {
|
||||||
|
if (this._baseGeometry === undefined) {
|
||||||
|
this._baseGeometry = new THREE.Geometry();
|
||||||
|
for (i = 0; i < data.length; i += step) {
|
||||||
|
lat = data[i];
|
||||||
|
lng = data[i + 1];
|
||||||
|
// size = data[i + 2];
|
||||||
|
color = colorFnWrapper(data,i);
|
||||||
|
size = 0;
|
||||||
|
addPoint(lat, lng, size, color, this._baseGeometry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(this._morphTargetId === undefined) {
|
||||||
|
this._morphTargetId = 0;
|
||||||
|
} else {
|
||||||
|
this._morphTargetId += 1;
|
||||||
|
}
|
||||||
|
opts.name = opts.name || 'morphTarget'+this._morphTargetId;
|
||||||
|
}
|
||||||
|
var subgeo = new THREE.Geometry();
|
||||||
|
for (i = 0; i < data.length; i += step) {
|
||||||
|
lat = data[i];
|
||||||
|
lng = data[i + 1];
|
||||||
|
color = colorFnWrapper(data,i);
|
||||||
|
size = data[i + 2];
|
||||||
|
size = size*200;
|
||||||
|
addPoint(lat, lng, size, color, subgeo);
|
||||||
|
}
|
||||||
|
if (opts.animated) {
|
||||||
|
this._baseGeometry.morphTargets.push({'name': opts.name, vertices: subgeo.vertices});
|
||||||
|
} else {
|
||||||
|
this._baseGeometry = subgeo;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
function createPoints() {
|
||||||
|
if (this._baseGeometry !== undefined) {
|
||||||
|
if (this.is_animated === false) {
|
||||||
|
this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
vertexColors: THREE.FaceColors,
|
||||||
|
morphTargets: false
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
if (this._baseGeometry.morphTargets.length < 8) {
|
||||||
|
console.log('t l',this._baseGeometry.morphTargets.length);
|
||||||
|
var padding = 8-this._baseGeometry.morphTargets.length;
|
||||||
|
console.log('padding', padding);
|
||||||
|
for(var i=0; i<=padding; i++) {
|
||||||
|
console.log('padding',i);
|
||||||
|
this._baseGeometry.morphTargets.push({'name': 'morphPadding'+i, vertices: this._baseGeometry.vertices});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.points = new THREE.Mesh(this._baseGeometry, new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
vertexColors: THREE.FaceColors,
|
||||||
|
morphTargets: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
scene.add(this.points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPoint(lat, lng, size, color, subgeo) {
|
||||||
|
|
||||||
|
var phi = (90 - lat) * Math.PI / 180;
|
||||||
|
var theta = (180 - lng) * Math.PI / 180;
|
||||||
|
|
||||||
|
point.position.x = 200 * Math.sin(phi) * Math.cos(theta);
|
||||||
|
point.position.y = 200 * Math.cos(phi);
|
||||||
|
point.position.z = 200 * Math.sin(phi) * Math.sin(theta);
|
||||||
|
|
||||||
|
point.lookAt(mesh.position);
|
||||||
|
|
||||||
|
point.scale.z = Math.max( size, 0.1 ); // avoid non-invertible matrix
|
||||||
|
point.updateMatrix();
|
||||||
|
|
||||||
|
for (var i = 0; i < point.geometry.faces.length; i++) {
|
||||||
|
|
||||||
|
point.geometry.faces[i].color = color;
|
||||||
|
|
||||||
|
}
|
||||||
|
if(point.matrixAutoUpdate){
|
||||||
|
point.updateMatrix();
|
||||||
|
}
|
||||||
|
subgeo.merge(point.geometry, point.matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
container.addEventListener('mousemove', onMouseMove, false);
|
||||||
|
container.addEventListener('mouseup', onMouseUp, false);
|
||||||
|
container.addEventListener('mouseout', onMouseOut, false);
|
||||||
|
|
||||||
|
mouseOnDown.x = - event.clientX;
|
||||||
|
mouseOnDown.y = event.clientY;
|
||||||
|
|
||||||
|
targetOnDown.x = target.x;
|
||||||
|
targetOnDown.y = target.y;
|
||||||
|
|
||||||
|
container.style.cursor = 'move';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
mouse.x = - event.clientX;
|
||||||
|
mouse.y = event.clientY;
|
||||||
|
|
||||||
|
var zoomDamp = distance/1000;
|
||||||
|
|
||||||
|
target.x = targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp;
|
||||||
|
target.y = targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp;
|
||||||
|
|
||||||
|
target.y = target.y > PI_HALF ? PI_HALF : target.y;
|
||||||
|
target.y = target.y < - PI_HALF ? - PI_HALF : target.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp(event) {
|
||||||
|
container.removeEventListener('mousemove', onMouseMove, false);
|
||||||
|
container.removeEventListener('mouseup', onMouseUp, false);
|
||||||
|
container.removeEventListener('mouseout', onMouseOut, false);
|
||||||
|
container.style.cursor = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseOut(event) {
|
||||||
|
container.removeEventListener('mousemove', onMouseMove, false);
|
||||||
|
container.removeEventListener('mouseup', onMouseUp, false);
|
||||||
|
container.removeEventListener('mouseout', onMouseOut, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseWheel(event) {
|
||||||
|
if (container.style.cursor != "move") return false;
|
||||||
|
event.preventDefault();
|
||||||
|
if (overRenderer) {
|
||||||
|
if (event.deltaY) {
|
||||||
|
zoom(-event.deltaY * (event.deltaMode == 0 ? 1 : 50));
|
||||||
|
} else {
|
||||||
|
zoom(event.wheelDeltaY * 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentKeyDown(event) {
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 38:
|
||||||
|
zoom(100);
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
case 40:
|
||||||
|
zoom(-100);
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowResize( event ) {
|
||||||
|
camera.aspect = container.offsetWidth / container.offsetHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize( container.offsetWidth, container.offsetHeight );
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoom(delta) {
|
||||||
|
distanceTarget -= delta;
|
||||||
|
distanceTarget = distanceTarget > 855 ? 855 : distanceTarget;
|
||||||
|
distanceTarget = distanceTarget < 350 ? 350 : distanceTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
if (!running) return
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
zoom(curZoomSpeed);
|
||||||
|
|
||||||
|
rotation.x += (target.x - rotation.x) * 0.1;
|
||||||
|
rotation.y += (target.y - rotation.y) * 0.1;
|
||||||
|
distance += (distanceTarget - distance) * 0.3;
|
||||||
|
|
||||||
|
camera.position.x = distance * Math.sin(rotation.x) * Math.cos(rotation.y);
|
||||||
|
camera.position.y = distance * Math.sin(rotation.y);
|
||||||
|
camera.position.z = distance * Math.cos(rotation.x) * Math.cos(rotation.y);
|
||||||
|
|
||||||
|
camera.lookAt(mesh.position);
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unload() {
|
||||||
|
running = false
|
||||||
|
container.removeEventListener('mousedown', onMouseDown, false);
|
||||||
|
if ('onwheel' in document) {
|
||||||
|
container.removeEventListener('wheel', onMouseWheel, false);
|
||||||
|
} else {
|
||||||
|
container.removeEventListener('mousewheel', onMouseWheel, false);
|
||||||
|
}
|
||||||
|
document.removeEventListener('keydown', onDocumentKeyDown, false);
|
||||||
|
window.removeEventListener('resize', onWindowResize, false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
this.animate = animate;
|
||||||
|
this.unload = unload;
|
||||||
|
|
||||||
|
|
||||||
|
this.__defineGetter__('time', function() {
|
||||||
|
return this._time || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.__defineSetter__('time', function(t) {
|
||||||
|
var validMorphs = [];
|
||||||
|
var morphDict = this.points.morphTargetDictionary;
|
||||||
|
for(var k in morphDict) {
|
||||||
|
if(k.indexOf('morphPadding') < 0) {
|
||||||
|
validMorphs.push(morphDict[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validMorphs.sort();
|
||||||
|
var l = validMorphs.length-1;
|
||||||
|
var scaledt = t*l+1;
|
||||||
|
var index = Math.floor(scaledt);
|
||||||
|
for (i=0;i<validMorphs.length;i++) {
|
||||||
|
this.points.morphTargetInfluences[validMorphs[i]] = 0;
|
||||||
|
}
|
||||||
|
var lastIndex = index - 1;
|
||||||
|
var leftover = scaledt - index;
|
||||||
|
if (lastIndex >= 0) {
|
||||||
|
this.points.morphTargetInfluences[lastIndex] = 1 - leftover;
|
||||||
|
}
|
||||||
|
this.points.morphTargetInfluences[index] = leftover;
|
||||||
|
this._time = t;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addData = addData;
|
||||||
|
this.createPoints = createPoints;
|
||||||
|
this.renderer = renderer;
|
||||||
|
this.scene = scene;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
|
||||||
|
};
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "Sidebar",
|
||||||
|
"description": "Access site management sidebar and console by dragging top-right 0 button to left or down.",
|
||||||
|
"default": "enabled"
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from TrackerShare import TrackerSharePlugin
|
||||||
|
from Peer import Peer
|
||||||
|
from Config import config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("resetSettings")
|
||||||
|
@pytest.mark.usefixtures("resetTempSettings")
|
||||||
|
class TestTrackerShare:
|
||||||
|
def testAnnounceList(self, file_server):
|
||||||
|
open("%s/trackers.json" % config.data_dir, "w").write("{}")
|
||||||
|
tracker_storage = TrackerSharePlugin.tracker_storage
|
||||||
|
tracker_storage.load()
|
||||||
|
peer = Peer(file_server.ip, 1544, connection_server=file_server)
|
||||||
|
assert peer.request("getTrackers")["trackers"] == []
|
||||||
|
|
||||||
|
tracker_storage.onTrackerFound("zero://%s:15441" % file_server.ip)
|
||||||
|
assert peer.request("getTrackers")["trackers"] == []
|
||||||
|
|
||||||
|
# It needs to have at least one successfull announce to be shared to other peers
|
||||||
|
tracker_storage.onTrackerSuccess("zero://%s:15441" % file_server.ip, 1.0)
|
||||||
|
assert peer.request("getTrackers")["trackers"] == ["zero://%s:15441" % file_server.ip]
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from src.Test.conftest import *
|
||||||
|
|
||||||
|
from Config import config
|
|
@ -0,0 +1,5 @@
|
||||||
|
[pytest]
|
||||||
|
python_files = Test*.py
|
||||||
|
addopts = -rsxX -v --durations=6
|
||||||
|
markers =
|
||||||
|
webtest: mark a test as a webtest.
|
|
@ -0,0 +1,500 @@
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import atexit
|
||||||
|
import re
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
|
||||||
|
from Config import config
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from util import helper
|
||||||
|
|
||||||
|
|
||||||
|
class TrackerStorage(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.site_announcer = None
|
||||||
|
self.log = logging.getLogger("TrackerStorage")
|
||||||
|
|
||||||
|
self.working_tracker_time_interval = 60 * 60
|
||||||
|
self.tracker_down_time_interval = 60 * 60
|
||||||
|
self.tracker_discover_time_interval = 5 * 60
|
||||||
|
|
||||||
|
self.shared_trackers_limit_per_protocol = {}
|
||||||
|
self.shared_trackers_limit_per_protocol["other"] = 2
|
||||||
|
|
||||||
|
self.file_path = "%s/shared-trackers.json" % config.data_dir
|
||||||
|
self.load()
|
||||||
|
self.time_discover = 0.0
|
||||||
|
self.time_success = 0.0
|
||||||
|
atexit.register(self.save)
|
||||||
|
|
||||||
|
def initTrackerLimitForProtocol(self):
|
||||||
|
for s in re.split("[,;]", config.shared_trackers_limit_per_protocol):
|
||||||
|
x = s.split("=", 1)
|
||||||
|
if len(x) == 1:
|
||||||
|
x = ["other", x[0]]
|
||||||
|
try:
|
||||||
|
self.shared_trackers_limit_per_protocol[x[0]] = int(x[1])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
self.log.info("Limits per protocol: %s" % self.shared_trackers_limit_per_protocol)
|
||||||
|
|
||||||
|
def getTrackerLimitForProtocol(self, protocol):
|
||||||
|
l = self.shared_trackers_limit_per_protocol
|
||||||
|
return l.get(protocol, l.get("other"))
|
||||||
|
|
||||||
|
def setSiteAnnouncer(self, site_announcer):
|
||||||
|
if not site_announcer:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.site_announcer:
|
||||||
|
self.site_announcer = site_announcer
|
||||||
|
self.initTrackerLimitForProtocol()
|
||||||
|
self.recheckValidTrackers()
|
||||||
|
else:
|
||||||
|
self.site_announcer = site_announcer
|
||||||
|
|
||||||
|
def isTrackerAddressValid(self, tracker_address):
|
||||||
|
if not self.site_announcer: # Not completely initialized, skip check
|
||||||
|
return True
|
||||||
|
|
||||||
|
address_parts = self.site_announcer.getAddressParts(tracker_address)
|
||||||
|
if not address_parts:
|
||||||
|
self.log.debug("Invalid tracker address: %s" % tracker_address)
|
||||||
|
return False
|
||||||
|
|
||||||
|
handler = self.site_announcer.getTrackerHandler(address_parts["protocol"])
|
||||||
|
if not handler:
|
||||||
|
self.log.debug("Invalid tracker address: Unknown protocol %s: %s" % (address_parts["protocol"], tracker_address))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def recheckValidTrackers(self):
|
||||||
|
trackers = self.getTrackers()
|
||||||
|
for address, tracker in list(trackers.items()):
|
||||||
|
if not self.isTrackerAddressValid(address):
|
||||||
|
del trackers[address]
|
||||||
|
|
||||||
|
def isUdpEnabled(self):
|
||||||
|
if config.disable_udp:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if config.trackers_proxy != "disable":
|
||||||
|
return False
|
||||||
|
|
||||||
|
if config.tor == "always":
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getNormalizedTrackerProtocol(self, tracker_address):
|
||||||
|
if not self.site_announcer:
|
||||||
|
return None
|
||||||
|
|
||||||
|
address_parts = self.site_announcer.getAddressParts(tracker_address)
|
||||||
|
if not address_parts:
|
||||||
|
return None
|
||||||
|
|
||||||
|
protocol = address_parts["protocol"]
|
||||||
|
if protocol == "https":
|
||||||
|
protocol = "http"
|
||||||
|
|
||||||
|
return protocol
|
||||||
|
|
||||||
|
def deleteUnusedTrackers(self, supported_trackers):
|
||||||
|
# If a tracker is in our list, but is absent from the results of getSupportedTrackers(),
|
||||||
|
# it seems to be supported by software, but forbidden by the settings or network configuration.
|
||||||
|
# We check and remove thoose trackers here, since onTrackerError() is never emitted for them.
|
||||||
|
trackers = self.getTrackers()
|
||||||
|
for tracker_address, tracker in list(trackers.items()):
|
||||||
|
t = max(trackers[tracker_address]["time_added"],
|
||||||
|
trackers[tracker_address]["time_success"])
|
||||||
|
if tracker_address not in supported_trackers and t < time.time() - self.tracker_down_time_interval:
|
||||||
|
self.log.info("Tracker %s seems to be disabled by the configuration, removing." % tracker_address)
|
||||||
|
del trackers[tracker_address]
|
||||||
|
|
||||||
|
def getSupportedProtocols(self):
|
||||||
|
if not self.site_announcer:
|
||||||
|
return None
|
||||||
|
|
||||||
|
supported_trackers = self.site_announcer.getSupportedTrackers()
|
||||||
|
|
||||||
|
self.deleteUnusedTrackers(supported_trackers)
|
||||||
|
|
||||||
|
protocols = set()
|
||||||
|
for tracker_address in supported_trackers:
|
||||||
|
protocol = self.getNormalizedTrackerProtocol(tracker_address)
|
||||||
|
if not protocol:
|
||||||
|
continue
|
||||||
|
if protocol == "udp" and not self.isUdpEnabled():
|
||||||
|
continue
|
||||||
|
protocols.add(protocol)
|
||||||
|
|
||||||
|
protocols = list(protocols)
|
||||||
|
|
||||||
|
self.log.debug("Supported tracker protocols: %s" % protocols)
|
||||||
|
|
||||||
|
return protocols
|
||||||
|
|
||||||
|
def getDefaultFile(self):
|
||||||
|
return {"trackers": {}}
|
||||||
|
|
||||||
|
def onTrackerFound(self, tracker_address, my=False, persistent=False):
|
||||||
|
if not self.isTrackerAddressValid(tracker_address):
|
||||||
|
return False
|
||||||
|
|
||||||
|
trackers = self.getTrackers()
|
||||||
|
added = False
|
||||||
|
if tracker_address not in trackers:
|
||||||
|
# "My" trackers never get deleted on announce errors, but aren't saved between restarts.
|
||||||
|
# They are to be used as automatically added addresses from the Bootstrap plugin.
|
||||||
|
# Persistent trackers never get deleted.
|
||||||
|
# They are to be used for entries manually added by the user.
|
||||||
|
# Private trackers never listed to other peer in response of the getTrackers command
|
||||||
|
trackers[tracker_address] = {
|
||||||
|
"time_added": time.time(),
|
||||||
|
"time_success": 0,
|
||||||
|
"time_error": 0,
|
||||||
|
"latency": 99.0,
|
||||||
|
"num_error": 0,
|
||||||
|
"my": False,
|
||||||
|
"persistent": False,
|
||||||
|
"private": False
|
||||||
|
}
|
||||||
|
self.log.info("New tracker found: %s" % tracker_address)
|
||||||
|
added = True
|
||||||
|
|
||||||
|
trackers[tracker_address]["time_found"] = time.time()
|
||||||
|
trackers[tracker_address]["my"] |= my
|
||||||
|
trackers[tracker_address]["persistent"] |= persistent
|
||||||
|
return added
|
||||||
|
|
||||||
|
def onTrackerSuccess(self, tracker_address, latency):
|
||||||
|
tracker = self.resolveTracker(tracker_address)
|
||||||
|
if not tracker:
|
||||||
|
return
|
||||||
|
|
||||||
|
tracker["latency"] = latency
|
||||||
|
tracker["time_success"] = time.time()
|
||||||
|
tracker["num_error"] = 0
|
||||||
|
|
||||||
|
self.time_success = time.time()
|
||||||
|
|
||||||
|
def onTrackerError(self, tracker_address):
|
||||||
|
tracker = self.resolveTracker(tracker_address)
|
||||||
|
if not tracker:
|
||||||
|
return
|
||||||
|
|
||||||
|
tracker["time_error"] = time.time()
|
||||||
|
tracker["num_error"] += 1
|
||||||
|
|
||||||
|
self.considerTrackerDeletion(tracker_address)
|
||||||
|
|
||||||
|
def considerTrackerDeletion(self, tracker_address):
|
||||||
|
tracker = self.resolveTracker(tracker_address)
|
||||||
|
if not tracker:
|
||||||
|
return
|
||||||
|
|
||||||
|
if tracker["my"] or tracker["persistent"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
error_limit = self.getSuccessiveErrorLimit(tracker_address)
|
||||||
|
|
||||||
|
if tracker["num_error"] > error_limit:
|
||||||
|
if self.isTrackerDown(tracker_address):
|
||||||
|
self.log.info("Tracker %s looks down, removing." % tracker_address)
|
||||||
|
self.deleteTracker(tracker_address)
|
||||||
|
elif self.areWayTooManyTrackers(tracker_address):
|
||||||
|
self.log.info(
|
||||||
|
"Tracker %s has %d successive errors. Looks like we have too many trackers, so removing." % (
|
||||||
|
tracker_address,
|
||||||
|
tracker["num_error"]))
|
||||||
|
self.deleteTracker(tracker_address)
|
||||||
|
|
||||||
|
def areWayTooManyTrackers(self, tracker_address):
|
||||||
|
# Prevent the tracker list overgrowth by hard limiting the maximum size
|
||||||
|
|
||||||
|
protocol = self.getNormalizedTrackerProtocol(tracker_address) or ""
|
||||||
|
|
||||||
|
nr_trackers_for_protocol = len(self.getTrackersPerProtocol().get(protocol, []))
|
||||||
|
nr_trackers = len(self.getTrackers())
|
||||||
|
|
||||||
|
hard_limit_mult = 5
|
||||||
|
hard_limit_for_protocol = self.getTrackerLimitForProtocol(protocol) * hard_limit_mult
|
||||||
|
hard_limit = config.shared_trackers_limit * hard_limit_mult
|
||||||
|
|
||||||
|
if (nr_trackers_for_protocol > hard_limit_for_protocol) and (nr_trackers > hard_limit):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getSuccessiveErrorLimit(self, tracker_address):
|
||||||
|
protocol = self.getNormalizedTrackerProtocol(tracker_address) or ""
|
||||||
|
|
||||||
|
nr_working_trackers_for_protocol = len(self.getTrackersPerProtocol(working_only=True).get(protocol, []))
|
||||||
|
nr_working_trackers = len(self.getWorkingTrackers())
|
||||||
|
|
||||||
|
error_limit = 30
|
||||||
|
if nr_working_trackers_for_protocol >= self.getTrackerLimitForProtocol(protocol):
|
||||||
|
error_limit = 10
|
||||||
|
if nr_working_trackers >= config.shared_trackers_limit:
|
||||||
|
error_limit = 5
|
||||||
|
|
||||||
|
return error_limit
|
||||||
|
|
||||||
|
# Returns the dict of known trackers.
|
||||||
|
# If condition is None the returned dict can be modified in place, and the
|
||||||
|
# modifications is reflected in the underlying storage.
|
||||||
|
# If condition is a function, the dict if filtered by the function,
|
||||||
|
# and the returned dict has no connection to the underlying storage.
|
||||||
|
def getTrackers(self, condition = None):
|
||||||
|
trackers = self.file_content.setdefault("trackers", {})
|
||||||
|
|
||||||
|
if condition:
|
||||||
|
trackers = {
|
||||||
|
key: tracker for key, tracker in trackers.items()
|
||||||
|
if condition(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackers
|
||||||
|
|
||||||
|
def deleteTracker(self, tracker):
|
||||||
|
trackers = self.getTrackers()
|
||||||
|
if isinstance(tracker, str):
|
||||||
|
if trackers[tracker]:
|
||||||
|
del trackers[tracker]
|
||||||
|
else:
|
||||||
|
trackers.remove(tracker)
|
||||||
|
|
||||||
|
def resolveTracker(self, tracker):
|
||||||
|
if isinstance(tracker, str):
|
||||||
|
tracker = self.getTrackers().get(tracker, None)
|
||||||
|
return tracker
|
||||||
|
|
||||||
|
def isTrackerDown(self, tracker):
|
||||||
|
tracker = self.resolveTracker(tracker)
|
||||||
|
if not tracker:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Don't consider any trackers down if there haven't been any successful announces at all
|
||||||
|
if self.time_success < 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
time_success = max(tracker["time_added"], tracker["time_success"])
|
||||||
|
time_error = max(tracker["time_added"], tracker["time_error"])
|
||||||
|
|
||||||
|
if time_success >= time_error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Deadline is calculated based on the time of the last successful announce,
|
||||||
|
# not based on the current time.
|
||||||
|
# There may be network connectivity issues, if there haven't been any
|
||||||
|
# successful announces recently.
|
||||||
|
|
||||||
|
deadline = self.time_success - self.tracker_down_time_interval
|
||||||
|
if time_success >= deadline:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def isTrackerWorking(self, tracker):
|
||||||
|
tracker = self.resolveTracker(tracker)
|
||||||
|
if not tracker:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if tracker["time_success"] > time.time() - self.working_tracker_time_interval:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isTrackerShared(self, tracker):
|
||||||
|
tracker = self.resolveTracker(tracker)
|
||||||
|
if not tracker:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if tracker["private"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if tracker["my"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return self.isTrackerWorking(tracker)
|
||||||
|
|
||||||
|
def getWorkingTrackers(self):
|
||||||
|
return self.getTrackers(self.isTrackerWorking)
|
||||||
|
|
||||||
|
def getSharedTrackers(self):
|
||||||
|
return self.getTrackers(self.isTrackerShared)
|
||||||
|
|
||||||
|
def getTrackersPerProtocol(self, working_only=False):
|
||||||
|
if not self.site_announcer:
|
||||||
|
return None
|
||||||
|
|
||||||
|
trackers = self.getTrackers()
|
||||||
|
|
||||||
|
trackers_per_protocol = {}
|
||||||
|
for tracker_address in trackers:
|
||||||
|
protocol = self.getNormalizedTrackerProtocol(tracker_address)
|
||||||
|
if not protocol:
|
||||||
|
continue
|
||||||
|
if not working_only or self.isTrackerWorking(tracker_address):
|
||||||
|
trackers_per_protocol.setdefault(protocol, []).append(tracker_address)
|
||||||
|
|
||||||
|
return trackers_per_protocol
|
||||||
|
|
||||||
|
def getFileContent(self):
|
||||||
|
if not os.path.isfile(self.file_path):
|
||||||
|
open(self.file_path, "w").write("{}")
|
||||||
|
return self.getDefaultFile()
|
||||||
|
try:
|
||||||
|
return json.load(open(self.file_path))
|
||||||
|
except Exception as err:
|
||||||
|
self.log.error("Error loading trackers list: %s" % err)
|
||||||
|
return self.getDefaultFile()
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self.file_content = self.getFileContent()
|
||||||
|
|
||||||
|
trackers = self.getTrackers()
|
||||||
|
self.log.debug("Loaded %s shared trackers" % len(trackers))
|
||||||
|
for address, tracker in list(trackers.items()):
|
||||||
|
tracker.setdefault("time_added", time.time())
|
||||||
|
tracker.setdefault("time_success", 0)
|
||||||
|
tracker.setdefault("time_error", 0)
|
||||||
|
tracker.setdefault("latency", 99.0)
|
||||||
|
tracker.setdefault("my", False)
|
||||||
|
tracker.setdefault("persistent", False)
|
||||||
|
tracker.setdefault("private", False)
|
||||||
|
tracker["num_error"] = 0
|
||||||
|
if tracker["my"]:
|
||||||
|
del trackers[address]
|
||||||
|
self.recheckValidTrackers()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
s = time.time()
|
||||||
|
helper.atomicWrite(self.file_path, json.dumps(self.file_content, indent=2, sort_keys=True).encode("utf8"))
|
||||||
|
self.log.debug("Saved in %.3fs" % (time.time() - s))
|
||||||
|
|
||||||
|
def enoughWorkingTrackers(self):
|
||||||
|
supported_protocols = self.getSupportedProtocols()
|
||||||
|
if not supported_protocols:
|
||||||
|
return False
|
||||||
|
|
||||||
|
trackers_per_protocol = self.getTrackersPerProtocol(working_only=True)
|
||||||
|
if not trackers_per_protocol:
|
||||||
|
return False
|
||||||
|
|
||||||
|
unmet_conditions = 0
|
||||||
|
|
||||||
|
total_nr = 0
|
||||||
|
|
||||||
|
for protocol in supported_protocols:
|
||||||
|
trackers = trackers_per_protocol.get(protocol, [])
|
||||||
|
if len(trackers) < self.getTrackerLimitForProtocol(protocol):
|
||||||
|
self.log.info("Not enough working trackers for protocol %s: %s < %s" % (
|
||||||
|
protocol, len(trackers), self.getTrackerLimitForProtocol(protocol)))
|
||||||
|
unmet_conditions += 1
|
||||||
|
total_nr += len(trackers)
|
||||||
|
|
||||||
|
if total_nr < config.shared_trackers_limit:
|
||||||
|
self.log.info("Not enough working trackers (total): %s < %s" % (
|
||||||
|
total_nr, config.shared_trackers_limit))
|
||||||
|
unmet_conditions + 1
|
||||||
|
|
||||||
|
return unmet_conditions == 0
|
||||||
|
|
||||||
|
def discoverTrackers(self, peers):
|
||||||
|
if self.enoughWorkingTrackers():
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.info("Discovering trackers from %s peers..." % len(peers))
|
||||||
|
|
||||||
|
s = time.time()
|
||||||
|
num_success = 0
|
||||||
|
num_trackers_discovered = 0
|
||||||
|
for peer in peers:
|
||||||
|
if peer.connection and peer.connection.handshake.get("rev", 0) < 3560:
|
||||||
|
continue # Not supported
|
||||||
|
|
||||||
|
res = peer.request("getTrackers")
|
||||||
|
if not res or "error" in res:
|
||||||
|
continue
|
||||||
|
|
||||||
|
num_success += 1
|
||||||
|
|
||||||
|
random.shuffle(res["trackers"])
|
||||||
|
for tracker_address in res["trackers"]:
|
||||||
|
if type(tracker_address) is bytes: # Backward compatibilitys
|
||||||
|
tracker_address = tracker_address.decode("utf8")
|
||||||
|
added = self.onTrackerFound(tracker_address)
|
||||||
|
if added: # Only add one tracker from one source
|
||||||
|
num_trackers_discovered += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
if not num_success and len(peers) < 20:
|
||||||
|
self.time_discover = 0.0
|
||||||
|
|
||||||
|
if num_success:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
self.log.info("Discovered %s new trackers from %s/%s peers in %.3fs" % (num_trackers_discovered, num_success, len(peers), time.time() - s))
|
||||||
|
|
||||||
|
def checkDiscoveringTrackers(self, peers):
|
||||||
|
if not peers or len(peers) < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if self.time_discover + self.tracker_discover_time_interval >= now:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.time_discover = now
|
||||||
|
gevent.spawn(self.discoverTrackers, peers)
|
||||||
|
|
||||||
|
if "tracker_storage" not in locals():
|
||||||
|
tracker_storage = TrackerStorage()
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("SiteAnnouncer")
|
||||||
|
class SiteAnnouncerPlugin(object):
|
||||||
|
def getTrackers(self):
|
||||||
|
tracker_storage.setSiteAnnouncer(self)
|
||||||
|
tracker_storage.checkDiscoveringTrackers(self.site.getConnectedPeers(only_fully_connected=True))
|
||||||
|
trackers = super(SiteAnnouncerPlugin, self).getTrackers()
|
||||||
|
shared_trackers = list(tracker_storage.getTrackers().keys())
|
||||||
|
if shared_trackers:
|
||||||
|
return trackers + shared_trackers
|
||||||
|
else:
|
||||||
|
return trackers
|
||||||
|
|
||||||
|
def announceTracker(self, tracker, *args, **kwargs):
|
||||||
|
tracker_storage.setSiteAnnouncer(self)
|
||||||
|
res = super(SiteAnnouncerPlugin, self).announceTracker(tracker, *args, **kwargs)
|
||||||
|
if res:
|
||||||
|
latency = res
|
||||||
|
tracker_storage.onTrackerSuccess(tracker, latency)
|
||||||
|
elif res is False:
|
||||||
|
tracker_storage.onTrackerError(tracker)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("FileRequest")
|
||||||
|
class FileRequestPlugin(object):
|
||||||
|
def actionGetTrackers(self, params):
|
||||||
|
shared_trackers = list(tracker_storage.getSharedTrackers().keys())
|
||||||
|
random.shuffle(shared_trackers)
|
||||||
|
self.response({"trackers": shared_trackers})
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("ConfigPlugin")
|
||||||
|
class ConfigPlugin(object):
|
||||||
|
def createArguments(self):
|
||||||
|
group = self.parser.add_argument_group("TrackerShare plugin")
|
||||||
|
group.add_argument('--shared_trackers_limit', help='Discover new shared trackers if this number of shared trackers isn\'t reached (total)', default=20, type=int, metavar='limit')
|
||||||
|
group.add_argument('--shared_trackers_limit_per_protocol', help='Discover new shared trackers if this number of shared trackers isn\'t reached per each supported protocol', default="zero=10,other=5", metavar='limit')
|
||||||
|
|
||||||
|
return super(ConfigPlugin, self).createArguments()
|
|
@ -0,0 +1 @@
|
||||||
|
from . import TrackerSharePlugin
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "TrackerShare",
|
||||||
|
"description": "Share possible trackers between clients.",
|
||||||
|
"default": "enabled"
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
|
import gevent
|
||||||
|
|
||||||
|
from Config import config
|
||||||
|
from Db import Db
|
||||||
|
from util import helper
|
||||||
|
|
||||||
|
|
||||||
|
class TrackerZeroDb(Db.Db):
|
||||||
|
def __init__(self):
|
||||||
|
self.version = 7
|
||||||
|
self.hash_ids = {} # hash -> id cache
|
||||||
|
super(TrackerZeroDb, self).__init__({"db_name": "TrackerZero"}, "%s/tracker-zero.db" % config.data_dir)
|
||||||
|
self.foreign_keys = True
|
||||||
|
self.checkTables()
|
||||||
|
self.updateHashCache()
|
||||||
|
gevent.spawn(self.cleanup)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
while 1:
|
||||||
|
time.sleep(4 * 60)
|
||||||
|
timeout = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 60 * 40))
|
||||||
|
self.execute("DELETE FROM peer WHERE date_announced < ?", [timeout])
|
||||||
|
|
||||||
|
def updateHashCache(self):
|
||||||
|
res = self.execute("SELECT * FROM hash")
|
||||||
|
self.hash_ids = {row["hash"]: row["hash_id"] for row in res}
|
||||||
|
self.log.debug("Loaded %s hash_ids" % len(self.hash_ids))
|
||||||
|
|
||||||
|
def checkTables(self):
|
||||||
|
version = int(self.execute("PRAGMA user_version").fetchone()[0])
|
||||||
|
self.log.debug("Db version: %s, needed: %s" % (version, self.version))
|
||||||
|
if version < self.version:
|
||||||
|
self.createTables()
|
||||||
|
else:
|
||||||
|
self.execute("VACUUM")
|
||||||
|
|
||||||
|
def createTables(self):
|
||||||
|
# Delete all tables
|
||||||
|
self.execute("PRAGMA writable_schema = 1")
|
||||||
|
self.execute("DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger')")
|
||||||
|
self.execute("PRAGMA writable_schema = 0")
|
||||||
|
self.execute("VACUUM")
|
||||||
|
self.execute("PRAGMA INTEGRITY_CHECK")
|
||||||
|
# Create new tables
|
||||||
|
self.execute("""
|
||||||
|
CREATE TABLE peer (
|
||||||
|
peer_id INTEGER PRIMARY KEY ASC AUTOINCREMENT NOT NULL UNIQUE,
|
||||||
|
type TEXT,
|
||||||
|
address TEXT,
|
||||||
|
port INTEGER NOT NULL,
|
||||||
|
date_added DATETIME DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
date_announced DATETIME DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
self.execute("CREATE UNIQUE INDEX peer_key ON peer (address, port);")
|
||||||
|
|
||||||
|
self.execute("""
|
||||||
|
CREATE TABLE peer_to_hash (
|
||||||
|
peer_to_hash_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
|
||||||
|
peer_id INTEGER REFERENCES peer (peer_id) ON DELETE CASCADE,
|
||||||
|
hash_id INTEGER REFERENCES hash (hash_id)
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
self.execute("CREATE INDEX peer_id ON peer_to_hash (peer_id);")
|
||||||
|
self.execute("CREATE INDEX hash_id ON peer_to_hash (hash_id);")
|
||||||
|
|
||||||
|
self.execute("""
|
||||||
|
CREATE TABLE hash (
|
||||||
|
hash_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
|
||||||
|
hash BLOB UNIQUE NOT NULL,
|
||||||
|
date_added DATETIME DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
self.execute("PRAGMA user_version = %s" % self.version)
|
||||||
|
|
||||||
|
def getHashId(self, hash):
|
||||||
|
if hash not in self.hash_ids:
|
||||||
|
self.log.debug("New hash: %s" % repr(hash))
|
||||||
|
self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash})
|
||||||
|
self.hash_ids[hash] = self.cur.cursor.lastrowid
|
||||||
|
return self.hash_ids[hash]
|
||||||
|
|
||||||
|
def peerAnnounce(self, ip_type, address, port=None, hashes=[], onion_signed=False, delete_missing_hashes=False):
|
||||||
|
hashes_ids_announced = []
|
||||||
|
for hash in hashes:
|
||||||
|
hashes_ids_announced.append(self.getHashId(hash))
|
||||||
|
|
||||||
|
# Check user
|
||||||
|
res = self.execute("SELECT peer_id FROM peer WHERE ? LIMIT 1", {"address": address, "port": port})
|
||||||
|
|
||||||
|
user_row = res.fetchone()
|
||||||
|
now = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if user_row:
|
||||||
|
peer_id = user_row["peer_id"]
|
||||||
|
self.execute("UPDATE peer SET date_announced = ? WHERE peer_id = ?", (now, peer_id))
|
||||||
|
else:
|
||||||
|
self.log.debug("New peer: %s signed: %s" % (address, onion_signed))
|
||||||
|
if ip_type == "onion" and not onion_signed:
|
||||||
|
return len(hashes)
|
||||||
|
self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now})
|
||||||
|
peer_id = self.cur.cursor.lastrowid
|
||||||
|
|
||||||
|
# Check user's hashes
|
||||||
|
res = self.execute("SELECT * FROM peer_to_hash WHERE ?", {"peer_id": peer_id})
|
||||||
|
hash_ids_db = [row["hash_id"] for row in res]
|
||||||
|
if hash_ids_db != hashes_ids_announced:
|
||||||
|
hash_ids_added = set(hashes_ids_announced) - set(hash_ids_db)
|
||||||
|
hash_ids_removed = set(hash_ids_db) - set(hashes_ids_announced)
|
||||||
|
if ip_type != "onion" or onion_signed:
|
||||||
|
for hash_id in hash_ids_added:
|
||||||
|
self.execute("INSERT INTO peer_to_hash ?", {"peer_id": peer_id, "hash_id": hash_id})
|
||||||
|
if hash_ids_removed and delete_missing_hashes:
|
||||||
|
self.execute("DELETE FROM peer_to_hash WHERE ?", {"peer_id": peer_id, "hash_id": list(hash_ids_removed)})
|
||||||
|
|
||||||
|
return len(hash_ids_added) + len(hash_ids_removed)
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def peerList(self, hash, address=None, onions=[], port=None, limit=30, need_types=["ipv4", "onion"], order=True):
|
||||||
|
back = {"ipv4": [], "ipv6": [], "onion": []}
|
||||||
|
if limit == 0:
|
||||||
|
return back
|
||||||
|
hashid = self.getHashId(hash)
|
||||||
|
|
||||||
|
if order:
|
||||||
|
order_sql = "ORDER BY date_announced DESC"
|
||||||
|
else:
|
||||||
|
order_sql = ""
|
||||||
|
where_sql = "hash_id = :hashid"
|
||||||
|
if onions:
|
||||||
|
onions_escaped = ["'%s'" % re.sub("[^a-z0-9,]", "", onion) for onion in onions if type(onion) is str]
|
||||||
|
where_sql += " AND address NOT IN (%s)" % ",".join(onions_escaped)
|
||||||
|
elif address:
|
||||||
|
where_sql += " AND NOT (address = :address AND port = :port)"
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT type, address, port
|
||||||
|
FROM peer_to_hash
|
||||||
|
LEFT JOIN peer USING (peer_id)
|
||||||
|
WHERE %s
|
||||||
|
%s
|
||||||
|
LIMIT :limit
|
||||||
|
""" % (where_sql, order_sql)
|
||||||
|
res = self.execute(query, {"hashid": hashid, "address": address, "port": port, "limit": limit})
|
||||||
|
|
||||||
|
for row in res:
|
||||||
|
if row["type"] in need_types:
|
||||||
|
if row["type"] == "onion":
|
||||||
|
packed = helper.packOnionAddress(row["address"], row["port"])
|
||||||
|
else:
|
||||||
|
packed = helper.packAddress(str(row["address"]), row["port"])
|
||||||
|
back[row["type"]].append(packed)
|
||||||
|
return back
|
|
@ -0,0 +1,325 @@
|
||||||
|
import atexit
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
from util import helper
|
||||||
|
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from .TrackerZeroDb import TrackerZeroDb
|
||||||
|
from Crypt import CryptRsa
|
||||||
|
from Config import config
|
||||||
|
|
||||||
|
class TrackerZero(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.log = logging.getLogger("TrackerZero")
|
||||||
|
self.log_once = set()
|
||||||
|
self.enabled_addresses = []
|
||||||
|
self.added_onions = set()
|
||||||
|
self.config_file_path = "%s/tracker-zero.json" % config.data_dir
|
||||||
|
self.config = None
|
||||||
|
self.load()
|
||||||
|
atexit.register(self.save)
|
||||||
|
|
||||||
|
def addOnion(self, tor_manager, onion, private_key):
|
||||||
|
# XXX: TorManager hangs if Tor returns a code different from 250 OK,
|
||||||
|
# so we keep the list of already added onions to avoid adding them twice.
|
||||||
|
# TODO: Report to the upstream.
|
||||||
|
|
||||||
|
if onion in self.added_onions:
|
||||||
|
return onion
|
||||||
|
|
||||||
|
res = tor_manager.request(
|
||||||
|
"ADD_ONION RSA1024:%s port=%s" % (private_key, tor_manager.fileserver_port)
|
||||||
|
)
|
||||||
|
match = re.search("ServiceID=([A-Za-z0-9]+)", res)
|
||||||
|
if match:
|
||||||
|
onion_address = match.groups()[0]
|
||||||
|
self.added_onions.add(onion_address)
|
||||||
|
return onion_address
|
||||||
|
return None
|
||||||
|
|
||||||
|
def logOnce(self, message):
|
||||||
|
if message in self.log_once:
|
||||||
|
return
|
||||||
|
self.log_once.add(message)
|
||||||
|
self.log.info(message)
|
||||||
|
|
||||||
|
def getDefaultConfig(self):
|
||||||
|
return {
|
||||||
|
"settings": {
|
||||||
|
"enable": False,
|
||||||
|
"enable_only_in_tor_always_mode": True,
|
||||||
|
"listen_on_public_ips": False,
|
||||||
|
"listen_on_temporary_onion_address": False,
|
||||||
|
"listen_on_persistent_onion_address": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def readJSON(self, file_path, default_value):
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
try:
|
||||||
|
self.writeJSON(file_path, default_value)
|
||||||
|
except Exception as err:
|
||||||
|
self.log.error("Error writing %s: %s" % (file_path, err))
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.load(open(file_path))
|
||||||
|
except Exception as err:
|
||||||
|
self.log.error("Error loading %s: %s" % (file_path, err))
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
def writeJSON(self, file_path, value):
|
||||||
|
helper.atomicWrite(file_path, json.dumps(value, indent=2, sort_keys=True).encode("utf8"))
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self.config = self.readJSON(self.config_file_path, self.getDefaultConfig())
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.writeJSON(self.config_file_path, self.config)
|
||||||
|
|
||||||
|
def checkOnionSigns(self, onions, onion_signs, onion_sign_this):
|
||||||
|
if not onion_signs or len(onion_signs) != len(set(onions)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if time.time() - float(onion_sign_this) > 3 * 60:
|
||||||
|
return False # Signed out of allowed 3 minutes
|
||||||
|
|
||||||
|
onions_signed = []
|
||||||
|
# Check onion signs
|
||||||
|
for onion_publickey, onion_sign in onion_signs.items():
|
||||||
|
if CryptRsa.verify(onion_sign_this.encode(), onion_publickey, onion_sign):
|
||||||
|
onions_signed.append(CryptRsa.publickeyToOnion(onion_publickey))
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check if the same onion addresses signed as the announced onces
|
||||||
|
if sorted(onions_signed) == sorted(set(onions)):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actionAnnounce(self, file_request, params):
|
||||||
|
if len(self.enabled_addresses) < 1:
|
||||||
|
file_request.actionUnknown("announce", params)
|
||||||
|
return
|
||||||
|
|
||||||
|
time_started = time.time()
|
||||||
|
s = time.time()
|
||||||
|
# Backward compatibility
|
||||||
|
if "ip4" in params["add"]:
|
||||||
|
params["add"].append("ipv4")
|
||||||
|
if "ip4" in params["need_types"]:
|
||||||
|
params["need_types"].append("ipv4")
|
||||||
|
|
||||||
|
hashes = params["hashes"]
|
||||||
|
|
||||||
|
all_onions_signed = self.checkOnionSigns(params.get("onions", []), params.get("onion_signs"), params.get("onion_sign_this"))
|
||||||
|
|
||||||
|
time_onion_check = time.time() - s
|
||||||
|
|
||||||
|
connection_server = file_request.server
|
||||||
|
ip_type = connection_server.getIpType(file_request.connection.ip)
|
||||||
|
|
||||||
|
if ip_type == "onion" or file_request.connection.ip in config.ip_local:
|
||||||
|
is_port_open = False
|
||||||
|
elif ip_type in params["add"]:
|
||||||
|
is_port_open = True
|
||||||
|
else:
|
||||||
|
is_port_open = False
|
||||||
|
|
||||||
|
s = time.time()
|
||||||
|
# Separatley add onions to sites or at once if no onions present
|
||||||
|
i = 0
|
||||||
|
onion_to_hash = {}
|
||||||
|
for onion in params.get("onions", []):
|
||||||
|
if onion not in onion_to_hash:
|
||||||
|
onion_to_hash[onion] = []
|
||||||
|
onion_to_hash[onion].append(hashes[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
hashes_changed = 0
|
||||||
|
for onion, onion_hashes in onion_to_hash.items():
|
||||||
|
hashes_changed += db.peerAnnounce(
|
||||||
|
ip_type="onion",
|
||||||
|
address=onion,
|
||||||
|
port=params["port"],
|
||||||
|
hashes=onion_hashes,
|
||||||
|
onion_signed=all_onions_signed
|
||||||
|
)
|
||||||
|
time_db_onion = time.time() - s
|
||||||
|
|
||||||
|
s = time.time()
|
||||||
|
|
||||||
|
if is_port_open:
|
||||||
|
hashes_changed += db.peerAnnounce(
|
||||||
|
ip_type=ip_type,
|
||||||
|
address=file_request.connection.ip,
|
||||||
|
port=params["port"],
|
||||||
|
hashes=hashes,
|
||||||
|
delete_missing_hashes=params.get("delete")
|
||||||
|
)
|
||||||
|
time_db_ip = time.time() - s
|
||||||
|
|
||||||
|
s = time.time()
|
||||||
|
# Query sites
|
||||||
|
back = {}
|
||||||
|
peers = []
|
||||||
|
if params.get("onions") and not all_onions_signed and hashes_changed:
|
||||||
|
back["onion_sign_this"] = "%.0f" % time.time() # Send back nonce for signing
|
||||||
|
|
||||||
|
if len(hashes) > 500 or not hashes_changed:
|
||||||
|
limit = 5
|
||||||
|
order = False
|
||||||
|
else:
|
||||||
|
limit = 30
|
||||||
|
order = True
|
||||||
|
for hash in hashes:
|
||||||
|
if time.time() - time_started > 1: # 1 sec limit on request
|
||||||
|
file_request.connection.log("Announce time limit exceeded after %s/%s sites" % (len(peers), len(hashes)))
|
||||||
|
break
|
||||||
|
|
||||||
|
hash_peers = db.peerList(
|
||||||
|
hash,
|
||||||
|
address=file_request.connection.ip, onions=list(onion_to_hash.keys()), port=params["port"],
|
||||||
|
limit=min(limit, params["need_num"]), need_types=params["need_types"], order=order
|
||||||
|
)
|
||||||
|
if "ip4" in params["need_types"]: # Backward compatibility
|
||||||
|
hash_peers["ip4"] = hash_peers["ipv4"]
|
||||||
|
del(hash_peers["ipv4"])
|
||||||
|
peers.append(hash_peers)
|
||||||
|
time_peerlist = time.time() - s
|
||||||
|
|
||||||
|
back["peers"] = peers
|
||||||
|
file_request.connection.log(
|
||||||
|
"Announce %s sites (onions: %s, onion_check: %.3fs, db_onion: %.3fs, db_ip: %.3fs, peerlist: %.3fs, limit: %s)" %
|
||||||
|
(len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip, time_peerlist, limit)
|
||||||
|
)
|
||||||
|
file_request.response(back)
|
||||||
|
|
||||||
|
|
||||||
|
def getTrackerStorage(self):
|
||||||
|
try:
|
||||||
|
if "TrackerShare" in PluginManager.plugin_manager.plugin_names:
|
||||||
|
from TrackerShare.TrackerSharePlugin import tracker_storage
|
||||||
|
return tracker_storage
|
||||||
|
elif "AnnounceShare" in PluginManager.plugin_manager.plugin_names:
|
||||||
|
from AnnounceShare.AnnounceSharePlugin import tracker_storage
|
||||||
|
return tracker_storage
|
||||||
|
except Exception as err:
|
||||||
|
self.log.error("%s" % Debug.formatException(err))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def registerTrackerAddress(self, message, address, port):
|
||||||
|
_tracker_storage = self.getTrackerStorage()
|
||||||
|
if not _tracker_storage:
|
||||||
|
return
|
||||||
|
|
||||||
|
my_tracker_address = "zero://%s:%s" % (address, port)
|
||||||
|
if _tracker_storage.onTrackerFound(my_tracker_address, my=True):
|
||||||
|
self.logOnce("listening on %s: %s" % (message, my_tracker_address))
|
||||||
|
self.enabled_addresses.append("%s:%s" % (address, port))
|
||||||
|
|
||||||
|
def registerTrackerAddresses(self, file_server, port_open):
|
||||||
|
_tracker_storage = self.getTrackerStorage()
|
||||||
|
if not _tracker_storage:
|
||||||
|
return
|
||||||
|
|
||||||
|
tor_manager = file_server.tor_manager
|
||||||
|
|
||||||
|
settings = self.config.get("settings", {})
|
||||||
|
|
||||||
|
if not settings.get("enable"):
|
||||||
|
self.logOnce("Plugin loaded, but disabled by the settings")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if settings.get("enable_only_in_tor_always_mode") and not config.tor == "always":
|
||||||
|
self.logOnce("Plugin loaded, but disabled from running in the modes other than 'tor = always'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.enabled_addresses = []
|
||||||
|
|
||||||
|
if settings.get("listen_on_public_ips") and port_open and not config.tor == "always":
|
||||||
|
for ip in file_server.ip_external_list:
|
||||||
|
self.registerTrackerAddress("public IP", ip, config.fileserver_port)
|
||||||
|
|
||||||
|
if settings.get("listen_on_temporary_onion_address") and tor_manager.enabled:
|
||||||
|
onion = tor_manager.getOnion(config.homepage)
|
||||||
|
if onion:
|
||||||
|
self.registerTrackerAddress("temporary onion address", "%s.onion" % onion, tor_manager.fileserver_port)
|
||||||
|
|
||||||
|
if settings.get("listen_on_persistent_onion_address") and tor_manager.enabled:
|
||||||
|
persistent_addresses = self.config.setdefault("persistent_addresses", {})
|
||||||
|
if len(persistent_addresses) == 0:
|
||||||
|
result = tor_manager.makeOnionAndKey()
|
||||||
|
if result:
|
||||||
|
onion_address, onion_privatekey = result
|
||||||
|
persistent_addresses[onion_address] = {
|
||||||
|
"private_key": onion_privatekey
|
||||||
|
}
|
||||||
|
self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port)
|
||||||
|
else:
|
||||||
|
for address, d in persistent_addresses.items():
|
||||||
|
private_key = d.get("private_key")
|
||||||
|
if not private_key:
|
||||||
|
continue
|
||||||
|
onion_address = self.addOnion(tor_manager, address, private_key)
|
||||||
|
if onion_address == address:
|
||||||
|
self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port)
|
||||||
|
|
||||||
|
return len(self.enabled_addresses) > 0
|
||||||
|
|
||||||
|
if "db" not in locals().keys(): # Share during reloads
|
||||||
|
db = TrackerZeroDb()
|
||||||
|
|
||||||
|
if "tracker_zero" not in locals():
|
||||||
|
tracker_zero = TrackerZero()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("FileRequest")
|
||||||
|
class FileRequestPlugin(object):
|
||||||
|
def actionAnnounce(self, params):
|
||||||
|
tracker_zero.actionAnnounce(self, params)
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("FileServer")
|
||||||
|
class FileServerPlugin(object):
|
||||||
|
def portCheck(self, *args, **kwargs):
|
||||||
|
res = super(FileServerPlugin, self).portCheck(*args, **kwargs)
|
||||||
|
tracker_zero.registerTrackerAddresses(self, res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiRequest")
|
||||||
|
class UiRequestPlugin(object):
|
||||||
|
|
||||||
|
@helper.encodeResponse
|
||||||
|
def actionStatsTrackerZero(self):
|
||||||
|
self.sendHeader()
|
||||||
|
|
||||||
|
# Style
|
||||||
|
yield """
|
||||||
|
<style>
|
||||||
|
* { font-family: monospace; white-space: pre }
|
||||||
|
table td, table th { text-align: right; padding: 0px 10px }
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
|
||||||
|
hash_rows = db.execute("SELECT * FROM hash").fetchall()
|
||||||
|
for hash_row in hash_rows:
|
||||||
|
peer_rows = db.execute(
|
||||||
|
"SELECT * FROM peer LEFT JOIN peer_to_hash USING (peer_id) WHERE hash_id = :hash_id",
|
||||||
|
{"hash_id": hash_row["hash_id"]}
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
yield "<br>%s (added: %s, peers: %s)<br>" % (
|
||||||
|
binascii.hexlify(hash_row["hash"]).decode("utf-8"), hash_row["date_added"], len(peer_rows)
|
||||||
|
)
|
||||||
|
for peer_row in peer_rows:
|
||||||
|
yield " - {type: <6} {address: <30} {port: >5} added: {date_added}, announced: {date_announced}<br>".format(**dict(peer_row))
|
|
@ -0,0 +1 @@
|
||||||
|
from . import TrackerZeroPlugin
|
|
@ -0,0 +1,80 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Translate import translate
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiRequest")
|
||||||
|
class UiRequestPlugin(object):
|
||||||
|
def actionSiteMedia(self, path, **kwargs):
|
||||||
|
file_name = path.split("/")[-1].lower()
|
||||||
|
if not file_name: # Path ends with /
|
||||||
|
file_name = "index.html"
|
||||||
|
extension = file_name.split(".")[-1]
|
||||||
|
|
||||||
|
if extension == "html": # Always replace translate variables in html files
|
||||||
|
should_translate = True
|
||||||
|
elif extension == "js" and translate.lang != "en":
|
||||||
|
should_translate = True
|
||||||
|
else:
|
||||||
|
should_translate = False
|
||||||
|
|
||||||
|
if should_translate:
|
||||||
|
path_parts = self.parsePath(path)
|
||||||
|
kwargs["header_length"] = False
|
||||||
|
file_generator = super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs)
|
||||||
|
if "__next__" in dir(file_generator): # File found and generator returned
|
||||||
|
site = self.server.sites.get(path_parts["address"])
|
||||||
|
if not site or not site.content_manager.contents.get("content.json"):
|
||||||
|
return file_generator
|
||||||
|
return self.actionPatchFile(site, path_parts["inner_path"], file_generator)
|
||||||
|
else:
|
||||||
|
return file_generator
|
||||||
|
|
||||||
|
else:
|
||||||
|
return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs)
|
||||||
|
|
||||||
|
def actionUiMedia(self, path):
|
||||||
|
file_generator = super(UiRequestPlugin, self).actionUiMedia(path)
|
||||||
|
if translate.lang != "en" and path.endswith(".js"):
|
||||||
|
s = time.time()
|
||||||
|
data = b"".join(list(file_generator))
|
||||||
|
data = translate.translateData(data.decode("utf8"))
|
||||||
|
self.log.debug("Patched %s (%s bytes) in %.3fs" % (path, len(data), time.time() - s))
|
||||||
|
return iter([data.encode("utf8")])
|
||||||
|
else:
|
||||||
|
return file_generator
|
||||||
|
|
||||||
|
def actionPatchFile(self, site, inner_path, file_generator):
|
||||||
|
content_json = site.content_manager.contents.get("content.json")
|
||||||
|
lang_file = "languages/%s.json" % translate.lang
|
||||||
|
lang_file_exist = False
|
||||||
|
if site.settings.get("own"): # My site, check if the file is exist (allow to add new lang without signing)
|
||||||
|
if site.storage.isFile(lang_file):
|
||||||
|
lang_file_exist = True
|
||||||
|
else: # Not my site the reference in content.json is enough (will wait for download later)
|
||||||
|
if lang_file in content_json.get("files", {}):
|
||||||
|
lang_file_exist = True
|
||||||
|
|
||||||
|
if not lang_file_exist or inner_path not in content_json.get("translate", []):
|
||||||
|
for part in file_generator:
|
||||||
|
if inner_path.endswith(".html"):
|
||||||
|
yield part.replace(b"lang={lang}", b"lang=" + translate.lang.encode("utf8")) # lang get parameter to .js file to avoid cache
|
||||||
|
else:
|
||||||
|
yield part
|
||||||
|
else:
|
||||||
|
s = time.time()
|
||||||
|
data = b"".join(list(file_generator)).decode("utf8")
|
||||||
|
|
||||||
|
# if site.content_manager.contents["content.json"]["files"].get(lang_file):
|
||||||
|
site.needFile(lang_file, priority=10)
|
||||||
|
try:
|
||||||
|
if inner_path.endswith("js"):
|
||||||
|
data = translate.translateData(data, site.storage.loadJson(lang_file), "js")
|
||||||
|
else:
|
||||||
|
data = translate.translateData(data, site.storage.loadJson(lang_file), "html")
|
||||||
|
except Exception as err:
|
||||||
|
site.log.error("Error loading translation file %s: %s" % (lang_file, err))
|
||||||
|
|
||||||
|
self.log.debug("Patched %s (%s bytes) in %.3fs" % (inner_path, len(data), time.time() - s))
|
||||||
|
yield data.encode("utf8")
|
|
@ -0,0 +1 @@
|
||||||
|
from . import TranslateSitePlugin
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "TranslateSite",
|
||||||
|
"description": "Transparent support translation of site javascript and html files.",
|
||||||
|
"default": "enabled"
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Config import config
|
||||||
|
from Translate import Translate
|
||||||
|
from util.Flag import flag
|
||||||
|
|
||||||
|
|
||||||
|
plugin_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
if "_" not in locals():
|
||||||
|
_ = Translate(plugin_dir + "/languages/")
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiRequest")
|
||||||
|
class UiRequestPlugin(object):
|
||||||
|
def actionWrapper(self, path, extra_headers=None):
|
||||||
|
if path.strip("/") != "Config":
|
||||||
|
return super(UiRequestPlugin, self).actionWrapper(path, extra_headers)
|
||||||
|
|
||||||
|
if not extra_headers:
|
||||||
|
extra_headers = {}
|
||||||
|
|
||||||
|
script_nonce = self.getScriptNonce()
|
||||||
|
|
||||||
|
self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce)
|
||||||
|
site = self.server.site_manager.get(config.homepage)
|
||||||
|
return iter([super(UiRequestPlugin, self).renderWrapper(
|
||||||
|
site, path, "uimedia/plugins/uiconfig/config.html",
|
||||||
|
"Config", extra_headers, show_loadingscreen=False, script_nonce=script_nonce
|
||||||
|
)])
|
||||||
|
|
||||||
|
def actionUiMedia(self, path, *args, **kwargs):
|
||||||
|
if path.startswith("/uimedia/plugins/uiconfig/"):
|
||||||
|
file_path = path.replace("/uimedia/plugins/uiconfig/", plugin_dir + "/media/")
|
||||||
|
if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")):
|
||||||
|
# If debugging merge *.css to all.css and *.js to all.js
|
||||||
|
from Debug import DebugMedia
|
||||||
|
DebugMedia.merge(file_path)
|
||||||
|
|
||||||
|
if file_path.endswith("js"):
|
||||||
|
data = _.translateData(open(file_path).read(), mode="js").encode("utf8")
|
||||||
|
elif file_path.endswith("html"):
|
||||||
|
data = _.translateData(open(file_path).read(), mode="html").encode("utf8")
|
||||||
|
else:
|
||||||
|
data = open(file_path, "rb").read()
|
||||||
|
|
||||||
|
return self.actionFile(file_path, file_obj=io.BytesIO(data), file_size=len(data))
|
||||||
|
else:
|
||||||
|
return super(UiRequestPlugin, self).actionUiMedia(path)
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiWebsocket")
|
||||||
|
class UiWebsocketPlugin(object):
|
||||||
|
@flag.admin
|
||||||
|
def actionConfigList(self, to):
|
||||||
|
back = {}
|
||||||
|
config_values = vars(config.arguments)
|
||||||
|
config_values.update(config.pending_changes)
|
||||||
|
for key, val in config_values.items():
|
||||||
|
if key not in config.keys_api_change_allowed:
|
||||||
|
continue
|
||||||
|
is_pending = key in config.pending_changes
|
||||||
|
if val is None and is_pending:
|
||||||
|
val = config.parser.get_default(key)
|
||||||
|
back[key] = {
|
||||||
|
"value": val,
|
||||||
|
"default": config.parser.get_default(key),
|
||||||
|
"pending": is_pending
|
||||||
|
}
|
||||||
|
return back
|
|
@ -0,0 +1 @@
|
||||||
|
from . import UiConfigPlugin
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"ZeroNet config": "ZeroNet beállítások",
|
||||||
|
"Web Interface": "Web felület",
|
||||||
|
"Open web browser on ZeroNet startup": "Bögésző megnyitása a ZeroNet indulásakor",
|
||||||
|
|
||||||
|
"Network": "Hálózat",
|
||||||
|
"File server port": "FIle szerver port",
|
||||||
|
"Other peers will use this port to reach your served sites. (default: 15441)": "Más kliensek ezen a porton tudják elérni a kiszolgált oldalaidat (alapbeállítás: 15441)",
|
||||||
|
|
||||||
|
"Disable: Don't connect to peers on Tor network": "Kikapcsolás: Ne csatlakozzon a Tor hálózatra",
|
||||||
|
"Enable: Only use Tor for Tor network peers": "Bekapcsolás: Csak a Tor kliensekhez használja a Tor hálózatot",
|
||||||
|
"Always: Use Tor for every connections to hide your IP address (slower)": "Mindig: Minden kapcsolatot a Tor hálózaton keresztül hozza létre az IP cím elrejtéséhez (lassabb)",
|
||||||
|
|
||||||
|
"Disable": "Kikapcsolás",
|
||||||
|
"Enable": "Bekapcsolás",
|
||||||
|
"Always": "Mindig",
|
||||||
|
|
||||||
|
"Use Tor bridges": "Tor bridge-ek használata",
|
||||||
|
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Tor elrejtő bridge-ek használata a hálózat szintű Tor tiltás megkerüléséhez (még lassabb)",
|
||||||
|
|
||||||
|
"Discover new peers using these adresses": "Új kapcsolat felfedezése ezen címek használatával",
|
||||||
|
|
||||||
|
"Trackers files": "Tracker file-ok",
|
||||||
|
"Load additional list of torrent trackers dynamically, from a file": "További trackerek felfedezése dinamikusan, ezen file használatával",
|
||||||
|
"Eg.: data/trackers.json": "Pl.: data/trackers.json",
|
||||||
|
|
||||||
|
"Proxy for tracker connections": "Proxy tracker kapcsolatohoz",
|
||||||
|
|
||||||
|
" configuration item value changed": " beállítás megváltoztatva",
|
||||||
|
"Save settings": "Beállítások mentése",
|
||||||
|
"Some changed settings requires restart": "A beállítások érvényesítéséhez a kliens újraindítása szükséges",
|
||||||
|
"Restart ZeroNet client": "ZeroNet kliens újraindítása"
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"ZeroNet config": "ZeroNetの設定",
|
||||||
|
"Web Interface": "WEBインターフェース",
|
||||||
|
"Open web browser on ZeroNet startup": "ZeroNet起動時に自動でブラウザーを開く",
|
||||||
|
|
||||||
|
"Network": "ネットワーク",
|
||||||
|
"Offline mode": "オフラインモード",
|
||||||
|
"Disable network communication.": "通信を無効化します",
|
||||||
|
"File server network": "ファイルサーバネットワーク",
|
||||||
|
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "IPv4とIPv6からの受信を許可(既定: 両方)",
|
||||||
|
"Dual (IPv4 & IPv6)": "両方 (IPv4 & IPv6)",
|
||||||
|
"File server port": "ファイルサーバのポート",
|
||||||
|
"Other peers will use this port to reach your served sites. (default: randomize)": "他のピアはこのポートを使用してあなたが所持しているサイトにアクセスします (既定: ランダム)",
|
||||||
|
"File server external ip": "ファイルサーバの外部IP",
|
||||||
|
"Detect automatically": "自動検出",
|
||||||
|
"Your file server is accessible on these ips. (default: detect automatically)": "あなたのファイルサーバへはここで設定したIPでアクセスできます (既定: 自動検出)",
|
||||||
|
|
||||||
|
"Disable: Don't connect to peers on Tor network": "無効: Torネットワーク上のピアに接続しない",
|
||||||
|
"Enable: Only use Tor for Tor network peers": "有効: Torネットワーク上のピアに対してのみTorを使って接続する",
|
||||||
|
"Always: Use Tor for every connections to hide your IP address (slower)": "常時: 全ての接続にTorを使いIPを秘匿する(低速)",
|
||||||
|
|
||||||
|
"Disable": "無効",
|
||||||
|
"Enable": "有効",
|
||||||
|
"Always": "常時",
|
||||||
|
|
||||||
|
"Use Tor bridges": "Torブリッジを使用",
|
||||||
|
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "難読化されたブリッジリレーを使用してネットワークレベルのTorブロックを避ける(超低速)",
|
||||||
|
|
||||||
|
"Discover new peers using these adresses": "ここで設定したアドレスを用いてピアを発見します",
|
||||||
|
|
||||||
|
"Trackers files": "トラッカーファイル",
|
||||||
|
"Load additional list of torrent trackers dynamically, from a file": "ファイルからトレントラッカーの追加リストを動的に読み込みます",
|
||||||
|
"Eg.: data/trackers.json": "例: data/trackers.json",
|
||||||
|
|
||||||
|
"Proxy for tracker connections": "トラッカーへの接続に使うプロキシ",
|
||||||
|
"Custom": "カスタム",
|
||||||
|
"Custom socks proxy address for trackers": "トラッカーに接続するためのカスタムsocksプロキシのアドレス",
|
||||||
|
|
||||||
|
"Performance": "性能",
|
||||||
|
"Level of logging to file": "ログレベル",
|
||||||
|
"Everything": "全て",
|
||||||
|
"Only important messages": "重要なメッセージのみ",
|
||||||
|
"Only errors": "エラーのみ",
|
||||||
|
"Threads for async file system reads": "非同期ファイルシステムの読み込みに使うスレッド",
|
||||||
|
"Threads for async file system writes": "非同期ファイルシステムの書き込みに使うスレッド",
|
||||||
|
"Threads for cryptographic functions": "暗号機能に使うスレッド",
|
||||||
|
"Threads for database operations": "データベースの操作に使うスレッド",
|
||||||
|
"Sync read": "同期読み取り",
|
||||||
|
"Sync write": "同期書き込み",
|
||||||
|
"Sync execution": "同期実行",
|
||||||
|
"1 thread": "1スレッド",
|
||||||
|
"2 threads": "2スレッド",
|
||||||
|
"3 threads": "3スレッド",
|
||||||
|
"4 threads": "4スレッド",
|
||||||
|
"5 threads": "5スレッド",
|
||||||
|
"10 threads": "10スレッド",
|
||||||
|
|
||||||
|
" configuration item value changed": " の項目の値が変更されました",
|
||||||
|
"Save settings": "設定を保存",
|
||||||
|
"Some changed settings requires restart": "一部の変更の適用には再起動が必要です。",
|
||||||
|
"Restart ZeroNet client": "ZeroNetクライアントを再起動"
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"ZeroNet config": "Konfiguracja ZeroNet",
|
||||||
|
"Web Interface": "Interfejs webowy",
|
||||||
|
"Open web browser on ZeroNet startup": "Otwórz przeglądarkę podczas uruchomienia ZeroNet",
|
||||||
|
|
||||||
|
"Network": "Sieć",
|
||||||
|
"Offline mode": "Tryb offline",
|
||||||
|
"Disable network communication.": "Wyłącz komunikacje sieciową",
|
||||||
|
"File server network": "Sieć serwera plików",
|
||||||
|
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "Akceptuj połączenia przychodzące używając IPv4 i IPv6. (domyślnie: oba)",
|
||||||
|
"Dual (IPv4 & IPv6)": "Oba (IPv4 i IPv6)",
|
||||||
|
"File server port": "Port serwera plików",
|
||||||
|
"Other peers will use this port to reach your served sites. (default: 15441)": "Inni użytkownicy będą używać tego portu do połączenia się z Tobą. (domyślnie 15441)",
|
||||||
|
"File server external ip": "Zewnętrzny adres IP serwera plików",
|
||||||
|
"Detect automatically": "Wykryj automatycznie",
|
||||||
|
"Your file server is accessible on these ips. (default: detect automatically)": "Twój serwer plików będzie dostępny na tych adresach IP. (domyślnie: wykryj automatycznie)",
|
||||||
|
|
||||||
|
"Disable: Don't connect to peers on Tor network": "Wyłącz: Nie łącz się do użytkowników sieci Tor",
|
||||||
|
"Enable: Only use Tor for Tor network peers": "Włącz: Łącz się do użytkowników sieci Tor",
|
||||||
|
"Always: Use Tor for every connections to hide your IP address (slower)": "Zawsze Tor: Użyj Tor dla wszystkich połączeń w celu ukrycia Twojego adresu IP (wolne działanie)",
|
||||||
|
|
||||||
|
"Disable": "Wyłącz",
|
||||||
|
"Enable": "Włącz",
|
||||||
|
"Always": "Zawsze Tor",
|
||||||
|
|
||||||
|
"Use Tor bridges": "Użyj Tor bridges",
|
||||||
|
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Użyj obfuskacji, aby uniknąć blokowania Tor na poziomie sieci (jeszcze wolniejsze działanie)",
|
||||||
|
"Trackers": "Trackery",
|
||||||
|
"Discover new peers using these adresses": "Wykryj użytkowników korzystając z tych adresów trackerów",
|
||||||
|
|
||||||
|
"Trackers files": "Pliki trackerów",
|
||||||
|
"Load additional list of torrent trackers dynamically, from a file": "Dynamicznie wczytaj dodatkową listę trackerów z pliku .json",
|
||||||
|
"Eg.: data/trackers.json": "Np.: data/trackers.json",
|
||||||
|
|
||||||
|
"Proxy for tracker connections": "Serwer proxy dla trackerów",
|
||||||
|
"Custom": "Własny",
|
||||||
|
"Custom socks proxy address for trackers": "Adres serwera proxy do łączenia z trackerami",
|
||||||
|
"Eg.: 127.0.0.1:1080": "Np.: 127.0.0.1:1080",
|
||||||
|
"Performance": "Wydajność",
|
||||||
|
"Level of logging to file": "Poziom logowania do pliku",
|
||||||
|
"Everything": "Wszystko",
|
||||||
|
"Only important messages": "Tylko ważne wiadomości",
|
||||||
|
"Only errors": "Tylko błędy",
|
||||||
|
"Threads for async file system reads": "Wątki asynchroniczne dla odczytu",
|
||||||
|
"Threads for async file system writes": "Wątki asynchroniczne dla zapisu",
|
||||||
|
"Threads for cryptographic functions": "Wątki dla funkcji kryptograficznych",
|
||||||
|
"Threads for database operations": "Wątki dla operacji na bazie danych",
|
||||||
|
"Sync read": "Synchroniczny odczyt",
|
||||||
|
"Sync write": "Synchroniczny zapis",
|
||||||
|
"Sync execution": "Synchroniczne wykonanie",
|
||||||
|
"1 thread": "1 wątek",
|
||||||
|
"2 threads": "2 wątki",
|
||||||
|
"3 threads": "3 wątki",
|
||||||
|
"4 threads": "4 wątki",
|
||||||
|
"5 threads": "5 wątków",
|
||||||
|
"10 threads": "10 wątków",
|
||||||
|
|
||||||
|
" configuration item value changed": " obiekt konfiguracji zmieniony",
|
||||||
|
"Save settings": "Zapisz ustawienia",
|
||||||
|
"Some changed settings requires restart": "Niektóre zmiany ustawień wymagają ponownego uruchomienia ZeroNet",
|
||||||
|
"Restart ZeroNet client": "Uruchom ponownie ZeroNet"
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"ZeroNet config": "Configurações da ZeroNet",
|
||||||
|
"Web Interface": "Interface Web",
|
||||||
|
"Open web browser on ZeroNet startup": "Abrir navegador da web quando a ZeroNet iniciar",
|
||||||
|
|
||||||
|
"Network": "Rede",
|
||||||
|
"File server port": "Porta de servidor de arquivos",
|
||||||
|
"Other peers will use this port to reach your served sites. (default: 15441)": "Outros peers usarão esta porta para alcançar seus sites servidos. (padrão: 15441)",
|
||||||
|
|
||||||
|
"Disable: Don't connect to peers on Tor network": "Desativar: Não conectar à peers na rede Tor",
|
||||||
|
"Enable: Only use Tor for Tor network peers": "Ativar: Somente usar Tor para os peers da rede Tor",
|
||||||
|
"Always: Use Tor for every connections to hide your IP address (slower)": "Sempre: Usar o Tor para todas as conexões, para esconder seu endereço IP (mais lento)",
|
||||||
|
|
||||||
|
"Disable": "Desativar",
|
||||||
|
"Enable": "Ativar",
|
||||||
|
"Always": "Sempre",
|
||||||
|
|
||||||
|
"Use Tor bridges": "Usar pontes do Tor",
|
||||||
|
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Usar relays de ponte ofuscados para evitar o bloqueio de Tor de nível de rede (ainda mais lento)",
|
||||||
|
|
||||||
|
"Discover new peers using these adresses": "Descobrir novos peers usando estes endereços",
|
||||||
|
|
||||||
|
"Trackers files": "Arquivos de trackers",
|
||||||
|
"Load additional list of torrent trackers dynamically, from a file": "Carregar lista adicional de trackers de torrent dinamicamente, à partir de um arquivo",
|
||||||
|
"Eg.: data/trackers.json": "Exemplo: data/trackers.json",
|
||||||
|
|
||||||
|
"Proxy for tracker connections": "Proxy para conexões de tracker",
|
||||||
|
|
||||||
|
"configuration item value changed": " valor do item de configuração foi alterado",
|
||||||
|
"Save settings": "Salvar configurações",
|
||||||
|
"Some changed settings requires restart": "Algumas configurações alteradas requrem reinicialização",
|
||||||
|
"Restart ZeroNet client": "Reiniciar cliente da ZeroNet",
|
||||||
|
|
||||||
|
"Offline mode": "Modo offline",
|
||||||
|
"Disable network communication.": "Desativar a comunicação em rede.",
|
||||||
|
"File server network": "Rede do servidor de arquivos",
|
||||||
|
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "Aceite pontos de entrada usando o endereço IPv4 ou IPv6. (padrão: dual)",
|
||||||
|
"File server external ip": "IP externo do servidor de arquivos",
|
||||||
|
"Performance": "Desempenho",
|
||||||
|
"Level of logging to file": "Nível de registro no arquivo",
|
||||||
|
"Everything": "Tudo",
|
||||||
|
"Only important messages": "Apenas mensagens importantes",
|
||||||
|
"Only errors": "Apenas erros",
|
||||||
|
|
||||||
|
"Threads for async file system reads": "Threads para leituras de sistema de arquivos assíncronas",
|
||||||
|
"Threads for async file system writes": "Threads para gravações do sistema de arquivos assíncrono",
|
||||||
|
"Threads for cryptographic functions": "Threads para funções criptográficas",
|
||||||
|
"Threads for database operations": "Threads para operações de banco de dados",
|
||||||
|
"Sync execution": "Execução de sincronização",
|
||||||
|
"Custom": "Personalizado",
|
||||||
|
"Custom socks proxy address for trackers": "Endereço de proxy de meias personalizadas para trackers",
|
||||||
|
"Your file server is accessible on these ips. (default: detect automatically)": "Seu servidor de arquivos está acessível nesses ips. (padrão: detectar automaticamente)",
|
||||||
|
"Detect automatically": "Detectar automaticamente",
|
||||||
|
" configuration item value changed": " valor do item de configuração alterado"
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"ZeroNet config": "ZeroNet配置",
|
||||||
|
"Web Interface": "网页界面",
|
||||||
|
"Open web browser on ZeroNet startup": "ZeroNet启动时,打开浏览器",
|
||||||
|
|
||||||
|
"Network": "网络",
|
||||||
|
"Offline mode": "离线模式",
|
||||||
|
"Disable network communication.": "关闭网络通信.",
|
||||||
|
"File server network": "文件服务器网络",
|
||||||
|
"Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "使用IPv4或IPv6地址接受传入的节点请求. (默认:双重)",
|
||||||
|
"Dual (IPv4 & IPv6)": "双重 (IPv4与IPv6)",
|
||||||
|
"File server port": "文件服务器端口",
|
||||||
|
"Other peers will use this port to reach your served sites. (default: 15441)": "其他节点可通过该端口访问您服务的站点. (默认:15441)",
|
||||||
|
"File server external ip": "文件服务器外部IP",
|
||||||
|
"Detect automatically": "自动检测",
|
||||||
|
"Your file server is accessible on these ips. (default: detect automatically)": "其他节点可通过这些IP访问您的文件服务器. (默认:自动检测)",
|
||||||
|
|
||||||
|
"Disable: Don't connect to peers on Tor network": "关闭: 不连接洋葱网络中的节点",
|
||||||
|
"Enable: Only use Tor for Tor network peers": "开启: Tor仅用于连接洋葱网络中的节点",
|
||||||
|
"Always: Use Tor for every connections to hide your IP address (slower)": "总是: 将Tor用于每个网络连接,从而隐藏您的IP地址 (很慢)",
|
||||||
|
|
||||||
|
"Disable": "关闭",
|
||||||
|
"Enable": "开启",
|
||||||
|
"Always": "总是",
|
||||||
|
|
||||||
|
"Use Tor bridges": "使用Tor网桥",
|
||||||
|
"Use obfuscated bridge relays to avoid network level Tor block (even slower)": "使用混淆网桥中继,从而避免网络层Tor阻碍 (超级慢)",
|
||||||
|
|
||||||
|
"Discover new peers using these adresses": "使用这些地址发现新节点",
|
||||||
|
|
||||||
|
"Trackers files": "Trackers文件",
|
||||||
|
"Load additional list of torrent trackers dynamically, from a file": "从一个文件中,动态加载额外的种子Trackers列表",
|
||||||
|
"Eg.: data/trackers.json": "例如: data/trackers.json",
|
||||||
|
|
||||||
|
"Proxy for tracker connections": "Tracker连接代理",
|
||||||
|
"Custom": "自定义",
|
||||||
|
"Custom socks proxy address for trackers": "Tracker的自定义socks代理地址",
|
||||||
|
|
||||||
|
"Performance": "性能",
|
||||||
|
"Level of logging to file": "日志记录级别",
|
||||||
|
"Everything": "记录全部",
|
||||||
|
"Only important messages": "仅记录重要信息",
|
||||||
|
"Only errors": "仅记录错误",
|
||||||
|
"Threads for async file system reads": "文件系统异步读取线程",
|
||||||
|
"Threads for async file system writes": "文件系统异步写入线程",
|
||||||
|
"Threads for cryptographic functions": "密码函数线程",
|
||||||
|
"Threads for database operations": "数据库操作线程",
|
||||||
|
"Sync read": "同步读取",
|
||||||
|
"Sync write": "同步写入",
|
||||||
|
"Sync execution": "同步执行",
|
||||||
|
"1 thread": "1个线程",
|
||||||
|
"2 threads": "2个线程",
|
||||||
|
"3 threads": "3个线程",
|
||||||
|
"4 threads": "4个线程",
|
||||||
|
"5 threads": "5个线程",
|
||||||
|
"10 threads": "10个线程",
|
||||||
|
|
||||||
|
" configuration item value changed": " 个配置值已经改变",
|
||||||
|
"Save settings": "保存配置",
|
||||||
|
"Some changed settings requires restart": "一些配置已改变,需要重启才能生效",
|
||||||
|
"Restart ZeroNet client": "重启ZeroNet客户端"
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Settings - ZeroNet</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<link rel="stylesheet" href="css/all.css?rev={rev}" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
<h1>ZeroNet config</h1>
|
||||||
|
|
||||||
|
<div class="content" id="content"></div>
|
||||||
|
<div class="bottom" id="bottom-save"></div>
|
||||||
|
<div class="bottom" id="bottom-restart"></div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="js/all.js?rev={rev}&lang={lang}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,68 @@
|
||||||
|
body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; }
|
||||||
|
h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px }
|
||||||
|
h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
|
||||||
|
h2 { margin-top: 10px; }
|
||||||
|
h3 { font-weight: normal }
|
||||||
|
a { color: #9760F9 }
|
||||||
|
a:hover { text-decoration: none }
|
||||||
|
|
||||||
|
.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s }
|
||||||
|
.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none }
|
||||||
|
|
||||||
|
.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; box-sizing: border-box; padding-bottom: 150px; }
|
||||||
|
.section { margin: 0px 10%; }
|
||||||
|
.config-items { font-size: 19px; margin-top: 25px; margin-bottom: 75px; }
|
||||||
|
.config-item { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; }
|
||||||
|
.config-item.hidden { opacity: 0; height: 0px; padding: 0px; }
|
||||||
|
.config-item .title { display: inline-block; line-height: 36px; }
|
||||||
|
.config-item .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; }
|
||||||
|
.config-item .description { font-size: 14px; color: #666; line-height: 24px; }
|
||||||
|
.config-item .value { display: inline-block; white-space: nowrap; }
|
||||||
|
.config-item .value-right { right: 0px; position: absolute; }
|
||||||
|
.config-item .value-fullwidth { width: 100% }
|
||||||
|
.config-item .marker {
|
||||||
|
font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px;
|
||||||
|
opacity: 0; pointer-events: none; transition: all 0.6s; transform: scale(2); color: #9760F9;
|
||||||
|
}
|
||||||
|
.config-item .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); }
|
||||||
|
.config-item .marker.changed { color: #2ecc71; }
|
||||||
|
.config-item .marker.pending { color: #ffa200; }
|
||||||
|
|
||||||
|
|
||||||
|
.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; border-radius: 3px; font-size: 17px; box-sizing: border-box; }
|
||||||
|
.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; }
|
||||||
|
.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; }
|
||||||
|
|
||||||
|
.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; }
|
||||||
|
|
||||||
|
.value-right .input-text { text-align: right; width: 100px; }
|
||||||
|
.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; }
|
||||||
|
.value-fullwidth { margin-top: 10px; }
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; }
|
||||||
|
.checkbox-skin:before {
|
||||||
|
content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
|
||||||
|
transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||||
|
}
|
||||||
|
.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; }
|
||||||
|
.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px }
|
||||||
|
.checkbox.checked .checkbox-skin:before { margin-left: 27px; }
|
||||||
|
.checkbox.checked .checkbox-skin { background-color: #2ECC71 }
|
||||||
|
|
||||||
|
/* Bottom */
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
|
||||||
|
transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; }
|
||||||
|
.bottom .button { float: right; }
|
||||||
|
.bottom.visible { bottom: 0px; box-shadow: 0px 0px 35px #dcdcdc; }
|
||||||
|
.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; }
|
||||||
|
.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; }
|
||||||
|
.bottom-restart .title:before { color: #ffa200; }
|
||||||
|
|
||||||
|
.animate { transition: all 0.3s ease-out !important; }
|
||||||
|
.animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; }
|
||||||
|
.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; }
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,12 @@
|
||||||
|
/* Button */
|
||||||
|
.button {
|
||||||
|
background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center;
|
||||||
|
border-radius: 2px; border-bottom: 2px solid #E8BE29; transition: all 0.5s ease-out; text-decoration: none;
|
||||||
|
}
|
||||||
|
.button:hover { border-color: white; border-bottom: 2px solid #BD960C; transition: none ; background-color: #FDEB07 }
|
||||||
|
.button:active { position: relative; top: 1px }
|
||||||
|
.button.loading {
|
||||||
|
color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center;
|
||||||
|
transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666
|
||||||
|
}
|
||||||
|
.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 }
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 723 B |
|
@ -0,0 +1,222 @@
|
||||||
|
class ConfigStorage extends Class
|
||||||
|
constructor: (@config) ->
|
||||||
|
@items = []
|
||||||
|
@createSections()
|
||||||
|
@setValues(@config)
|
||||||
|
|
||||||
|
setValues: (values) ->
|
||||||
|
for section in @items
|
||||||
|
for item in section.items
|
||||||
|
if not values[item.key]
|
||||||
|
continue
|
||||||
|
item.value = @formatValue(values[item.key].value)
|
||||||
|
item.default = @formatValue(values[item.key].default)
|
||||||
|
item.pending = values[item.key].pending
|
||||||
|
values[item.key].item = item
|
||||||
|
|
||||||
|
formatValue: (value) ->
|
||||||
|
if not value
|
||||||
|
return false
|
||||||
|
else if typeof(value) == "object"
|
||||||
|
return value.join("\n")
|
||||||
|
else if typeof(value) == "number"
|
||||||
|
return value.toString()
|
||||||
|
else
|
||||||
|
return value
|
||||||
|
|
||||||
|
deformatValue: (value, type) ->
|
||||||
|
if type == "object" and typeof(value) == "string"
|
||||||
|
if not value.length
|
||||||
|
return value = null
|
||||||
|
else
|
||||||
|
return value.split("\n")
|
||||||
|
if type == "boolean" and not value
|
||||||
|
return false
|
||||||
|
else if type == "number"
|
||||||
|
if typeof(value) == "number"
|
||||||
|
return value.toString()
|
||||||
|
else if not value
|
||||||
|
return "0"
|
||||||
|
else
|
||||||
|
return value
|
||||||
|
else
|
||||||
|
return value
|
||||||
|
|
||||||
|
createSections: ->
|
||||||
|
# Web Interface
|
||||||
|
section = @createSection("Web Interface")
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "open_browser"
|
||||||
|
title: "Open web browser on ZeroNet startup"
|
||||||
|
type: "checkbox"
|
||||||
|
|
||||||
|
# Network
|
||||||
|
section = @createSection("Network")
|
||||||
|
section.items.push
|
||||||
|
key: "offline"
|
||||||
|
title: "Offline mode"
|
||||||
|
type: "checkbox"
|
||||||
|
description: "Disable network communication."
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "fileserver_ip_type"
|
||||||
|
title: "File server network"
|
||||||
|
type: "select"
|
||||||
|
options: [
|
||||||
|
{title: "IPv4", value: "ipv4"}
|
||||||
|
{title: "IPv6", value: "ipv6"}
|
||||||
|
{title: "Dual (IPv4 & IPv6)", value: "dual"}
|
||||||
|
]
|
||||||
|
description: "Accept incoming peers using IPv4 or IPv6 address. (default: dual)"
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "fileserver_port"
|
||||||
|
title: "File server port"
|
||||||
|
type: "text"
|
||||||
|
valid_pattern: /[0-9]*/
|
||||||
|
description: "Other peers will use this port to reach your served sites. (default: randomize)"
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "ip_external"
|
||||||
|
title: "File server external ip"
|
||||||
|
type: "textarea"
|
||||||
|
placeholder: "Detect automatically"
|
||||||
|
description: "Your file server is accessible on these ips. (default: detect automatically)"
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
title: "Tor"
|
||||||
|
key: "tor"
|
||||||
|
type: "select"
|
||||||
|
options: [
|
||||||
|
{title: "Disable", value: "disable"}
|
||||||
|
{title: "Enable", value: "enable"}
|
||||||
|
{title: "Always", value: "always"}
|
||||||
|
]
|
||||||
|
description: [
|
||||||
|
"Disable: Don't connect to peers on Tor network", h("br"),
|
||||||
|
"Enable: Only use Tor for Tor network peers", h("br"),
|
||||||
|
"Always: Use Tor for every connections to hide your IP address (slower)"
|
||||||
|
]
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
title: "Use Tor bridges"
|
||||||
|
key: "tor_use_bridges"
|
||||||
|
type: "checkbox"
|
||||||
|
description: "Use obfuscated bridge relays to avoid network level Tor block (even slower)"
|
||||||
|
isHidden: ->
|
||||||
|
return not Page.server_info.tor_has_meek_bridges
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
title: "Trackers"
|
||||||
|
key: "trackers"
|
||||||
|
type: "textarea"
|
||||||
|
description: "Discover new peers using these adresses"
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
title: "Trackers files"
|
||||||
|
key: "trackers_file"
|
||||||
|
type: "textarea"
|
||||||
|
description: "Load additional list of torrent trackers dynamically, from a file"
|
||||||
|
placeholder: "Eg.: {data_dir}/trackers.json"
|
||||||
|
value_pos: "fullwidth"
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
title: "Proxy for tracker connections"
|
||||||
|
key: "trackers_proxy"
|
||||||
|
type: "select"
|
||||||
|
options: [
|
||||||
|
{title: "Custom", value: ""}
|
||||||
|
{title: "Tor", value: "tor"}
|
||||||
|
{title: "Disable", value: "disable"}
|
||||||
|
]
|
||||||
|
isHidden: ->
|
||||||
|
Page.values["tor"] == "always"
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
title: "Custom socks proxy address for trackers"
|
||||||
|
key: "trackers_proxy"
|
||||||
|
type: "text"
|
||||||
|
placeholder: "Eg.: 127.0.0.1:1080"
|
||||||
|
value_pos: "fullwidth"
|
||||||
|
valid_pattern: /.+:[0-9]+/
|
||||||
|
isHidden: =>
|
||||||
|
Page.values["trackers_proxy"] in ["tor", "disable"]
|
||||||
|
|
||||||
|
# Performance
|
||||||
|
section = @createSection("Performance")
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "log_level"
|
||||||
|
title: "Level of logging to file"
|
||||||
|
type: "select"
|
||||||
|
options: [
|
||||||
|
{title: "Everything", value: "DEBUG"}
|
||||||
|
{title: "Only important messages", value: "INFO"}
|
||||||
|
{title: "Only errors", value: "ERROR"}
|
||||||
|
]
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "threads_fs_read"
|
||||||
|
title: "Threads for async file system reads"
|
||||||
|
type: "select"
|
||||||
|
options: [
|
||||||
|
{title: "Sync read", value: 0}
|
||||||
|
{title: "1 thread", value: 1}
|
||||||
|
{title: "2 threads", value: 2}
|
||||||
|
{title: "3 threads", value: 3}
|
||||||
|
{title: "4 threads", value: 4}
|
||||||
|
{title: "5 threads", value: 5}
|
||||||
|
{title: "10 threads", value: 10}
|
||||||
|
]
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "threads_fs_write"
|
||||||
|
title: "Threads for async file system writes"
|
||||||
|
type: "select"
|
||||||
|
options: [
|
||||||
|
{title: "Sync write", value: 0}
|
||||||
|
{title: "1 thread", value: 1}
|
||||||
|
{title: "2 threads", value: 2}
|
||||||
|
{title: "3 threads", value: 3}
|
||||||
|
{title: "4 threads", value: 4}
|
||||||
|
{title: "5 threads", value: 5}
|
||||||
|
{title: "10 threads", value: 10}
|
||||||
|
]
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "threads_crypt"
|
||||||
|
title: "Threads for cryptographic functions"
|
||||||
|
type: "select"
|
||||||
|
options: [
|
||||||
|
{title: "Sync execution", value: 0}
|
||||||
|
{title: "1 thread", value: 1}
|
||||||
|
{title: "2 threads", value: 2}
|
||||||
|
{title: "3 threads", value: 3}
|
||||||
|
{title: "4 threads", value: 4}
|
||||||
|
{title: "5 threads", value: 5}
|
||||||
|
{title: "10 threads", value: 10}
|
||||||
|
]
|
||||||
|
|
||||||
|
section.items.push
|
||||||
|
key: "threads_db"
|
||||||
|
title: "Threads for database operations"
|
||||||
|
type: "select"
|
||||||
|
options: [
|
||||||
|
{title: "Sync execution", value: 0}
|
||||||
|
{title: "1 thread", value: 1}
|
||||||
|
{title: "2 threads", value: 2}
|
||||||
|
{title: "3 threads", value: 3}
|
||||||
|
{title: "4 threads", value: 4}
|
||||||
|
{title: "5 threads", value: 5}
|
||||||
|
{title: "10 threads", value: 10}
|
||||||
|
]
|
||||||
|
|
||||||
|
createSection: (title) =>
|
||||||
|
section = {}
|
||||||
|
section.title = title
|
||||||
|
section.items = []
|
||||||
|
@items.push(section)
|
||||||
|
return section
|
||||||
|
|
||||||
|
window.ConfigStorage = ConfigStorage
|
|
@ -0,0 +1,124 @@
|
||||||
|
class ConfigView extends Class
|
||||||
|
constructor: () ->
|
||||||
|
@
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@config_storage.items.map @renderSection
|
||||||
|
|
||||||
|
renderSection: (section) =>
|
||||||
|
h("div.section", {key: section.title}, [
|
||||||
|
h("h2", section.title),
|
||||||
|
h("div.config-items", section.items.map @renderSectionItem)
|
||||||
|
])
|
||||||
|
|
||||||
|
handleResetClick: (e) =>
|
||||||
|
node = e.currentTarget
|
||||||
|
config_key = node.attributes.config_key.value
|
||||||
|
default_value = node.attributes.default_value?.value
|
||||||
|
Page.cmd "wrapperConfirm", ["Reset #{config_key} value?", "Reset to default"], (res) =>
|
||||||
|
if (res)
|
||||||
|
@values[config_key] = default_value
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
|
||||||
|
renderSectionItem: (item) =>
|
||||||
|
value_pos = item.value_pos
|
||||||
|
|
||||||
|
if item.type == "textarea"
|
||||||
|
value_pos ?= "fullwidth"
|
||||||
|
else
|
||||||
|
value_pos ?= "right"
|
||||||
|
|
||||||
|
value_changed = @config_storage.formatValue(@values[item.key]) != item.value
|
||||||
|
value_default = @config_storage.formatValue(@values[item.key]) == item.default
|
||||||
|
|
||||||
|
if item.key in ["open_browser", "fileserver_port"] # Value default for some settings makes no sense
|
||||||
|
value_default = true
|
||||||
|
|
||||||
|
marker_title = "Changed from default value: #{item.default} -> #{@values[item.key]}"
|
||||||
|
if item.pending
|
||||||
|
marker_title += " (change pending until client restart)"
|
||||||
|
|
||||||
|
if item.isHidden?()
|
||||||
|
return null
|
||||||
|
|
||||||
|
h("div.config-item", {key: item.title, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUpInout}, [
|
||||||
|
h("div.title", [
|
||||||
|
h("h3", item.title),
|
||||||
|
h("div.description", item.description)
|
||||||
|
])
|
||||||
|
h("div.value.value-#{value_pos}",
|
||||||
|
if item.type == "select"
|
||||||
|
@renderValueSelect(item)
|
||||||
|
else if item.type == "checkbox"
|
||||||
|
@renderValueCheckbox(item)
|
||||||
|
else if item.type == "textarea"
|
||||||
|
@renderValueTextarea(item)
|
||||||
|
else
|
||||||
|
@renderValueText(item)
|
||||||
|
h("a.marker", {
|
||||||
|
href: "#Reset", title: marker_title,
|
||||||
|
onclick: @handleResetClick, config_key: item.key, default_value: item.default,
|
||||||
|
classes: {default: value_default, changed: value_changed, visible: not value_default or value_changed or item.pending, pending: item.pending}
|
||||||
|
}, "\u2022")
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Values
|
||||||
|
handleInputChange: (e) =>
|
||||||
|
node = e.target
|
||||||
|
config_key = node.attributes.config_key.value
|
||||||
|
@values[config_key] = node.value
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
|
||||||
|
handleCheckboxChange: (e) =>
|
||||||
|
node = e.currentTarget
|
||||||
|
config_key = node.attributes.config_key.value
|
||||||
|
value = not node.classList.contains("checked")
|
||||||
|
@values[config_key] = value
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
|
||||||
|
renderValueText: (item) =>
|
||||||
|
value = @values[item.key]
|
||||||
|
if not value
|
||||||
|
value = ""
|
||||||
|
h("input.input-#{item.type}", {type: item.type, config_key: item.key, value: value, placeholder: item.placeholder, oninput: @handleInputChange})
|
||||||
|
|
||||||
|
autosizeTextarea: (e) =>
|
||||||
|
if e.currentTarget
|
||||||
|
# @handleInputChange(e)
|
||||||
|
node = e.currentTarget
|
||||||
|
else
|
||||||
|
node = e
|
||||||
|
height_before = node.style.height
|
||||||
|
if height_before
|
||||||
|
node.style.height = "0px"
|
||||||
|
h = node.offsetHeight
|
||||||
|
scrollh = node.scrollHeight + 20
|
||||||
|
if scrollh > h
|
||||||
|
node.style.height = scrollh + "px"
|
||||||
|
else
|
||||||
|
node.style.height = height_before
|
||||||
|
|
||||||
|
renderValueTextarea: (item) =>
|
||||||
|
value = @values[item.key]
|
||||||
|
if not value
|
||||||
|
value = ""
|
||||||
|
h("textarea.input-#{item.type}.input-text",{
|
||||||
|
type: item.type, config_key: item.key, oninput: @handleInputChange, afterCreate: @autosizeTextarea,
|
||||||
|
updateAnimation: @autosizeTextarea, value: value, placeholder: item.placeholder
|
||||||
|
})
|
||||||
|
|
||||||
|
renderValueCheckbox: (item) =>
|
||||||
|
if @values[item.key] and @values[item.key] != "False"
|
||||||
|
checked = true
|
||||||
|
else
|
||||||
|
checked = false
|
||||||
|
h("div.checkbox", {onclick: @handleCheckboxChange, config_key: item.key, classes: {checked: checked}}, h("div.checkbox-skin"))
|
||||||
|
|
||||||
|
renderValueSelect: (item) =>
|
||||||
|
h("select.input-select", {config_key: item.key, oninput: @handleInputChange},
|
||||||
|
item.options.map (option) =>
|
||||||
|
h("option", {selected: option.value.toString() == @values[item.key], value: option.value}, option.title)
|
||||||
|
)
|
||||||
|
|
||||||
|
window.ConfigView = ConfigView
|
|
@ -0,0 +1,129 @@
|
||||||
|
window.h = maquette.h
|
||||||
|
|
||||||
|
class UiConfig extends ZeroFrame
|
||||||
|
init: ->
|
||||||
|
@save_visible = true
|
||||||
|
@config = null # Setting currently set on the server
|
||||||
|
@values = null # Entered values on the page
|
||||||
|
@config_view = new ConfigView()
|
||||||
|
window.onbeforeunload = =>
|
||||||
|
if @getValuesChanged().length > 0
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return null
|
||||||
|
|
||||||
|
onOpenWebsocket: =>
|
||||||
|
@cmd("wrapperSetTitle", "Config - ZeroNet")
|
||||||
|
@cmd "serverInfo", {}, (server_info) =>
|
||||||
|
@server_info = server_info
|
||||||
|
@restart_loading = false
|
||||||
|
@updateConfig()
|
||||||
|
|
||||||
|
updateConfig: (cb) =>
|
||||||
|
@cmd "configList", [], (res) =>
|
||||||
|
@config = res
|
||||||
|
@values = {}
|
||||||
|
@config_storage = new ConfigStorage(@config)
|
||||||
|
@config_view.values = @values
|
||||||
|
@config_view.config_storage = @config_storage
|
||||||
|
for key, item of res
|
||||||
|
value = item.value
|
||||||
|
@values[key] = @config_storage.formatValue(value)
|
||||||
|
@projector.scheduleRender()
|
||||||
|
cb?()
|
||||||
|
|
||||||
|
createProjector: =>
|
||||||
|
@projector = maquette.createProjector()
|
||||||
|
@projector.replace($("#content"), @render)
|
||||||
|
@projector.replace($("#bottom-save"), @renderBottomSave)
|
||||||
|
@projector.replace($("#bottom-restart"), @renderBottomRestart)
|
||||||
|
|
||||||
|
getValuesChanged: =>
|
||||||
|
values_changed = []
|
||||||
|
for key, value of @values
|
||||||
|
if @config_storage.formatValue(value) != @config_storage.formatValue(@config[key]?.value)
|
||||||
|
values_changed.push({key: key, value: value})
|
||||||
|
return values_changed
|
||||||
|
|
||||||
|
getValuesPending: =>
|
||||||
|
values_pending = []
|
||||||
|
for key, item of @config
|
||||||
|
if item.pending
|
||||||
|
values_pending.push(key)
|
||||||
|
return values_pending
|
||||||
|
|
||||||
|
saveValues: (cb) =>
|
||||||
|
changed_values = @getValuesChanged()
|
||||||
|
for item, i in changed_values
|
||||||
|
last = i == changed_values.length - 1
|
||||||
|
value = @config_storage.deformatValue(item.value, typeof(@config[item.key].default))
|
||||||
|
default_value = @config_storage.deformatValue(@config[item.key].default, typeof(@config[item.key].default))
|
||||||
|
value_same_as_default = JSON.stringify(default_value) == JSON.stringify(value)
|
||||||
|
|
||||||
|
if @config[item.key].item.valid_pattern and not @config[item.key].item.isHidden?()
|
||||||
|
match = value.match(@config[item.key].item.valid_pattern)
|
||||||
|
if not match or match[0] != value
|
||||||
|
message = "Invalid value of #{@config[item.key].item.title}: #{value} (does not matches #{@config[item.key].item.valid_pattern})"
|
||||||
|
Page.cmd("wrapperNotification", ["error", message])
|
||||||
|
cb(false)
|
||||||
|
break
|
||||||
|
|
||||||
|
if value_same_as_default
|
||||||
|
value = null
|
||||||
|
|
||||||
|
@saveValue(item.key, value, if last then cb else null)
|
||||||
|
|
||||||
|
saveValue: (key, value, cb) =>
|
||||||
|
if key == "open_browser"
|
||||||
|
if value
|
||||||
|
value = "default_browser"
|
||||||
|
else
|
||||||
|
value = "False"
|
||||||
|
|
||||||
|
Page.cmd "configSet", [key, value], (res) =>
|
||||||
|
if res != "ok"
|
||||||
|
Page.cmd "wrapperNotification", ["error", res.error]
|
||||||
|
cb?(true)
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
if not @config
|
||||||
|
return h("div.content")
|
||||||
|
|
||||||
|
h("div.content", [
|
||||||
|
@config_view.render()
|
||||||
|
])
|
||||||
|
|
||||||
|
handleSaveClick: =>
|
||||||
|
@save_loading = true
|
||||||
|
@logStart "Save"
|
||||||
|
@saveValues (success) =>
|
||||||
|
@save_loading = false
|
||||||
|
@logEnd "Save"
|
||||||
|
if success
|
||||||
|
@updateConfig()
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
return false
|
||||||
|
|
||||||
|
renderBottomSave: =>
|
||||||
|
values_changed = @getValuesChanged()
|
||||||
|
h("div.bottom.bottom-save", {classes: {visible: values_changed.length}}, h("div.bottom-content", [
|
||||||
|
h("div.title", "#{values_changed.length} configuration item value changed"),
|
||||||
|
h("a.button.button-submit.button-save", {href: "#Save", classes: {loading: @save_loading}, onclick: @handleSaveClick}, "Save settings")
|
||||||
|
]))
|
||||||
|
|
||||||
|
handleRestartClick: =>
|
||||||
|
@restart_loading = true
|
||||||
|
Page.cmd("serverShutdown", {restart: true})
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
return false
|
||||||
|
|
||||||
|
renderBottomRestart: =>
|
||||||
|
values_pending = @getValuesPending()
|
||||||
|
values_changed = @getValuesChanged()
|
||||||
|
h("div.bottom.bottom-restart", {classes: {visible: values_pending.length and not values_changed.length}}, h("div.bottom-content", [
|
||||||
|
h("div.title", "Some changed settings requires restart"),
|
||||||
|
h("a.button.button-submit.button-restart", {href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick}, "Restart ZeroNet client")
|
||||||
|
]))
|
||||||
|
|
||||||
|
window.Page = new UiConfig()
|
||||||
|
window.Page.createProjector()
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,23 @@
|
||||||
|
class Class
|
||||||
|
trace: true
|
||||||
|
|
||||||
|
log: (args...) ->
|
||||||
|
return unless @trace
|
||||||
|
return if typeof console is 'undefined'
|
||||||
|
args.unshift("[#{@.constructor.name}]")
|
||||||
|
console.log(args...)
|
||||||
|
@
|
||||||
|
|
||||||
|
logStart: (name, args...) ->
|
||||||
|
return unless @trace
|
||||||
|
@logtimers or= {}
|
||||||
|
@logtimers[name] = +(new Date)
|
||||||
|
@log "#{name}", args..., "(started)" if args.length > 0
|
||||||
|
@
|
||||||
|
|
||||||
|
logEnd: (name, args...) ->
|
||||||
|
ms = +(new Date)-@logtimers[name]
|
||||||
|
@log "#{name}", args..., "(Done in #{ms}ms)"
|
||||||
|
@
|
||||||
|
|
||||||
|
window.Class = Class
|
|
@ -0,0 +1,74 @@
|
||||||
|
# From: http://dev.bizo.com/2011/12/promises-in-javascriptcoffeescript.html
|
||||||
|
|
||||||
|
class Promise
|
||||||
|
@when: (tasks...) ->
|
||||||
|
num_uncompleted = tasks.length
|
||||||
|
args = new Array(num_uncompleted)
|
||||||
|
promise = new Promise()
|
||||||
|
|
||||||
|
for task, task_id in tasks
|
||||||
|
((task_id) ->
|
||||||
|
task.then(() ->
|
||||||
|
args[task_id] = Array.prototype.slice.call(arguments)
|
||||||
|
num_uncompleted--
|
||||||
|
promise.complete.apply(promise, args) if num_uncompleted == 0
|
||||||
|
)
|
||||||
|
)(task_id)
|
||||||
|
|
||||||
|
return promise
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@resolved = false
|
||||||
|
@end_promise = null
|
||||||
|
@result = null
|
||||||
|
@callbacks = []
|
||||||
|
|
||||||
|
resolve: ->
|
||||||
|
if @resolved
|
||||||
|
return false
|
||||||
|
@resolved = true
|
||||||
|
@data = arguments
|
||||||
|
if not arguments.length
|
||||||
|
@data = [true]
|
||||||
|
@result = @data[0]
|
||||||
|
for callback in @callbacks
|
||||||
|
back = callback.apply callback, @data
|
||||||
|
if @end_promise
|
||||||
|
@end_promise.resolve(back)
|
||||||
|
|
||||||
|
fail: ->
|
||||||
|
@resolve(false)
|
||||||
|
|
||||||
|
then: (callback) ->
|
||||||
|
if @resolved == true
|
||||||
|
callback.apply callback, @data
|
||||||
|
return
|
||||||
|
|
||||||
|
@callbacks.push callback
|
||||||
|
|
||||||
|
@end_promise = new Promise()
|
||||||
|
|
||||||
|
window.Promise = Promise
|
||||||
|
|
||||||
|
###
|
||||||
|
s = Date.now()
|
||||||
|
log = (text) ->
|
||||||
|
console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ")
|
||||||
|
|
||||||
|
log "Started"
|
||||||
|
|
||||||
|
cmd = (query) ->
|
||||||
|
p = new Promise()
|
||||||
|
setTimeout ( ->
|
||||||
|
p.resolve query+" Result"
|
||||||
|
), 100
|
||||||
|
return p
|
||||||
|
|
||||||
|
back = cmd("SELECT * FROM message").then (res) ->
|
||||||
|
log res
|
||||||
|
return "Return from query"
|
||||||
|
.then (res) ->
|
||||||
|
log "Back then", res
|
||||||
|
|
||||||
|
log "Query started", back
|
||||||
|
###
|
|
@ -0,0 +1,8 @@
|
||||||
|
String::startsWith = (s) -> @[...s.length] is s
|
||||||
|
String::endsWith = (s) -> s is '' or @[-s.length..] is s
|
||||||
|
String::repeat = (count) -> new Array( count + 1 ).join(@)
|
||||||
|
|
||||||
|
window.isEmpty = (obj) ->
|
||||||
|
for key of obj
|
||||||
|
return false
|
||||||
|
return true
|
|
@ -0,0 +1,770 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
// AMD. Register as an anonymous module.
|
||||||
|
define(['exports'], factory);
|
||||||
|
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
|
||||||
|
// CommonJS
|
||||||
|
factory(exports);
|
||||||
|
} else {
|
||||||
|
// Browser globals
|
||||||
|
factory(root.maquette = {});
|
||||||
|
}
|
||||||
|
}(this, function (exports) {
|
||||||
|
'use strict';
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
;
|
||||||
|
var NAMESPACE_W3 = 'http://www.w3.org/';
|
||||||
|
var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
|
||||||
|
var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
|
||||||
|
// Utilities
|
||||||
|
var emptyArray = [];
|
||||||
|
var extend = function (base, overrides) {
|
||||||
|
var result = {};
|
||||||
|
Object.keys(base).forEach(function (key) {
|
||||||
|
result[key] = base[key];
|
||||||
|
});
|
||||||
|
if (overrides) {
|
||||||
|
Object.keys(overrides).forEach(function (key) {
|
||||||
|
result[key] = overrides[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
// Hyperscript helper functions
|
||||||
|
var same = function (vnode1, vnode2) {
|
||||||
|
if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (vnode1.properties && vnode2.properties) {
|
||||||
|
if (vnode1.properties.key !== vnode2.properties.key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return vnode1.properties.bind === vnode2.properties.bind;
|
||||||
|
}
|
||||||
|
return !vnode1.properties && !vnode2.properties;
|
||||||
|
};
|
||||||
|
var toTextVNode = function (data) {
|
||||||
|
return {
|
||||||
|
vnodeSelector: '',
|
||||||
|
properties: undefined,
|
||||||
|
children: undefined,
|
||||||
|
text: data.toString(),
|
||||||
|
domNode: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var appendChildren = function (parentSelector, insertions, main) {
|
||||||
|
for (var i = 0; i < insertions.length; i++) {
|
||||||
|
var item = insertions[i];
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
appendChildren(parentSelector, item, main);
|
||||||
|
} else {
|
||||||
|
if (item !== null && item !== undefined) {
|
||||||
|
if (!item.hasOwnProperty('vnodeSelector')) {
|
||||||
|
item = toTextVNode(item);
|
||||||
|
}
|
||||||
|
main.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Render helper functions
|
||||||
|
var missingTransition = function () {
|
||||||
|
throw new Error('Provide a transitions object to the projectionOptions to do animations');
|
||||||
|
};
|
||||||
|
var DEFAULT_PROJECTION_OPTIONS = {
|
||||||
|
namespace: undefined,
|
||||||
|
eventHandlerInterceptor: undefined,
|
||||||
|
styleApplyer: function (domNode, styleName, value) {
|
||||||
|
// Provides a hook to add vendor prefixes for browsers that still need it.
|
||||||
|
domNode.style[styleName] = value;
|
||||||
|
},
|
||||||
|
transitions: {
|
||||||
|
enter: missingTransition,
|
||||||
|
exit: missingTransition
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var applyDefaultProjectionOptions = function (projectorOptions) {
|
||||||
|
return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
|
||||||
|
};
|
||||||
|
var checkStyleValue = function (styleValue) {
|
||||||
|
if (typeof styleValue !== 'string') {
|
||||||
|
throw new Error('Style values must be strings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var setProperties = function (domNode, properties, projectionOptions) {
|
||||||
|
if (!properties) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
|
||||||
|
var propNames = Object.keys(properties);
|
||||||
|
var propCount = propNames.length;
|
||||||
|
for (var i = 0; i < propCount; i++) {
|
||||||
|
var propName = propNames[i];
|
||||||
|
/* tslint:disable:no-var-keyword: edge case */
|
||||||
|
var propValue = properties[propName];
|
||||||
|
/* tslint:enable:no-var-keyword */
|
||||||
|
if (propName === 'className') {
|
||||||
|
throw new Error('Property "className" is not supported, use "class".');
|
||||||
|
} else if (propName === 'class') {
|
||||||
|
if (domNode.className) {
|
||||||
|
// May happen if classes is specified before class
|
||||||
|
domNode.className += ' ' + propValue;
|
||||||
|
} else {
|
||||||
|
domNode.className = propValue;
|
||||||
|
}
|
||||||
|
} else if (propName === 'classes') {
|
||||||
|
// object with string keys and boolean values
|
||||||
|
var classNames = Object.keys(propValue);
|
||||||
|
var classNameCount = classNames.length;
|
||||||
|
for (var j = 0; j < classNameCount; j++) {
|
||||||
|
var className = classNames[j];
|
||||||
|
if (propValue[className]) {
|
||||||
|
domNode.classList.add(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (propName === 'styles') {
|
||||||
|
// object with string keys and string (!) values
|
||||||
|
var styleNames = Object.keys(propValue);
|
||||||
|
var styleCount = styleNames.length;
|
||||||
|
for (var j = 0; j < styleCount; j++) {
|
||||||
|
var styleName = styleNames[j];
|
||||||
|
var styleValue = propValue[styleName];
|
||||||
|
if (styleValue) {
|
||||||
|
checkStyleValue(styleValue);
|
||||||
|
projectionOptions.styleApplyer(domNode, styleName, styleValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (propName === 'key') {
|
||||||
|
continue;
|
||||||
|
} else if (propValue === null || propValue === undefined) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
var type = typeof propValue;
|
||||||
|
if (type === 'function') {
|
||||||
|
if (propName.lastIndexOf('on', 0) === 0) {
|
||||||
|
if (eventHandlerInterceptor) {
|
||||||
|
propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers
|
||||||
|
}
|
||||||
|
if (propName === 'oninput') {
|
||||||
|
(function () {
|
||||||
|
// record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
|
||||||
|
var oldPropValue = propValue;
|
||||||
|
propValue = function (evt) {
|
||||||
|
evt.target['oninput-value'] = evt.target.value;
|
||||||
|
// may be HTMLTextAreaElement as well
|
||||||
|
oldPropValue.apply(this, [evt]);
|
||||||
|
};
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
domNode[propName] = propValue;
|
||||||
|
}
|
||||||
|
} else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
|
||||||
|
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
|
||||||
|
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
|
||||||
|
} else {
|
||||||
|
domNode.setAttribute(propName, propValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
domNode[propName] = propValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
|
||||||
|
if (!properties) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var propertiesUpdated = false;
|
||||||
|
var propNames = Object.keys(properties);
|
||||||
|
var propCount = propNames.length;
|
||||||
|
for (var i = 0; i < propCount; i++) {
|
||||||
|
var propName = propNames[i];
|
||||||
|
// assuming that properties will be nullified instead of missing is by design
|
||||||
|
var propValue = properties[propName];
|
||||||
|
var previousValue = previousProperties[propName];
|
||||||
|
if (propName === 'class') {
|
||||||
|
if (previousValue !== propValue) {
|
||||||
|
throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
|
||||||
|
}
|
||||||
|
} else if (propName === 'classes') {
|
||||||
|
var classList = domNode.classList;
|
||||||
|
var classNames = Object.keys(propValue);
|
||||||
|
var classNameCount = classNames.length;
|
||||||
|
for (var j = 0; j < classNameCount; j++) {
|
||||||
|
var className = classNames[j];
|
||||||
|
var on = !!propValue[className];
|
||||||
|
var previousOn = !!previousValue[className];
|
||||||
|
if (on === previousOn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
propertiesUpdated = true;
|
||||||
|
if (on) {
|
||||||
|
classList.add(className);
|
||||||
|
} else {
|
||||||
|
classList.remove(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (propName === 'styles') {
|
||||||
|
var styleNames = Object.keys(propValue);
|
||||||
|
var styleCount = styleNames.length;
|
||||||
|
for (var j = 0; j < styleCount; j++) {
|
||||||
|
var styleName = styleNames[j];
|
||||||
|
var newStyleValue = propValue[styleName];
|
||||||
|
var oldStyleValue = previousValue[styleName];
|
||||||
|
if (newStyleValue === oldStyleValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
propertiesUpdated = true;
|
||||||
|
if (newStyleValue) {
|
||||||
|
checkStyleValue(newStyleValue);
|
||||||
|
projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
|
||||||
|
} else {
|
||||||
|
projectionOptions.styleApplyer(domNode, styleName, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!propValue && typeof previousValue === 'string') {
|
||||||
|
propValue = '';
|
||||||
|
}
|
||||||
|
if (propName === 'value') {
|
||||||
|
if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) {
|
||||||
|
domNode[propName] = propValue;
|
||||||
|
// Reset the value, even if the virtual DOM did not change
|
||||||
|
domNode['oninput-value'] = undefined;
|
||||||
|
}
|
||||||
|
// else do not update the domNode, otherwise the cursor position would be changed
|
||||||
|
if (propValue !== previousValue) {
|
||||||
|
propertiesUpdated = true;
|
||||||
|
}
|
||||||
|
} else if (propValue !== previousValue) {
|
||||||
|
var type = typeof propValue;
|
||||||
|
if (type === 'function') {
|
||||||
|
throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.');
|
||||||
|
}
|
||||||
|
if (type === 'string' && propName !== 'innerHTML') {
|
||||||
|
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
|
||||||
|
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
|
||||||
|
} else {
|
||||||
|
domNode.setAttribute(propName, propValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (domNode[propName] !== propValue) {
|
||||||
|
domNode[propName] = propValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
propertiesUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return propertiesUpdated;
|
||||||
|
};
|
||||||
|
var findIndexOfChild = function (children, sameAs, start) {
|
||||||
|
if (sameAs.vnodeSelector !== '') {
|
||||||
|
// Never scan for text-nodes
|
||||||
|
for (var i = start; i < children.length; i++) {
|
||||||
|
if (same(children[i], sameAs)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
var nodeAdded = function (vNode, transitions) {
|
||||||
|
if (vNode.properties) {
|
||||||
|
var enterAnimation = vNode.properties.enterAnimation;
|
||||||
|
if (enterAnimation) {
|
||||||
|
if (typeof enterAnimation === 'function') {
|
||||||
|
enterAnimation(vNode.domNode, vNode.properties);
|
||||||
|
} else {
|
||||||
|
transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var nodeToRemove = function (vNode, transitions) {
|
||||||
|
var domNode = vNode.domNode;
|
||||||
|
if (vNode.properties) {
|
||||||
|
var exitAnimation = vNode.properties.exitAnimation;
|
||||||
|
if (exitAnimation) {
|
||||||
|
domNode.style.pointerEvents = 'none';
|
||||||
|
var removeDomNode = function () {
|
||||||
|
if (domNode.parentNode) {
|
||||||
|
domNode.parentNode.removeChild(domNode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (typeof exitAnimation === 'function') {
|
||||||
|
exitAnimation(domNode, removeDomNode, vNode.properties);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (domNode.parentNode) {
|
||||||
|
domNode.parentNode.removeChild(domNode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) {
|
||||||
|
var childNode = childNodes[indexToCheck];
|
||||||
|
if (childNode.vnodeSelector === '') {
|
||||||
|
return; // Text nodes need not be distinguishable
|
||||||
|
}
|
||||||
|
var properties = childNode.properties;
|
||||||
|
var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined;
|
||||||
|
if (!key) {
|
||||||
|
for (var i = 0; i < childNodes.length; i++) {
|
||||||
|
if (i !== indexToCheck) {
|
||||||
|
var node = childNodes[i];
|
||||||
|
if (same(node, childNode)) {
|
||||||
|
if (operation === 'added') {
|
||||||
|
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.');
|
||||||
|
} else {
|
||||||
|
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var createDom;
|
||||||
|
var updateDom;
|
||||||
|
var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
|
||||||
|
if (oldChildren === newChildren) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
oldChildren = oldChildren || emptyArray;
|
||||||
|
newChildren = newChildren || emptyArray;
|
||||||
|
var oldChildrenLength = oldChildren.length;
|
||||||
|
var newChildrenLength = newChildren.length;
|
||||||
|
var transitions = projectionOptions.transitions;
|
||||||
|
var oldIndex = 0;
|
||||||
|
var newIndex = 0;
|
||||||
|
var i;
|
||||||
|
var textUpdated = false;
|
||||||
|
while (newIndex < newChildrenLength) {
|
||||||
|
var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined;
|
||||||
|
var newChild = newChildren[newIndex];
|
||||||
|
if (oldChild !== undefined && same(oldChild, newChild)) {
|
||||||
|
textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
|
||||||
|
oldIndex++;
|
||||||
|
} else {
|
||||||
|
var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
|
||||||
|
if (findOldIndex >= 0) {
|
||||||
|
// Remove preceding missing children
|
||||||
|
for (i = oldIndex; i < findOldIndex; i++) {
|
||||||
|
nodeToRemove(oldChildren[i], transitions);
|
||||||
|
checkDistinguishable(oldChildren, i, vnode, 'removed');
|
||||||
|
}
|
||||||
|
textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
|
||||||
|
oldIndex = findOldIndex + 1;
|
||||||
|
} else {
|
||||||
|
// New child
|
||||||
|
createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
|
||||||
|
nodeAdded(newChild, transitions);
|
||||||
|
checkDistinguishable(newChildren, newIndex, vnode, 'added');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newIndex++;
|
||||||
|
}
|
||||||
|
if (oldChildrenLength > oldIndex) {
|
||||||
|
// Remove child fragments
|
||||||
|
for (i = oldIndex; i < oldChildrenLength; i++) {
|
||||||
|
nodeToRemove(oldChildren[i], transitions);
|
||||||
|
checkDistinguishable(oldChildren, i, vnode, 'removed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return textUpdated;
|
||||||
|
};
|
||||||
|
var addChildren = function (domNode, children, projectionOptions) {
|
||||||
|
if (!children) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < children.length; i++) {
|
||||||
|
createDom(children[i], domNode, undefined, projectionOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
|
||||||
|
addChildren(domNode, vnode.children, projectionOptions);
|
||||||
|
// children before properties, needed for value property of <select>.
|
||||||
|
if (vnode.text) {
|
||||||
|
domNode.textContent = vnode.text;
|
||||||
|
}
|
||||||
|
setProperties(domNode, vnode.properties, projectionOptions);
|
||||||
|
if (vnode.properties && vnode.properties.afterCreate) {
|
||||||
|
vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
|
||||||
|
var domNode, i, c, start = 0, type, found;
|
||||||
|
var vnodeSelector = vnode.vnodeSelector;
|
||||||
|
if (vnodeSelector === '') {
|
||||||
|
domNode = vnode.domNode = document.createTextNode(vnode.text);
|
||||||
|
if (insertBefore !== undefined) {
|
||||||
|
parentNode.insertBefore(domNode, insertBefore);
|
||||||
|
} else {
|
||||||
|
parentNode.appendChild(domNode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i = 0; i <= vnodeSelector.length; ++i) {
|
||||||
|
c = vnodeSelector.charAt(i);
|
||||||
|
if (i === vnodeSelector.length || c === '.' || c === '#') {
|
||||||
|
type = vnodeSelector.charAt(start - 1);
|
||||||
|
found = vnodeSelector.slice(start, i);
|
||||||
|
if (type === '.') {
|
||||||
|
domNode.classList.add(found);
|
||||||
|
} else if (type === '#') {
|
||||||
|
domNode.id = found;
|
||||||
|
} else {
|
||||||
|
if (found === 'svg') {
|
||||||
|
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
|
||||||
|
}
|
||||||
|
if (projectionOptions.namespace !== undefined) {
|
||||||
|
domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
|
||||||
|
} else {
|
||||||
|
domNode = vnode.domNode = document.createElement(found);
|
||||||
|
}
|
||||||
|
if (insertBefore !== undefined) {
|
||||||
|
parentNode.insertBefore(domNode, insertBefore);
|
||||||
|
} else {
|
||||||
|
parentNode.appendChild(domNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initPropertiesAndChildren(domNode, vnode, projectionOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateDom = function (previous, vnode, projectionOptions) {
|
||||||
|
var domNode = previous.domNode;
|
||||||
|
var textUpdated = false;
|
||||||
|
if (previous === vnode) {
|
||||||
|
return false; // By contract, VNode objects may not be modified anymore after passing them to maquette
|
||||||
|
}
|
||||||
|
var updated = false;
|
||||||
|
if (vnode.vnodeSelector === '') {
|
||||||
|
if (vnode.text !== previous.text) {
|
||||||
|
var newVNode = document.createTextNode(vnode.text);
|
||||||
|
domNode.parentNode.replaceChild(newVNode, domNode);
|
||||||
|
vnode.domNode = newVNode;
|
||||||
|
textUpdated = true;
|
||||||
|
return textUpdated;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) {
|
||||||
|
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
|
||||||
|
}
|
||||||
|
if (previous.text !== vnode.text) {
|
||||||
|
updated = true;
|
||||||
|
if (vnode.text === undefined) {
|
||||||
|
domNode.removeChild(domNode.firstChild); // the only textnode presumably
|
||||||
|
} else {
|
||||||
|
domNode.textContent = vnode.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
|
||||||
|
updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
|
||||||
|
if (vnode.properties && vnode.properties.afterUpdate) {
|
||||||
|
vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated && vnode.properties && vnode.properties.updateAnimation) {
|
||||||
|
vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
|
||||||
|
}
|
||||||
|
vnode.domNode = previous.domNode;
|
||||||
|
return textUpdated;
|
||||||
|
};
|
||||||
|
var createProjection = function (vnode, projectionOptions) {
|
||||||
|
return {
|
||||||
|
update: function (updatedVnode) {
|
||||||
|
if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
|
||||||
|
throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
|
||||||
|
}
|
||||||
|
updateDom(vnode, updatedVnode, projectionOptions);
|
||||||
|
vnode = updatedVnode;
|
||||||
|
},
|
||||||
|
domNode: vnode.domNode
|
||||||
|
};
|
||||||
|
};
|
||||||
|
;
|
||||||
|
// The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
|
||||||
|
exports.h = function (selector) {
|
||||||
|
var properties = arguments[1];
|
||||||
|
if (typeof selector !== 'string') {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
var childIndex = 1;
|
||||||
|
if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
|
||||||
|
childIndex = 2;
|
||||||
|
} else {
|
||||||
|
// Optional properties argument was omitted
|
||||||
|
properties = undefined;
|
||||||
|
}
|
||||||
|
var text = undefined;
|
||||||
|
var children = undefined;
|
||||||
|
var argsLength = arguments.length;
|
||||||
|
// Recognize a common special case where there is only a single text node
|
||||||
|
if (argsLength === childIndex + 1) {
|
||||||
|
var onlyChild = arguments[childIndex];
|
||||||
|
if (typeof onlyChild === 'string') {
|
||||||
|
text = onlyChild;
|
||||||
|
} else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
|
||||||
|
text = onlyChild[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text === undefined) {
|
||||||
|
children = [];
|
||||||
|
for (; childIndex < arguments.length; childIndex++) {
|
||||||
|
var child = arguments[childIndex];
|
||||||
|
if (child === null || child === undefined) {
|
||||||
|
continue;
|
||||||
|
} else if (Array.isArray(child)) {
|
||||||
|
appendChildren(selector, child, children);
|
||||||
|
} else if (child.hasOwnProperty('vnodeSelector')) {
|
||||||
|
children.push(child);
|
||||||
|
} else {
|
||||||
|
children.push(toTextVNode(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
vnodeSelector: selector,
|
||||||
|
properties: properties,
|
||||||
|
children: children,
|
||||||
|
text: text === '' ? undefined : text,
|
||||||
|
domNode: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Contains simple low-level utility functions to manipulate the real DOM.
|
||||||
|
*/
|
||||||
|
exports.dom = {
|
||||||
|
/**
|
||||||
|
* Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
|
||||||
|
* its [[Projection.domNode|domNode]] property.
|
||||||
|
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
||||||
|
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
|
||||||
|
* objects may only be rendered once.
|
||||||
|
* @param projectionOptions - Options to be used to create and update the projection.
|
||||||
|
* @returns The [[Projection]] which also contains the DOM Node that was created.
|
||||||
|
*/
|
||||||
|
create: function (vnode, projectionOptions) {
|
||||||
|
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
||||||
|
createDom(vnode, document.createElement('div'), undefined, projectionOptions);
|
||||||
|
return createProjection(vnode, projectionOptions);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Appends a new childnode to the DOM which is generated from a [[VNode]].
|
||||||
|
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
||||||
|
* @param parentNode - The parent node for the new childNode.
|
||||||
|
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
|
||||||
|
* objects may only be rendered once.
|
||||||
|
* @param projectionOptions - Options to be used to create and update the [[Projection]].
|
||||||
|
* @returns The [[Projection]] that was created.
|
||||||
|
*/
|
||||||
|
append: function (parentNode, vnode, projectionOptions) {
|
||||||
|
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
||||||
|
createDom(vnode, parentNode, undefined, projectionOptions);
|
||||||
|
return createProjection(vnode, projectionOptions);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Inserts a new DOM node which is generated from a [[VNode]].
|
||||||
|
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
||||||
|
* @param beforeNode - The node that the DOM Node is inserted before.
|
||||||
|
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
|
||||||
|
* NOTE: [[VNode]] objects may only be rendered once.
|
||||||
|
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
|
||||||
|
* @returns The [[Projection]] that was created.
|
||||||
|
*/
|
||||||
|
insertBefore: function (beforeNode, vnode, projectionOptions) {
|
||||||
|
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
||||||
|
createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
|
||||||
|
return createProjection(vnode, projectionOptions);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
|
||||||
|
* This means that the virtual DOM and the real DOM will have one overlapping element.
|
||||||
|
* Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
|
||||||
|
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
||||||
|
* @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
|
||||||
|
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
|
||||||
|
* may only be rendered once.
|
||||||
|
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
|
||||||
|
* @returns The [[Projection]] that was created.
|
||||||
|
*/
|
||||||
|
merge: function (element, vnode, projectionOptions) {
|
||||||
|
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
||||||
|
vnode.domNode = element;
|
||||||
|
initPropertiesAndChildren(element, vnode, projectionOptions);
|
||||||
|
return createProjection(vnode, projectionOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
|
||||||
|
* In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
|
||||||
|
* For more information, see [[CalculationCache]].
|
||||||
|
*
|
||||||
|
* @param <Result> The type of the value that is cached.
|
||||||
|
*/
|
||||||
|
exports.createCache = function () {
|
||||||
|
var cachedInputs = undefined;
|
||||||
|
var cachedOutcome = undefined;
|
||||||
|
var result = {
|
||||||
|
invalidate: function () {
|
||||||
|
cachedOutcome = undefined;
|
||||||
|
cachedInputs = undefined;
|
||||||
|
},
|
||||||
|
result: function (inputs, calculation) {
|
||||||
|
if (cachedInputs) {
|
||||||
|
for (var i = 0; i < inputs.length; i++) {
|
||||||
|
if (cachedInputs[i] !== inputs[i]) {
|
||||||
|
cachedOutcome = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cachedOutcome) {
|
||||||
|
cachedOutcome = calculation();
|
||||||
|
cachedInputs = inputs;
|
||||||
|
}
|
||||||
|
return cachedOutcome;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
|
||||||
|
* See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
|
||||||
|
*
|
||||||
|
* @param <Source> The type of source items. A database-record for instance.
|
||||||
|
* @param <Target> The type of target items. A [[Component]] for instance.
|
||||||
|
* @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
|
||||||
|
* @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical
|
||||||
|
* to the `callback` argument in `Array.map(callback)`.
|
||||||
|
* @param updateResult `function(source, target, index)` that updates a result to an updated source.
|
||||||
|
*/
|
||||||
|
exports.createMapping = function (getSourceKey, createResult, updateResult) {
|
||||||
|
var keys = [];
|
||||||
|
var results = [];
|
||||||
|
return {
|
||||||
|
results: results,
|
||||||
|
map: function (newSources) {
|
||||||
|
var newKeys = newSources.map(getSourceKey);
|
||||||
|
var oldTargets = results.slice();
|
||||||
|
var oldIndex = 0;
|
||||||
|
for (var i = 0; i < newSources.length; i++) {
|
||||||
|
var source = newSources[i];
|
||||||
|
var sourceKey = newKeys[i];
|
||||||
|
if (sourceKey === keys[oldIndex]) {
|
||||||
|
results[i] = oldTargets[oldIndex];
|
||||||
|
updateResult(source, oldTargets[oldIndex], i);
|
||||||
|
oldIndex++;
|
||||||
|
} else {
|
||||||
|
var found = false;
|
||||||
|
for (var j = 1; j < keys.length; j++) {
|
||||||
|
var searchIndex = (oldIndex + j) % keys.length;
|
||||||
|
if (keys[searchIndex] === sourceKey) {
|
||||||
|
results[i] = oldTargets[searchIndex];
|
||||||
|
updateResult(newSources[i], oldTargets[searchIndex], i);
|
||||||
|
oldIndex = searchIndex + 1;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
results[i] = createResult(source, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.length = newSources.length;
|
||||||
|
keys = newKeys;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Creates a [[Projector]] instance using the provided projectionOptions.
|
||||||
|
*
|
||||||
|
* For more information, see [[Projector]].
|
||||||
|
*
|
||||||
|
* @param projectionOptions Options that influence how the DOM is rendered and updated.
|
||||||
|
*/
|
||||||
|
exports.createProjector = function (projectorOptions) {
|
||||||
|
var projector;
|
||||||
|
var projectionOptions = applyDefaultProjectionOptions(projectorOptions);
|
||||||
|
projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) {
|
||||||
|
return function () {
|
||||||
|
// intercept function calls (event handlers) to do a render afterwards.
|
||||||
|
projector.scheduleRender();
|
||||||
|
return eventHandler.apply(properties.bind || this, arguments);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var renderCompleted = true;
|
||||||
|
var scheduled;
|
||||||
|
var stopped = false;
|
||||||
|
var projections = [];
|
||||||
|
var renderFunctions = [];
|
||||||
|
// matches the projections array
|
||||||
|
var doRender = function () {
|
||||||
|
scheduled = undefined;
|
||||||
|
if (!renderCompleted) {
|
||||||
|
return; // The last render threw an error, it should be logged in the browser console.
|
||||||
|
}
|
||||||
|
renderCompleted = false;
|
||||||
|
for (var i = 0; i < projections.length; i++) {
|
||||||
|
var updatedVnode = renderFunctions[i]();
|
||||||
|
projections[i].update(updatedVnode);
|
||||||
|
}
|
||||||
|
renderCompleted = true;
|
||||||
|
};
|
||||||
|
projector = {
|
||||||
|
scheduleRender: function () {
|
||||||
|
if (!scheduled && !stopped) {
|
||||||
|
scheduled = requestAnimationFrame(doRender);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stop: function () {
|
||||||
|
if (scheduled) {
|
||||||
|
cancelAnimationFrame(scheduled);
|
||||||
|
scheduled = undefined;
|
||||||
|
}
|
||||||
|
stopped = true;
|
||||||
|
},
|
||||||
|
resume: function () {
|
||||||
|
stopped = false;
|
||||||
|
renderCompleted = true;
|
||||||
|
projector.scheduleRender();
|
||||||
|
},
|
||||||
|
append: function (parentNode, renderMaquetteFunction) {
|
||||||
|
projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
|
||||||
|
renderFunctions.push(renderMaquetteFunction);
|
||||||
|
},
|
||||||
|
insertBefore: function (beforeNode, renderMaquetteFunction) {
|
||||||
|
projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
|
||||||
|
renderFunctions.push(renderMaquetteFunction);
|
||||||
|
},
|
||||||
|
merge: function (domNode, renderMaquetteFunction) {
|
||||||
|
projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
|
||||||
|
renderFunctions.push(renderMaquetteFunction);
|
||||||
|
},
|
||||||
|
replace: function (domNode, renderMaquetteFunction) {
|
||||||
|
var vnode = renderMaquetteFunction();
|
||||||
|
createDom(vnode, domNode.parentNode, domNode, projectionOptions);
|
||||||
|
domNode.parentNode.removeChild(domNode);
|
||||||
|
projections.push(createProjection(vnode, projectionOptions));
|
||||||
|
renderFunctions.push(renderMaquetteFunction);
|
||||||
|
},
|
||||||
|
detach: function (renderMaquetteFunction) {
|
||||||
|
for (var i = 0; i < renderFunctions.length; i++) {
|
||||||
|
if (renderFunctions[i] === renderMaquetteFunction) {
|
||||||
|
renderFunctions.splice(i, 1);
|
||||||
|
return projections.splice(i, 1)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('renderMaquetteFunction was not found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return projector;
|
||||||
|
};
|
||||||
|
}));
|
|
@ -0,0 +1,138 @@
|
||||||
|
class Animation
|
||||||
|
slideDown: (elem, props) ->
|
||||||
|
if elem.offsetTop > 2000
|
||||||
|
return
|
||||||
|
|
||||||
|
h = elem.offsetHeight
|
||||||
|
cstyle = window.getComputedStyle(elem)
|
||||||
|
margin_top = cstyle.marginTop
|
||||||
|
margin_bottom = cstyle.marginBottom
|
||||||
|
padding_top = cstyle.paddingTop
|
||||||
|
padding_bottom = cstyle.paddingBottom
|
||||||
|
transition = cstyle.transition
|
||||||
|
|
||||||
|
elem.style.boxSizing = "border-box"
|
||||||
|
elem.style.overflow = "hidden"
|
||||||
|
elem.style.transform = "scale(0.6)"
|
||||||
|
elem.style.opacity = "0"
|
||||||
|
elem.style.height = "0px"
|
||||||
|
elem.style.marginTop = "0px"
|
||||||
|
elem.style.marginBottom = "0px"
|
||||||
|
elem.style.paddingTop = "0px"
|
||||||
|
elem.style.paddingBottom = "0px"
|
||||||
|
elem.style.transition = "none"
|
||||||
|
|
||||||
|
setTimeout (->
|
||||||
|
elem.className += " animate-inout"
|
||||||
|
elem.style.height = h+"px"
|
||||||
|
elem.style.transform = "scale(1)"
|
||||||
|
elem.style.opacity = "1"
|
||||||
|
elem.style.marginTop = margin_top
|
||||||
|
elem.style.marginBottom = margin_bottom
|
||||||
|
elem.style.paddingTop = padding_top
|
||||||
|
elem.style.paddingBottom = padding_bottom
|
||||||
|
), 1
|
||||||
|
|
||||||
|
elem.addEventListener "transitionend", ->
|
||||||
|
elem.classList.remove("animate-inout")
|
||||||
|
elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null
|
||||||
|
elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null
|
||||||
|
elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null
|
||||||
|
elem.removeEventListener "transitionend", arguments.callee, false
|
||||||
|
|
||||||
|
|
||||||
|
slideUp: (elem, remove_func, props) ->
|
||||||
|
if elem.offsetTop > 1000
|
||||||
|
return remove_func()
|
||||||
|
|
||||||
|
elem.className += " animate-back"
|
||||||
|
elem.style.boxSizing = "border-box"
|
||||||
|
elem.style.height = elem.offsetHeight+"px"
|
||||||
|
elem.style.overflow = "hidden"
|
||||||
|
elem.style.transform = "scale(1)"
|
||||||
|
elem.style.opacity = "1"
|
||||||
|
elem.style.pointerEvents = "none"
|
||||||
|
setTimeout (->
|
||||||
|
elem.style.height = "0px"
|
||||||
|
elem.style.marginTop = "0px"
|
||||||
|
elem.style.marginBottom = "0px"
|
||||||
|
elem.style.paddingTop = "0px"
|
||||||
|
elem.style.paddingBottom = "0px"
|
||||||
|
elem.style.transform = "scale(0.8)"
|
||||||
|
elem.style.borderTopWidth = "0px"
|
||||||
|
elem.style.borderBottomWidth = "0px"
|
||||||
|
elem.style.opacity = "0"
|
||||||
|
), 1
|
||||||
|
elem.addEventListener "transitionend", (e) ->
|
||||||
|
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
|
||||||
|
elem.removeEventListener "transitionend", arguments.callee, false
|
||||||
|
remove_func()
|
||||||
|
|
||||||
|
|
||||||
|
slideUpInout: (elem, remove_func, props) ->
|
||||||
|
elem.className += " animate-inout"
|
||||||
|
elem.style.boxSizing = "border-box"
|
||||||
|
elem.style.height = elem.offsetHeight+"px"
|
||||||
|
elem.style.overflow = "hidden"
|
||||||
|
elem.style.transform = "scale(1)"
|
||||||
|
elem.style.opacity = "1"
|
||||||
|
elem.style.pointerEvents = "none"
|
||||||
|
setTimeout (->
|
||||||
|
elem.style.height = "0px"
|
||||||
|
elem.style.marginTop = "0px"
|
||||||
|
elem.style.marginBottom = "0px"
|
||||||
|
elem.style.paddingTop = "0px"
|
||||||
|
elem.style.paddingBottom = "0px"
|
||||||
|
elem.style.transform = "scale(0.8)"
|
||||||
|
elem.style.borderTopWidth = "0px"
|
||||||
|
elem.style.borderBottomWidth = "0px"
|
||||||
|
elem.style.opacity = "0"
|
||||||
|
), 1
|
||||||
|
elem.addEventListener "transitionend", (e) ->
|
||||||
|
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
|
||||||
|
elem.removeEventListener "transitionend", arguments.callee, false
|
||||||
|
remove_func()
|
||||||
|
|
||||||
|
|
||||||
|
showRight: (elem, props) ->
|
||||||
|
elem.className += " animate"
|
||||||
|
elem.style.opacity = 0
|
||||||
|
elem.style.transform = "TranslateX(-20px) Scale(1.01)"
|
||||||
|
setTimeout (->
|
||||||
|
elem.style.opacity = 1
|
||||||
|
elem.style.transform = "TranslateX(0px) Scale(1)"
|
||||||
|
), 1
|
||||||
|
elem.addEventListener "transitionend", ->
|
||||||
|
elem.classList.remove("animate")
|
||||||
|
elem.style.transform = elem.style.opacity = null
|
||||||
|
|
||||||
|
|
||||||
|
show: (elem, props) ->
|
||||||
|
delay = arguments[arguments.length-2]?.delay*1000 or 1
|
||||||
|
elem.style.opacity = 0
|
||||||
|
setTimeout (->
|
||||||
|
elem.className += " animate"
|
||||||
|
), 1
|
||||||
|
setTimeout (->
|
||||||
|
elem.style.opacity = 1
|
||||||
|
), delay
|
||||||
|
elem.addEventListener "transitionend", ->
|
||||||
|
elem.classList.remove("animate")
|
||||||
|
elem.style.opacity = null
|
||||||
|
elem.removeEventListener "transitionend", arguments.callee, false
|
||||||
|
|
||||||
|
hide: (elem, remove_func, props) ->
|
||||||
|
delay = arguments[arguments.length-2]?.delay*1000 or 1
|
||||||
|
elem.className += " animate"
|
||||||
|
setTimeout (->
|
||||||
|
elem.style.opacity = 0
|
||||||
|
), delay
|
||||||
|
elem.addEventListener "transitionend", (e) ->
|
||||||
|
if e.propertyName == "opacity"
|
||||||
|
remove_func()
|
||||||
|
|
||||||
|
addVisibleClass: (elem, props) ->
|
||||||
|
setTimeout ->
|
||||||
|
elem.classList.add("visible")
|
||||||
|
|
||||||
|
window.Animation = new Animation()
|
|
@ -0,0 +1,3 @@
|
||||||
|
window.$ = (selector) ->
|
||||||
|
if selector.startsWith("#")
|
||||||
|
return document.getElementById(selector.replace("#", ""))
|
|
@ -0,0 +1,85 @@
|
||||||
|
class ZeroFrame extends Class
|
||||||
|
constructor: (url) ->
|
||||||
|
@url = url
|
||||||
|
@waiting_cb = {}
|
||||||
|
@wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1")
|
||||||
|
@connect()
|
||||||
|
@next_message_id = 1
|
||||||
|
@history_state = {}
|
||||||
|
@init()
|
||||||
|
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@
|
||||||
|
|
||||||
|
|
||||||
|
connect: ->
|
||||||
|
@target = window.parent
|
||||||
|
window.addEventListener("message", @onMessage, false)
|
||||||
|
@cmd("innerReady")
|
||||||
|
|
||||||
|
# Save scrollTop
|
||||||
|
window.addEventListener "beforeunload", (e) =>
|
||||||
|
@log "save scrollTop", window.pageYOffset
|
||||||
|
@history_state["scrollTop"] = window.pageYOffset
|
||||||
|
@cmd "wrapperReplaceState", [@history_state, null]
|
||||||
|
|
||||||
|
# Restore scrollTop
|
||||||
|
@cmd "wrapperGetState", [], (state) =>
|
||||||
|
@history_state = state if state?
|
||||||
|
@log "restore scrollTop", state, window.pageYOffset
|
||||||
|
if window.pageYOffset == 0 and state
|
||||||
|
window.scroll(window.pageXOffset, state.scrollTop)
|
||||||
|
|
||||||
|
|
||||||
|
onMessage: (e) =>
|
||||||
|
message = e.data
|
||||||
|
cmd = message.cmd
|
||||||
|
if cmd == "response"
|
||||||
|
if @waiting_cb[message.to]?
|
||||||
|
@waiting_cb[message.to](message.result)
|
||||||
|
else
|
||||||
|
@log "Websocket callback not found:", message
|
||||||
|
else if cmd == "wrapperReady" # Wrapper inited later
|
||||||
|
@cmd("innerReady")
|
||||||
|
else if cmd == "ping"
|
||||||
|
@response message.id, "pong"
|
||||||
|
else if cmd == "wrapperOpenedWebsocket"
|
||||||
|
@onOpenWebsocket()
|
||||||
|
else if cmd == "wrapperClosedWebsocket"
|
||||||
|
@onCloseWebsocket()
|
||||||
|
else
|
||||||
|
@onRequest cmd, message.params
|
||||||
|
|
||||||
|
|
||||||
|
onRequest: (cmd, message) =>
|
||||||
|
@log "Unknown request", message
|
||||||
|
|
||||||
|
|
||||||
|
response: (to, result) ->
|
||||||
|
@send {"cmd": "response", "to": to, "result": result}
|
||||||
|
|
||||||
|
|
||||||
|
cmd: (cmd, params={}, cb=null) ->
|
||||||
|
@send {"cmd": cmd, "params": params}, cb
|
||||||
|
|
||||||
|
|
||||||
|
send: (message, cb=null) ->
|
||||||
|
message.wrapper_nonce = @wrapper_nonce
|
||||||
|
message.id = @next_message_id
|
||||||
|
@next_message_id += 1
|
||||||
|
@target.postMessage(message, "*")
|
||||||
|
if cb
|
||||||
|
@waiting_cb[message.id] = cb
|
||||||
|
|
||||||
|
|
||||||
|
onOpenWebsocket: =>
|
||||||
|
@log "Websocket open"
|
||||||
|
|
||||||
|
|
||||||
|
onCloseWebsocket: =>
|
||||||
|
@log "Websocket close"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.ZeroFrame = ZeroFrame
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "UiConfig",
|
||||||
|
"description": "Change client settings using the web interface.",
|
||||||
|
"default": "enabled"
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
import logging, json, os, re, sys, time, socket
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Config import config
|
||||||
|
from Debug import Debug
|
||||||
|
from http.client import HTTPSConnection, HTTPConnection, HTTPException
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
allow_reload = False # No reload supported
|
||||||
|
|
||||||
|
@PluginManager.registerTo("SiteManager")
|
||||||
|
class SiteManagerPlugin(object):
|
||||||
|
def load(self, *args, **kwargs):
|
||||||
|
super(SiteManagerPlugin, self).load(*args, **kwargs)
|
||||||
|
self.log = logging.getLogger("ZeronetLocal Plugin")
|
||||||
|
self.error_message = None
|
||||||
|
if not config.namecoin_host or not config.namecoin_rpcport or not config.namecoin_rpcuser or not config.namecoin_rpcpassword:
|
||||||
|
self.error_message = "Missing parameters"
|
||||||
|
self.log.error("Missing parameters to connect to namecoin node. Please check all the arguments needed with '--help'. Zeronet will continue working without it.")
|
||||||
|
return
|
||||||
|
|
||||||
|
url = "%(host)s:%(port)s" % {"host": config.namecoin_host, "port": config.namecoin_rpcport}
|
||||||
|
self.c = HTTPConnection(url, timeout=3)
|
||||||
|
user_pass = "%(user)s:%(password)s" % {"user": config.namecoin_rpcuser, "password": config.namecoin_rpcpassword}
|
||||||
|
userAndPass = b64encode(bytes(user_pass, "utf-8")).decode("ascii")
|
||||||
|
self.headers = {"Authorization" : "Basic %s" % userAndPass, "Content-Type": " application/json " }
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "zeronet",
|
||||||
|
"method": "ping",
|
||||||
|
"params": []
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.c.request("POST", "/", payload, headers=self.headers)
|
||||||
|
response = self.c.getresponse()
|
||||||
|
data = response.read()
|
||||||
|
self.c.close()
|
||||||
|
if response.status == 200:
|
||||||
|
result = json.loads(data.decode())["result"]
|
||||||
|
else:
|
||||||
|
raise Exception(response.reason)
|
||||||
|
except Exception as err:
|
||||||
|
self.log.error("The Namecoin node is unreachable. Please check the configuration value are correct. Zeronet will continue working without it.")
|
||||||
|
self.error_message = err
|
||||||
|
self.cache = dict()
|
||||||
|
|
||||||
|
# Checks if it's a valid address
|
||||||
|
def isAddress(self, address):
|
||||||
|
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isAddress(address)
|
||||||
|
|
||||||
|
# Return: True if the address is domain
|
||||||
|
def isDomain(self, address):
|
||||||
|
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address)
|
||||||
|
|
||||||
|
# Return: True if the address is .bit domain
|
||||||
|
def isBitDomain(self, address):
|
||||||
|
return re.match(r"(.*?)([A-Za-z0-9_-]+\.bit)$", address)
|
||||||
|
|
||||||
|
# Return: Site object or None if not found
|
||||||
|
def get(self, address):
|
||||||
|
if self.isBitDomain(address): # Its looks like a domain
|
||||||
|
address_resolved = self.resolveDomain(address)
|
||||||
|
if address_resolved: # Domain found
|
||||||
|
site = self.sites.get(address_resolved)
|
||||||
|
if site:
|
||||||
|
site_domain = site.settings.get("domain")
|
||||||
|
if site_domain != address:
|
||||||
|
site.settings["domain"] = address
|
||||||
|
else: # Domain not found
|
||||||
|
site = self.sites.get(address)
|
||||||
|
|
||||||
|
else: # Access by site address
|
||||||
|
site = super(SiteManagerPlugin, self).get(address)
|
||||||
|
return site
|
||||||
|
|
||||||
|
# Return or create site and start download site files
|
||||||
|
# Return: Site or None if dns resolve failed
|
||||||
|
def need(self, address, *args, **kwargs):
|
||||||
|
if self.isBitDomain(address): # Its looks like a domain
|
||||||
|
address_resolved = self.resolveDomain(address)
|
||||||
|
if address_resolved:
|
||||||
|
address = address_resolved
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return super(SiteManagerPlugin, self).need(address, *args, **kwargs)
|
||||||
|
|
||||||
|
# Resolve domain
|
||||||
|
# Return: The address or None
|
||||||
|
def resolveDomain(self, domain):
|
||||||
|
domain = domain.lower()
|
||||||
|
|
||||||
|
#remove .bit on end
|
||||||
|
if domain[-4:] == ".bit":
|
||||||
|
domain = domain[0:-4]
|
||||||
|
|
||||||
|
domain_array = domain.split(".")
|
||||||
|
|
||||||
|
if self.error_message:
|
||||||
|
self.log.error("Not able to connect to Namecoin node : {!s}".format(self.error_message))
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(domain_array) > 2:
|
||||||
|
self.log.error("Too many subdomains! Can only handle one level (eg. staging.mixtape.bit)")
|
||||||
|
return None
|
||||||
|
|
||||||
|
subdomain = ""
|
||||||
|
if len(domain_array) == 1:
|
||||||
|
domain = domain_array[0]
|
||||||
|
else:
|
||||||
|
subdomain = domain_array[0]
|
||||||
|
domain = domain_array[1]
|
||||||
|
|
||||||
|
if domain in self.cache:
|
||||||
|
delta = time.time() - self.cache[domain]["time"]
|
||||||
|
if delta < 3600:
|
||||||
|
# Must have been less than 1hour
|
||||||
|
return self.cache[domain]["addresses_resolved"][subdomain]
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "zeronet",
|
||||||
|
"method": "name_show",
|
||||||
|
"params": ["d/"+domain]
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.c.request("POST", "/", payload, headers=self.headers)
|
||||||
|
response = self.c.getresponse()
|
||||||
|
data = response.read()
|
||||||
|
self.c.close()
|
||||||
|
domain_object = json.loads(data.decode())["result"]
|
||||||
|
except Exception as err:
|
||||||
|
#domain doesn't exist
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "zeronet" in domain_object["value"]:
|
||||||
|
zeronet_domains = json.loads(domain_object["value"])["zeronet"]
|
||||||
|
|
||||||
|
if isinstance(zeronet_domains, str):
|
||||||
|
# {
|
||||||
|
# "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9"
|
||||||
|
# } is valid
|
||||||
|
zeronet_domains = {"": zeronet_domains}
|
||||||
|
|
||||||
|
self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
|
||||||
|
|
||||||
|
elif "map" in domain_object["value"]:
|
||||||
|
# Namecoin standard use {"map": { "blog": {"zeronet": "1D..."} }}
|
||||||
|
data_map = json.loads(domain_object["value"])["map"]
|
||||||
|
|
||||||
|
zeronet_domains = dict()
|
||||||
|
for subdomain in data_map:
|
||||||
|
if "zeronet" in data_map[subdomain]:
|
||||||
|
zeronet_domains[subdomain] = data_map[subdomain]["zeronet"]
|
||||||
|
if "zeronet" in data_map and isinstance(data_map["zeronet"], str):
|
||||||
|
# {"map":{
|
||||||
|
# "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9",
|
||||||
|
# }}
|
||||||
|
zeronet_domains[""] = data_map["zeronet"]
|
||||||
|
|
||||||
|
self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# No Zeronet address registered
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.cache[domain]["addresses_resolved"][subdomain]
|
||||||
|
|
||||||
|
@PluginManager.registerTo("ConfigPlugin")
|
||||||
|
class ConfigPlugin(object):
|
||||||
|
def createArguments(self):
|
||||||
|
group = self.parser.add_argument_group("Zeroname Local plugin")
|
||||||
|
group.add_argument('--namecoin_host', help="Host to namecoin node (eg. 127.0.0.1)")
|
||||||
|
group.add_argument('--namecoin_rpcport', help="Port to connect (eg. 8336)")
|
||||||
|
group.add_argument('--namecoin_rpcuser', help="RPC user to connect to the namecoin node (eg. nofish)")
|
||||||
|
group.add_argument('--namecoin_rpcpassword', help="RPC password to connect to namecoin node")
|
||||||
|
|
||||||
|
return super(ConfigPlugin, self).createArguments()
|
|
@ -0,0 +1,39 @@
|
||||||
|
import re
|
||||||
|
from Plugin import PluginManager
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiRequest")
|
||||||
|
class UiRequestPlugin(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
from Site import SiteManager
|
||||||
|
self.site_manager = SiteManager.site_manager
|
||||||
|
super(UiRequestPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# Media request
|
||||||
|
def actionSiteMedia(self, path):
|
||||||
|
match = re.match(r"/media/(?P<address>[A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", path)
|
||||||
|
if match: # Its a valid domain, resolve first
|
||||||
|
domain = match.group("address")
|
||||||
|
address = self.site_manager.resolveDomain(domain)
|
||||||
|
if address:
|
||||||
|
path = "/media/"+address+match.group("inner_path")
|
||||||
|
return super(UiRequestPlugin, self).actionSiteMedia(path) # Get the wrapper frame output
|
||||||
|
|
||||||
|
|
||||||
|
# Is mediarequest allowed from that referer
|
||||||
|
def isMediaRequestAllowed(self, site_address, referer):
|
||||||
|
referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address
|
||||||
|
referer_path = re.sub(r"\?.*", "", referer_path) # Remove http params
|
||||||
|
|
||||||
|
if self.isProxyRequest(): # Match to site domain
|
||||||
|
referer = re.sub("^http://zero[/]+", "http://", referer) # Allow /zero access
|
||||||
|
referer_site_address = re.match("http[s]{0,1}://(.*?)(/|$)", referer).group(1)
|
||||||
|
else: # Match to request path
|
||||||
|
referer_site_address = re.match(r"/(?P<address>[A-Za-z0-9\.-]+)(?P<inner_path>/.*|$)", referer_path).group("address")
|
||||||
|
|
||||||
|
if referer_site_address == site_address: # Referer site address as simple address
|
||||||
|
return True
|
||||||
|
elif self.site_manager.resolveDomain(referer_site_address) == site_address: # Referer site address as dns
|
||||||
|
return True
|
||||||
|
else: # Invalid referer
|
||||||
|
return False
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import UiRequestPlugin
|
||||||
|
from . import SiteManagerPlugin
|
Loading…
Reference in New Issue