Merge branch 'py3' into plugins
This commit is contained in:
commit
920ddd944f
222 changed files with 13121 additions and 7213 deletions
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
custom: https://zeronet.io/docs/help_zeronet/donate/
|
49
.github/workflows/tests.yml
vendored
Normal file
49
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
name: tests
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
runs-on: ubuntu-16.04
|
||||
strategy:
|
||||
max-parallel: 16
|
||||
matrix:
|
||||
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Prepare for installation
|
||||
run: |
|
||||
python3 -m pip install setuptools
|
||||
python3 -m pip install --upgrade pip wheel
|
||||
python3 -m pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
|
||||
|
||||
- name: Install
|
||||
run: |
|
||||
python3 -m pip install --upgrade -r requirements.txt
|
||||
python3 -m pip list
|
||||
|
||||
- name: Prepare for tests
|
||||
run: |
|
||||
openssl version -a
|
||||
echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
catchsegv python3 -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
|
||||
export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python3 -m pytest -x plugins/CryptMessage/Test
|
||||
export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python3 -m pytest -x plugins/Bigfile/Test
|
||||
export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python3 -m pytest -x plugins/AnnounceLocal/Test
|
||||
export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python3 -m pytest -x plugins/OptionalManager/Test
|
||||
export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test
|
||||
export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test
|
||||
find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
|
||||
find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
|
||||
flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -7,8 +7,10 @@ __pycache__/
|
|||
|
||||
# Hidden files
|
||||
.*
|
||||
!/.github
|
||||
!/.gitignore
|
||||
!/.travis.yml
|
||||
!/.gitlab-ci.yml
|
||||
|
||||
# Temporary files
|
||||
*.bak
|
||||
|
@ -28,3 +30,6 @@ tools/phantomjs
|
|||
|
||||
# ZeroNet config file
|
||||
zeronet.conf
|
||||
|
||||
# ZeroNet log files
|
||||
log/*
|
||||
|
|
48
.gitlab-ci.yml
Normal file
48
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
stages:
|
||||
- test
|
||||
|
||||
.test_template: &test_template
|
||||
stage: test
|
||||
before_script:
|
||||
- pip install --upgrade pip wheel
|
||||
# Selenium and requests can't be installed without a requests hint on Python 3.4
|
||||
- pip install --upgrade requests>=2.22.0
|
||||
- pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium
|
||||
- pip install --upgrade -r requirements.txt
|
||||
script:
|
||||
- pip list
|
||||
- openssl version -a
|
||||
- python -m pytest -x plugins/CryptMessage/Test --color=yes
|
||||
- python -m pytest -x plugins/Bigfile/Test --color=yes
|
||||
- python -m pytest -x plugins/AnnounceLocal/Test --color=yes
|
||||
- python -m pytest -x plugins/OptionalManager/Test --color=yes
|
||||
- python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini --color=yes
|
||||
- mv plugins/disabled-Multiuser plugins/Multiuser
|
||||
- python -m pytest -x plugins/Multiuser/Test --color=yes
|
||||
- mv plugins/disabled-Bootstrapper plugins/Bootstrapper
|
||||
- python -m pytest -x plugins/Bootstrapper/Test --color=yes
|
||||
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
|
||||
|
||||
test:py3.4:
|
||||
image: python:3.4.3
|
||||
<<: *test_template
|
||||
|
||||
test:py3.5:
|
||||
image: python:3.5.7
|
||||
<<: *test_template
|
||||
|
||||
test:py3.6:
|
||||
image: python:3.6.9
|
||||
<<: *test_template
|
||||
|
||||
test:py3.7-openssl1.1.0:
|
||||
image: python:3.7.0b5
|
||||
<<: *test_template
|
||||
|
||||
test:py3.7-openssl1.1.1:
|
||||
image: python:3.7.4
|
||||
<<: *test_template
|
||||
|
||||
test:py3.8:
|
||||
image: python:3.8.0b3
|
||||
<<: *test_template
|
24
.travis.yml
24
.travis.yml
|
@ -4,8 +4,7 @@ python:
|
|||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
- 3.8-dev
|
||||
dist: xenial
|
||||
- 3.8
|
||||
services:
|
||||
- docker
|
||||
cache: pip
|
||||
|
@ -25,14 +24,19 @@ before_script:
|
|||
sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
|
||||
fi
|
||||
script:
|
||||
- python -m pytest -x plugins/CryptMessage/Test
|
||||
- python -m pytest -x plugins/Bigfile/Test
|
||||
- python -m pytest -x plugins/AnnounceLocal/Test
|
||||
- python -m pytest -x plugins/OptionalManager/Test
|
||||
- python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
|
||||
- mv plugins/disabled-Multiuser plugins/Multiuser && python -m pytest -x plugins/Multiuser/Test
|
||||
- mv plugins/disabled-Bootstrapper plugins/Bootstrapper && python -m pytest -x plugins/Bootstrapper/Test
|
||||
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/
|
||||
- catchsegv python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini
|
||||
- export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python -m pytest -x plugins/CryptMessage/Test
|
||||
- export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python -m pytest -x plugins/Bigfile/Test
|
||||
- export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python -m pytest -x plugins/AnnounceLocal/Test
|
||||
- export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python -m pytest -x plugins/OptionalManager/Test
|
||||
- export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test
|
||||
- export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test
|
||||
- find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
|
||||
- find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')"
|
||||
- flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/
|
||||
after_failure:
|
||||
- zip -r log.zip log/
|
||||
- curl --upload-file ./log.zip https://transfer.sh/log.zip
|
||||
after_success:
|
||||
- codecov
|
||||
- coveralls --rcfile=src/Test/coverage.ini
|
||||
|
|
191
README-zh-cn.md
191
README-zh-cn.md
|
@ -1,51 +1,49 @@
|
|||
# ZeroNet [](https://travis-ci.org/HelloZeroNet/ZeroNet) [](https://zeronet.io/docs/faq/) [](https://zeronet.io/docs/help_zeronet/donate/)
|
||||
# ZeroNet [](https://travis-ci.org/HelloZeroNet/ZeroNet) [](https://zeronet.io/docs/faq/) [](https://zeronet.io/docs/help_zeronet/donate/)
|
||||
|
||||
[English](./README.md)
|
||||
|
||||
使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.io
|
||||
|
||||
|
||||
## 为什么?
|
||||
## 为什么?
|
||||
|
||||
* 我们相信开放,自由,无审查的网络
|
||||
* 我们相信开放,自由,无审查的网络和通讯
|
||||
* 不会受单点故障影响:只要有在线的节点,站点就会保持在线
|
||||
* 无托管费用: 站点由访问者托管
|
||||
* 无法关闭: 因为节点无处不在
|
||||
* 快速并可离线运行: 即使没有互联网连接也可以使用
|
||||
* 无托管费用:站点由访问者托管
|
||||
* 无法关闭:因为节点无处不在
|
||||
* 快速并可离线运行:即使没有互联网连接也可以使用
|
||||
|
||||
|
||||
## 功能
|
||||
* 实时站点更新
|
||||
* 支持 Namecoin 的 .bit 域名
|
||||
* 安装方便: 只需解压并运行
|
||||
* 安装方便:只需解压并运行
|
||||
* 一键克隆存在的站点
|
||||
* 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 的认证:用与比特币钱包相同的加密方法用来保护你的账户
|
||||
你的账户被使用和比特币钱包相同的加密方法
|
||||
* 内建 SQL 服务器和 P2P 数据同步: 让开发更简单并提升加载速度
|
||||
* 匿名性: 完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过IPv4地址连接
|
||||
* 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
|
||||
的认证:您的账户被与比特币钱包相同的加密方法保护
|
||||
* 内建 SQL 服务器和 P2P 数据同步:让开发更简单并提升加载速度
|
||||
* 匿名性:完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过 IPv4 地址连接
|
||||
* TLS 加密连接
|
||||
* 自动打开 uPnP 端口
|
||||
* 插件和多用户 (开放式代理) 支持
|
||||
* 全平台兼容
|
||||
* 多用户(openproxy)支持的插件
|
||||
* 适用于任何浏览器 / 操作系统
|
||||
|
||||
|
||||
## 原理
|
||||
|
||||
* 在你运行`zeronet.py`后你将可以通过`http://127.0.0.1:43110/{zeronet_address}` (比如.
|
||||
`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)。访问 zeronet 中的站点。
|
||||
* 在运行 `zeronet.py` 后,您将可以通过
|
||||
`http://127.0.0.1:43110/{zeronet_address}`(例如:
|
||||
`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)访问 zeronet 中的站点
|
||||
* 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件(html,css,js...)
|
||||
* 您将会储存每一个浏览过的站点
|
||||
* 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名
|
||||
* 如果站点的所有者(拥有站点地址的私钥)修改了站点,并且他 / 她签名了新的 `content.json` 然后推送至其他节点,
|
||||
那么这些节点将会在使用签名验证 `content.json` 的真实性后,下载修改后的文件并将新内容推送至另外的节点
|
||||
|
||||
* 在你浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件 (html, css, js...)
|
||||
|
||||
* 你将会储存每一个浏览过的站点
|
||||
* 每个站点都包含一个名为 `content.json` ,它储存了其他所有文件的 sha512 hash 值
|
||||
和一个通过站点私钥建立的签名
|
||||
* 如果站点的所有者 (拥有私钥的那个人) 修改了站点, 并且他/她签名了新的 `content.json` 然后推送至其他节点,
|
||||
那么所有节点将会在验证 `content.json` 的真实性 (使用签名)后, 下载修改后的文件并推送至其他节点。
|
||||
|
||||
#### [有关于 ZeroNet 加密, 站点更新, 多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1qBxkroB_iiX2zHEn0dt-N-qRZgyEzui46XS2hEa3AA4/pub?start=false&loop=false&delayms=3000)
|
||||
#### [关于 ZeroNet 加密,站点更新,多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000)
|
||||
#### [常见问题 »](https://zeronet.io/docs/faq/)
|
||||
|
||||
#### [ZeroNet开发者文档 »](https://zeronet.io/docs/site_development/getting_started/)
|
||||
#### [ZeroNet 开发者文档 »](https://zeronet.io/docs/site_development/getting_started/)
|
||||
|
||||
|
||||
## 屏幕截图
|
||||
|
@ -53,123 +51,60 @@
|
|||

|
||||

|
||||
|
||||
#### [在 ZeroNet 文档里查看更多的屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/)
|
||||
#### [ZeroNet 文档中的更多屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/)
|
||||
|
||||
|
||||
## 如何加入 ?
|
||||
## 如何加入
|
||||
|
||||
* 下载 ZeroBundle 文件包:
|
||||
* [Microsoft Windows](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist/ZeroNet-win.zip)
|
||||
* [Apple macOS](https://github.com/HelloZeroNet/ZeroNet-mac/archive/dist/ZeroNet-mac.zip)
|
||||
* [Linux 64bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz)
|
||||
* [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz)
|
||||
* 解压缩
|
||||
* 运行 `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux)
|
||||
### Windows
|
||||
|
||||
### Linux 命令行
|
||||
- 下载 [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB)
|
||||
- 在任意位置解压缩
|
||||
- 运行 `ZeroNet.exe`
|
||||
|
||||
* `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz`
|
||||
* `tar xvpfz ZeroBundle-linux64.tar.gz`
|
||||
* `cd ZeroBundle`
|
||||
* 执行 `./ZeroNet.sh` 来启动
|
||||
### macOS
|
||||
|
||||
在你打开时他将会自动下载最新版本的 ZeroNet 。
|
||||
- 下载 [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB)
|
||||
- 在任意位置解压缩
|
||||
- 运行 `ZeroNet.app`
|
||||
|
||||
#### 在 Debian Linux 中手动安装
|
||||
### Linux (x86-64bit)
|
||||
|
||||
* `sudo apt-get update`
|
||||
* `sudo apt-get install msgpack-python python-gevent`
|
||||
* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz`
|
||||
* `tar xvpfz master.tar.gz`
|
||||
* `cd ZeroNet-master`
|
||||
* 执行 `python2 zeronet.py` 来启动
|
||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
||||
- `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz`
|
||||
- `tar xvpfz ZeroNet-py3-linux64.tar.gz`
|
||||
- `cd ZeroNet-linux-dist-linux64/`
|
||||
- 使用以下命令启动 `./ZeroNet.sh`
|
||||
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
|
||||
|
||||
### [FreeBSD](https://www.freebsd.org/)
|
||||
__提示:__ 若要允许在 Web 界面上的远程连接,使用以下命令启动 `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address`
|
||||
|
||||
* `pkg install zeronet` 或者 `cd /usr/ports/security/zeronet/ && make install clean`
|
||||
* `sysrc zeronet_enable="YES"`
|
||||
* `service zeronet start`
|
||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
||||
### 从源代码安装
|
||||
|
||||
### [Vagrant](https://www.vagrantup.com/)
|
||||
|
||||
* `vagrant up`
|
||||
* 通过 `vagrant ssh` 连接到 VM
|
||||
* `cd /vagrant`
|
||||
* 运行 `python2 zeronet.py --ui_ip 0.0.0.0`
|
||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
||||
|
||||
### [Docker](https://www.docker.com/)
|
||||
* `docker run -d -v <local_data_folder>:/root/data -p 26552:26552 -p 43110:43110 nofish/zeronet`
|
||||
* 这个 Docker 镜像包含了 Tor ,但默认是禁用的,因为一些托管商不允许你在他们的服务器上运行 Tor。如果你希望启用它,
|
||||
设置 `ENABLE_TOR` 环境变量为 `true` (默认: `false`). E.g.:
|
||||
|
||||
`docker run -d -e "ENABLE_TOR=true" -v <local_data_folder>:/root/data -p 26552:26552 -p 43110:43110 nofish/zeronet`
|
||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
||||
|
||||
### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/)
|
||||
|
||||
* `virtualenv env`
|
||||
* `source env/bin/activate`
|
||||
* `pip install msgpack gevent`
|
||||
* `python2 zeronet.py`
|
||||
* 在你的浏览器中打开 http://127.0.0.1:43110/
|
||||
- `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz`
|
||||
- `tar xvpfz ZeroNet-py3.tar.gz`
|
||||
- `cd ZeroNet-py3`
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install python3-pip`
|
||||
- `sudo python3 -m pip install -r requirements.txt`
|
||||
- 使用以下命令启动 `python3 zeronet.py`
|
||||
- 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面
|
||||
|
||||
## 现有限制
|
||||
|
||||
* ~~没有类似于 BitTorrent 的文件拆分来支持大文件~~ (已添加大文件支持)
|
||||
* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持)
|
||||
* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持)
|
||||
* ~~没有类似于 torrent 的文件拆分来支持大文件~~ (已添加大文件支持)
|
||||
* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持)
|
||||
* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持)
|
||||
* 不支持私有站点
|
||||
|
||||
|
||||
## 如何创建一个 ZeroNet 站点?
|
||||
## 如何创建一个 ZeroNet 站点?
|
||||
|
||||
* 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D) 站点的 **⋮** > **「新建空站点」** 菜单项
|
||||
* 您将被**重定向**到一个全新的站点,该站点只能由您修改
|
||||
* 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容
|
||||
* 修改后打开您的网站,将右上角的「0」按钮拖到左侧,然后点击底部的**签名**并**发布**按钮
|
||||
|
||||
如果 zeronet 在运行,把它关掉
|
||||
执行:
|
||||
```bash
|
||||
$ zeronet.py siteCreate
|
||||
...
|
||||
- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
|
||||
- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
||||
...
|
||||
- Site created!
|
||||
$ zeronet.py
|
||||
...
|
||||
```
|
||||
|
||||
你已经完成了! 现在任何人都可以通过
|
||||
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
|
||||
来访问你的站点
|
||||
|
||||
下一步: [ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/)
|
||||
|
||||
|
||||
## 我要如何修改 ZeroNet 站点?
|
||||
|
||||
* 修改位于 data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 的目录.
|
||||
在你改好之后:
|
||||
|
||||
```bash
|
||||
$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
||||
- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
|
||||
Private key (input hidden):
|
||||
```
|
||||
|
||||
* 输入你在创建站点时获得的私钥
|
||||
|
||||
```bash
|
||||
$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
||||
...
|
||||
Site:13DNDk..bhC2 Publishing to 3/10 peers...
|
||||
Site:13DNDk..bhC2 Successfuly published to 3 peers
|
||||
- Serving files....
|
||||
```
|
||||
|
||||
* 就是这样! 你现在已经成功的签名并推送了你的更改。
|
||||
|
||||
接下来的步骤:[ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/)
|
||||
|
||||
## 帮助这个项目
|
||||
|
||||
|
@ -178,11 +113,11 @@ Site:13DNDk..bhC2 Successfuly published to 3 peers
|
|||
|
||||
### 赞助商
|
||||
|
||||
* 在 OSX/Safari 下 [BrowserStack.com](https://www.browserstack.com) 带来更好的兼容性
|
||||
* [BrowserStack.com](https://www.browserstack.com) 使更好的 macOS/Safari 兼容性成为可能
|
||||
|
||||
#### 感谢!
|
||||
#### 感谢您!
|
||||
|
||||
* 更多信息, 帮助, 变更记录和 zeronet 站点: https://www.reddit.com/r/zeronet/
|
||||
* 在: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 和我们聊天,或者使用 [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
|
||||
* 更多信息,帮助,变更记录和 zeronet 站点:https://www.reddit.com/r/zeronet/
|
||||
* 前往 [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 或 [gitter](https://gitter.im/HelloZeroNet/ZeroNet) 和我们聊天
|
||||
* [这里](https://gitter.im/ZeroNet-zh/Lobby)是一个 gitter 上的中文聊天室
|
||||
* Email: hello@noloop.me
|
||||
* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc))
|
||||
|
|
96
README.md
96
README.md
|
@ -1,4 +1,4 @@
|
|||
# ZeroNet [](https://travis-ci.org/HelloZeroNet/ZeroNet) [](https://zeronet.io/docs/faq/) [](https://zeronet.io/docs/help_zeronet/donate/)
|
||||
# ZeroNet [](https://travis-ci.org/HelloZeroNet/ZeroNet) [](https://zeronet.io/docs/faq/) [](https://zeronet.io/docs/help_zeronet/donate/)
|
||||
|
||||
Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io
|
||||
|
||||
|
@ -67,39 +67,31 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
|
|||
- Unpack anywhere
|
||||
- Run `ZeroNet.exe`
|
||||
|
||||
### Other platforms: Install from source
|
||||
### macOS
|
||||
|
||||
Fetch and extract the source:
|
||||
- Download [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB)
|
||||
- Unpack anywhere
|
||||
- Run `ZeroNet.app`
|
||||
|
||||
wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz
|
||||
tar xvpfz ZeroNet-py3.tar.gz
|
||||
cd ZeroNet-py3
|
||||
### Linux (x86-64bit)
|
||||
- `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz`
|
||||
- `tar xvpfz ZeroNet-py3-linux64.tar.gz`
|
||||
- `cd ZeroNet-linux-dist-linux64/`
|
||||
- Start with: `./ZeroNet.sh`
|
||||
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
|
||||
|
||||
Install Python module dependencies either:
|
||||
__Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface.
|
||||
|
||||
* (Option A) into a [virtual env](https://virtualenv.readthedocs.org/en/latest/)
|
||||
### Install from source
|
||||
|
||||
```
|
||||
virtualenv zeronet
|
||||
source zeronet/bin/activate
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
* (Option B) into the system (requires root), for example, on Debian/Ubuntu:
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3-pip
|
||||
sudo python3 -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Start Zeronet:
|
||||
|
||||
python3 zeronet.py
|
||||
|
||||
Open the ZeroHello landing page in your browser by navigating to:
|
||||
|
||||
http://127.0.0.1:43110/
|
||||
- `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz`
|
||||
- `tar xvpfz ZeroNet-py3.tar.gz`
|
||||
- `cd ZeroNet-py3`
|
||||
- `sudo apt-get update`
|
||||
- `sudo apt-get install python3-pip`
|
||||
- `sudo python3 -m pip install -r requirements.txt`
|
||||
- Start with: `python3 zeronet.py`
|
||||
- Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
|
||||
|
||||
## Current limitations
|
||||
|
||||
|
@ -111,49 +103,13 @@ Open the ZeroHello landing page in your browser by navigating to:
|
|||
|
||||
## How can I create a ZeroNet site?
|
||||
|
||||
Shut down zeronet if you are running it already
|
||||
|
||||
```bash
|
||||
$ zeronet.py siteCreate
|
||||
...
|
||||
- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq
|
||||
- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
||||
...
|
||||
- Site created!
|
||||
$ zeronet.py
|
||||
...
|
||||
```
|
||||
|
||||
Congratulations, you're finished! Now anyone can access your site using
|
||||
`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2`
|
||||
* Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D).
|
||||
* You will be **redirected** to a completely new site that is only modifiable by you!
|
||||
* You can find and modify your site's content in **data/[yoursiteaddress]** directory
|
||||
* After the modifications open your site, drag the topright "0" button to left, then press **sign** and **publish** buttons on the bottom
|
||||
|
||||
Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/)
|
||||
|
||||
|
||||
## How can I modify a ZeroNet site?
|
||||
|
||||
* Modify files located in data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 directory.
|
||||
After you're finished:
|
||||
|
||||
```bash
|
||||
$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
||||
- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2...
|
||||
Private key (input hidden):
|
||||
```
|
||||
|
||||
* Enter the private key you got when you created the site, then:
|
||||
|
||||
```bash
|
||||
$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2
|
||||
...
|
||||
Site:13DNDk..bhC2 Publishing to 3/10 peers...
|
||||
Site:13DNDk..bhC2 Successfuly published to 3 peers
|
||||
- Serving files....
|
||||
```
|
||||
|
||||
* That's it! You've successfully signed and published your modifications.
|
||||
|
||||
|
||||
## Help keep this project alive
|
||||
|
||||
- Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX
|
||||
|
@ -167,4 +123,4 @@ Site:13DNDk..bhC2 Successfuly published to 3 peers
|
|||
|
||||
* More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/
|
||||
* Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet)
|
||||
* Email: hello@zeronet.io (PGP: CB9613AE)
|
||||
* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc))
|
||||
|
|
|
@ -3,7 +3,7 @@ import urllib.request
|
|||
import struct
|
||||
import socket
|
||||
|
||||
import bencode
|
||||
import lib.bencode_open as bencode_open
|
||||
from lib.subtl.subtl import UdpTrackerClient
|
||||
import socks
|
||||
import sockshandler
|
||||
|
@ -133,9 +133,7 @@ class SiteAnnouncerPlugin(object):
|
|||
|
||||
# Decode peers
|
||||
try:
|
||||
peer_data = bencode.decode(response)["peers"]
|
||||
if type(peer_data) is not bytes:
|
||||
peer_data = peer_data.encode()
|
||||
peer_data = bencode_open.loads(response)[b"peers"]
|
||||
response = None
|
||||
peer_count = int(len(peer_data) / 6)
|
||||
peers = []
|
||||
|
|
5
plugins/AnnounceBitTorrent/plugin_info.json
Normal file
5
plugins/AnnounceBitTorrent/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "AnnounceBitTorrent",
|
||||
"description": "Discover new peers using BitTorrent trackers.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -120,12 +120,11 @@ class LocalAnnouncer(BroadcastServer.BroadcastServer):
|
|||
@PluginManager.registerTo("FileServer")
|
||||
class FileServerPlugin(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
res = super(FileServerPlugin, self).__init__(*args, **kwargs)
|
||||
super(FileServerPlugin, self).__init__(*args, **kwargs)
|
||||
if config.broadcast_port and config.tor != "always" and not config.disable_udp:
|
||||
self.local_announcer = LocalAnnouncer(self, config.broadcast_port)
|
||||
else:
|
||||
self.local_announcer = None
|
||||
return res
|
||||
|
||||
def start(self, *args, **kwargs):
|
||||
if self.local_announcer:
|
||||
|
|
5
plugins/AnnounceLocal/plugin_info.json
Normal file
5
plugins/AnnounceLocal/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "AnnounceLocal",
|
||||
"description": "Discover LAN clients using UDP broadcasting.",
|
||||
"default": "enabled"
|
||||
}
|
5
plugins/AnnounceShare/plugin_info.json
Normal file
5
plugins/AnnounceShare/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "AnnounceShare",
|
||||
"description": "Share possible trackers between clients.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -21,6 +21,15 @@ def importHostClasses():
|
|||
# Process result got back from tracker
|
||||
def processPeerRes(tracker_address, site, peers):
|
||||
added = 0
|
||||
|
||||
# Onion
|
||||
found_onion = 0
|
||||
for packed_address in peers["onion"]:
|
||||
found_onion += 1
|
||||
peer_onion, peer_port = helper.unpackOnionAddress(packed_address)
|
||||
if site.addPeer(peer_onion, peer_port, source="tracker"):
|
||||
added += 1
|
||||
|
||||
# Ip4
|
||||
found_ipv4 = 0
|
||||
peers_normal = itertools.chain(peers.get("ip4", []), peers.get("ipv4", []), peers.get("ipv6", []))
|
||||
|
@ -29,13 +38,6 @@ def processPeerRes(tracker_address, site, peers):
|
|||
peer_ip, peer_port = helper.unpackAddress(packed_address)
|
||||
if site.addPeer(peer_ip, peer_port, source="tracker"):
|
||||
added += 1
|
||||
# Onion
|
||||
found_onion = 0
|
||||
for packed_address in peers["onion"]:
|
||||
found_onion += 1
|
||||
peer_onion, peer_port = helper.unpackOnionAddress(packed_address)
|
||||
if site.addPeer(peer_onion, peer_port, source="tracker"):
|
||||
added += 1
|
||||
|
||||
if added:
|
||||
site.worker_manager.onPeers()
|
||||
|
|
5
plugins/AnnounceZero/plugin_info.json
Normal file
5
plugins/AnnounceZero/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "AnnounceZero",
|
||||
"description": "Announce using ZeroNet protocol.",
|
||||
"default": "enabled"
|
||||
}
|
143
plugins/Benchmark/BenchmarkDb.py
Normal file
143
plugins/Benchmark/BenchmarkDb.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
import os
|
||||
import json
|
||||
import contextlib
|
||||
import time
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Config import config
|
||||
|
||||
|
||||
@PluginManager.registerTo("Actions")
|
||||
class ActionsPlugin:
|
||||
def getBenchmarkTests(self, online=False):
|
||||
tests = super().getBenchmarkTests(online)
|
||||
tests.extend([
|
||||
{"func": self.testDbConnect, "num": 10, "time_standard": 0.27},
|
||||
{"func": self.testDbInsert, "num": 10, "time_standard": 0.91},
|
||||
{"func": self.testDbInsertMultiuser, "num": 1, "time_standard": 0.57},
|
||||
{"func": self.testDbQueryIndexed, "num": 1000, "time_standard": 0.84},
|
||||
{"func": self.testDbQueryNotIndexed, "num": 1000, "time_standard": 1.30}
|
||||
])
|
||||
return tests
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def getTestDb(self):
|
||||
from Db import Db
|
||||
path = "%s/benchmark.db" % config.data_dir
|
||||
if os.path.isfile(path):
|
||||
os.unlink(path)
|
||||
schema = {
|
||||
"db_name": "TestDb",
|
||||
"db_file": path,
|
||||
"maps": {
|
||||
".*": {
|
||||
"to_table": {
|
||||
"test": "test"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tables": {
|
||||
"test": {
|
||||
"cols": [
|
||||
["test_id", "INTEGER"],
|
||||
["title", "TEXT"],
|
||||
["json_id", "INTEGER REFERENCES json (json_id)"]
|
||||
],
|
||||
"indexes": ["CREATE UNIQUE INDEX test_key ON test(test_id, json_id)"],
|
||||
"schema_changed": 1426195822
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db = Db.Db(schema, path)
|
||||
|
||||
yield db
|
||||
|
||||
db.close()
|
||||
if os.path.isfile(path):
|
||||
os.unlink(path)
|
||||
|
||||
def testDbConnect(self, num_run=1):
|
||||
import sqlite3
|
||||
for i in range(num_run):
|
||||
with self.getTestDb() as db:
|
||||
db.checkTables()
|
||||
yield "."
|
||||
yield "(SQLite version: %s, API: %s)" % (sqlite3.sqlite_version, sqlite3.version)
|
||||
|
||||
def testDbInsert(self, num_run=1):
|
||||
yield "x 1000 lines "
|
||||
for u in range(num_run):
|
||||
with self.getTestDb() as db:
|
||||
db.checkTables()
|
||||
data = {"test": []}
|
||||
for i in range(1000): # 1000 line of data
|
||||
data["test"].append({"test_id": i, "title": "Testdata for %s message %s" % (u, i)})
|
||||
json.dump(data, open("%s/test_%s.json" % (config.data_dir, u), "w"))
|
||||
db.updateJson("%s/test_%s.json" % (config.data_dir, u))
|
||||
os.unlink("%s/test_%s.json" % (config.data_dir, u))
|
||||
assert db.execute("SELECT COUNT(*) FROM test").fetchone()[0] == 1000
|
||||
yield "."
|
||||
|
||||
def fillTestDb(self, db):
|
||||
db.checkTables()
|
||||
cur = db.getCursor()
|
||||
cur.logging = False
|
||||
for u in range(100, 200): # 100 user
|
||||
data = {"test": []}
|
||||
for i in range(100): # 1000 line of data
|
||||
data["test"].append({"test_id": i, "title": "Testdata for %s message %s" % (u, i)})
|
||||
json.dump(data, open("%s/test_%s.json" % (config.data_dir, u), "w"))
|
||||
db.updateJson("%s/test_%s.json" % (config.data_dir, u), cur=cur)
|
||||
os.unlink("%s/test_%s.json" % (config.data_dir, u))
|
||||
if u % 10 == 0:
|
||||
yield "."
|
||||
|
||||
def testDbInsertMultiuser(self, num_run=1):
|
||||
yield "x 100 users x 100 lines "
|
||||
for u in range(num_run):
|
||||
with self.getTestDb() as db:
|
||||
for progress in self.fillTestDb(db):
|
||||
yield progress
|
||||
num_rows = db.execute("SELECT COUNT(*) FROM test").fetchone()[0]
|
||||
assert num_rows == 10000, "%s != 10000" % num_rows
|
||||
|
||||
def testDbQueryIndexed(self, num_run=1):
|
||||
s = time.time()
|
||||
with self.getTestDb() as db:
|
||||
for progress in self.fillTestDb(db):
|
||||
pass
|
||||
yield " (Db warmup done in %.3fs) " % (time.time() - s)
|
||||
found_total = 0
|
||||
for i in range(num_run): # 1000x by test_id
|
||||
found = 0
|
||||
res = db.execute("SELECT * FROM test WHERE test_id = %s" % (i % 100))
|
||||
for row in res:
|
||||
found_total += 1
|
||||
found += 1
|
||||
del(res)
|
||||
yield "."
|
||||
assert found == 100, "%s != 100 (i: %s)" % (found, i)
|
||||
yield "Found: %s" % found_total
|
||||
|
||||
def testDbQueryNotIndexed(self, num_run=1):
|
||||
s = time.time()
|
||||
with self.getTestDb() as db:
|
||||
for progress in self.fillTestDb(db):
|
||||
pass
|
||||
yield " (Db warmup done in %.3fs) " % (time.time() - s)
|
||||
found_total = 0
|
||||
for i in range(num_run): # 1000x by test_id
|
||||
found = 0
|
||||
res = db.execute("SELECT * FROM test WHERE json_id = %s" % i)
|
||||
for row in res:
|
||||
found_total += 1
|
||||
found += 1
|
||||
yield "."
|
||||
del(res)
|
||||
if i == 0 or i > 100:
|
||||
assert found == 0, "%s != 0 (i: %s)" % (found, i)
|
||||
else:
|
||||
assert found == 100, "%s != 100 (i: %s)" % (found, i)
|
||||
yield "Found: %s" % found_total
|
183
plugins/Benchmark/BenchmarkPack.py
Normal file
183
plugins/Benchmark/BenchmarkPack.py
Normal file
|
@ -0,0 +1,183 @@
|
|||
import os
|
||||
import io
|
||||
from collections import OrderedDict
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Config import config
|
||||
from util import Msgpack
|
||||
|
||||
|
||||
@PluginManager.registerTo("Actions")
|
||||
class ActionsPlugin:
|
||||
def createZipFile(self, path):
|
||||
import zipfile
|
||||
test_data = b"Test" * 1024
|
||||
file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8")
|
||||
with zipfile.ZipFile(path, 'w') as archive:
|
||||
for y in range(100):
|
||||
zip_info = zipfile.ZipInfo(file_name % y, (1980, 1, 1, 0, 0, 0))
|
||||
zip_info.compress_type = zipfile.ZIP_DEFLATED
|
||||
zip_info.create_system = 3
|
||||
zip_info.flag_bits = 0
|
||||
zip_info.external_attr = 25165824
|
||||
archive.writestr(zip_info, test_data)
|
||||
|
||||
def testPackZip(self, num_run=1):
|
||||
"""
|
||||
Test zip file creating
|
||||
"""
|
||||
yield "x 100 x 5KB "
|
||||
from Crypt import CryptHash
|
||||
zip_path = '%s/test.zip' % config.data_dir
|
||||
for i in range(num_run):
|
||||
self.createZipFile(zip_path)
|
||||
yield "."
|
||||
|
||||
archive_size = os.path.getsize(zip_path) / 1024
|
||||
yield "(Generated file size: %.2fkB)" % archive_size
|
||||
|
||||
hash = CryptHash.sha512sum(open(zip_path, "rb"))
|
||||
valid = "cb32fb43783a1c06a2170a6bc5bb228a032b67ff7a1fd7a5efb9b467b400f553"
|
||||
assert hash == valid, "Invalid hash: %s != %s<br>" % (hash, valid)
|
||||
os.unlink(zip_path)
|
||||
|
||||
def testUnpackZip(self, num_run=1):
|
||||
"""
|
||||
Test zip file reading
|
||||
"""
|
||||
yield "x 100 x 5KB "
|
||||
import zipfile
|
||||
zip_path = '%s/test.zip' % config.data_dir
|
||||
test_data = b"Test" * 1024
|
||||
file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91".decode("utf8")
|
||||
|
||||
self.createZipFile(zip_path)
|
||||
for i in range(num_run):
|
||||
with zipfile.ZipFile(zip_path) as archive:
|
||||
for f in archive.filelist:
|
||||
assert f.filename.startswith(file_name), "Invalid filename: %s != %s" % (f.filename, file_name)
|
||||
data = archive.open(f.filename).read()
|
||||
assert archive.open(f.filename).read() == test_data, "Invalid data: %s..." % data[0:30]
|
||||
yield "."
|
||||
|
||||
os.unlink(zip_path)
|
||||
|
||||
def createArchiveFile(self, path, archive_type="gz"):
|
||||
import tarfile
|
||||
import gzip
|
||||
|
||||
# Monkey patch _init_write_gz to use fixed date in order to keep the hash independent from datetime
|
||||
def nodate_write_gzip_header(self):
|
||||
self._write_mtime = 0
|
||||
original_write_gzip_header(self)
|
||||
|
||||
test_data_io = io.BytesIO(b"Test" * 1024)
|
||||
file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8")
|
||||
|
||||
original_write_gzip_header = gzip.GzipFile._write_gzip_header
|
||||
gzip.GzipFile._write_gzip_header = nodate_write_gzip_header
|
||||
with tarfile.open(path, 'w:%s' % archive_type) as archive:
|
||||
for y in range(100):
|
||||
test_data_io.seek(0)
|
||||
tar_info = tarfile.TarInfo(file_name % y)
|
||||
tar_info.size = 4 * 1024
|
||||
archive.addfile(tar_info, test_data_io)
|
||||
|
||||
def testPackArchive(self, num_run=1, archive_type="gz"):
|
||||
"""
|
||||
Test creating tar archive files
|
||||
"""
|
||||
yield "x 100 x 5KB "
|
||||
from Crypt import CryptHash
|
||||
|
||||
hash_valid_db = {
|
||||
"gz": "92caec5121a31709cbbc8c11b0939758e670b055bbbe84f9beb3e781dfde710f",
|
||||
"bz2": "b613f41e6ee947c8b9b589d3e8fa66f3e28f63be23f4faf015e2f01b5c0b032d",
|
||||
"xz": "ae43892581d770959c8d993daffab25fd74490b7cf9fafc7aaee746f69895bcb",
|
||||
}
|
||||
archive_path = '%s/test.tar.%s' % (config.data_dir, archive_type)
|
||||
for i in range(num_run):
|
||||
self.createArchiveFile(archive_path, archive_type=archive_type)
|
||||
yield "."
|
||||
|
||||
archive_size = os.path.getsize(archive_path) / 1024
|
||||
yield "(Generated file size: %.2fkB)" % archive_size
|
||||
|
||||
hash = CryptHash.sha512sum(open("%s/test.tar.%s" % (config.data_dir, archive_type), "rb"))
|
||||
valid = hash_valid_db[archive_type]
|
||||
assert hash == valid, "Invalid hash: %s != %s<br>" % (hash, valid)
|
||||
|
||||
if os.path.isfile(archive_path):
|
||||
os.unlink(archive_path)
|
||||
|
||||
def testUnpackArchive(self, num_run=1, archive_type="gz"):
|
||||
"""
|
||||
Test reading tar archive files
|
||||
"""
|
||||
yield "x 100 x 5KB "
|
||||
import tarfile
|
||||
|
||||
test_data = b"Test" * 1024
|
||||
file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8")
|
||||
archive_path = '%s/test.tar.%s' % (config.data_dir, archive_type)
|
||||
self.createArchiveFile(archive_path, archive_type=archive_type)
|
||||
for i in range(num_run):
|
||||
with tarfile.open(archive_path, 'r:%s' % archive_type) as archive:
|
||||
for y in range(100):
|
||||
assert archive.extractfile(file_name % y).read() == test_data
|
||||
yield "."
|
||||
if os.path.isfile(archive_path):
|
||||
os.unlink(archive_path)
|
||||
|
||||
def testPackMsgpack(self, num_run=1):
|
||||
"""
|
||||
Test msgpack encoding
|
||||
"""
|
||||
yield "x 100 x 5KB "
|
||||
binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv'
|
||||
data = OrderedDict(
|
||||
sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items())
|
||||
)
|
||||
data_packed_valid = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00'
|
||||
data_packed_valid += b'hello' * 1024
|
||||
for y in range(num_run):
|
||||
for i in range(100):
|
||||
data_packed = Msgpack.pack(data)
|
||||
yield "."
|
||||
assert data_packed == data_packed_valid, "%s<br>!=<br>%s" % (repr(data_packed), repr(data_packed_valid))
|
||||
|
||||
def testUnpackMsgpack(self, num_run=1):
|
||||
"""
|
||||
Test msgpack decoding
|
||||
"""
|
||||
yield "x 5KB "
|
||||
binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv'
|
||||
data = OrderedDict(
|
||||
sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items())
|
||||
)
|
||||
data_packed = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00'
|
||||
data_packed += b'hello' * 1024
|
||||
for y in range(num_run):
|
||||
data_unpacked = Msgpack.unpack(data_packed, decode=False)
|
||||
yield "."
|
||||
assert data_unpacked == data, "%s<br>!=<br>%s" % (data_unpacked, data)
|
||||
|
||||
def testUnpackMsgpackStreaming(self, num_run=1, fallback=False):
|
||||
"""
|
||||
Test streaming msgpack decoding
|
||||
"""
|
||||
yield "x 1000 x 5KB "
|
||||
binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv'
|
||||
data = OrderedDict(
|
||||
sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items())
|
||||
)
|
||||
data_packed = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00'
|
||||
data_packed += b'hello' * 1024
|
||||
for i in range(num_run):
|
||||
unpacker = Msgpack.getUnpacker(decode=False, fallback=fallback)
|
||||
for y in range(1000):
|
||||
unpacker.feed(data_packed)
|
||||
for data_unpacked in unpacker:
|
||||
pass
|
||||
yield "."
|
||||
assert data == data_unpacked, "%s != %s" % (data_unpacked, data)
|
364
plugins/Benchmark/BenchmarkPlugin.py
Normal file
364
plugins/Benchmark/BenchmarkPlugin.py
Normal file
|
@ -0,0 +1,364 @@
|
|||
import os
|
||||
import time
|
||||
import io
|
||||
import math
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
from Config import config
|
||||
from Crypt import CryptHash
|
||||
from Plugin import PluginManager
|
||||
from Debug import Debug
|
||||
from util import helper
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
benchmark_key = None
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiRequest")
|
||||
class UiRequestPlugin(object):
|
||||
@helper.encodeResponse
|
||||
def actionBenchmark(self):
|
||||
global benchmark_key
|
||||
script_nonce = self.getScriptNonce()
|
||||
if not benchmark_key:
|
||||
benchmark_key = CryptHash.random(encoding="base64")
|
||||
self.sendHeader(script_nonce=script_nonce)
|
||||
|
||||
if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
|
||||
yield "This function is disabled on this proxy"
|
||||
return
|
||||
|
||||
data = self.render(
|
||||
plugin_dir + "/media/benchmark.html",
|
||||
script_nonce=script_nonce,
|
||||
benchmark_key=benchmark_key,
|
||||
filter=re.sub("[^A-Za-z0-9]", "", self.get.get("filter", ""))
|
||||
)
|
||||
yield data
|
||||
|
||||
@helper.encodeResponse
|
||||
def actionBenchmarkResult(self):
|
||||
global benchmark_key
|
||||
if self.get.get("benchmark_key", "") != benchmark_key:
|
||||
return self.error403("Invalid benchmark key")
|
||||
|
||||
self.sendHeader(content_type="text/plain", noscript=True)
|
||||
|
||||
if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local:
|
||||
yield "This function is disabled on this proxy"
|
||||
return
|
||||
|
||||
yield " " * 1024 # Head (required for streaming)
|
||||
|
||||
import main
|
||||
s = time.time()
|
||||
|
||||
for part in main.actions.testBenchmark(filter=self.get.get("filter", "")):
|
||||
yield part
|
||||
|
||||
yield "\n - Total time: %.3fs" % (time.time() - s)
|
||||
|
||||
|
||||
@PluginManager.registerTo("Actions")
|
||||
class ActionsPlugin:
|
||||
def getMultiplerTitle(self, multipler):
|
||||
if multipler < 0.3:
|
||||
multipler_title = "Sloooow"
|
||||
elif multipler < 0.6:
|
||||
multipler_title = "Ehh"
|
||||
elif multipler < 0.8:
|
||||
multipler_title = "Goodish"
|
||||
elif multipler < 1.2:
|
||||
multipler_title = "OK"
|
||||
elif multipler < 1.7:
|
||||
multipler_title = "Fine"
|
||||
elif multipler < 2.5:
|
||||
multipler_title = "Fast"
|
||||
elif multipler < 3.5:
|
||||
multipler_title = "WOW"
|
||||
else:
|
||||
multipler_title = "Insane!!"
|
||||
return multipler_title
|
||||
|
||||
def formatResult(self, taken, standard):
|
||||
if not standard:
|
||||
return " Done in %.3fs" % taken
|
||||
|
||||
if taken > 0:
|
||||
multipler = standard / taken
|
||||
else:
|
||||
multipler = 99
|
||||
multipler_title = self.getMultiplerTitle(multipler)
|
||||
|
||||
return " Done in %.3fs = %s (%.2fx)" % (taken, multipler_title, multipler)
|
||||
|
||||
def getBenchmarkTests(self, online=False):
|
||||
if hasattr(super(), "getBenchmarkTests"):
|
||||
tests = super().getBenchmarkTests(online)
|
||||
else:
|
||||
tests = []
|
||||
|
||||
tests.extend([
|
||||
{"func": self.testHdPrivatekey, "num": 50, "time_standard": 0.57},
|
||||
{"func": self.testSign, "num": 20, "time_standard": 0.46},
|
||||
{"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto_fallback"}, "num": 20, "time_standard": 0.38},
|
||||
{"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto"}, "num": 200, "time_standard": 0.30},
|
||||
{"func": self.testVerify, "kwargs": {"lib_verify": "libsecp256k1"}, "num": 200, "time_standard": 0.10},
|
||||
|
||||
{"func": self.testPackMsgpack, "num": 100, "time_standard": 0.35},
|
||||
{"func": self.testUnpackMsgpackStreaming, "kwargs": {"fallback": False}, "num": 100, "time_standard": 0.35},
|
||||
{"func": self.testUnpackMsgpackStreaming, "kwargs": {"fallback": True}, "num": 10, "time_standard": 0.5},
|
||||
|
||||
{"func": self.testPackZip, "num": 5, "time_standard": 0.065},
|
||||
{"func": self.testPackArchive, "kwargs": {"archive_type": "gz"}, "num": 5, "time_standard": 0.08},
|
||||
{"func": self.testPackArchive, "kwargs": {"archive_type": "bz2"}, "num": 5, "time_standard": 0.68},
|
||||
{"func": self.testPackArchive, "kwargs": {"archive_type": "xz"}, "num": 5, "time_standard": 0.47},
|
||||
{"func": self.testUnpackZip, "num": 20, "time_standard": 0.25},
|
||||
{"func": self.testUnpackArchive, "kwargs": {"archive_type": "gz"}, "num": 20, "time_standard": 0.28},
|
||||
{"func": self.testUnpackArchive, "kwargs": {"archive_type": "bz2"}, "num": 20, "time_standard": 0.83},
|
||||
{"func": self.testUnpackArchive, "kwargs": {"archive_type": "xz"}, "num": 20, "time_standard": 0.38},
|
||||
|
||||
{"func": self.testCryptHash, "kwargs": {"hash_type": "sha256"}, "num": 10, "time_standard": 0.50},
|
||||
{"func": self.testCryptHash, "kwargs": {"hash_type": "sha512"}, "num": 10, "time_standard": 0.33},
|
||||
{"func": self.testCryptHashlib, "kwargs": {"hash_type": "sha3_256"}, "num": 10, "time_standard": 0.33},
|
||||
{"func": self.testCryptHashlib, "kwargs": {"hash_type": "sha3_512"}, "num": 10, "time_standard": 0.65},
|
||||
|
||||
{"func": self.testRandom, "num": 100, "time_standard": 0.08},
|
||||
])
|
||||
|
||||
if online:
|
||||
tests += [
|
||||
{"func": self.testHttps, "num": 1, "time_standard": 2.1}
|
||||
]
|
||||
return tests
|
||||
|
||||
def testBenchmark(self, num_multipler=1, online=False, num_run=None, filter=None):
|
||||
"""
|
||||
Run benchmark on client functions
|
||||
"""
|
||||
tests = self.getBenchmarkTests(online=online)
|
||||
|
||||
if filter:
|
||||
tests = [test for test in tests[:] if filter.lower() in test["func"].__name__.lower()]
|
||||
|
||||
yield "\n"
|
||||
res = {}
|
||||
multiplers = []
|
||||
for test in tests:
|
||||
s = time.time()
|
||||
if num_run:
|
||||
num_run_test = num_run
|
||||
else:
|
||||
num_run_test = math.ceil(test["num"] * num_multipler)
|
||||
func = test["func"]
|
||||
func_name = func.__name__
|
||||
kwargs = test.get("kwargs", {})
|
||||
key = "%s %s" % (func_name, kwargs)
|
||||
if kwargs:
|
||||
yield "* Running %s (%s) x %s " % (func_name, kwargs, num_run_test)
|
||||
else:
|
||||
yield "* Running %s x %s " % (func_name, num_run_test)
|
||||
i = 0
|
||||
try:
|
||||
for progress in func(num_run_test, **kwargs):
|
||||
i += 1
|
||||
if num_run_test > 10:
|
||||
should_print = i % (num_run_test / 10) == 0 or progress != "."
|
||||
else:
|
||||
should_print = True
|
||||
|
||||
if should_print:
|
||||
if num_run_test == 1 and progress == ".":
|
||||
progress = "..."
|
||||
yield progress
|
||||
time_taken = time.time() - s
|
||||
if num_run:
|
||||
time_standard = 0
|
||||
else:
|
||||
time_standard = test["time_standard"] * num_multipler
|
||||
yield self.formatResult(time_taken, time_standard)
|
||||
yield "\n"
|
||||
res[key] = "ok"
|
||||
multiplers.append(time_standard / max(time_taken, 0.001))
|
||||
except Exception as err:
|
||||
res[key] = err
|
||||
yield "Failed!\n! Error: %s\n\n" % Debug.formatException(err)
|
||||
|
||||
if not res:
|
||||
yield "! No tests found"
|
||||
if config.action == "test":
|
||||
sys.exit(1)
|
||||
else:
|
||||
num_failed = len([res_key for res_key, res_val in res.items() if res_val != "ok"])
|
||||
num_success = len([res_key for res_key, res_val in res.items() if res_val != "ok"])
|
||||
yield "* Result:\n"
|
||||
yield " - Total: %s tests\n" % len(res)
|
||||
yield " - Success: %s tests\n" % num_success
|
||||
yield " - Failed: %s tests\n" % num_failed
|
||||
if any(multiplers):
|
||||
multipler_avg = sum(multiplers) / len(multiplers)
|
||||
multipler_title = self.getMultiplerTitle(multipler_avg)
|
||||
yield " - Average speed factor: %.2fx (%s)" % (multipler_avg, multipler_title)
|
||||
if num_failed == 0 and config.action == "test":
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def testHttps(self, num_run=1):
|
||||
"""
|
||||
Test https connection with valid and invalid certs
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
body = urllib.request.urlopen("https://google.com").read()
|
||||
assert len(body) > 100
|
||||
yield "."
|
||||
|
||||
badssl_urls = [
|
||||
"https://expired.badssl.com/",
|
||||
"https://wrong.host.badssl.com/",
|
||||
"https://self-signed.badssl.com/",
|
||||
"https://untrusted-root.badssl.com/"
|
||||
]
|
||||
for badssl_url in badssl_urls:
|
||||
try:
|
||||
body = urllib.request.urlopen(badssl_url).read()
|
||||
https_err = None
|
||||
except urllib.error.URLError as err:
|
||||
https_err = err
|
||||
assert https_err
|
||||
yield "."
|
||||
|
||||
def testCryptHash(self, num_run=1, hash_type="sha256"):
|
||||
"""
|
||||
Test hashing functions
|
||||
"""
|
||||
yield "(5MB) "
|
||||
|
||||
from Crypt import CryptHash
|
||||
|
||||
hash_types = {
|
||||
"sha256": {"func": CryptHash.sha256sum, "hash_valid": "8cd629d9d6aff6590da8b80782a5046d2673d5917b99d5603c3dcb4005c45ffa"},
|
||||
"sha512": {"func": CryptHash.sha512sum, "hash_valid": "9ca7e855d430964d5b55b114e95c6bbb114a6d478f6485df93044d87b108904d"}
|
||||
}
|
||||
hash_func = hash_types[hash_type]["func"]
|
||||
hash_valid = hash_types[hash_type]["hash_valid"]
|
||||
|
||||
data = io.BytesIO(b"Hello" * 1024 * 1024) # 5MB
|
||||
for i in range(num_run):
|
||||
data.seek(0)
|
||||
hash = hash_func(data)
|
||||
yield "."
|
||||
assert hash == hash_valid, "%s != %s" % (hash, hash_valid)
|
||||
|
||||
def testCryptHashlib(self, num_run=1, hash_type="sha3_256"):
|
||||
"""
|
||||
Test SHA3 hashing functions
|
||||
"""
|
||||
yield "x 5MB "
|
||||
|
||||
hash_types = {
|
||||
"sha3_256": {"func": hashlib.sha3_256, "hash_valid": "c8aeb3ef9fe5d6404871c0d2a4410a4d4e23268e06735648c9596f436c495f7e"},
|
||||
"sha3_512": {"func": hashlib.sha3_512, "hash_valid": "b75dba9472d8af3cc945ce49073f3f8214d7ac12086c0453fb08944823dee1ae83b3ffbc87a53a57cc454521d6a26fe73ff0f3be38dddf3f7de5d7692ebc7f95"},
|
||||
}
|
||||
|
||||
hash_func = hash_types[hash_type]["func"]
|
||||
hash_valid = hash_types[hash_type]["hash_valid"]
|
||||
|
||||
data = io.BytesIO(b"Hello" * 1024 * 1024) # 5MB
|
||||
for i in range(num_run):
|
||||
data.seek(0)
|
||||
h = hash_func()
|
||||
while 1:
|
||||
buff = data.read(1024 * 64)
|
||||
if not buff:
|
||||
break
|
||||
h.update(buff)
|
||||
hash = h.hexdigest()
|
||||
yield "."
|
||||
assert hash == hash_valid, "%s != %s" % (hash, hash_valid)
|
||||
|
||||
def testRandom(self, num_run=1):
|
||||
"""
|
||||
Test generating random data
|
||||
"""
|
||||
yield "x 1000 x 256 bytes "
|
||||
for i in range(num_run):
|
||||
data_last = None
|
||||
for y in range(1000):
|
||||
data = os.urandom(256)
|
||||
assert data != data_last
|
||||
assert len(data) == 256
|
||||
data_last = data
|
||||
yield "."
|
||||
|
||||
def testHdPrivatekey(self, num_run=2):
|
||||
"""
|
||||
Test generating deterministic private keys from a master seed
|
||||
"""
|
||||
from Crypt import CryptBitcoin
|
||||
seed = "e180efa477c63b0f2757eac7b1cce781877177fe0966be62754ffd4c8592ce38"
|
||||
privatekeys = []
|
||||
for i in range(num_run):
|
||||
privatekeys.append(CryptBitcoin.hdPrivatekey(seed, i * 10))
|
||||
yield "."
|
||||
valid = "5JSbeF5PevdrsYjunqpg7kAGbnCVYa1T4APSL3QRu8EoAmXRc7Y"
|
||||
assert privatekeys[0] == valid, "%s != %s" % (privatekeys[0], valid)
|
||||
if len(privatekeys) > 1:
|
||||
assert privatekeys[0] != privatekeys[-1]
|
||||
|
||||
def testSign(self, num_run=1):
|
||||
"""
|
||||
Test signing data using a private key
|
||||
"""
|
||||
from Crypt import CryptBitcoin
|
||||
data = "Hello" * 1024
|
||||
privatekey = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk"
|
||||
for i in range(num_run):
|
||||
yield "."
|
||||
sign = CryptBitcoin.sign(data, privatekey)
|
||||
valid = "G1GXaDauZ8vX/N9Jn+MRiGm9h+I94zUhDnNYFaqMGuOiBHB+kp4cRPZOL7l1yqK5BHa6J+W97bMjvTXtxzljp6w="
|
||||
assert sign == valid, "%s != %s" % (sign, valid)
|
||||
|
||||
def testVerify(self, num_run=1, lib_verify="btctools"):
|
||||
"""
|
||||
Test verification of generated signatures
|
||||
"""
|
||||
from Crypt import CryptBitcoin
|
||||
CryptBitcoin.loadLib(lib_verify, silent=True)
|
||||
|
||||
data = "Hello" * 1024
|
||||
privatekey = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk"
|
||||
address = CryptBitcoin.privatekeyToAddress(privatekey)
|
||||
sign = "G1GXaDauZ8vX/N9Jn+MRiGm9h+I94zUhDnNYFaqMGuOiBHB+kp4cRPZOL7l1yqK5BHa6J+W97bMjvTXtxzljp6w="
|
||||
|
||||
for i in range(num_run):
|
||||
ok = CryptBitcoin.verify(data, address, sign, lib_verify=lib_verify)
|
||||
yield "."
|
||||
assert ok, "does not verify from %s" % address
|
||||
|
||||
def testAll(self):
|
||||
"""
|
||||
Run all tests to check system compatibility with ZeroNet functions
|
||||
"""
|
||||
for progress in self.testBenchmark(online=not config.offline, num_run=1):
|
||||
yield progress
|
||||
|
||||
|
||||
@PluginManager.registerTo("ConfigPlugin")
|
||||
class ConfigPlugin(object):
|
||||
def createArguments(self):
|
||||
back = super(ConfigPlugin, self).createArguments()
|
||||
if self.getCmdlineValue("test") == "benchmark":
|
||||
self.test_parser.add_argument(
|
||||
'--num_multipler', help='Benchmark run time multipler',
|
||||
default=1.0, type=float, metavar='num'
|
||||
)
|
||||
self.test_parser.add_argument(
|
||||
'--filter', help='Filter running benchmark',
|
||||
default=None, metavar='test name'
|
||||
)
|
||||
return back
|
3
plugins/Benchmark/__init__.py
Normal file
3
plugins/Benchmark/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from . import BenchmarkPlugin
|
||||
from . import BenchmarkDb
|
||||
from . import BenchmarkPack
|
123
plugins/Benchmark/media/benchmark.html
Normal file
123
plugins/Benchmark/media/benchmark.html
Normal file
|
@ -0,0 +1,123 @@
|
|||
<html>
|
||||
<script nonce="{script_nonce}">
|
||||
window.benchmark_key = "{benchmark_key}";
|
||||
|
||||
function setState(elem, text) {
|
||||
var formatted = text
|
||||
var parts = text.match(/\* Running (.*?)(\n|$)/g)
|
||||
if (parts) {
|
||||
for (var i=0; i < parts.length; i++) {
|
||||
part = parts[i];
|
||||
var details = part.match(/\* Running (.*?) (\.+|$)(.*)/);
|
||||
if (details) {
|
||||
var title = details[1]
|
||||
var progress = details[2]
|
||||
var result = details[3]
|
||||
|
||||
result_parts = result.match(/(.*) Done in ([0-9\.]+)s = (.*?) \(([0-9\.]+)x\)/)
|
||||
var percent = Math.min(100, progress.length * 10)
|
||||
if (result_parts) percent = 100
|
||||
var style = "background-image: linear-gradient(90deg, #FFF " + percent + "%, #FFF 0%, #d9d5de 0%);"
|
||||
var part_formatted = "<div class='test' style='" + style + "'>"
|
||||
part_formatted += "<span class='title'>" + title + "</span><span class='percent percent-" + percent + "'>" + percent + "%</span> "
|
||||
if (result_parts) {
|
||||
var result_extra = result_parts[1]
|
||||
var taken = result_parts[2]
|
||||
var multipler_title = result_parts[3]
|
||||
var multipler = result_parts[4]
|
||||
part_formatted += "<div class='result result-" + multipler_title.replace(/[^A-Za-z]/g, "") + "'>"
|
||||
part_formatted += " <span class='taken'>" + taken + "s</span>"
|
||||
part_formatted += " <span class='multipler'>" + multipler + "x</span>"
|
||||
part_formatted += " <span class='multipler-title'>" + multipler_title + "</span>"
|
||||
part_formatted += "</div>"
|
||||
} else {
|
||||
part_formatted += "<div class='result'>" + result + "</div>"
|
||||
}
|
||||
part_formatted += "</div>"
|
||||
formatted = formatted.replace(part, part_formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
formatted = formatted.replace(/(\! Error:.*)/, "<div class='test error'>$1</div>");
|
||||
formatted = formatted.replace(/(\* Result:[^]*)/, "<div class='test summary'>$1</div>");
|
||||
var is_bottom = document.body.scrollTop + document.body.clientHeight >= document.body.scrollHeight - 5;
|
||||
elem.innerHTML = formatted.trim();
|
||||
if (is_bottom)
|
||||
document.body.scrollTop = document.body.scrollHeight;
|
||||
}
|
||||
|
||||
function stream(url, elem) {
|
||||
document.getElementById("h1").innerText = "Benchmark: Starting..."
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, true);
|
||||
xhr.setRequestHeader('Accept', 'text/html');
|
||||
xhr.send(null);
|
||||
xhr.onreadystatechange = function(state) {
|
||||
document.getElementById("h1").innerText = "Benchmark: Running..."
|
||||
setState(elem, xhr.responseText);
|
||||
if (xhr.readyState == 4) {
|
||||
document.getElementById("h1").innerText = "Benchmark: Done."
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<body>
|
||||
<style>
|
||||
body {
|
||||
background-color: #3c3546;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23cfcfcf' fill-opacity='0.09'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");}
|
||||
h1 {
|
||||
font-family: monospace; color: white; font-weight: normal; text-transform: uppercase;
|
||||
max-width: 690px; margin: 30px auto; margin-bottom: 10px;
|
||||
}
|
||||
#out {
|
||||
white-space: pre-line; background-color: #ffffff1a; padding: 20px; font-family: Consolas, monospace;
|
||||
font-size: 11px; width: 90%; margin: auto; max-width: 650px; box-shadow: 0px 10px 30px -10px #5c5c5c6b;
|
||||
}
|
||||
.test { padding: 12px; box-shadow: 0px 5px 13px -5px #5c5c5c6b; margin-bottom: -2px; background-color: white; border: 1px solid #dbdbdb; }
|
||||
.test .percent { float: right; }
|
||||
.test .percent-100 { display: none; }
|
||||
.test .result { float: right; }
|
||||
.test .title { max-width: calc(100% - 150px); display: inline-block; }
|
||||
.test .multipler-title { display: inline-block; width: 50px; text-align: right; }
|
||||
.test:last-child { margin-bottom: 15px; border-color: #c1c1c1; }
|
||||
|
||||
.test .result-Sloooow { color: red; }
|
||||
.test .result-Ehh { color: #ad1457; }
|
||||
.test .result-Goodish { color: #ef6c00; }
|
||||
.test .result-Ok { color: #00cf03; }
|
||||
.test .result-Fine { color: #00bcd4; }
|
||||
.test .result-Fast { color: #4b78ff; }
|
||||
.test .result-WOW { color: #9c27b0; }
|
||||
.test .result-Insane { color: #d603f4; }
|
||||
|
||||
.test.summary { margin-top: 20px; text-transform: uppercase; border-left: 10px solid #00ff63; border-color: #00ff63; }
|
||||
.test.error { background-color: #ff2259; color: white; border-color: red; }
|
||||
|
||||
#start { text-align: center }
|
||||
.button {
|
||||
background-color: white; padding: 10px 20px; display: inline-block; border-radius: 5px;
|
||||
text-decoration: none; color: #673AB7; text-transform: uppercase; margin-bottom: 11px; border-bottom: 2px solid #c1bff8;
|
||||
}
|
||||
.button:hover { border-bottom-color: #673AB7; }
|
||||
.button:active { transform: translateY(1px) }
|
||||
small { text-transform: uppercase; opacity: 0.7; color: white; letter-spacing: 1px; }
|
||||
</style>
|
||||
|
||||
<h1 id="h1">Benchmark</h1>
|
||||
<div id="out">
|
||||
<div id="start">
|
||||
<a href="#Start" class="button" id="start_button">Start benchmark</a>
|
||||
<small>(It will take around 20 sec)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="{script_nonce}">
|
||||
function start() {
|
||||
stream("/BenchmarkResult?benchmark_key={benchmark_key}&filter={filter}", document.getElementById("out"));
|
||||
return false;
|
||||
}
|
||||
document.getElementById("start_button").onclick = start
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
5
plugins/Benchmark/plugin_info.json
Normal file
5
plugins/Benchmark/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Benchmark",
|
||||
"description": "Test and benchmark database and cryptographic functions related to ZeroNet.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -21,6 +21,7 @@ with warnings.catch_warnings():
|
|||
|
||||
from util import helper
|
||||
from util import Msgpack
|
||||
from util.Flag import flag
|
||||
import util
|
||||
from .BigfilePiecefield import BigfilePiecefield, BigfilePiecefieldPacked
|
||||
|
||||
|
@ -32,6 +33,7 @@ def importPluginnedClasses():
|
|||
from Content.ContentManager import VerifyError
|
||||
from Config import config
|
||||
|
||||
|
||||
if "upload_nonces" not in locals():
|
||||
upload_nonces = {}
|
||||
|
||||
|
@ -59,13 +61,44 @@ class UiRequestPlugin(object):
|
|||
})
|
||||
|
||||
self.readMultipartHeaders(self.env['wsgi.input']) # Skip http headers
|
||||
result = self.handleBigfileUpload(upload_info, self.env['wsgi.input'].read)
|
||||
return json.dumps(result)
|
||||
|
||||
def actionBigfileUploadWebsocket(self):
|
||||
ws = self.env.get("wsgi.websocket")
|
||||
|
||||
if not ws:
|
||||
self.start_response("400 Bad Request", [])
|
||||
return [b"Not a websocket request!"]
|
||||
|
||||
nonce = self.get.get("upload_nonce")
|
||||
if nonce not in upload_nonces:
|
||||
return self.error403("Upload nonce error.")
|
||||
|
||||
upload_info = upload_nonces[nonce]
|
||||
del upload_nonces[nonce]
|
||||
|
||||
ws.send("poll")
|
||||
|
||||
buffer = b""
|
||||
def read(size):
|
||||
nonlocal buffer
|
||||
while len(buffer) < size:
|
||||
buffer += ws.receive()
|
||||
ws.send("poll")
|
||||
part, buffer = buffer[:size], buffer[size:]
|
||||
return part
|
||||
|
||||
result = self.handleBigfileUpload(upload_info, read)
|
||||
ws.send(json.dumps(result))
|
||||
|
||||
def handleBigfileUpload(self, upload_info, read):
|
||||
site = upload_info["site"]
|
||||
inner_path = upload_info["inner_path"]
|
||||
|
||||
with site.storage.open(inner_path, "wb", create_dirs=True) as out_file:
|
||||
merkle_root, piece_size, piecemap_info = site.content_manager.hashBigfile(
|
||||
self.env['wsgi.input'], upload_info["size"], upload_info["piece_size"], out_file
|
||||
read, upload_info["size"], upload_info["piece_size"], out_file
|
||||
)
|
||||
|
||||
if len(piecemap_info["sha512_pieces"]) == 1: # Small file, don't split
|
||||
|
@ -104,12 +137,12 @@ class UiRequestPlugin(object):
|
|||
|
||||
site.content_manager.contents.loadItem(file_info["content_inner_path"]) # reload cache
|
||||
|
||||
return json.dumps({
|
||||
return {
|
||||
"merkle_root": merkle_root,
|
||||
"piece_num": len(piecemap_info["sha512_pieces"]),
|
||||
"piece_size": piece_size,
|
||||
"inner_path": inner_path
|
||||
})
|
||||
}
|
||||
|
||||
def readMultipartHeaders(self, wsgi_input):
|
||||
found = False
|
||||
|
@ -136,7 +169,7 @@ class UiRequestPlugin(object):
|
|||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
def actionBigfileUploadInit(self, to, inner_path, size):
|
||||
def actionBigfileUploadInit(self, to, inner_path, size, protocol="xhr"):
|
||||
valid_signers = self.site.content_manager.getValidSigners(inner_path)
|
||||
auth_address = self.user.getAuthAddress(self.site.address)
|
||||
if not self.site.settings["own"] and auth_address not in valid_signers:
|
||||
|
@ -160,13 +193,31 @@ class UiWebsocketPlugin(object):
|
|||
"piece_size": piece_size,
|
||||
"piecemap": inner_path + ".piecemap.msgpack"
|
||||
}
|
||||
|
||||
if protocol == "xhr":
|
||||
return {
|
||||
"url": "/ZeroNet-Internal/BigfileUpload?upload_nonce=" + nonce,
|
||||
"piece_size": piece_size,
|
||||
"inner_path": inner_path,
|
||||
"file_relative_path": file_relative_path
|
||||
}
|
||||
elif protocol == "websocket":
|
||||
server_url = self.request.getWsServerUrl()
|
||||
if server_url:
|
||||
proto, host = server_url.split("://")
|
||||
origin = proto.replace("http", "ws") + "://" + host
|
||||
else:
|
||||
origin = "{origin}"
|
||||
return {
|
||||
"url": origin + "/ZeroNet-Internal/BigfileUploadWebsocket?upload_nonce=" + nonce,
|
||||
"piece_size": piece_size,
|
||||
"inner_path": inner_path,
|
||||
"file_relative_path": file_relative_path
|
||||
}
|
||||
else:
|
||||
return {"error": "Unknown protocol"}
|
||||
|
||||
@flag.no_multiuser
|
||||
def actionSiteSetAutodownloadBigfileLimit(self, to, limit):
|
||||
permissions = self.getPermissions(to)
|
||||
if "ADMIN" not in permissions:
|
||||
|
@ -207,14 +258,14 @@ class ContentManagerPlugin(object):
|
|||
file_info = super(ContentManagerPlugin, self).getFileInfo(inner_path, *args, **kwargs)
|
||||
return file_info
|
||||
|
||||
def readFile(self, file_in, size, buff_size=1024 * 64):
|
||||
def readFile(self, read_func, size, buff_size=1024 * 64):
|
||||
part_num = 0
|
||||
recv_left = size
|
||||
|
||||
while 1:
|
||||
part_num += 1
|
||||
read_size = min(buff_size, recv_left)
|
||||
part = file_in.read(read_size)
|
||||
part = read_func(read_size)
|
||||
|
||||
if not part:
|
||||
break
|
||||
|
@ -227,7 +278,7 @@ class ContentManagerPlugin(object):
|
|||
if recv_left <= 0:
|
||||
break
|
||||
|
||||
def hashBigfile(self, file_in, size, piece_size=1024 * 1024, file_out=None):
|
||||
def hashBigfile(self, read_func, size, piece_size=1024 * 1024, file_out=None):
|
||||
self.site.settings["has_bigfile"] = True
|
||||
|
||||
recv = 0
|
||||
|
@ -240,7 +291,7 @@ class ContentManagerPlugin(object):
|
|||
mt.hash_function = CryptHash.sha512t
|
||||
|
||||
part = ""
|
||||
for part in self.readFile(file_in, size):
|
||||
for part in self.readFile(read_func, size):
|
||||
if file_out:
|
||||
file_out.write(part)
|
||||
|
||||
|
@ -306,7 +357,7 @@ class ContentManagerPlugin(object):
|
|||
return super(ContentManagerPlugin, self).hashFile(dir_inner_path, file_relative_path, optional)
|
||||
|
||||
self.log.info("- [HASHING] %s" % file_relative_path)
|
||||
merkle_root, piece_size, piecemap_info = self.hashBigfile(self.site.storage.open(inner_path, "rb"), file_size)
|
||||
merkle_root, piece_size, piecemap_info = self.hashBigfile(self.site.storage.open(inner_path, "rb").read, file_size)
|
||||
if not hash:
|
||||
hash = merkle_root
|
||||
|
||||
|
@ -338,7 +389,11 @@ class ContentManagerPlugin(object):
|
|||
return piecemap
|
||||
|
||||
def verifyPiece(self, inner_path, pos, piece):
|
||||
try:
|
||||
piecemap = self.getPiecemap(inner_path)
|
||||
except OSError as err:
|
||||
raise VerifyError("Unable to download piecemap: %s" % err)
|
||||
|
||||
piece_i = int(pos / piecemap["piece_size"])
|
||||
if CryptHash.sha512sum(piece, format="digest") != piecemap["sha512_pieces"][piece_i]:
|
||||
raise VerifyError("Invalid hash")
|
||||
|
@ -404,9 +459,7 @@ class SiteStoragePlugin(object):
|
|||
def createSparseFile(self, inner_path, size, sha512=None):
|
||||
file_path = self.getPath(inner_path)
|
||||
|
||||
file_dir = os.path.dirname(file_path)
|
||||
if not os.path.isdir(file_dir):
|
||||
os.makedirs(file_dir)
|
||||
self.ensureDir(os.path.dirname(inner_path))
|
||||
|
||||
f = open(file_path, 'wb')
|
||||
f.truncate(min(1024 * 1024 * 5, size)) # Only pre-allocate up to 5MB
|
||||
|
@ -430,9 +483,7 @@ class SiteStoragePlugin(object):
|
|||
file_path = self.getPath(inner_path)
|
||||
|
||||
# Create dir if not exist
|
||||
file_dir = os.path.dirname(file_path)
|
||||
if not os.path.isdir(file_dir):
|
||||
os.makedirs(file_dir)
|
||||
self.ensureDir(os.path.dirname(inner_path))
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
file_info = self.site.content_manager.getFileInfo(inner_path)
|
||||
|
@ -751,6 +802,11 @@ class SitePlugin(object):
|
|||
|
||||
inner_path = inner_path.replace("|all", "")
|
||||
file_info = self.needFileInfo(inner_path)
|
||||
|
||||
# Use default function to download non-optional file
|
||||
if "piece_size" not in file_info:
|
||||
return super(SitePlugin, self).needFile(inner_path, *args, **kwargs)
|
||||
|
||||
file_size = file_info["size"]
|
||||
piece_size = file_info["piece_size"]
|
||||
|
||||
|
|
|
@ -134,7 +134,7 @@ class TestBigfile:
|
|||
peer_client = site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"]
|
||||
assert not bad_files
|
||||
|
@ -172,7 +172,7 @@ class TestBigfile:
|
|||
site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
# Open virtual file
|
||||
assert not site_temp.storage.isFile(inner_path)
|
||||
|
@ -255,7 +255,7 @@ class TestBigfile:
|
|||
site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
# Download second block
|
||||
with site_temp.storage.openBigfile(inner_path) as f:
|
||||
|
@ -380,7 +380,7 @@ class TestBigfile:
|
|||
site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
# Open virtual file
|
||||
assert not site_temp.storage.isFile(inner_path)
|
||||
|
@ -417,7 +417,7 @@ class TestBigfile:
|
|||
site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
# Open virtual file
|
||||
assert not site_temp.storage.isFile(inner_path)
|
||||
|
@ -453,7 +453,7 @@ class TestBigfile:
|
|||
site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
# Open virtual file
|
||||
assert not site_temp.storage.isFile(inner_path)
|
||||
|
@ -482,7 +482,7 @@ class TestBigfile:
|
|||
site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
# Open virtual file
|
||||
assert not site_temp.storage.isFile(inner_path)
|
||||
|
@ -507,7 +507,7 @@ class TestBigfile:
|
|||
site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
with Spy.Spy(FileRequest, "route") as requests:
|
||||
site_temp.needFile("%s|%s-%s" % (inner_path, 0, 1 * self.piece_size))
|
||||
|
@ -529,7 +529,7 @@ class TestBigfile:
|
|||
with Spy.Spy(FileRequest, "route") as requests:
|
||||
site.publish()
|
||||
time.sleep(0.1)
|
||||
site_temp.download(blind_includes=True).join(timeout=5) # Wait for download
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Wait for download
|
||||
|
||||
assert len([req[1] for req in requests if req[1] == "streamFile"]) == 0
|
||||
|
||||
|
@ -563,7 +563,7 @@ class TestBigfile:
|
|||
site_temp.addPeer(file_server.ip, 1544)
|
||||
|
||||
# Download site
|
||||
site_temp.download(blind_includes=True).join(timeout=5)
|
||||
site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10)
|
||||
|
||||
if "piecemap" in site.content_manager.getFileInfo(inner_path): # Bigfile
|
||||
site_temp.needFile(inner_path + "|all")
|
||||
|
|
|
@ -119,6 +119,8 @@ class ChartCollector(object):
|
|||
value = collector(peers)
|
||||
else:
|
||||
value = collector()
|
||||
except ValueError:
|
||||
value = None
|
||||
except Exception as err:
|
||||
self.log.info("Collector %s error: %s" % (key, err))
|
||||
value = None
|
||||
|
@ -146,15 +148,14 @@ class ChartCollector(object):
|
|||
|
||||
s = time.time()
|
||||
cur = self.db.getCursor()
|
||||
cur.cursor.executemany("INSERT INTO data (type_id, value, date_added) VALUES (?, ?, ?)", values)
|
||||
cur.close()
|
||||
cur.executemany("INSERT INTO data (type_id, value, date_added) VALUES (?, ?, ?)", values)
|
||||
self.log.debug("Global collectors inserted in %.3fs" % (time.time() - s))
|
||||
|
||||
def collectSites(self, sites, collectors, last_values):
|
||||
now = int(time.time())
|
||||
s = time.time()
|
||||
values = []
|
||||
for address, site in sites.items():
|
||||
for address, site in list(sites.items()):
|
||||
site_datas = self.collectDatas(collectors, last_values["site:%s" % address], site)
|
||||
for key, value in site_datas.items():
|
||||
values.append((self.db.getTypeId(key), self.db.getSiteId(address), value, now))
|
||||
|
@ -163,8 +164,7 @@ class ChartCollector(object):
|
|||
|
||||
s = time.time()
|
||||
cur = self.db.getCursor()
|
||||
cur.cursor.executemany("INSERT INTO data (type_id, site_id, value, date_added) VALUES (?, ?, ?, ?)", values)
|
||||
cur.close()
|
||||
cur.executemany("INSERT INTO data (type_id, site_id, value, date_added) VALUES (?, ?, ?, ?)", values)
|
||||
self.log.debug("Site collectors inserted in %.3fs" % (time.time() - s))
|
||||
|
||||
def collector(self):
|
||||
|
|
|
@ -48,15 +48,15 @@ class ChartDb(Db):
|
|||
|
||||
def getTypeId(self, name):
|
||||
if name not in self.types:
|
||||
self.execute("INSERT INTO type ?", {"name": name})
|
||||
self.types[name] = self.cur.cursor.lastrowid
|
||||
res = self.execute("INSERT INTO type ?", {"name": name})
|
||||
self.types[name] = res.lastrowid
|
||||
|
||||
return self.types[name]
|
||||
|
||||
def getSiteId(self, address):
|
||||
if address not in self.sites:
|
||||
self.execute("INSERT INTO site ?", {"address": address})
|
||||
self.sites[address] = self.cur.cursor.lastrowid
|
||||
res = self.execute("INSERT INTO site ?", {"address": address})
|
||||
self.sites[address] = res.lastrowid
|
||||
|
||||
return self.sites[address]
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import gevent
|
|||
|
||||
from Config import config
|
||||
from util import helper
|
||||
from util.Flag import flag
|
||||
from Plugin import PluginManager
|
||||
from .ChartDb import ChartDb
|
||||
from .ChartCollector import ChartCollector
|
||||
|
@ -28,10 +29,8 @@ class SiteManagerPlugin(object):
|
|||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
@flag.admin
|
||||
def actionChartDbQuery(self, to, query, params=None):
|
||||
if not "ADMIN" in self.permissions:
|
||||
return {"error": "No permission"}
|
||||
|
||||
if config.debug or config.verbose:
|
||||
s = time.time()
|
||||
rows = []
|
||||
|
@ -49,10 +48,8 @@ class UiWebsocketPlugin(object):
|
|||
self.log.debug("Slow query: %s (%.3fs)" % (query, time.time() - s))
|
||||
return rows
|
||||
|
||||
@flag.admin
|
||||
def actionChartGetPeerLocations(self, to):
|
||||
if not "ADMIN" in self.permissions:
|
||||
return {"error": "No permission"}
|
||||
|
||||
peers = {}
|
||||
for site in self.server.sites.values():
|
||||
peers.update(site.peers)
|
||||
|
|
5
plugins/Chart/plugin_info.json
Normal file
5
plugins/Chart/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Chart",
|
||||
"description": "Collect and provide stats of client information.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -1,17 +1,20 @@
|
|||
import time
|
||||
import re
|
||||
import html
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Translate import Translate
|
||||
from Config import config
|
||||
from util.Flag import flag
|
||||
|
||||
from .ContentFilterStorage import ContentFilterStorage
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate("plugins/ContentFilter/languages/")
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
@PluginManager.registerTo("SiteManager")
|
||||
|
@ -21,6 +24,24 @@ class SiteManagerPlugin(object):
|
|||
super(SiteManagerPlugin, self).load(*args, **kwargs)
|
||||
filter_storage = ContentFilterStorage(site_manager=self)
|
||||
|
||||
def add(self, address, *args, **kwargs):
|
||||
should_ignore_block = kwargs.get("ignore_block") or kwargs.get("settings")
|
||||
if should_ignore_block:
|
||||
block_details = None
|
||||
elif filter_storage.isSiteblocked(address):
|
||||
block_details = filter_storage.getSiteblockDetails(address)
|
||||
else:
|
||||
address_hashed = filter_storage.getSiteAddressHashed(address)
|
||||
if filter_storage.isSiteblocked(address_hashed):
|
||||
block_details = filter_storage.getSiteblockDetails(address_hashed)
|
||||
else:
|
||||
block_details = None
|
||||
|
||||
if block_details:
|
||||
raise Exception("Site blocked: %s" % html.escape(block_details.get("reason", "unknown reason")))
|
||||
else:
|
||||
return super(SiteManagerPlugin, self).add(address, *args, **kwargs)
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
|
@ -33,6 +54,7 @@ class UiWebsocketPlugin(object):
|
|||
filter_storage.changeDbs(auth_address, "remove")
|
||||
self.response(to, "ok")
|
||||
|
||||
@flag.no_multiuser
|
||||
def actionMuteAdd(self, to, auth_address, cert_user_id, reason):
|
||||
if "ADMIN" in self.getPermissions(to):
|
||||
self.cbMuteAdd(to, auth_address, cert_user_id, reason)
|
||||
|
@ -43,50 +65,73 @@ class UiWebsocketPlugin(object):
|
|||
lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, reason)
|
||||
)
|
||||
|
||||
@flag.no_multiuser
|
||||
def cbMuteRemove(self, to, auth_address):
|
||||
del filter_storage.file_content["mutes"][auth_address]
|
||||
filter_storage.save()
|
||||
filter_storage.changeDbs(auth_address, "load")
|
||||
self.response(to, "ok")
|
||||
|
||||
@flag.no_multiuser
|
||||
def actionMuteRemove(self, to, auth_address):
|
||||
if "ADMIN" in self.getPermissions(to):
|
||||
self.cbMuteRemove(to, auth_address)
|
||||
else:
|
||||
cert_user_id = html.escape(filter_storage.file_content["mutes"][auth_address]["cert_user_id"])
|
||||
self.cmd(
|
||||
"confirm",
|
||||
[_["Unmute <b>%s</b>?"] % html.escape(filter_storage.file_content["mutes"][auth_address]["cert_user_id"]), _["Unmute"]],
|
||||
[_["Unmute <b>%s</b>?"] % cert_user_id, _["Unmute"]],
|
||||
lambda res: self.cbMuteRemove(to, auth_address)
|
||||
)
|
||||
|
||||
@flag.admin
|
||||
def actionMuteList(self, to):
|
||||
if "ADMIN" in self.getPermissions(to):
|
||||
self.response(to, filter_storage.file_content["mutes"])
|
||||
else:
|
||||
return self.response(to, {"error": "Forbidden: Only ADMIN sites can list mutes"})
|
||||
|
||||
# Siteblock
|
||||
@flag.no_multiuser
|
||||
@flag.admin
|
||||
def actionSiteblockIgnoreAddSite(self, to, site_address):
|
||||
if site_address in filter_storage.site_manager.sites:
|
||||
return {"error": "Site already added"}
|
||||
else:
|
||||
if filter_storage.site_manager.need(site_address, ignore_block=True):
|
||||
return "ok"
|
||||
else:
|
||||
return {"error": "Invalid address"}
|
||||
|
||||
@flag.no_multiuser
|
||||
@flag.admin
|
||||
def actionSiteblockAdd(self, to, site_address, reason=None):
|
||||
if "ADMIN" not in self.getPermissions(to):
|
||||
return self.response(to, {"error": "Forbidden: Only ADMIN sites can add to blocklist"})
|
||||
filter_storage.file_content["siteblocks"][site_address] = {"date_added": time.time(), "reason": reason}
|
||||
filter_storage.save()
|
||||
self.response(to, "ok")
|
||||
|
||||
@flag.no_multiuser
|
||||
@flag.admin
|
||||
def actionSiteblockRemove(self, to, site_address):
|
||||
if "ADMIN" not in self.getPermissions(to):
|
||||
return self.response(to, {"error": "Forbidden: Only ADMIN sites can remove from blocklist"})
|
||||
del filter_storage.file_content["siteblocks"][site_address]
|
||||
filter_storage.save()
|
||||
self.response(to, "ok")
|
||||
|
||||
@flag.admin
|
||||
def actionSiteblockList(self, to):
|
||||
if "ADMIN" in self.getPermissions(to):
|
||||
self.response(to, filter_storage.file_content["siteblocks"])
|
||||
|
||||
@flag.admin
|
||||
def actionSiteblockGet(self, to, site_address):
|
||||
if filter_storage.isSiteblocked(site_address):
|
||||
res = filter_storage.getSiteblockDetails(site_address)
|
||||
else:
|
||||
return self.response(to, {"error": "Forbidden: Only ADMIN sites can list blocklists"})
|
||||
site_address_hashed = filter_storage.getSiteAddressHashed(site_address)
|
||||
if filter_storage.isSiteblocked(site_address_hashed):
|
||||
res = filter_storage.getSiteblockDetails(site_address_hashed)
|
||||
else:
|
||||
res = {"error": "Site block not found"}
|
||||
self.response(to, res)
|
||||
|
||||
# Include
|
||||
@flag.no_multiuser
|
||||
def actionFilterIncludeAdd(self, to, inner_path, description=None, address=None):
|
||||
if address:
|
||||
if "ADMIN" not in self.getPermissions(to):
|
||||
|
@ -118,6 +163,7 @@ class UiWebsocketPlugin(object):
|
|||
filter_storage.includeAdd(address, inner_path, description)
|
||||
self.response(to, "ok")
|
||||
|
||||
@flag.no_multiuser
|
||||
def actionFilterIncludeRemove(self, to, inner_path, address=None):
|
||||
if address:
|
||||
if "ADMIN" not in self.getPermissions(to):
|
||||
|
@ -177,7 +223,7 @@ class SiteStoragePlugin(object):
|
|||
@PluginManager.registerTo("UiRequest")
|
||||
class UiRequestPlugin(object):
|
||||
def actionWrapper(self, path, extra_headers=None):
|
||||
match = re.match("/(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
|
||||
match = re.match(r"/(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
|
||||
if not match:
|
||||
return False
|
||||
address = match.group("address")
|
||||
|
@ -185,15 +231,15 @@ class UiRequestPlugin(object):
|
|||
if self.server.site_manager.get(address): # Site already exists
|
||||
return super(UiRequestPlugin, self).actionWrapper(path, extra_headers)
|
||||
|
||||
if self.server.site_manager.isDomain(address):
|
||||
address = self.server.site_manager.resolveDomain(address)
|
||||
if self.isDomain(address):
|
||||
address = self.resolveDomain(address)
|
||||
|
||||
if address:
|
||||
address_sha256 = "0x" + hashlib.sha256(address.encode("utf8")).hexdigest()
|
||||
address_hashed = filter_storage.getSiteAddressHashed(address)
|
||||
else:
|
||||
address_sha256 = None
|
||||
address_hashed = None
|
||||
|
||||
if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_sha256):
|
||||
if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_hashed):
|
||||
site = self.server.site_manager.get(config.homepage)
|
||||
if not extra_headers:
|
||||
extra_headers = {}
|
||||
|
@ -210,7 +256,7 @@ class UiRequestPlugin(object):
|
|||
|
||||
def actionUiMedia(self, path, *args, **kwargs):
|
||||
if path.startswith("/uimedia/plugins/contentfilter/"):
|
||||
file_path = path.replace("/uimedia/plugins/contentfilter/", "plugins/ContentFilter/media/")
|
||||
file_path = path.replace("/uimedia/plugins/contentfilter/", plugin_dir + "/media/")
|
||||
return self.actionFile(file_path)
|
||||
else:
|
||||
return super(UiRequestPlugin, self).actionUiMedia(path)
|
||||
|
|
|
@ -3,12 +3,14 @@ import json
|
|||
import logging
|
||||
import collections
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
from Debug import Debug
|
||||
from Plugin import PluginManager
|
||||
from Config import config
|
||||
from util import helper
|
||||
|
||||
|
||||
class ContentFilterStorage(object):
|
||||
def __init__(self, site_manager):
|
||||
self.log = logging.getLogger("ContentFilterStorage")
|
||||
|
@ -114,12 +116,34 @@ class ContentFilterStorage(object):
|
|||
else:
|
||||
return False
|
||||
|
||||
def getSiteAddressHashed(self, address):
|
||||
return "0x" + hashlib.sha256(address.encode("ascii")).hexdigest()
|
||||
|
||||
def isSiteblocked(self, address):
|
||||
if address in self.file_content["siteblocks"] or address in self.include_filters["siteblocks"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def getSiteblockDetails(self, address):
|
||||
details = self.file_content["siteblocks"].get(address)
|
||||
if not details:
|
||||
address_sha256 = self.getSiteAddressHashed(address)
|
||||
details = self.file_content["siteblocks"].get(address_sha256)
|
||||
|
||||
if not details:
|
||||
includes = self.file_content.get("includes", {}).values()
|
||||
for include in includes:
|
||||
include_site = self.site_manager.get(include["address"])
|
||||
if not include_site:
|
||||
continue
|
||||
content = include_site.storage.loadJson(include["inner_path"])
|
||||
details = content.get("siteblocks").get(address)
|
||||
if details:
|
||||
details["include"] = include
|
||||
break
|
||||
|
||||
return details
|
||||
|
||||
# Search and remove or readd files of an user
|
||||
def changeDbs(self, auth_address, action):
|
||||
self.log.debug("Mute action %s on user %s" % (action, auth_address))
|
||||
|
|
|
@ -62,25 +62,7 @@ class Page extends ZeroFrame {
|
|||
}
|
||||
|
||||
async updateSiteblockDetails(address) {
|
||||
var address_sha256 = await sha256hex(address)
|
||||
var blocks = await this.cmdp("siteblockList")
|
||||
if (blocks[address] || blocks[address_sha256]) {
|
||||
block = blocks[address]
|
||||
} else {
|
||||
var includes = await this.cmdp("filterIncludeList", {all_sites: true, filters: true})
|
||||
for (let include of includes) {
|
||||
if (include["siteblocks"][address]) {
|
||||
var block = include["siteblocks"][address]
|
||||
block["include"] = include
|
||||
}
|
||||
if (include["siteblocks"][address_sha256]) {
|
||||
var block = include["siteblocks"][address_sha256]
|
||||
block["include"] = include
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.blocks = blocks
|
||||
var block = await this.cmdp("siteblockGet", address)
|
||||
var reason = block["reason"]
|
||||
if (!reason) reason = "Unknown reason"
|
||||
var date = new Date(block["date_added"] * 1000)
|
||||
|
@ -95,7 +77,7 @@ class Page extends ZeroFrame {
|
|||
document.getElementById("visit").style.opacity = "1"
|
||||
document.getElementById("visit").onclick = () => {
|
||||
if (block["include"])
|
||||
this.cmd("siteAdd", address, () => { this.cmd("wrapperReload") })
|
||||
this.cmd("siteblockIgnoreAddSite", address, () => { this.cmd("wrapperReload") })
|
||||
else
|
||||
this.cmd("siteblockRemove", address, () => { this.cmd("wrapperReload") })
|
||||
}
|
||||
|
|
5
plugins/ContentFilter/plugin_info.json
Normal file
5
plugins/ContentFilter/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "ContentFilter",
|
||||
"description": "Manage site and user block list.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
import re
|
||||
import html
|
||||
import copy
|
||||
import os
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Translate import Translate
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate("plugins/Cors/languages/")
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
def getCorsPath(site, inner_path):
|
||||
|
@ -27,7 +32,11 @@ class UiWebsocketPlugin(object):
|
|||
if super(UiWebsocketPlugin, self).hasSitePermission(address, cmd=cmd):
|
||||
return True
|
||||
|
||||
if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in ["fileGet", "fileList", "dirList", "fileRules", "optionalFileInfo", "fileQuery", "dbQuery", "userGetSettings", "siteInfo"]:
|
||||
allowed_commands = [
|
||||
"fileGet", "fileList", "dirList", "fileRules", "optionalFileInfo",
|
||||
"fileQuery", "dbQuery", "userGetSettings", "siteInfo"
|
||||
]
|
||||
if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in allowed_commands:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
@ -99,6 +108,6 @@ class UiRequestPlugin(object):
|
|||
site = self.server.sites[path_parts["address"]]
|
||||
try:
|
||||
path_parts["address"], path_parts["inner_path"] = getCorsPath(site, path_parts["inner_path"])
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
return path_parts
|
||||
|
|
5
plugins/Cors/plugin_info.json
Normal file
5
plugins/Cors/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Cors",
|
||||
"description": "Cross site resource read.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -1,59 +1,58 @@
|
|||
import hashlib
|
||||
import base64
|
||||
|
||||
import lib.pybitcointools as btctools
|
||||
|
||||
ecc_cache = {}
|
||||
import struct
|
||||
from lib import sslcrypto
|
||||
from Crypt import Crypt
|
||||
|
||||
|
||||
def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'):
|
||||
import pyelliptic
|
||||
pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey))
|
||||
curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey_openssl)
|
||||
if ephemcurve is None:
|
||||
ephemcurve = curve
|
||||
ephem = pyelliptic.ECC(curve=ephemcurve)
|
||||
key = hashlib.sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest()
|
||||
key_e, key_m = key[:32], key[32:]
|
||||
pubkey = ephem.get_pubkey()
|
||||
iv = pyelliptic.OpenSSL.rand(pyelliptic.OpenSSL.get_cipher(ciphername).get_blocksize())
|
||||
ctx = pyelliptic.Cipher(key_e, iv, 1, ciphername)
|
||||
ciphertext = iv + pubkey + ctx.ciphering(data)
|
||||
mac = pyelliptic.hmac_sha256(key_m, ciphertext)
|
||||
return key_e, ciphertext + mac
|
||||
curve = sslcrypto.ecc.get_curve("secp256k1")
|
||||
|
||||
|
||||
def eciesEncrypt(data, pubkey, ciphername="aes-256-cbc"):
|
||||
ciphertext, key_e = curve.encrypt(
|
||||
data,
|
||||
base64.b64decode(pubkey),
|
||||
algo=ciphername,
|
||||
derivation="sha512",
|
||||
return_aes_key=True
|
||||
)
|
||||
return key_e, ciphertext
|
||||
|
||||
|
||||
@Crypt.thread_pool_crypt.wrap
|
||||
def eciesDecryptMulti(encrypted_datas, privatekey):
|
||||
texts = [] # Decoded texts
|
||||
for encrypted_data in encrypted_datas:
|
||||
try:
|
||||
text = eciesDecrypt(encrypted_data, privatekey).decode("utf8")
|
||||
texts.append(text)
|
||||
except Exception:
|
||||
texts.append(None)
|
||||
return texts
|
||||
|
||||
|
||||
def eciesDecrypt(ciphertext, privatekey):
|
||||
return curve.decrypt(base64.b64decode(ciphertext), curve.wif_to_private(privatekey), derivation="sha512")
|
||||
|
||||
|
||||
def decodePubkey(pubkey):
|
||||
i = 0
|
||||
curve = struct.unpack('!H', pubkey[i:i + 2])[0]
|
||||
i += 2
|
||||
tmplen = struct.unpack('!H', pubkey[i:i + 2])[0]
|
||||
i += 2
|
||||
pubkey_x = pubkey[i:i + tmplen]
|
||||
i += tmplen
|
||||
tmplen = struct.unpack('!H', pubkey[i:i + 2])[0]
|
||||
i += 2
|
||||
pubkey_y = pubkey[i:i + tmplen]
|
||||
i += tmplen
|
||||
return curve, pubkey_x, pubkey_y, i
|
||||
|
||||
def eciesDecrypt(encrypted_data, privatekey):
|
||||
ecc_key = getEcc(privatekey)
|
||||
return ecc_key.decrypt(base64.b64decode(encrypted_data))
|
||||
|
||||
def split(encrypted):
|
||||
iv = encrypted[0:16]
|
||||
ciphertext = encrypted[16 + 70:-32]
|
||||
curve, pubkey_x, pubkey_y, i = decodePubkey(encrypted[16:])
|
||||
ciphertext = encrypted[16 + i:-32]
|
||||
|
||||
return iv, ciphertext
|
||||
|
||||
|
||||
def getEcc(privatekey=None):
|
||||
import pyelliptic
|
||||
global ecc_cache
|
||||
if privatekey not in ecc_cache:
|
||||
if privatekey:
|
||||
publickey_bin = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin")
|
||||
publickey_openssl = toOpensslPublickey(publickey_bin)
|
||||
privatekey_openssl = toOpensslPrivatekey(privatekey)
|
||||
ecc_cache[privatekey] = pyelliptic.ECC(curve='secp256k1', privkey=privatekey_openssl, pubkey=publickey_openssl)
|
||||
else:
|
||||
ecc_cache[None] = pyelliptic.ECC()
|
||||
return ecc_cache[privatekey]
|
||||
|
||||
|
||||
def toOpensslPrivatekey(privatekey):
|
||||
privatekey_bin = btctools.encode_privkey(privatekey, "bin")
|
||||
return b'\x02\xca\x00\x20' + privatekey_bin
|
||||
|
||||
|
||||
def toOpensslPublickey(publickey):
|
||||
publickey_bin = btctools.encode_pubkey(publickey, "bin")
|
||||
publickey_bin = publickey_bin[1:]
|
||||
publickey_openssl = b'\x02\xca\x00 ' + publickey_bin[:32] + b'\x00 ' + publickey_bin[32:]
|
||||
return publickey_openssl
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import base64
|
||||
import os
|
||||
|
||||
import gevent
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Crypt import CryptBitcoin, CryptHash
|
||||
import lib.pybitcointools as btctools
|
||||
from Config import config
|
||||
import sslcrypto
|
||||
|
||||
from . import CryptMessage
|
||||
|
||||
curve = sslcrypto.ecc.get_curve("secp256k1")
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
def eciesDecrypt(self, encrypted, privatekey):
|
||||
back = CryptMessage.getEcc(privatekey).decrypt(encrypted)
|
||||
return back.decode("utf8")
|
||||
|
||||
# - Actions -
|
||||
|
||||
# Returns user's public key unique to site
|
||||
# Return: Public key
|
||||
def actionUserPublickey(self, to, index=0):
|
||||
publickey = self.user.getEncryptPublickey(self.site.address, index)
|
||||
self.response(to, publickey)
|
||||
self.response(to, self.user.getEncryptPublickey(self.site.address, index))
|
||||
|
||||
# Encrypt a text using the publickey or user's sites unique publickey
|
||||
# Return: Encrypted text using base64 encoding
|
||||
|
@ -44,13 +44,7 @@ class UiWebsocketPlugin(object):
|
|||
else:
|
||||
encrypted_texts = [param]
|
||||
|
||||
texts = [] # Decoded texts
|
||||
for encrypted_text in encrypted_texts:
|
||||
try:
|
||||
text = CryptMessage.eciesDecrypt(encrypted_text, privatekey).decode("utf8")
|
||||
texts.append(text)
|
||||
except Exception as err:
|
||||
texts.append(None)
|
||||
texts = CryptMessage.eciesDecryptMulti(encrypted_texts, privatekey)
|
||||
|
||||
if type(param) == list:
|
||||
self.response(to, texts)
|
||||
|
@ -59,23 +53,16 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
# Encrypt a text using AES
|
||||
# Return: Iv, AES key, Encrypted text
|
||||
def actionAesEncrypt(self, to, text, key=None, iv=None):
|
||||
import pyelliptic
|
||||
|
||||
def actionAesEncrypt(self, to, text, key=None):
|
||||
if key:
|
||||
key = base64.b64decode(key)
|
||||
else:
|
||||
key = os.urandom(32)
|
||||
|
||||
if iv: # Generate new AES key if not definied
|
||||
iv = base64.b64decode(iv)
|
||||
else:
|
||||
iv = pyelliptic.Cipher.gen_IV('aes-256-cbc')
|
||||
key = sslcrypto.aes.new_key()
|
||||
|
||||
if text:
|
||||
encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(text.encode("utf8"))
|
||||
encrypted, iv = sslcrypto.aes.encrypt(text.encode("utf8"), key)
|
||||
else:
|
||||
encrypted = b""
|
||||
encrypted, iv = b"", b""
|
||||
|
||||
res = [base64.b64encode(item).decode("utf8") for item in [key, iv, encrypted]]
|
||||
self.response(to, res)
|
||||
|
@ -83,8 +70,6 @@ class UiWebsocketPlugin(object):
|
|||
# Decrypt a text using AES
|
||||
# Return: Decrypted text
|
||||
def actionAesDecrypt(self, to, *args):
|
||||
import pyelliptic
|
||||
|
||||
if len(args) == 3: # Single decrypt
|
||||
encrypted_texts = [(args[0], args[1])]
|
||||
keys = [args[2]]
|
||||
|
@ -97,9 +82,8 @@ class UiWebsocketPlugin(object):
|
|||
iv = base64.b64decode(iv)
|
||||
text = None
|
||||
for key in keys:
|
||||
ctx = pyelliptic.Cipher(base64.b64decode(key), iv, 0, ciphername='aes-256-cbc')
|
||||
try:
|
||||
decrypted = ctx.ciphering(encrypted_text)
|
||||
decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, base64.b64decode(key))
|
||||
if decrypted and decrypted.decode("utf8"): # Valid text decoded
|
||||
text = decrypted.decode("utf8")
|
||||
except Exception as err:
|
||||
|
@ -126,12 +110,11 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
# Gets the publickey of a given privatekey
|
||||
def actionEccPrivToPub(self, to, privatekey):
|
||||
self.response(to, btctools.privtopub(privatekey))
|
||||
self.response(to, curve.private_to_public(curve.wif_to_private(privatekey)))
|
||||
|
||||
# Gets the address of a given publickey
|
||||
def actionEccPubToAddr(self, to, publickey):
|
||||
address = btctools.pubtoaddr(btctools.decode_pubkey(publickey))
|
||||
self.response(to, address)
|
||||
self.response(to, curve.public_to_address(bytes.fromhex(publickey)))
|
||||
|
||||
|
||||
@PluginManager.registerTo("User")
|
||||
|
@ -167,6 +150,76 @@ class UserPlugin(object):
|
|||
|
||||
if "encrypt_publickey_%s" % index not in site_data:
|
||||
privatekey = self.getEncryptPrivatekey(address, param_index)
|
||||
publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed")
|
||||
publickey = curve.private_to_public(curve.wif_to_private(privatekey))
|
||||
site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey).decode("utf8")
|
||||
return site_data["encrypt_publickey_%s" % index]
|
||||
|
||||
|
||||
@PluginManager.registerTo("Actions")
|
||||
class ActionsPlugin:
|
||||
publickey = "A3HatibU4S6eZfIQhVs2u7GLN5G9wXa9WwlkyYIfwYaj"
|
||||
privatekey = "5JBiKFYBm94EUdbxtnuLi6cvNcPzcKymCUHBDf2B6aq19vvG3rL"
|
||||
utf8_text = '\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9p'
|
||||
|
||||
def getBenchmarkTests(self, online=False):
|
||||
if hasattr(super(), "getBenchmarkTests"):
|
||||
tests = super().getBenchmarkTests(online)
|
||||
else:
|
||||
tests = []
|
||||
|
||||
aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) # Warm-up
|
||||
tests.extend([
|
||||
{"func": self.testCryptEciesEncrypt, "kwargs": {}, "num": 100, "time_standard": 1.2},
|
||||
{"func": self.testCryptEciesDecrypt, "kwargs": {}, "num": 500, "time_standard": 1.3},
|
||||
{"func": self.testCryptEciesDecryptMulti, "kwargs": {}, "num": 5, "time_standard": 0.68},
|
||||
{"func": self.testCryptAesEncrypt, "kwargs": {}, "num": 10000, "time_standard": 0.27},
|
||||
{"func": self.testCryptAesDecrypt, "kwargs": {}, "num": 10000, "time_standard": 0.25}
|
||||
])
|
||||
return tests
|
||||
|
||||
def testCryptEciesEncrypt(self, num_run=1):
|
||||
for i in range(num_run):
|
||||
aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey)
|
||||
assert len(aes_key) == 32
|
||||
yield "."
|
||||
|
||||
def testCryptEciesDecrypt(self, num_run=1):
|
||||
aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey)
|
||||
for i in range(num_run):
|
||||
assert len(aes_key) == 32
|
||||
decrypted = CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey)
|
||||
assert decrypted == self.utf8_text.encode("utf8"), "%s != %s" % (decrypted, self.utf8_text.encode("utf8"))
|
||||
yield "."
|
||||
|
||||
def testCryptEciesDecryptMulti(self, num_run=1):
|
||||
yield "x 100 (%s threads) " % config.threads_crypt
|
||||
aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey)
|
||||
|
||||
threads = []
|
||||
for i in range(num_run):
|
||||
assert len(aes_key) == 32
|
||||
threads.append(gevent.spawn(
|
||||
CryptMessage.eciesDecryptMulti, [base64.b64encode(encrypted)] * 100, self.privatekey
|
||||
))
|
||||
|
||||
for thread in threads:
|
||||
res = thread.get()
|
||||
assert res[0] == self.utf8_text, "%s != %s" % (res[0], self.utf8_text)
|
||||
assert res[0] == res[-1], "%s != %s" % (res[0], res[-1])
|
||||
yield "."
|
||||
gevent.joinall(threads)
|
||||
|
||||
def testCryptAesEncrypt(self, num_run=1):
|
||||
for i in range(num_run):
|
||||
key = os.urandom(32)
|
||||
encrypted = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key)
|
||||
yield "."
|
||||
|
||||
def testCryptAesDecrypt(self, num_run=1):
|
||||
key = os.urandom(32)
|
||||
encrypted_text, iv = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key)
|
||||
|
||||
for i in range(num_run):
|
||||
decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, key).decode("utf8")
|
||||
assert decrypted == self.utf8_text
|
||||
yield "."
|
||||
|
|
|
@ -18,13 +18,10 @@ class TestCrypt:
|
|||
assert len(aes_key) == 32
|
||||
# assert len(encrypted) == 134 + int(len(text) / 16) * 16 # Not always true
|
||||
|
||||
ecc = CryptMessage.getEcc(self.privatekey)
|
||||
assert ecc.decrypt(encrypted) == text_repeated
|
||||
assert CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) == text_repeated
|
||||
|
||||
def testDecryptEcies(self, user):
|
||||
encrypted = base64.b64decode(self.ecies_encrypted_text)
|
||||
ecc = CryptMessage.getEcc(self.privatekey)
|
||||
assert ecc.decrypt(encrypted) == b"hello"
|
||||
assert CryptMessage.eciesDecrypt(self.ecies_encrypted_text, self.privatekey) == b"hello"
|
||||
|
||||
def testPublickey(self, ui_websocket):
|
||||
pub = ui_websocket.testAction("UserPublickey", 0)
|
||||
|
@ -47,26 +44,25 @@ class TestCrypt:
|
|||
assert pub1 != pub2
|
||||
|
||||
def testEcies(self, ui_websocket):
|
||||
ui_websocket.actionUserPublickey(0, 0)
|
||||
pub = ui_websocket.ws.getResult()
|
||||
pub = ui_websocket.testAction("UserPublickey")
|
||||
|
||||
ui_websocket.actionEciesEncrypt(0, "hello", pub)
|
||||
encrypted = ui_websocket.ws.getResult()
|
||||
encrypted = ui_websocket.testAction("EciesEncrypt", "hello", pub)
|
||||
assert len(encrypted) == 180
|
||||
|
||||
# Don't allow decrypt using other privatekey index
|
||||
ui_websocket.actionEciesDecrypt(0, encrypted, 123)
|
||||
decrypted = ui_websocket.ws.getResult()
|
||||
decrypted = ui_websocket.testAction("EciesDecrypt", encrypted, 123)
|
||||
assert decrypted != "hello"
|
||||
|
||||
# Decrypt using correct privatekey
|
||||
ui_websocket.actionEciesDecrypt(0, encrypted)
|
||||
decrypted = ui_websocket.ws.getResult()
|
||||
decrypted = ui_websocket.testAction("EciesDecrypt", encrypted)
|
||||
assert decrypted == "hello"
|
||||
|
||||
# Decrypt incorrect text
|
||||
decrypted = ui_websocket.testAction("EciesDecrypt", "baad")
|
||||
assert decrypted is None
|
||||
|
||||
# Decrypt batch
|
||||
ui_websocket.actionEciesDecrypt(0, [encrypted, "baad", encrypted])
|
||||
decrypted = ui_websocket.ws.getResult()
|
||||
decrypted = ui_websocket.testAction("EciesDecrypt", [encrypted, "baad", encrypted])
|
||||
assert decrypted == ["hello", None, "hello"]
|
||||
|
||||
def testEciesUtf8(self, ui_websocket):
|
||||
|
@ -91,6 +87,21 @@ class TestCrypt:
|
|||
ui_websocket.actionAesDecrypt(0, base64.b64encode(aes_iv), base64.b64encode(aes_encrypted), aes_key)
|
||||
assert ui_websocket.ws.getResult() == "hello"
|
||||
|
||||
def testEciesAesLongpubkey(self, ui_websocket):
|
||||
privatekey = "5HwVS1bTFnveNk9EeGaRenWS1QFzLFb5kuncNbiY3RiHZrVR6ok"
|
||||
|
||||
ecies_encrypted, aes_key = ["lWiXfEikIjw1ac3J/RaY/gLKACALRUfksc9rXYRFyKDSaxhwcSFBYCgAdIyYlY294g/6VgAf/68PYBVMD3xKH1n7Zbo+ge8b4i/XTKmCZRJvy0eutMKWckYCMVcxgIYNa/ZL1BY1kvvH7omgzg1wBraoLfdbNmVtQgdAZ9XS8PwRy6OB2Q==", "Rvlf7zsMuBFHZIGHcbT1rb4If+YTmsWDv6kGwcvSeMM="]
|
||||
|
||||
# Decrypt using Ecies
|
||||
ui_websocket.actionEciesDecrypt(0, ecies_encrypted, privatekey)
|
||||
assert ui_websocket.ws.getResult() == "hello"
|
||||
|
||||
# Decrypt using AES
|
||||
aes_iv, aes_encrypted = CryptMessage.split(base64.b64decode(ecies_encrypted))
|
||||
|
||||
ui_websocket.actionAesDecrypt(0, base64.b64encode(aes_iv), base64.b64encode(aes_encrypted), aes_key)
|
||||
assert ui_websocket.ws.getResult() == "hello"
|
||||
|
||||
def testAes(self, ui_websocket):
|
||||
ui_websocket.actionAesEncrypt(0, "hello")
|
||||
key, iv, encrypted = ui_websocket.ws.getResult()
|
||||
|
|
5
plugins/CryptMessage/plugin_info.json
Normal file
5
plugins/CryptMessage/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "CryptMessage",
|
||||
"description": "Cryptographic functions of ECIES and AES data encryption/decryption.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -21,9 +21,6 @@ def openArchive(archive_path, file_obj=None):
|
|||
if archive_path.endswith("tar.gz"):
|
||||
import tarfile
|
||||
archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:gz")
|
||||
elif archive_path.endswith("tar.bz2"):
|
||||
import tarfile
|
||||
archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:bz2")
|
||||
else:
|
||||
import zipfile
|
||||
archive_cache[archive_path] = zipfile.ZipFile(file_obj or archive_path)
|
||||
|
@ -48,7 +45,7 @@ class UiRequestPlugin(object):
|
|||
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|tar.bz2|zip))/(.*)", file_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"])
|
||||
|
@ -102,7 +99,7 @@ class UiRequestPlugin(object):
|
|||
class SiteStoragePlugin(object):
|
||||
def isFile(self, inner_path):
|
||||
if ".zip/" in inner_path or ".tar.gz/" in inner_path:
|
||||
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", 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:
|
||||
|
@ -130,7 +127,7 @@ class SiteStoragePlugin(object):
|
|||
|
||||
def walk(self, inner_path, *args, **kwags):
|
||||
if ".zip" in inner_path or ".tar.gz" in inner_path:
|
||||
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", 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("/")
|
||||
|
@ -154,7 +151,7 @@ class SiteStoragePlugin(object):
|
|||
|
||||
def list(self, inner_path, *args, **kwags):
|
||||
if ".zip" in inner_path or ".tar.gz" in inner_path:
|
||||
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", 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("/")
|
||||
|
@ -179,9 +176,9 @@ class SiteStoragePlugin(object):
|
|||
else:
|
||||
return super(SiteStoragePlugin, self).list(inner_path, *args, **kwags)
|
||||
|
||||
def read(self, inner_path, mode="rb"):
|
||||
def read(self, inner_path, mode="rb", **kwargs):
|
||||
if ".zip/" in inner_path or ".tar.gz/" in inner_path:
|
||||
match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", 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("/")
|
||||
|
@ -192,5 +189,5 @@ class SiteStoragePlugin(object):
|
|||
return archive.extractfile(path_within).read()
|
||||
|
||||
else:
|
||||
return super(SiteStoragePlugin, self).read(inner_path, mode)
|
||||
return super(SiteStoragePlugin, self).read(inner_path, mode, **kwargs)
|
||||
|
||||
|
|
5
plugins/FilePack/plugin_info.json
Normal file
5
plugins/FilePack/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "FilePack",
|
||||
"description": "Transparent web access for Zip and Tar.gz files.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import re
|
||||
import time
|
||||
import copy
|
||||
import os
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Translate import Translate
|
||||
from util import RateLimit
|
||||
from util import helper
|
||||
from util.Flag import flag
|
||||
from Debug import Debug
|
||||
try:
|
||||
import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible
|
||||
|
@ -18,8 +20,11 @@ if "merger_db" not in locals().keys(): # To keep merger_sites between module re
|
|||
merged_to_merger = {} # {address: [site1, site2, ...]} cache
|
||||
site_manager = None # Site manager for merger sites
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate("plugins/MergerSite/languages/")
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
# Check if the site has permission to this merger site
|
||||
|
@ -74,14 +79,18 @@ class UiWebsocketPlugin(object):
|
|||
def cbMergerSiteAdd(self, to, addresses):
|
||||
added = 0
|
||||
for address in addresses:
|
||||
added += 1
|
||||
try:
|
||||
site_manager.need(address)
|
||||
added += 1
|
||||
except Exception as err:
|
||||
self.cmd("notification", ["error", _["Adding <b>%s</b> failed: %s"] % (address, err)])
|
||||
if added:
|
||||
self.cmd("notification", ["done", _["Added <b>%s</b> new site"] % added, 5000])
|
||||
RateLimit.called(self.site.address + "-MergerSiteAdd")
|
||||
site_manager.updateMergerSites()
|
||||
|
||||
# Delete a merged site
|
||||
@flag.no_multiuser
|
||||
def actionMergerSiteDelete(self, to, address):
|
||||
site = self.server.sites.get(address)
|
||||
if not site:
|
||||
|
@ -221,7 +230,7 @@ class UiWebsocketPlugin(object):
|
|||
site = self.server.sites.get(address)
|
||||
try:
|
||||
merged_sites.append(site.content_manager.contents.get("content.json").get("title", address))
|
||||
except Exception as err:
|
||||
except Exception:
|
||||
merged_sites.append(address)
|
||||
|
||||
details = _["Read and write permissions to sites with merged type of <b>%s</b> "] % merger_type
|
||||
|
@ -289,6 +298,9 @@ class SiteStoragePlugin(object):
|
|||
|
||||
# Also notice merger sites on a merged site file change
|
||||
def onUpdated(self, inner_path, file=None):
|
||||
if inner_path == "content.json":
|
||||
site_manager.updateMergerSites()
|
||||
|
||||
super(SiteStoragePlugin, self).onUpdated(inner_path, file)
|
||||
|
||||
merged_type = merged_db.get(self.site.address)
|
||||
|
@ -333,9 +345,9 @@ class SiteManagerPlugin(object):
|
|||
def updateMergerSites(self):
|
||||
global merger_db, merged_db, merged_to_merger, site_manager
|
||||
s = time.time()
|
||||
merger_db = {}
|
||||
merged_db = {}
|
||||
merged_to_merger = {}
|
||||
merger_db_new = {}
|
||||
merged_db_new = {}
|
||||
merged_to_merger_new = {}
|
||||
site_manager = self
|
||||
if not self.sites:
|
||||
return
|
||||
|
@ -347,7 +359,7 @@ class SiteManagerPlugin(object):
|
|||
self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err)))
|
||||
continue
|
||||
if merged_type:
|
||||
merged_db[site.address] = merged_type
|
||||
merged_db_new[site.address] = merged_type
|
||||
|
||||
# Update merger sites
|
||||
for permission in site.settings["permissions"]:
|
||||
|
@ -361,18 +373,24 @@ class SiteManagerPlugin(object):
|
|||
site.settings["permissions"].remove(permission)
|
||||
continue
|
||||
merger_type = permission.replace("Merger:", "")
|
||||
if site.address not in merger_db:
|
||||
merger_db[site.address] = []
|
||||
merger_db[site.address].append(merger_type)
|
||||
if site.address not in merger_db_new:
|
||||
merger_db_new[site.address] = []
|
||||
merger_db_new[site.address].append(merger_type)
|
||||
site_manager.sites[site.address] = site
|
||||
|
||||
# Update merged to merger
|
||||
if merged_type:
|
||||
for merger_site in self.sites.values():
|
||||
if "Merger:" + merged_type in merger_site.settings["permissions"]:
|
||||
if site.address not in merged_to_merger:
|
||||
merged_to_merger[site.address] = []
|
||||
merged_to_merger[site.address].append(merger_site)
|
||||
if site.address not in merged_to_merger_new:
|
||||
merged_to_merger_new[site.address] = []
|
||||
merged_to_merger_new[site.address].append(merger_site)
|
||||
|
||||
# Update globals
|
||||
merger_db = merger_db_new
|
||||
merged_db = merged_db_new
|
||||
merged_to_merger = merged_to_merger_new
|
||||
|
||||
self.log.debug("Updated merger sites in %.3fs" % (time.time() - s))
|
||||
|
||||
def load(self, *args, **kwags):
|
||||
|
|
|
@ -5,6 +5,7 @@ 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")
|
||||
|
@ -27,10 +28,8 @@ class UiWebsocketPlugin(object):
|
|||
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):
|
||||
if "ADMIN" not in self.site.settings["permissions"]:
|
||||
return self.response(to, "FeedQuery not allowed")
|
||||
|
||||
from Site import SiteManager
|
||||
rows = []
|
||||
stats = []
|
||||
|
|
|
@ -24,8 +24,8 @@ class ContentDbPlugin(object):
|
|||
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_loading = False
|
||||
helper.timer(60 * 5, self.checkOptionalLimit)
|
||||
self.optional_files_loaded = False
|
||||
self.timer_check_optional = helper.timer(60 * 5, self.checkOptionalLimit)
|
||||
super(ContentDbPlugin, self).__init__(*args, **kwargs)
|
||||
|
||||
def getSchema(self):
|
||||
|
@ -60,9 +60,6 @@ class ContentDbPlugin(object):
|
|||
super(ContentDbPlugin, self).initSite(site)
|
||||
if self.need_filling:
|
||||
self.fillTableFileOptional(site)
|
||||
if not self.optional_files_loading:
|
||||
gevent.spawn_later(1, self.loadFilesOptional)
|
||||
self.optional_files_loading = True
|
||||
|
||||
def checkTables(self):
|
||||
changed_tables = super(ContentDbPlugin, self).checkTables()
|
||||
|
@ -91,7 +88,7 @@ class ContentDbPlugin(object):
|
|||
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:
|
||||
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]
|
||||
|
@ -100,7 +97,7 @@ class ContentDbPlugin(object):
|
|||
total += stats["size_optional"]
|
||||
total_downloaded += stats["optional_downloaded"]
|
||||
|
||||
self.log.debug(
|
||||
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)
|
||||
)
|
||||
|
@ -108,7 +105,7 @@ class ContentDbPlugin(object):
|
|||
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.debug(
|
||||
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)
|
||||
)
|
||||
|
@ -142,14 +139,14 @@ class ContentDbPlugin(object):
|
|||
if not user:
|
||||
user = UserManager.user_manager.create()
|
||||
auth_address = user.getAuthAddress(site.address)
|
||||
self.execute(
|
||||
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, self.cur.cursor.rowcount)
|
||||
(site.address, time.time() - s, num, res.rowcount)
|
||||
)
|
||||
self.filled[site.address] = True
|
||||
|
||||
|
@ -405,3 +402,13 @@ class ContentDbPlugin(object):
|
|||
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
|
|
@ -17,32 +17,46 @@ def importPluginnedClasses():
|
|||
|
||||
|
||||
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:
|
||||
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[site_id].keys())}
|
||||
{"site_id": site_id, "inner_path": list(access_log_prev[site_id].keys())}
|
||||
)
|
||||
num += len(access_log[site_id])
|
||||
access_log.clear()
|
||||
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
|
||||
cur = content_db.getCursor()
|
||||
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:
|
||||
for inner_path, uploaded in request_log[site_id].items():
|
||||
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
|
||||
request_log.clear()
|
||||
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
|
||||
|
@ -72,12 +86,12 @@ class ContentManagerPlugin(object):
|
|||
return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own)
|
||||
|
||||
def optionalRemoved(self, inner_path, hash_id, size=None):
|
||||
self.contents.db.execute(
|
||||
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 self.contents.db.cur.cursor.rowcount > 0:
|
||||
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):
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
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("plugins/OptionalManager/languages/")
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
bigfile_sha512_cache = {}
|
||||
|
||||
|
@ -210,6 +215,7 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
return "ok"
|
||||
|
||||
@flag.no_multiuser
|
||||
def actionOptionalFilePin(self, to, inner_path, address=None):
|
||||
if type(inner_path) is not list:
|
||||
inner_path = [inner_path]
|
||||
|
@ -222,6 +228,7 @@ class UiWebsocketPlugin(object):
|
|||
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]
|
||||
|
@ -234,6 +241,7 @@ class UiWebsocketPlugin(object):
|
|||
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
|
||||
|
@ -271,10 +279,8 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
# Limit functions
|
||||
|
||||
@flag.admin
|
||||
def actionOptionalLimitStats(self, to):
|
||||
if "ADMIN" not in self.site.settings["permissions"]:
|
||||
return self.response(to, "Forbidden")
|
||||
|
||||
back = {}
|
||||
back["limit"] = config.optional_limit
|
||||
back["used"] = self.site.content_manager.contents.db.getOptionalUsedBytes()
|
||||
|
@ -282,10 +288,10 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
self.response(to, back)
|
||||
|
||||
@flag.no_multiuser
|
||||
@flag.admin
|
||||
def actionOptionalLimitSet(self, to, limit):
|
||||
if "ADMIN" not in self.site.settings["permissions"]:
|
||||
return self.response(to, {"error": "Forbidden"})
|
||||
config.optional_limit = re.sub("\.0+$", "", limit) # Remove unnecessary digits from end
|
||||
config.optional_limit = re.sub(r"\.0+$", "", limit) # Remove unnecessary digits from end
|
||||
config.saveValue("optional_limit", limit)
|
||||
self.response(to, "ok")
|
||||
|
||||
|
@ -302,6 +308,7 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
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
|
||||
|
@ -338,6 +345,7 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
self.response(to, dict(stats))
|
||||
|
||||
@flag.no_multiuser
|
||||
def actionOptionalHelpRemove(self, to, directory, address=None):
|
||||
if not address:
|
||||
address = self.site.address
|
||||
|
@ -357,6 +365,7 @@ class UiWebsocketPlugin(object):
|
|||
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
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import OptionalManagerPlugin
|
||||
from . import UiWebsocketPlugin
|
||||
|
|
|
@ -70,7 +70,7 @@ class ContentDbPlugin(object):
|
|||
def savePeers(self, site, spawn=False):
|
||||
if spawn:
|
||||
# Save peers every hour (+random some secs to not update very site at same time)
|
||||
gevent.spawn_later(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True)
|
||||
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
|
||||
|
@ -79,7 +79,7 @@ class ContentDbPlugin(object):
|
|||
cur = self.getCursor()
|
||||
try:
|
||||
cur.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id})
|
||||
cur.cursor.executemany(
|
||||
cur.executemany(
|
||||
"INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
self.iteratePeers(site)
|
||||
)
|
||||
|
@ -89,8 +89,8 @@ class ContentDbPlugin(object):
|
|||
|
||||
def initSite(self, site):
|
||||
super(ContentDbPlugin, self).initSite(site)
|
||||
gevent.spawn_later(0.5, self.loadPeers, site)
|
||||
gevent.spawn_later(60*60, self.savePeers, site, spawn=True)
|
||||
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()):
|
||||
|
|
5
plugins/PeerDb/plugin_info.json
Normal file
5
plugins/PeerDb/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "PeerDb",
|
||||
"description": "Save/restore peer list on client restart.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -5,6 +5,7 @@ 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):
|
||||
|
@ -37,10 +38,11 @@ class WsLogStreamer(logging.StreamHandler):
|
|||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.admin_commands.update(["consoleLogRead", "consoleLogStream", "consoleLogStreamRemove"])
|
||||
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")
|
||||
|
@ -56,9 +58,16 @@ class UiWebsocketPlugin(object):
|
|||
assert SafeRe.isSafePattern(filter)
|
||||
filter_re = re.compile(".*" + filter)
|
||||
|
||||
last_match = False
|
||||
for line in log_file:
|
||||
if filter and not filter_re.match(line):
|
||||
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)
|
||||
|
@ -74,11 +83,15 @@ class UiWebsocketPlugin(object):
|
|||
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()
|
||||
|
|
|
@ -11,16 +11,17 @@ 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 = "plugins/Sidebar"
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
media_dir = plugin_dir + "/media"
|
||||
sys.path.append(plugin_dir) # To able to load geoip lib
|
||||
|
||||
loc_cache = {}
|
||||
if "_" not in locals():
|
||||
|
@ -86,10 +87,6 @@ class UiRequestPlugin(object):
|
|||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.async_commands.add("sidebarGetPeers")
|
||||
return super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
|
||||
|
||||
def sidebarRenderPeerStats(self, body, site):
|
||||
connected = len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected])
|
||||
connectable = len([peer_id for peer_id in list(site.peers.keys()) if not peer_id.endswith(":0")])
|
||||
|
@ -318,16 +315,18 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
body.append(_("""
|
||||
<li>
|
||||
<label>{_[Download and help distribute all files]}</label>
|
||||
<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>")
|
||||
|
@ -485,7 +484,7 @@ class UiWebsocketPlugin(object):
|
|||
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='#Forgot+private+key' id='privatekey-forgot' class='link-right'>{_[Forgot]}</a>")
|
||||
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>")
|
||||
|
||||
|
@ -511,11 +510,8 @@ class UiWebsocketPlugin(object):
|
|||
body.append("</div>")
|
||||
body.append("</li>")
|
||||
|
||||
@flag.admin
|
||||
def actionSidebarGetHtmlTag(self, to):
|
||||
permissions = self.getPermissions(to)
|
||||
if "ADMIN" not in permissions:
|
||||
return self.response(to, "You don't have permission to run this command")
|
||||
|
||||
site = self.site
|
||||
|
||||
body = []
|
||||
|
@ -566,7 +562,7 @@ class UiWebsocketPlugin(object):
|
|||
self.log.info("Downloading GeoLite2 City database...")
|
||||
self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
|
||||
db_urls = [
|
||||
"https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz",
|
||||
"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:
|
||||
|
@ -635,6 +631,7 @@ class UiWebsocketPlugin(object):
|
|||
loc_cache[ip] = loc
|
||||
return loc
|
||||
|
||||
@util.Noparallel()
|
||||
def getGeoipDb(self):
|
||||
db_name = 'GeoLite2-City.mmdb'
|
||||
|
||||
|
@ -664,7 +661,6 @@ class UiWebsocketPlugin(object):
|
|||
self.log.debug("Not showing peer locations: no GeoIP database")
|
||||
return False
|
||||
|
||||
self.log.info("Loading GeoIP database from: %s" % db_path)
|
||||
geodb = maxminddb.open_database(db_path)
|
||||
|
||||
peers = list(peers.values())
|
||||
|
@ -706,11 +702,9 @@ class UiWebsocketPlugin(object):
|
|||
|
||||
return peer_locations
|
||||
|
||||
|
||||
@flag.admin
|
||||
@flag.async_run
|
||||
def actionSidebarGetPeers(self, to):
|
||||
permissions = self.getPermissions(to)
|
||||
if "ADMIN" not in permissions:
|
||||
return self.response(to, "You don't have permission to run this command")
|
||||
try:
|
||||
peer_locations = self.getPeerLocations(self.site.peers)
|
||||
globe_data = []
|
||||
|
@ -739,53 +733,41 @@ class UiWebsocketPlugin(object):
|
|||
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):
|
||||
permissions = self.getPermissions(to)
|
||||
if "ADMIN" not in permissions:
|
||||
return self.response(to, "You don't have permission to run this command")
|
||||
|
||||
if self.site.address == config.updatesite:
|
||||
return self.response(to, "You can't change the ownership of the updater site")
|
||||
|
||||
self.site.settings["own"] = bool(owned)
|
||||
self.site.updateWebsocket(owned=owned)
|
||||
|
||||
@flag.admin
|
||||
@flag.no_multiuser
|
||||
def actionUserSetSitePrivatekey(self, to, privatekey):
|
||||
permissions = self.getPermissions(to)
|
||||
if "ADMIN" not in permissions:
|
||||
return self.response(to, "You don't have permission to run this command")
|
||||
|
||||
site_data = self.user.sites[self.site.address]
|
||||
site_data["privatekey"] = privatekey
|
||||
self.site.updateWebsocket(set_privatekey=bool(privatekey))
|
||||
|
||||
return "ok"
|
||||
|
||||
@flag.admin
|
||||
@flag.no_multiuser
|
||||
def actionSiteSetAutodownloadoptional(self, to, owned):
|
||||
permissions = self.getPermissions(to)
|
||||
if "ADMIN" not in permissions:
|
||||
return self.response(to, "You don't have permission to run this command")
|
||||
|
||||
self.site.settings["autodownloadoptional"] = bool(owned)
|
||||
self.site.bad_files = {}
|
||||
gevent.spawn(self.site.update, check_files=True)
|
||||
self.site.worker_manager.removeSolvedFileTasks()
|
||||
|
||||
@flag.no_multiuser
|
||||
@flag.admin
|
||||
def actionDbReload(self, to):
|
||||
permissions = self.getPermissions(to)
|
||||
if "ADMIN" not in permissions:
|
||||
return self.response(to, "You don't have permission to run this command")
|
||||
|
||||
self.site.storage.closeDb()
|
||||
self.site.storage.getDb()
|
||||
|
||||
return self.response(to, "ok")
|
||||
|
||||
@flag.no_multiuser
|
||||
@flag.admin
|
||||
def actionDbRebuild(self, to):
|
||||
permissions = self.getPermissions(to)
|
||||
if "ADMIN" not in permissions:
|
||||
return self.response(to, "You don't have permission to run this command")
|
||||
|
||||
try:
|
||||
self.site.storage.rebuildDb()
|
||||
except Exception as err:
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
"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 prviát kulcsod:",
|
||||
"Enter your private key:": "Add meg a privát kulcsod:",
|
||||
" Signed!": " Aláírva!",
|
||||
"WebGL not supported": "WebGL nem támogatott"
|
||||
}
|
|
@ -62,7 +62,6 @@
|
|||
"Save site settings": "Salvar definições do site",
|
||||
"Open site directory": "Abrir diretório do site",
|
||||
|
||||
"Content publishing": "内容发布",
|
||||
"Content publishing": "Publicação do conteúdo",
|
||||
"Choose": "Escolher",
|
||||
"Sign": "Assinar",
|
||||
|
@ -93,5 +92,6 @@
|
|||
"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"
|
||||
"WebGL not supported": "WebGL não é suportado",
|
||||
"Save as .zip": "Salvar como .zip"
|
||||
}
|
||||
|
|
|
@ -27,8 +27,11 @@
|
|||
|
||||
"Optional files": "可选文件",
|
||||
"Downloaded": "已下载",
|
||||
"Download and help distribute all files": "下载并帮助分发所有文件",
|
||||
"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": "已下载文件",
|
||||
|
||||
|
|
|
@ -3,6 +3,14 @@ class Console extends Class
|
|||
@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) =>
|
||||
|
@ -11,7 +19,11 @@ class Console extends Class
|
|||
else
|
||||
handleMessageWebsocket_original(message)
|
||||
|
||||
if window.top.location.hash == "#console"
|
||||
$(window).on "hashchange", =>
|
||||
if window.top.location.hash == "#ZeroNet:Console"
|
||||
@open()
|
||||
|
||||
if window.top.location.hash == "#ZeroNet:Console"
|
||||
setTimeout (=> @open()), 10
|
||||
|
||||
createHtmltag: ->
|
||||
|
@ -20,6 +32,7 @@ class Console extends Class
|
|||
<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">
|
||||
|
@ -33,6 +46,7 @@ class Console extends Class
|
|||
""")
|
||||
@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
|
||||
|
@ -43,6 +57,12 @@ class Console extends Class
|
|||
|
||||
@container.appendTo(document.body)
|
||||
@tag = @container.find(".console")
|
||||
for tab_type in @tab_types
|
||||
tab = $("<a></a>", {href: "#", "data-filter": tab_type.filter}).text(tab_type.title)
|
||||
if tab_type.filter == @tab_active
|
||||
tab.addClass("active")
|
||||
tab.on("click", @handleTabClick)
|
||||
@tabs.append(tab)
|
||||
|
||||
@container.on "mousedown touchend touchcancel", (e) =>
|
||||
if e.target != e.currentTarget
|
||||
|
@ -94,26 +114,31 @@ class Console extends Class
|
|||
|
||||
|
||||
loadConsoleText: =>
|
||||
@sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter}, (res) =>
|
||||
@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: =>
|
||||
@createHtmltag()
|
||||
@sidebar.fixbutton_targety = @sidebar.page_height
|
||||
@stopDragY()
|
||||
@sidebar.startDrag()
|
||||
@sidebar.moved("y")
|
||||
@sidebar.fixbutton_targety = @sidebar.page_height - @sidebar.fixbutton_inity - 50
|
||||
@sidebar.stopDrag()
|
||||
|
||||
onOpened: =>
|
||||
@sidebar.onClosed()
|
||||
|
@ -153,4 +178,20 @@ class Console extends Class
|
|||
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)
|
||||
return false
|
||||
|
||||
window.Console = Console
|
|
@ -1,9 +1,19 @@
|
|||
.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; padding: 5px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;}
|
||||
.console-text { overflow-y: scroll; height: 100%; color: #DDD; }
|
||||
|
||||
.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 {
|
||||
|
|
|
@ -23,9 +23,9 @@ class Sidebar extends Class
|
|||
@original_set_site_info = @wrapper.setSiteInfo # We going to override this, save the original
|
||||
|
||||
# Start in opened state for debugging
|
||||
if false
|
||||
if window.top.location.hash == "#ZeroNet:OpenSidebar"
|
||||
@startDrag()
|
||||
@moved()
|
||||
@moved("x")
|
||||
@fixbutton_targetx = @fixbutton_initx - @width
|
||||
@stopDrag()
|
||||
|
||||
|
@ -204,15 +204,15 @@ class Sidebar extends Class
|
|||
return true
|
||||
}
|
||||
|
||||
# Save and forgot privatekey for site signing
|
||||
# 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-forgot").off("click, touchend").on "click touchend", (e) =>
|
||||
@wrapper.displayConfirm "Remove saved private key for this site?", "Forgot", (res) =>
|
||||
@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) =>
|
||||
|
@ -372,6 +372,14 @@ class Sidebar extends Class
|
|||
@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", [], =>
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
|
||||
/* ---- plugins/Sidebar/media/Console.css ---- */
|
||||
/* ---- 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; padding: 5px; 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: 100%; color: #DDD; }
|
||||
|
||||
.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 {
|
||||
|
@ -25,7 +35,7 @@
|
|||
}
|
||||
|
||||
|
||||
/* ---- plugins/Sidebar/media/Menu.css ---- */
|
||||
/* ---- Menu.css ---- */
|
||||
|
||||
|
||||
.menu {
|
||||
|
@ -48,7 +58,7 @@
|
|||
.menu, .menu.visible { position: absolute; left: unset !important; right: 20px; }
|
||||
}
|
||||
|
||||
/* ---- plugins/Sidebar/media/Scrollbable.css ---- */
|
||||
/* ---- Scrollbable.css ---- */
|
||||
|
||||
|
||||
.scrollable {
|
||||
|
@ -97,7 +107,7 @@
|
|||
}
|
||||
|
||||
|
||||
/* ---- plugins/Sidebar/media/Sidebar.css ---- */
|
||||
/* ---- Sidebar.css ---- */
|
||||
|
||||
|
||||
.menu {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
/* ---- plugins/Sidebar/media/Class.coffee ---- */
|
||||
/* ---- Class.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -56,7 +56,7 @@
|
|||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/Sidebar/media/Console.coffee ---- */
|
||||
/* ---- Console.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -71,6 +71,8 @@
|
|||
function Console(sidebar) {
|
||||
var handleMessageWebsocket_original;
|
||||
this.sidebar = sidebar;
|
||||
this.handleTabClick = bind(this.handleTabClick, this);
|
||||
this.changeFilter = bind(this.changeFilter, this);
|
||||
this.stopDragY = bind(this.stopDragY, this);
|
||||
this.cleanup = bind(this.cleanup, this);
|
||||
this.onClosed = bind(this.onClosed, this);
|
||||
|
@ -84,6 +86,23 @@
|
|||
this.tag = null;
|
||||
this.opened = false;
|
||||
this.filter = null;
|
||||
this.tab_types = [
|
||||
{
|
||||
title: "All",
|
||||
filter: ""
|
||||
}, {
|
||||
title: "Info",
|
||||
filter: "INFO"
|
||||
}, {
|
||||
title: "Warning",
|
||||
filter: "WARNING"
|
||||
}, {
|
||||
title: "Error",
|
||||
filter: "ERROR"
|
||||
}
|
||||
];
|
||||
this.read_size = 32 * 1024;
|
||||
this.tab_active = "";
|
||||
handleMessageWebsocket_original = this.sidebar.wrapper.handleMessageWebsocket;
|
||||
this.sidebar.wrapper.handleMessageWebsocket = (function(_this) {
|
||||
return function(message) {
|
||||
|
@ -94,7 +113,14 @@
|
|||
}
|
||||
};
|
||||
})(this);
|
||||
if (window.top.location.hash === "#console") {
|
||||
$(window).on("hashchange", (function(_this) {
|
||||
return function() {
|
||||
if (window.top.location.hash === "#ZeroNet:Console") {
|
||||
return _this.open();
|
||||
}
|
||||
};
|
||||
})(this));
|
||||
if (window.top.location.hash === "#ZeroNet:Console") {
|
||||
setTimeout(((function(_this) {
|
||||
return function() {
|
||||
return _this.open();
|
||||
|
@ -104,10 +130,12 @@
|
|||
}
|
||||
|
||||
Console.prototype.createHtmltag = function() {
|
||||
var j, len, ref, tab, tab_type;
|
||||
if (!this.container) {
|
||||
this.container = $("<div class=\"console-container\">\n <div class=\"console\">\n <div class=\"console-top\">\n <div class=\"console-text\">Loading...</div>\n </div>\n <div class=\"console-middle\">\n <div class=\"mynode\"></div>\n <div class=\"peers\">\n <div class=\"peer\"><div class=\"line\"></div><a href=\"#\" class=\"icon\">\u25BD</div></div>\n </div>\n </div>\n </div>\n</div>");
|
||||
this.container = $("<div class=\"console-container\">\n <div class=\"console\">\n <div class=\"console-top\">\n <div class=\"console-tabs\"></div>\n <div class=\"console-text\">Loading...</div>\n </div>\n <div class=\"console-middle\">\n <div class=\"mynode\"></div>\n <div class=\"peers\">\n <div class=\"peer\"><div class=\"line\"></div><a href=\"#\" class=\"icon\">\u25BD</div></div>\n </div>\n </div>\n </div>\n</div>");
|
||||
this.text = this.container.find(".console-text");
|
||||
this.text_elem = this.text[0];
|
||||
this.tabs = this.container.find(".console-tabs");
|
||||
this.text.on("mousewheel", (function(_this) {
|
||||
return function(e) {
|
||||
if (e.originalEvent.deltaY < 0) {
|
||||
|
@ -119,6 +147,19 @@
|
|||
this.text.is_bottom = true;
|
||||
this.container.appendTo(document.body);
|
||||
this.tag = this.container.find(".console");
|
||||
ref = this.tab_types;
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
tab_type = ref[j];
|
||||
tab = $("<a></a>", {
|
||||
href: "#",
|
||||
"data-filter": tab_type.filter
|
||||
}).text(tab_type.title);
|
||||
if (tab_type.filter === this.tab_active) {
|
||||
tab.addClass("active");
|
||||
}
|
||||
tab.on("click", this.handleTabClick);
|
||||
this.tabs.append(tab);
|
||||
}
|
||||
this.container.on("mousedown touchend touchcancel", (function(_this) {
|
||||
return function(e) {
|
||||
if (e.target !== e.currentTarget) {
|
||||
|
@ -194,7 +235,8 @@
|
|||
|
||||
Console.prototype.loadConsoleText = function() {
|
||||
this.sidebar.wrapper.ws.cmd("consoleLogRead", {
|
||||
filter: this.filter
|
||||
filter: this.filter,
|
||||
read_size: this.read_size
|
||||
}, (function(_this) {
|
||||
return function(res) {
|
||||
var pos_diff, size_read, size_total;
|
||||
|
@ -202,11 +244,17 @@
|
|||
pos_diff = res["pos_end"] - res["pos_start"];
|
||||
size_read = Math.round(pos_diff / 1024);
|
||||
size_total = Math.round(res['pos_end'] / 1024);
|
||||
_this.text.append("<br><br>");
|
||||
_this.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>");
|
||||
_this.addLines(res.lines, false);
|
||||
return _this.text_elem.scrollTop = _this.text_elem.scrollHeight;
|
||||
};
|
||||
})(this));
|
||||
if (this.stream_id) {
|
||||
this.sidebar.wrapper.ws.cmd("consoleLogStreamRemove", {
|
||||
stream_id: this.stream_id
|
||||
});
|
||||
}
|
||||
return this.sidebar.wrapper.ws.cmd("consoleLogStream", {
|
||||
filter: this.filter
|
||||
}, (function(_this) {
|
||||
|
@ -217,15 +265,17 @@
|
|||
};
|
||||
|
||||
Console.prototype.close = function() {
|
||||
window.top.location.hash = "";
|
||||
this.sidebar.move_lock = "y";
|
||||
this.sidebar.startDrag();
|
||||
return this.sidebar.stopDrag();
|
||||
};
|
||||
|
||||
Console.prototype.open = function() {
|
||||
this.createHtmltag();
|
||||
this.sidebar.fixbutton_targety = this.sidebar.page_height;
|
||||
return this.stopDragY();
|
||||
this.sidebar.startDrag();
|
||||
this.sidebar.moved("y");
|
||||
this.sidebar.fixbutton_targety = this.sidebar.page_height - this.sidebar.fixbutton_inity - 50;
|
||||
return this.sidebar.stopDrag();
|
||||
};
|
||||
|
||||
Console.prototype.onOpened = function() {
|
||||
|
@ -276,6 +326,26 @@
|
|||
}
|
||||
};
|
||||
|
||||
Console.prototype.changeFilter = function(filter) {
|
||||
this.filter = filter;
|
||||
if (this.filter === "") {
|
||||
this.read_size = 32 * 1024;
|
||||
} else {
|
||||
this.read_size = 5 * 1024 * 1024;
|
||||
}
|
||||
return this.loadConsoleText();
|
||||
};
|
||||
|
||||
Console.prototype.handleTabClick = function(e) {
|
||||
var elem;
|
||||
elem = $(e.currentTarget);
|
||||
this.tab_active = elem.data("filter");
|
||||
$("a", this.tabs).removeClass("active");
|
||||
elem.addClass("active");
|
||||
this.changeFilter(this.tab_active);
|
||||
return false;
|
||||
};
|
||||
|
||||
return Console;
|
||||
|
||||
})(Class);
|
||||
|
@ -285,7 +355,7 @@
|
|||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/Sidebar/media/Menu.coffee ---- */
|
||||
/* ---- Menu.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -368,7 +438,7 @@
|
|||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/Sidebar/media/RateLimit.coffee ---- */
|
||||
/* ---- RateLimit.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -397,7 +467,7 @@
|
|||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/Sidebar/media/Scrollable.js ---- */
|
||||
/* ---- Scrollable.js ---- */
|
||||
|
||||
|
||||
/* via http://jsfiddle.net/elGrecode/00dgurnn/ */
|
||||
|
@ -492,7 +562,7 @@ window.initScrollable = function () {
|
|||
return updateHeight;
|
||||
};
|
||||
|
||||
/* ---- plugins/Sidebar/media/Sidebar.coffee ---- */
|
||||
/* ---- Sidebar.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -534,9 +604,9 @@ window.initScrollable = function () {
|
|||
this.globe = null;
|
||||
this.preload_html = null;
|
||||
this.original_set_site_info = this.wrapper.setSiteInfo;
|
||||
if (false) {
|
||||
if (window.top.location.hash === "#ZeroNet:OpenSidebar") {
|
||||
this.startDrag();
|
||||
this.moved();
|
||||
this.moved("x");
|
||||
this.fixbutton_targetx = this.fixbutton_initx - this.width;
|
||||
this.stopDrag();
|
||||
}
|
||||
|
@ -744,9 +814,9 @@ window.initScrollable = function () {
|
|||
return false;
|
||||
};
|
||||
})(this));
|
||||
return this.tag.find("#privatekey-forgot").off("click, touchend").on("click touchend", (function(_this) {
|
||||
return this.tag.find("#privatekey-forget").off("click, touchend").on("click touchend", (function(_this) {
|
||||
return function(e) {
|
||||
_this.wrapper.displayConfirm("Remove saved private key for this site?", "Forgot", function(res) {
|
||||
_this.wrapper.displayConfirm("Remove saved private key for this site?", "Forget", function(res) {
|
||||
if (!res) {
|
||||
return false;
|
||||
}
|
||||
|
@ -946,6 +1016,18 @@ window.initScrollable = function () {
|
|||
return false;
|
||||
};
|
||||
})(this));
|
||||
this.tag.find("#button-autodownload_previous").off("click touchend").on("click touchend", (function(_this) {
|
||||
return function() {
|
||||
_this.wrapper.ws.cmd("siteUpdate", {
|
||||
"address": _this.wrapper.site_info.address,
|
||||
"check_files": true
|
||||
}, function() {
|
||||
return _this.wrapper.notifications.add("done-download_optional", "done", "Optional files downloaded", 5000);
|
||||
});
|
||||
_this.wrapper.notifications.add("start-download_optional", "info", "Optional files download started", 5000);
|
||||
return false;
|
||||
};
|
||||
})(this));
|
||||
this.tag.find("#button-dbreload").off("click touchend").on("click touchend", (function(_this) {
|
||||
return function() {
|
||||
_this.wrapper.ws.cmd("dbReload", [], function() {
|
||||
|
@ -1279,7 +1361,7 @@ window.initScrollable = function () {
|
|||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/Sidebar/media/morphdom.js ---- */
|
||||
/* ---- morphdom.js ---- */
|
||||
|
||||
|
||||
(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){
|
||||
|
|
5
plugins/Sidebar/plugin_info.json
Normal file
5
plugins/Sidebar/plugin_info.json
Normal file
|
@ -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"
|
||||
}
|
File diff suppressed because one or more lines are too long
5
plugins/Stats/plugin_info.json
Normal file
5
plugins/Stats/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Stats",
|
||||
"description": "/Stats and /Benchmark pages.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -25,6 +25,8 @@ class UiRequestPlugin(object):
|
|||
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
|
||||
|
@ -44,7 +46,7 @@ class UiRequestPlugin(object):
|
|||
return file_generator
|
||||
|
||||
def actionPatchFile(self, site, inner_path, file_generator):
|
||||
content_json = site.content_manager.contents["content.json"]
|
||||
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)
|
||||
|
|
5
plugins/TranslateSite/plugin_info.json
Normal file
5
plugins/TranslateSite/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "TranslateSite",
|
||||
"description": "Transparent support translation of site javascript and html files.",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -8,8 +8,11 @@ from Translate import Translate
|
|||
|
||||
allow_reload = False # No source reload supported in this plugin
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate("plugins/Trayicon/languages/")
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
@PluginManager.registerTo("Actions")
|
||||
|
@ -23,25 +26,20 @@ class ActionsPlugin(object):
|
|||
|
||||
self.main = main
|
||||
|
||||
fs_encoding = sys.getfilesystemencoding()
|
||||
|
||||
icon = notificationicon.NotificationIcon(
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'trayicon.ico'),
|
||||
"ZeroNet %s" % config.version
|
||||
)
|
||||
self.icon = icon
|
||||
|
||||
if not config.debug: # Hide console if not in debug mode
|
||||
notificationicon.hideConsole()
|
||||
self.console = False
|
||||
else:
|
||||
self.console = True
|
||||
|
||||
@atexit.register
|
||||
def hideIcon():
|
||||
if not config.debug:
|
||||
notificationicon.showConsole()
|
||||
try:
|
||||
icon.die()
|
||||
except Exception as err:
|
||||
print("Error removing trayicon: %s" % err)
|
||||
|
||||
ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1"
|
||||
|
||||
|
@ -134,25 +132,30 @@ class ActionsPlugin(object):
|
|||
else:
|
||||
cwd = os.path.dirname(sys.executable)
|
||||
|
||||
ignored_args = [
|
||||
"--open_browser", "default_browser",
|
||||
"--dist_type", "bundle_win64"
|
||||
]
|
||||
|
||||
if sys.platform == 'win32':
|
||||
args = ['"%s"' % arg for arg in args if arg]
|
||||
args = ['"%s"' % arg for arg in args if arg and arg not in ignored_args]
|
||||
cmd = " ".join(args)
|
||||
|
||||
# Dont open browser on autorun
|
||||
cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip()
|
||||
cmd = cmd.replace("start.py", "zeronet.py").strip()
|
||||
cmd += ' --open_browser ""'
|
||||
|
||||
return """
|
||||
@echo off
|
||||
chcp 65001 > nul
|
||||
set PYTHONIOENCODING=utf-8
|
||||
cd /D \"%s\"
|
||||
start "" %s
|
||||
""" % (cwd, cmd)
|
||||
return "\r\n".join([
|
||||
'@echo off',
|
||||
'chcp 65001 > nul',
|
||||
'set PYTHONIOENCODING=utf-8',
|
||||
'cd /D \"%s\"' % cwd,
|
||||
'start "" %s' % cmd
|
||||
])
|
||||
|
||||
def isAutorunEnabled(self):
|
||||
path = self.getAutorunPath()
|
||||
return os.path.isfile(path) and open(path).read() == self.formatAutorun()
|
||||
return os.path.isfile(path) and open(path, "rb").read().decode("utf8") == self.formatAutorun()
|
||||
|
||||
def titleAutorun(self):
|
||||
translate = _["Start ZeroNet when Windows starts"]
|
||||
|
@ -165,4 +168,4 @@ class ActionsPlugin(object):
|
|||
if self.isAutorunEnabled():
|
||||
os.unlink(self.getAutorunPath())
|
||||
else:
|
||||
open(self.getAutorunPath(), "w").write(self.formatAutorun())
|
||||
open(self.getAutorunPath(), "wb").write(self.formatAutorun().encode("utf8"))
|
||||
|
|
14
plugins/Trayicon/languages/pl.json
Normal file
14
plugins/Trayicon/languages/pl.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ZeroNet Twitter": "ZeroNet Twitter",
|
||||
"ZeroNet Reddit": "ZeroNet Reddit",
|
||||
"ZeroNet Github": "ZeroNet Github",
|
||||
"Report bug/request feature": "Zgłoś błąd / propozycję",
|
||||
"!Open ZeroNet": "!Otwórz ZeroNet",
|
||||
"Quit": "Zamknij",
|
||||
"(active)": "(aktywny)",
|
||||
"(passive)": "(pasywny)",
|
||||
"Connections: %s": "Połączenia: %s",
|
||||
"Received: %.2f MB | Sent: %.2f MB": "Odebrano: %.2f MB | Wysłano: %.2f MB",
|
||||
"Show console window": "Pokaż okno konsoli",
|
||||
"Start ZeroNet when Windows starts": "Uruchom ZeroNet podczas startu Windows"
|
||||
}
|
5
plugins/Trayicon/plugin_info.json
Normal file
5
plugins/Trayicon/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Trayicon",
|
||||
"description": "Icon for system tray. (Windows only)",
|
||||
"default": "enabled"
|
||||
}
|
|
@ -1,18 +1,16 @@
|
|||
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("plugins/UiConfig/languages/")
|
||||
|
||||
|
||||
@PluginManager.afterLoad
|
||||
def importPluginnedClasses():
|
||||
from Ui import UiWebsocket
|
||||
UiWebsocket.admin_commands.add("configList")
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiRequest")
|
||||
|
@ -35,7 +33,7 @@ class UiRequestPlugin(object):
|
|||
|
||||
def actionUiMedia(self, path, *args, **kwargs):
|
||||
if path.startswith("/uimedia/plugins/uiconfig/"):
|
||||
file_path = path.replace("/uimedia/plugins/uiconfig/", "plugins/UiConfig/media/")
|
||||
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
|
||||
|
@ -55,6 +53,7 @@ class UiRequestPlugin(object):
|
|||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
@flag.admin
|
||||
def actionConfigList(self, to):
|
||||
back = {}
|
||||
config_values = vars(config.arguments)
|
||||
|
|
62
plugins/UiConfig/languages/pl.json
Normal file
62
plugins/UiConfig/languages/pl.json
Normal file
|
@ -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"
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
"Disable": "Desativar",
|
||||
"Enable": "Ativar",
|
||||
"Sempre": "Mindig",
|
||||
"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)",
|
||||
|
@ -26,8 +26,31 @@
|
|||
|
||||
"Proxy for tracker connections": "Proxy para conexões de tracker",
|
||||
|
||||
" configuration item value changed": " valor do item de configuração foi alterado",
|
||||
"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"
|
||||
"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"
|
||||
|
||||
}
|
||||
|
|
|
@ -41,6 +41,19 @@
|
|||
"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": "保存配置",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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 }
|
||||
h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
|
||||
a { color: #9760F9 }
|
||||
a:hover { text-decoration: none }
|
||||
|
||||
|
@ -53,7 +53,7 @@ a:hover { text-decoration: none }
|
|||
/* Bottom */
|
||||
|
||||
.bottom {
|
||||
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px;
|
||||
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; }
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
|
||||
|
||||
/* ---- plugins/UiConfig/media/css/Config.css ---- */
|
||||
/* ---- Config.css ---- */
|
||||
|
||||
|
||||
body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; 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: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
|
||||
h2 { margin-top: 10px; }
|
||||
h3 { font-weight: normal }
|
||||
h1 { background: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
|
||||
a { color: #9760F9 }
|
||||
a:hover { text-decoration: none }
|
||||
|
||||
|
@ -58,7 +57,7 @@ a:hover { text-decoration: none }
|
|||
/* Bottom */
|
||||
|
||||
.bottom {
|
||||
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px;
|
||||
width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
|
||||
-webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ;
|
||||
}
|
||||
.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; }
|
||||
|
@ -73,7 +72,7 @@ a:hover { text-decoration: none }
|
|||
.animate-inout { -webkit-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -moz-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -o-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -ms-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important ; }
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/css/button.css ---- */
|
||||
/* ---- button.css ---- */
|
||||
|
||||
|
||||
/* Button */
|
||||
|
@ -90,7 +89,7 @@ a:hover { text-decoration: none }
|
|||
.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 }
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/css/fonts.css ---- */
|
||||
/* ---- fonts.css ---- */
|
||||
|
||||
|
||||
/* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */
|
||||
|
|
|
@ -109,7 +109,7 @@ class ConfigStorage extends Class
|
|||
section.items.push
|
||||
title: "Trackers files"
|
||||
key: "trackers_file"
|
||||
type: "text"
|
||||
type: "textarea"
|
||||
description: "Load additional list of torrent trackers dynamically, from a file"
|
||||
placeholder: "Eg.: data/trackers.json"
|
||||
value_pos: "fullwidth"
|
||||
|
@ -123,6 +123,8 @@ class ConfigStorage extends Class
|
|||
{title: "Tor", value: "tor"}
|
||||
{title: "Disable", value: "disable"}
|
||||
]
|
||||
isHidden: ->
|
||||
Page.values["tor"] == "always"
|
||||
|
||||
section.items.push
|
||||
title: "Custom socks proxy address for trackers"
|
||||
|
@ -147,6 +149,62 @@ class ConfigStorage extends Class
|
|||
{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
|
||||
|
|
|
@ -118,7 +118,7 @@ class ConfigView extends Class
|
|||
renderValueSelect: (item) =>
|
||||
h("select.input-select", {config_key: item.key, oninput: @handleInputChange},
|
||||
item.options.map (option) =>
|
||||
h("option", {selected: option.value == @values[item.key], value: option.value}, option.title)
|
||||
h("option", {selected: option.value.toString() == @values[item.key], value: option.value}, option.title)
|
||||
)
|
||||
|
||||
window.ConfigView = ConfigView
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
/* ---- plugins/UiConfig/media/js/lib/Class.coffee ---- */
|
||||
/* ---- lib/Class.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -55,8 +55,7 @@
|
|||
|
||||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/lib/Promise.coffee ---- */
|
||||
/* ---- lib/Promise.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -159,8 +158,7 @@
|
|||
|
||||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/lib/Prototypes.coffee ---- */
|
||||
/* ---- lib/Prototypes.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -186,8 +184,7 @@
|
|||
|
||||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/lib/maquette.js ---- */
|
||||
/* ---- lib/maquette.js ---- */
|
||||
|
||||
|
||||
(function (root, factory) {
|
||||
|
@ -962,7 +959,7 @@
|
|||
}));
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/utils/Animation.coffee ---- */
|
||||
/* ---- utils/Animation.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -1128,8 +1125,7 @@
|
|||
|
||||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/utils/Dollar.coffee ---- */
|
||||
/* ---- utils/Dollar.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -1141,8 +1137,7 @@
|
|||
|
||||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/utils/ZeroFrame.coffee ---- */
|
||||
/* ---- utils/ZeroFrame.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -1273,8 +1268,7 @@
|
|||
|
||||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/ConfigStorage.coffee ---- */
|
||||
/* ---- ConfigStorage.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -1430,7 +1424,7 @@
|
|||
section.items.push({
|
||||
title: "Trackers files",
|
||||
key: "trackers_file",
|
||||
type: "text",
|
||||
type: "textarea",
|
||||
description: "Load additional list of torrent trackers dynamically, from a file",
|
||||
placeholder: "Eg.: data/trackers.json",
|
||||
value_pos: "fullwidth"
|
||||
|
@ -1450,7 +1444,10 @@
|
|||
title: "Disable",
|
||||
value: "disable"
|
||||
}
|
||||
]
|
||||
],
|
||||
isHidden: function() {
|
||||
return Page.values["tor"] === "always";
|
||||
}
|
||||
});
|
||||
section.items.push({
|
||||
title: "Custom socks proxy address for trackers",
|
||||
|
@ -1467,7 +1464,7 @@
|
|||
})(this)
|
||||
});
|
||||
section = this.createSection("Performance");
|
||||
return section.items.push({
|
||||
section.items.push({
|
||||
key: "log_level",
|
||||
title: "Level of logging to file",
|
||||
type: "select",
|
||||
|
@ -1484,6 +1481,122 @@
|
|||
}
|
||||
]
|
||||
});
|
||||
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
|
||||
}
|
||||
]
|
||||
});
|
||||
return 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
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
ConfigStorage.prototype.createSection = function(title) {
|
||||
|
@ -1504,7 +1617,7 @@
|
|||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/ConfigView.coffee ---- */
|
||||
/* ---- ConfigView.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
@ -1692,7 +1805,7 @@
|
|||
}, item.options.map((function(_this) {
|
||||
return function(option) {
|
||||
return h("option", {
|
||||
selected: option.value === _this.values[item.key],
|
||||
selected: option.value.toString() === _this.values[item.key],
|
||||
value: option.value
|
||||
}, option.title);
|
||||
};
|
||||
|
@ -1707,8 +1820,7 @@
|
|||
|
||||
}).call(this);
|
||||
|
||||
|
||||
/* ---- plugins/UiConfig/media/js/UiConfig.coffee ---- */
|
||||
/* ---- UiConfig.coffee ---- */
|
||||
|
||||
|
||||
(function() {
|
||||
|
|
5
plugins/UiConfig/plugin_info.json
Normal file
5
plugins/UiConfig/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "UiConfig",
|
||||
"description": "Change client settings using the web interface.",
|
||||
"default": "enabled"
|
||||
}
|
221
plugins/UiPluginManager/UiPluginManagerPlugin.py
Normal file
221
plugins/UiPluginManager/UiPluginManagerPlugin.py
Normal file
|
@ -0,0 +1,221 @@
|
|||
import io
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Config import config
|
||||
from Debug import Debug
|
||||
from Translate import Translate
|
||||
from util.Flag import flag
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
# Convert non-str,int,float values to str in a dict
|
||||
def restrictDictValues(input_dict):
|
||||
allowed_types = (int, str, float)
|
||||
return {
|
||||
key: val if type(val) in allowed_types else str(val)
|
||||
for key, val in input_dict.items()
|
||||
}
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiRequest")
|
||||
class UiRequestPlugin(object):
|
||||
def actionWrapper(self, path, extra_headers=None):
|
||||
if path.strip("/") != "Plugins":
|
||||
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/plugin_manager/plugin_manager.html",
|
||||
"Plugin Manager", extra_headers, show_loadingscreen=False, script_nonce=script_nonce
|
||||
)])
|
||||
|
||||
def actionUiMedia(self, path, *args, **kwargs):
|
||||
if path.startswith("/uimedia/plugins/plugin_manager/"):
|
||||
file_path = path.replace("/uimedia/plugins/plugin_manager/", 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 actionPluginList(self, to):
|
||||
plugins = []
|
||||
for plugin in PluginManager.plugin_manager.listPlugins(list_disabled=True):
|
||||
plugin_info_path = plugin["dir_path"] + "/plugin_info.json"
|
||||
plugin_info = {}
|
||||
if os.path.isfile(plugin_info_path):
|
||||
try:
|
||||
plugin_info = json.load(open(plugin_info_path))
|
||||
except Exception as err:
|
||||
self.log.error(
|
||||
"Error loading plugin info for %s: %s" %
|
||||
(plugin["name"], Debug.formatException(err))
|
||||
)
|
||||
if plugin_info:
|
||||
plugin_info = restrictDictValues(plugin_info) # For security reasons don't allow complex values
|
||||
plugin["info"] = plugin_info
|
||||
|
||||
if plugin["source"] != "builtin":
|
||||
plugin_site = self.server.sites.get(plugin["source"])
|
||||
if plugin_site:
|
||||
try:
|
||||
plugin_site_info = plugin_site.storage.loadJson(plugin["inner_path"] + "/plugin_info.json")
|
||||
plugin_site_info = restrictDictValues(plugin_site_info)
|
||||
plugin["site_info"] = plugin_site_info
|
||||
plugin["site_title"] = plugin_site.content_manager.contents["content.json"].get("title")
|
||||
plugin_key = "%s/%s" % (plugin["source"], plugin["inner_path"])
|
||||
plugin["updated"] = plugin_key in PluginManager.plugin_manager.plugins_updated
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
plugins.append(plugin)
|
||||
|
||||
return {"plugins": plugins}
|
||||
|
||||
@flag.admin
|
||||
@flag.no_multiuser
|
||||
def actionPluginConfigSet(self, to, source, inner_path, key, value):
|
||||
plugin_manager = PluginManager.plugin_manager
|
||||
plugins = plugin_manager.listPlugins(list_disabled=True)
|
||||
plugin = None
|
||||
for item in plugins:
|
||||
if item["source"] == source and item["inner_path"] in (inner_path, "disabled-" + inner_path):
|
||||
plugin = item
|
||||
break
|
||||
|
||||
if not plugin:
|
||||
return {"error": "Plugin not found"}
|
||||
|
||||
config_source = plugin_manager.config.setdefault(source, {})
|
||||
config_plugin = config_source.setdefault(inner_path, {})
|
||||
|
||||
if key in config_plugin and value is None:
|
||||
del config_plugin[key]
|
||||
else:
|
||||
config_plugin[key] = value
|
||||
|
||||
plugin_manager.saveConfig()
|
||||
|
||||
return "ok"
|
||||
|
||||
def pluginAction(self, action, address, inner_path):
|
||||
site = self.server.sites.get(address)
|
||||
plugin_manager = PluginManager.plugin_manager
|
||||
|
||||
# Install/update path should exists
|
||||
if action in ("add", "update", "add_request"):
|
||||
if not site:
|
||||
raise Exception("Site not found")
|
||||
|
||||
if not site.storage.isDir(inner_path):
|
||||
raise Exception("Directory not found on the site")
|
||||
|
||||
try:
|
||||
plugin_info = site.storage.loadJson(inner_path + "/plugin_info.json")
|
||||
plugin_data = (plugin_info["rev"], plugin_info["description"], plugin_info["name"])
|
||||
except Exception as err:
|
||||
raise Exception("Invalid plugin_info.json: %s" % Debug.formatExceptionMessage(err))
|
||||
|
||||
source_path = site.storage.getPath(inner_path)
|
||||
|
||||
target_path = plugin_manager.path_installed_plugins + "/" + address + "/" + inner_path
|
||||
plugin_config = plugin_manager.config.setdefault(site.address, {}).setdefault(inner_path, {})
|
||||
|
||||
# Make sure plugin (not)installed
|
||||
if action in ("add", "add_request") and os.path.isdir(target_path):
|
||||
raise Exception("Plugin already installed")
|
||||
|
||||
if action in ("update", "remove") and not os.path.isdir(target_path):
|
||||
raise Exception("Plugin not installed")
|
||||
|
||||
# Do actions
|
||||
if action == "add":
|
||||
shutil.copytree(source_path, target_path)
|
||||
|
||||
plugin_config["date_added"] = int(time.time())
|
||||
plugin_config["rev"] = plugin_info["rev"]
|
||||
plugin_config["enabled"] = True
|
||||
|
||||
if action == "update":
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
shutil.copytree(source_path, target_path)
|
||||
|
||||
plugin_config["rev"] = plugin_info["rev"]
|
||||
plugin_config["date_updated"] = time.time()
|
||||
|
||||
if action == "remove":
|
||||
del plugin_manager.config[address][inner_path]
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
def doPluginAdd(self, to, inner_path, res):
|
||||
if not res:
|
||||
return None
|
||||
|
||||
self.pluginAction("add", self.site.address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
|
||||
self.cmd(
|
||||
"confirm",
|
||||
["Plugin installed!<br>You have to restart the client to load the plugin", "Restart"],
|
||||
lambda res: self.actionServerShutdown(to, restart=True)
|
||||
)
|
||||
|
||||
self.response(to, "ok")
|
||||
|
||||
@flag.no_multiuser
|
||||
def actionPluginAddRequest(self, to, inner_path):
|
||||
self.pluginAction("add_request", self.site.address, inner_path)
|
||||
plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json")
|
||||
warning = "<b>Warning!<br/>Plugins has the same permissions as the ZeroNet client.<br/>"
|
||||
warning += "Do not install it if you don't trust the developer.</b>"
|
||||
|
||||
self.cmd(
|
||||
"confirm",
|
||||
["Install new plugin: %s?<br>%s" % (plugin_info["name"], warning), "Trust & Install"],
|
||||
lambda res: self.doPluginAdd(to, inner_path, res)
|
||||
)
|
||||
|
||||
@flag.admin
|
||||
@flag.no_multiuser
|
||||
def actionPluginRemove(self, to, address, inner_path):
|
||||
self.pluginAction("remove", address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
return "ok"
|
||||
|
||||
@flag.admin
|
||||
@flag.no_multiuser
|
||||
def actionPluginUpdate(self, to, address, inner_path):
|
||||
self.pluginAction("update", address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
PluginManager.plugin_manager.plugins_updated["%s/%s" % (address, inner_path)] = True
|
||||
return "ok"
|
1
plugins/UiPluginManager/__init__.py
Normal file
1
plugins/UiPluginManager/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from . import UiPluginManagerPlugin
|
75
plugins/UiPluginManager/media/css/PluginManager.css
Normal file
75
plugins/UiPluginManager/media/css/PluginManager.css
Normal file
|
@ -0,0 +1,75 @@
|
|||
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 }
|
||||
h4 { font-size: 19px; font-weight: lighter; margin-right: 100px; margin-top: 30px; }
|
||||
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%; }
|
||||
.plugins { font-size: 19px; margin-top: 25px; margin-bottom: 75px; }
|
||||
.plugin { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; }
|
||||
.plugin.hidden { opacity: 0; height: 0px; padding: 0px; }
|
||||
.plugin .title { display: inline-block; line-height: 36px; }
|
||||
.plugin .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; }
|
||||
.plugin .title .version { font-size: 70%; margin-left: 5px; }
|
||||
.plugin .title .version .version-latest { color: #2ecc71; font-weight: normal; }
|
||||
.plugin .title .version .version-missing { color: #ffa200; font-weight: normal; }
|
||||
.plugin .title .version .version-update { padding: 0px 15px; margin-left: 5px; line-height: 28px; }
|
||||
.plugin .description { font-size: 14px; color: #666; line-height: 24px; }
|
||||
.plugin .description .source { color: #999; font-size: 90%; }
|
||||
.plugin .description .source a { color: #666; }
|
||||
.plugin .value { display: inline-block; white-space: nowrap; }
|
||||
.plugin .value-right { right: 0px; position: absolute; }
|
||||
.plugin .value-fullwidth { width: 100% }
|
||||
.plugin .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;
|
||||
}
|
||||
.plugin .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); }
|
||||
.plugin .marker.changed { color: #2ecc71; }
|
||||
.plugin .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; }
|
129
plugins/UiPluginManager/media/css/all.css
Normal file
129
plugins/UiPluginManager/media/css/all.css
Normal file
File diff suppressed because one or more lines are too long
12
plugins/UiPluginManager/media/css/button.css
Normal file
12
plugins/UiPluginManager/media/css/button.css
Normal file
|
@ -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 }
|
30
plugins/UiPluginManager/media/css/fonts.css
Normal file
30
plugins/UiPluginManager/media/css/fonts.css
Normal file
File diff suppressed because one or more lines are too long
BIN
plugins/UiPluginManager/media/img/loading.gif
Normal file
BIN
plugins/UiPluginManager/media/img/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 723 B |
132
plugins/UiPluginManager/media/js/PluginList.coffee
Normal file
132
plugins/UiPluginManager/media/js/PluginList.coffee
Normal file
|
@ -0,0 +1,132 @@
|
|||
class PluginList extends Class
|
||||
constructor: (plugins) ->
|
||||
@plugins = plugins
|
||||
|
||||
savePluginStatus: (plugin, is_enabled) =>
|
||||
Page.cmd "pluginConfigSet", [plugin.source, plugin.inner_path, "enabled", is_enabled], (res) =>
|
||||
if res == "ok"
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
|
||||
Page.projector.scheduleRender()
|
||||
|
||||
handleCheckboxChange: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
node.classList.toggle("checked")
|
||||
value = node.classList.contains("checked")
|
||||
|
||||
@savePluginStatus(plugin, value)
|
||||
|
||||
handleResetClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
|
||||
@savePluginStatus(plugin, null)
|
||||
|
||||
handleUpdateClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
node.classList.add("loading")
|
||||
|
||||
Page.cmd "pluginUpdate", [plugin.source, plugin.inner_path], (res) =>
|
||||
if res == "ok"
|
||||
Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} updated to latest version"]
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
node.classList.remove("loading")
|
||||
|
||||
return false
|
||||
|
||||
handleDeleteClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
if plugin.loaded
|
||||
Page.cmd "wrapperNotification", ["info", "You can only delete plugin that are not currently active"]
|
||||
return false
|
||||
|
||||
node.classList.add("loading")
|
||||
|
||||
Page.cmd "wrapperConfirm", ["Delete #{plugin.name} plugin?", "Delete"], (res) =>
|
||||
if not res
|
||||
node.classList.remove("loading")
|
||||
return false
|
||||
|
||||
Page.cmd "pluginRemove", [plugin.source, plugin.inner_path], (res) =>
|
||||
if res == "ok"
|
||||
Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} deleted"]
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
node.classList.remove("loading")
|
||||
|
||||
return false
|
||||
|
||||
render: ->
|
||||
h("div.plugins", @plugins.map (plugin) =>
|
||||
if not plugin.info
|
||||
return
|
||||
descr = plugin.info.description
|
||||
plugin.info.default ?= "enabled"
|
||||
if plugin.info.default
|
||||
descr += " (default: #{plugin.info.default})"
|
||||
|
||||
tag_version = ""
|
||||
tag_source = ""
|
||||
tag_delete = ""
|
||||
if plugin.source != "builtin"
|
||||
tag_update = ""
|
||||
if plugin.site_info?.rev
|
||||
if plugin.site_info.rev > plugin.info.rev
|
||||
tag_update = h("a.version-update.button",
|
||||
{href: "#Update+plugin", onclick: @handleUpdateClick, "data-plugin": plugin},
|
||||
"Update to rev#{plugin.site_info.rev}"
|
||||
)
|
||||
|
||||
else
|
||||
tag_update = h("span.version-missing", "(unable to get latest vesion: update site missing)")
|
||||
|
||||
tag_version = h("span.version",[
|
||||
"rev#{plugin.info.rev} ",
|
||||
tag_update,
|
||||
])
|
||||
|
||||
tag_source = h("div.source",[
|
||||
"Source: ",
|
||||
h("a", {"href": "/#{plugin.source}", "target": "_top"}, if plugin.site_title then plugin.site_title else plugin.source),
|
||||
" /" + plugin.inner_path
|
||||
])
|
||||
|
||||
tag_delete = h("a.delete", {"href": "#Delete+plugin", onclick: @handleDeleteClick, "data-plugin": plugin}, "Delete plugin")
|
||||
|
||||
|
||||
enabled_default = plugin.info.default == "enabled"
|
||||
if plugin.enabled != plugin.loaded or plugin.updated
|
||||
marker_title = "Change pending"
|
||||
is_pending = true
|
||||
else
|
||||
marker_title = "Changed from default status (click to reset to #{plugin.info.default})"
|
||||
is_pending = false
|
||||
|
||||
is_changed = plugin.enabled != enabled_default and plugin.owner == "builtin"
|
||||
|
||||
h("div.plugin", {key: plugin.name}, [
|
||||
h("div.title", [
|
||||
h("h3", [plugin.name, tag_version]),
|
||||
h("div.description", [descr, tag_source, tag_delete]),
|
||||
])
|
||||
h("div.value.value-right",
|
||||
h("div.checkbox", {onclick: @handleCheckboxChange, "data-plugin": plugin, classes: {checked: plugin.enabled}}, h("div.checkbox-skin"))
|
||||
h("a.marker", {
|
||||
href: "#Reset", title: marker_title,
|
||||
onclick: @handleResetClick, "data-plugin": plugin,
|
||||
classes: {visible: is_pending or is_changed, pending: is_pending}
|
||||
}, "\u2022")
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
window.PluginList = PluginList
|
71
plugins/UiPluginManager/media/js/UiPluginManager.coffee
Normal file
71
plugins/UiPluginManager/media/js/UiPluginManager.coffee
Normal file
|
@ -0,0 +1,71 @@
|
|||
window.h = maquette.h
|
||||
|
||||
class UiPluginManager extends ZeroFrame
|
||||
init: ->
|
||||
@plugin_list_builtin = new PluginList()
|
||||
@plugin_list_custom = new PluginList()
|
||||
@plugins_changed = null
|
||||
@need_restart = null
|
||||
@
|
||||
|
||||
onOpenWebsocket: =>
|
||||
@cmd("wrapperSetTitle", "Plugin manager - ZeroNet")
|
||||
@cmd "serverInfo", {}, (server_info) =>
|
||||
@server_info = server_info
|
||||
@updatePlugins()
|
||||
|
||||
updatePlugins: (cb) =>
|
||||
@cmd "pluginList", [], (res) =>
|
||||
@plugins_changed = (item for item in res.plugins when item.enabled != item.loaded or item.updated)
|
||||
|
||||
plugins_builtin = (item for item in res.plugins when item.source == "builtin")
|
||||
@plugin_list_builtin.plugins = plugins_builtin.sort (a, b) ->
|
||||
return a.name.localeCompare(b.name)
|
||||
|
||||
plugins_custom = (item for item in res.plugins when item.source != "builtin")
|
||||
@plugin_list_custom.plugins = plugins_custom.sort (a, b) ->
|
||||
return a.name.localeCompare(b.name)
|
||||
|
||||
@projector.scheduleRender()
|
||||
cb?()
|
||||
|
||||
createProjector: =>
|
||||
@projector = maquette.createProjector()
|
||||
@projector.replace($("#content"), @render)
|
||||
@projector.replace($("#bottom-restart"), @renderBottomRestart)
|
||||
|
||||
render: =>
|
||||
if not @plugin_list_builtin.plugins
|
||||
return h("div.content")
|
||||
|
||||
h("div.content", [
|
||||
h("div.section", [
|
||||
if @plugin_list_custom.plugins?.length
|
||||
[
|
||||
h("h2", "Installed third-party plugins"),
|
||||
@plugin_list_custom.render()
|
||||
]
|
||||
h("h2", "Built-in plugins")
|
||||
@plugin_list_builtin.render()
|
||||
])
|
||||
])
|
||||
|
||||
handleRestartClick: =>
|
||||
@restart_loading = true
|
||||
setTimeout ( =>
|
||||
Page.cmd("serverShutdown", {restart: true})
|
||||
), 300
|
||||
Page.projector.scheduleRender()
|
||||
return false
|
||||
|
||||
renderBottomRestart: =>
|
||||
h("div.bottom.bottom-restart", {classes: {visible: @plugins_changed?.length}}, h("div.bottom-content", [
|
||||
h("div.title", "Some plugins status has been changed"),
|
||||
h("a.button.button-submit.button-restart",
|
||||
{href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick},
|
||||
"Restart ZeroNet client"
|
||||
)
|
||||
]))
|
||||
|
||||
window.Page = new UiPluginManager()
|
||||
window.Page.createProjector()
|
1606
plugins/UiPluginManager/media/js/all.js
Normal file
1606
plugins/UiPluginManager/media/js/all.js
Normal file
File diff suppressed because it is too large
Load diff
23
plugins/UiPluginManager/media/js/lib/Class.coffee
Normal file
23
plugins/UiPluginManager/media/js/lib/Class.coffee
Normal file
|
@ -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
|
74
plugins/UiPluginManager/media/js/lib/Promise.coffee
Normal file
74
plugins/UiPluginManager/media/js/lib/Promise.coffee
Normal file
|
@ -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
|
||||
###
|
8
plugins/UiPluginManager/media/js/lib/Prototypes.coffee
Normal file
8
plugins/UiPluginManager/media/js/lib/Prototypes.coffee
Normal file
|
@ -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
|
770
plugins/UiPluginManager/media/js/lib/maquette.js
Normal file
770
plugins/UiPluginManager/media/js/lib/maquette.js
Normal file
|
@ -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;
|
||||
};
|
||||
}));
|
138
plugins/UiPluginManager/media/js/utils/Animation.coffee
Normal file
138
plugins/UiPluginManager/media/js/utils/Animation.coffee
Normal file
|
@ -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()
|
3
plugins/UiPluginManager/media/js/utils/Dollar.coffee
Normal file
3
plugins/UiPluginManager/media/js/utils/Dollar.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
window.$ = (selector) ->
|
||||
if selector.startsWith("#")
|
||||
return document.getElementById(selector.replace("#", ""))
|
85
plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee
Normal file
85
plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee
Normal file
|
@ -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
|
19
plugins/UiPluginManager/media/plugin_manager.html
Normal file
19
plugins/UiPluginManager/media/plugin_manager.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<!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 plugin manager</h1>
|
||||
|
||||
<div class="content" id="content"></div>
|
||||
<div class="bottom" id="bottom-restart"></div>
|
||||
|
||||
<script type="text/javascript" src="js/all.js?rev={rev}&lang={lang}"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -22,6 +22,7 @@ rpcpassword=your-password
|
|||
rpcport=8336
|
||||
server=1
|
||||
txindex=1
|
||||
valueencoding=utf8
|
||||
```
|
||||
|
||||
Don't forget to change the `rpcuser` value and `rpcpassword` value!
|
||||
|
|
|
@ -13,7 +13,7 @@ log = logging.getLogger("ZeronamePlugin")
|
|||
@PluginManager.registerTo("SiteManager")
|
||||
class SiteManagerPlugin(object):
|
||||
site_zeroname = None
|
||||
db_domains = None
|
||||
db_domains = {}
|
||||
db_domains_modified = None
|
||||
|
||||
def load(self, *args, **kwargs):
|
||||
|
@ -21,21 +21,13 @@ class SiteManagerPlugin(object):
|
|||
if not self.get(config.bit_resolver):
|
||||
self.need(config.bit_resolver) # Need ZeroName site
|
||||
|
||||
# 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)
|
||||
|
||||
# Resolve domain
|
||||
# Return: The address or None
|
||||
def resolveDomain(self, domain):
|
||||
def resolveBitDomain(self, domain):
|
||||
domain = domain.lower()
|
||||
if not self.site_zeroname:
|
||||
self.site_zeroname = self.need(config.bit_resolver)
|
||||
|
@ -44,7 +36,11 @@ class SiteManagerPlugin(object):
|
|||
if not self.db_domains or self.db_domains_modified != site_zeroname_modified:
|
||||
self.site_zeroname.needFile("data/names.json", priority=10)
|
||||
s = time.time()
|
||||
try:
|
||||
self.db_domains = self.site_zeroname.storage.loadJson("data/names.json")
|
||||
except Exception as err:
|
||||
log.error("Error loading names.json: %s" % err)
|
||||
|
||||
log.debug(
|
||||
"Domain db with %s entries loaded in %.3fs (modification: %s -> %s)" %
|
||||
(len(self.db_domains), time.time() - s, self.db_domains_modified, site_zeroname_modified)
|
||||
|
@ -52,33 +48,22 @@ class SiteManagerPlugin(object):
|
|||
self.db_domains_modified = site_zeroname_modified
|
||||
return self.db_domains.get(domain)
|
||||
|
||||
# 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
|
||||
# Turn domain into address
|
||||
def resolveDomain(self, domain):
|
||||
return self.resolveBitDomain(domain) or super(SiteManagerPlugin, self).resolveDomain(domain)
|
||||
|
||||
return super(SiteManagerPlugin, self).need(address, *args, **kwargs)
|
||||
# Return: True if the address is domain
|
||||
def isDomain(self, address):
|
||||
return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address)
|
||||
|
||||
# Return: Site object or None if not found
|
||||
def get(self, address):
|
||||
if not self.loaded: # Not loaded yet
|
||||
self.load()
|
||||
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
|
||||
@PluginManager.registerTo("ConfigPlugin")
|
||||
class ConfigPlugin(object):
|
||||
def createArguments(self):
|
||||
group = self.parser.add_argument_group("Zeroname plugin")
|
||||
group.add_argument(
|
||||
"--bit_resolver", help="ZeroNet site to resolve .bit domains",
|
||||
default="1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F", metavar="address"
|
||||
)
|
||||
|
||||
return super(ConfigPlugin, self).createArguments()
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
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, **kwargs):
|
||||
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, **kwargs) # Get the wrapper frame output
|
||||
|
||||
@PluginManager.registerTo("ConfigPlugin")
|
||||
class ConfigPlugin(object):
|
||||
def createArguments(self):
|
||||
group = self.parser.add_argument_group("Zeroname plugin")
|
||||
group.add_argument('--bit_resolver', help='ZeroNet site to resolve .bit domains', default="1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F", metavar="address")
|
||||
|
||||
return super(ConfigPlugin, self).createArguments()
|
|
@ -1,2 +1 @@
|
|||
from . import UiRequestPlugin
|
||||
from . import SiteManagerPlugin
|
|
@ -79,8 +79,8 @@ class BootstrapperDb(Db.Db):
|
|||
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
|
||||
res = self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash})
|
||||
self.hash_ids[hash] = res.lastrowid
|
||||
return self.hash_ids[hash]
|
||||
|
||||
def peerAnnounce(self, ip_type, address, port=None, hashes=[], onion_signed=False, delete_missing_hashes=False):
|
||||
|
@ -100,8 +100,8 @@ class BootstrapperDb(Db.Db):
|
|||
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
|
||||
res = self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now})
|
||||
peer_id = res.lastrowid
|
||||
|
||||
# Check user's hashes
|
||||
res = self.execute("SELECT * FROM peer_to_hash WHERE ?", {"peer_id": peer_id})
|
||||
|
|
|
@ -150,7 +150,7 @@ class UiRequestPlugin(object):
|
|||
).fetchall()
|
||||
|
||||
yield "<br>%s (added: %s, peers: %s)<br>" % (
|
||||
str(hash_row["hash"]).encode("hex"), hash_row["date_added"], len(peer_rows)
|
||||
str(hash_row["hash"]).encode().hex(), hash_row["date_added"], len(peer_rows)
|
||||
)
|
||||
for peer_row in peer_rows:
|
||||
yield " - {ip4: <30} {onion: <30} added: {date_added}, announced: {date_announced}<br>".format(**dict(peer_row))
|
||||
yield " - {type} {address}:{port} added: {date_added}, announced: {date_announced}<br>".format(**dict(peer_row))
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
python_files = Test*.py
|
||||
addopts = -rsxX -v --durations=6
|
||||
markers =
|
||||
slow: mark a tests as slow.
|
||||
webtest: mark a test as a webtest.
|
5
plugins/disabled-Bootstrapper/plugin_info.json
Normal file
5
plugins/disabled-Bootstrapper/plugin_info.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Bootstrapper",
|
||||
"description": "Add BitTorrent tracker server like features to your ZeroNet client.",
|
||||
"default": "disabled"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue