bugfix
This commit is contained in:
parent
a8104356b7
commit
7ef6a33056
47
README.md
Normal file → Executable file
47
README.md
Normal file → Executable file
@ -1,2 +1,49 @@
|
||||
# gadget
|
||||
a light wight web server base on aiohttp
|
||||
|
||||
## Documentation
|
||||
* 中文文档请看[这里](docs/cn/index.md)
|
||||
* English documents is under writing
|
||||
|
||||
## Dependent
|
||||
* [ahserver](https://github.com/yumoqing/ahserver)
|
||||
* [sqlor](https://github.com/yumoqing/sqlor) if you want to use database
|
||||
* [apppublic](https://github.com/yumoqing/apppublic)
|
||||
|
||||
## Download
|
||||
```
|
||||
git clone git@github.com:yumoqing/gadget.git
|
||||
```
|
||||
|
||||
## Configuration
|
||||
please look [ahserver](https://github.com/yumoqing/ahserver) to learn how to configure
|
||||
|
||||
### support https
|
||||
under "website" in the conf/config.json file, identify ssl with "crtfile" and "keyfile"
|
||||
like this.
|
||||
```
|
||||
"website":{
|
||||
"ssl":{
|
||||
"crtfile":"$[workdir]$/conf/www.bsppo.com.pem",
|
||||
"keyfile":"$[workdir]$/conf/www.bsppo.com.key"
|
||||
}
|
||||
}
|
||||
```
|
||||
### log configure
|
||||
In the conf/config.json, need to config log, you need to identify "name", "levelname" and "logfile"
|
||||
|
||||
|
||||
```
|
||||
"logger":{
|
||||
"name":"gadget",
|
||||
"levelname":"debug",
|
||||
"logfile":"$[workdir]$/logs/gadget.log"
|
||||
}
|
||||
```
|
||||
## Test
|
||||
the test folder contains everything need for a base test.
|
||||
|
||||
please to go test folder and run
|
||||
```
|
||||
python ../src/gadget.py
|
||||
```
|
||||
|
65
docs/cn/README.md
Executable file
65
docs/cn/README.md
Executable file
@ -0,0 +1,65 @@
|
||||
# gadget
|
||||
一个python开发的基于aiohttp的异步应用服务器,集成了1)用户认证与权健,2)数据库操作,3)后台开发必须的包和函数
|
||||
|
||||
gadget支持多进程,或多主机部署(需安装redis),在nginx后面提供负载均衡,可按照业务需求动态扩展,提升系统处理能力。
|
||||
|
||||
功能拓展能力,提供两种扩展方法,gadget源码级扩展,或在工作目录的plugins目录下部署扩展的源码
|
||||
|
||||
gadget运行需要指定一个工作目录,在工作目录中需有以下文件或目录:
|
||||
* conf目录 - 配置文件目录
|
||||
* logs目录 - 日志文件目录
|
||||
* plugins目录 - 扩展目录
|
||||
* i18n目录 - 国际化支持
|
||||
* files目录(可在conf/config.json文件中指定其他位置)
|
||||
* wwwroot目录(可在conf/config.json文件中指定其他位置)
|
||||
|
||||
## 运行环境和依赖
|
||||
* 操作系统支持Windows, MacOS, linux
|
||||
* 数据库支持:sqlite3,mysql, mariadb, postgresql,sqlserver, oracle, teradata,华为的guass等(源码方式运行时需提供相应数据库的驱动包)
|
||||
* python 3.7以上版本
|
||||
* redis 当使用redis_storage时需要
|
||||
* [apppublic](https://github.com/yumoqing/appPublic)
|
||||
* [sqlor](https://github.com/yumoqing/sqlor)
|
||||
* [ahserver](https://github.com/yumoqing/ahserver)
|
||||
* [dataui](https://github.com/yumoqing/dataui)
|
||||
* 以及apppublic, sqlor, ahserver和dataui包所依赖的包或模块
|
||||
|
||||
## 运行gadget
|
||||
运行gadget的步骤
|
||||
1 转到工作目录
|
||||
2 gadget [ -p port ]
|
||||
|
||||
## 配置
|
||||
gadget的配置信息需放在工作目录的conf/config.json文件中,有关配置相关内容请看[config.json](config.md)
|
||||
## 模版
|
||||
gadget支持jinja2模版,模版中可以使用gadget定义的所有模块,变量和函数
|
||||
|
||||
## 数据库操作
|
||||
gadget为每个请求提供数据库操作, gadget不提供跨请求的数据库操作,请求中的数据库操作要么整体成功,要么整体失败,gadget使用[sqlor](https://github/yumoqing/sqlor)提供数据库支持。 请看[数据库操作说明](sql.md)
|
||||
|
||||
## 脚本
|
||||
gadget支持在后台使用受限的python脚本,后缀为“.dspy”
|
||||
|
||||
## 用户认证和授权
|
||||
gadget在缺少状态下不做用户认证和权健,要提供此功能需在plugins目录下放一个如下形式的脚本
|
||||
```
|
||||
# auth.py
|
||||
from ahserver.auth_api import AuthAPI
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
async def checkUserPermission(self, user, path):
|
||||
“”“
|
||||
user:用户id,None代表没有登录
|
||||
path:访问目录, path等于URL(“http(s)://x.com/ll/ll”) 中的“/ll/ll"部分
|
||||
返回值:True,有权限
|
||||
False,无权限
|
||||
如果返回False并且user为None,返回前端401,否则403
|
||||
”“”
|
||||
self.info(f'checkUserPermission():{user} access to {path} ..')
|
||||
# 这里做实际的判断,看看user是否有访问path的权限
|
||||
|
||||
return True
|
||||
|
||||
AuthAPI.checkUserPermission = checkUserPermission
|
||||
```
|
||||
|
206
docs/cn/config.md
Executable file
206
docs/cn/config.md
Executable file
@ -0,0 +1,206 @@
|
||||
#
|
||||
|
||||
## 宏
|
||||
使用$[x]$来引用宏,目前config.json文件中支持两个宏
|
||||
### workdir
|
||||
工作目录,gadget程序启动时的目录
|
||||
### ProgramPath
|
||||
gadget程序所在目录
|
||||
|
||||
例子
|
||||
```
|
||||
"logger":{
|
||||
"name":"iptvserver",
|
||||
"levelname":"debug",
|
||||
"logfile":"$[workdir]$/logs/iptv.log"
|
||||
}
|
||||
```
|
||||
上述例子定义了logfile为工作目录下的logs/iptv.log
|
||||
|
||||
## logger
|
||||
定义日志
|
||||
|
||||
例子
|
||||
```
|
||||
"logger":{
|
||||
"name":"iptvserver",
|
||||
"levelname":"debug",
|
||||
"logfile":"$[workdir]$/logs/iptv.log"
|
||||
},
|
||||
```
|
||||
### name
|
||||
日志名称
|
||||
### levelname
|
||||
日志级别名称
|
||||
* debug
|
||||
* info
|
||||
* error
|
||||
### logfile
|
||||
日志文件路径
|
||||
|
||||
## databases
|
||||
字典类型,每个key定义一个数据库定义, 支持链接多个数据库,可以同时定义多个不同类型的数据库
|
||||
|
||||
例子
|
||||
```
|
||||
“databases”:{
|
||||
"kboss":{
|
||||
"driver":"aiomysql",
|
||||
"async_mode":true,
|
||||
"coding":"utf8",
|
||||
"dbname":"kboss",
|
||||
"maxconn":100,
|
||||
"kwargs":{
|
||||
"user":"test",
|
||||
"db":"kboss",
|
||||
"password":"QUZVcXg5V1p1STMybG5Ia6mX9D0v7+g=",
|
||||
"host":"localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
上述定义了一个名为“kboss”的数据库,使用aiomysql“模块操作数据库
|
||||
异步模式
|
||||
最大支持100个链接,链接参数等
|
||||
|
||||
## website
|
||||
字典类型,定义website属性
|
||||
### port
|
||||
端口号
|
||||
|
||||
### host
|
||||
定义网站主机ip地址,通常设置为”0.0.0.0“, 内网外网都能访问
|
||||
|
||||
### ssl
|
||||
定义网站使用https访问,格式如下
|
||||
```
|
||||
"ssl":{
|
||||
"crtfile":证书绝对路径,
|
||||
"keyfile":证书密码文件绝对路径
|
||||
}
|
||||
```
|
||||
|
||||
### coding
|
||||
定义网站编码,建议使用”utf-8“编码
|
||||
|
||||
### processors
|
||||
数组,定义文件后缀网站路由,每一个元素定义一个后缀
|
||||
|
||||
例子
|
||||
```
|
||||
"processors":[
|
||||
[".proxy","proxy"],
|
||||
[".xlsxds","xlsxds"],
|
||||
[".sqlds","sqlds"],
|
||||
[".tmpl.js","tmpl"],
|
||||
[".tmpl.css","tmpl"],
|
||||
[".tmpl.html","tmpl"],
|
||||
[".bcrud", "bricks_crud"],
|
||||
[".tmpl","tmpl"],
|
||||
[".ui","tmpl"],
|
||||
[".dspy","dspy"],
|
||||
[".md","md"]
|
||||
|
||||
```
|
||||
上述例子定义:
|
||||
* .proxy 后缀的路径,使用ProxyProcessor来处理
|
||||
* .xlsxds后缀的路径,使用XlsxdsProcessor处理器来处理
|
||||
* .sqlds后缀的路径,采用SqldsProcessor处理器来处理
|
||||
* .tmpl.js后缀的路径,使用TmplProcessor处理器来处理
|
||||
* .tmpl.css后缀的路径,使用TmplProcessor处理器来处理
|
||||
* .tmpl.html后缀的路径,使用TmplProcessor处理器来处理
|
||||
* .bcrud后缀的路径,使用BricksCrudProcessor处理器来处理
|
||||
* .tmpl后缀的路径,使用TmplProcessor处理器来处理
|
||||
* .ui后缀的路径,使用TmplProcessor处理器来处理
|
||||
* .dspy后缀的路径,使用DspyProcessor处理器来处理
|
||||
* .md后缀的路径,使用MarkdownProcessor处理器来处理
|
||||
|
||||
### startswiths
|
||||
数组类型,定义网站的前缀路由,前缀路由使用注册函数实现
|
||||
|
||||
例子
|
||||
```
|
||||
"startswiths":[
|
||||
{
|
||||
"leading":"/public/publickey",
|
||||
"registerfunction":"getPublicKey"
|
||||
},
|
||||
{
|
||||
"leading":"/public/i18n",
|
||||
"registerfunction":"getI18nMapping"
|
||||
},
|
||||
{
|
||||
"leading":"/idfile",
|
||||
"registerfunction":"idFileDownload"
|
||||
},
|
||||
{
|
||||
"leading":"/thumb",
|
||||
"registerfunction":"makeThumb",
|
||||
"width":512,
|
||||
"quality":50,
|
||||
"keep_ratio":1
|
||||
}
|
||||
],
|
||||
```
|
||||
上述定义了4个前缀路由,除“leading”,”registerfunction“外的属性均被当作参数传递给注册函数
|
||||
|
||||
### rsakey
|
||||
支持rsa公开密钥加密,在这个属性下提供rsa的私钥和公钥
|
||||
例子
|
||||
```
|
||||
"rsakey":{
|
||||
"privatekey":"$[workdir]$/conf/rsa_private_key.pem",
|
||||
"publickey":"$[workdir]$/conf/rsa_public_key.pem"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 登录控制
|
||||
|
||||
在config.json文件对website属性下
|
||||
### authpaths
|
||||
authpaths是一个数组, 每个元素定义一个路径或路径的起始字符串,在authpaths中定义的路径,用户访问的URL的起始字符串与其相同时需要登录
|
||||
|
||||
例子1
|
||||
```
|
||||
"authpaths":[
|
||||
"/"
|
||||
]
|
||||
```
|
||||
上面的例子整个网站都需要登录后才能访问,这种情况需要设置例外,放出登录界面让用户访问
|
||||
|
||||
例子2
|
||||
```
|
||||
"authparhs":[
|
||||
"/scrent",
|
||||
"/private"
|
||||
]
|
||||
```
|
||||
以上例子定义了需要登录后才能访问的路径,所以以上述两个字符串开始的路径的访问都需要登录
|
||||
|
||||
### whitelist
|
||||
白名单,数组,每项定义一个例外
|
||||
例子1
|
||||
```
|
||||
"whitelist":[
|
||||
"/login",
|
||||
"/login"
|
||||
]
|
||||
```
|
||||
以上例子将根目录下所有以“login”开始的路径都设置成例外,不用登录也可以访问
|
||||
|
||||
## langMapping
|
||||
语言对照
|
||||
不同的浏览器和操作系统,向后台船队的语言差异挺大,这里定义了一个映射
|
||||
|
||||
例子
|
||||
```
|
||||
"langMapping":{
|
||||
"zh-Hans-CN":"zh-cn",
|
||||
"zh-CN":"zh-cn",
|
||||
"en-us":"en",
|
||||
"en-US":"en"
|
||||
}
|
||||
|
||||
```
|
17
docs/cn/dspy.md
Executable file
17
docs/cn/dspy.md
Executable file
@ -0,0 +1,17 @@
|
||||
# DspyProcessor
|
||||
dspy处理器处理受限py脚本,应用服务器规定了dspy脚本可以引用的模块,变量,和函数
|
||||
|
||||
## 环境
|
||||
环境是dspy脚本可以引用的一个资源集合,gadget服务器给dspy脚本给定了一个特定的执行环境
|
||||
|
||||
dspy环境请参看[环境](environment.md)
|
||||
|
||||
## 返回
|
||||
dspy 需要使用return 显式给应用服务器返回内容,
|
||||
可以返回的数据类型有:
|
||||
* dict
|
||||
* array
|
||||
* string
|
||||
* RESPONSE 对象及其衍生对象
|
||||
|
||||
|
77
docs/cn/environment.md
Executable file
77
docs/cn/environment.md
Executable file
@ -0,0 +1,77 @@
|
||||
# 环境
|
||||
gadget服务器给处理器提供了一个执行环境,处理器可以引用环境提供的模块,变量,函数等资源来完成处理器等处理任务
|
||||
|
||||
## 请求相关环境
|
||||
|
||||
### request
|
||||
服务器接收到的服务请求对象,具体内容请参考[AioHttp](https://github.com/aiohttp/aiohttp)
|
||||
|
||||
### params_kw
|
||||
服务器接收到浏览器发送过来的参数,支持各种http(s)方法过来的数据
|
||||
|
||||
### get_user()
|
||||
获得用户id, get_user()是一个协程
|
||||
|
||||
#### 例子
|
||||
```
|
||||
uid = await get_user()
|
||||
```
|
||||
#### 返回值
|
||||
null 或者当前登录用户id
|
||||
|
||||
### remember_user()
|
||||
#### 例子
|
||||
```
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
pwd = password(params_kw.password)
|
||||
ns = {
|
||||
"password":pwd,
|
||||
"name":params_kw.get('name')
|
||||
}
|
||||
|
||||
sql = "select * from users where name=${name}$ and password=${pwd}"
|
||||
x = await sor.sqlExe(sql, ns)
|
||||
if len(x) < 1:
|
||||
return Error(msg="user or password error")
|
||||
userid = x[0].['id']
|
||||
await remember_user(userid)
|
||||
```
|
||||
从前台接收用户名和密码,密码加密后从数据库检索符合名字和密码与参数重相同的用户记录,如果没有找到提示前提错误,否则用户登录成功,服务器记住登录用户的ID
|
||||
|
||||
#### 参数说明
|
||||
##### userid
|
||||
用户id
|
||||
|
||||
##### 返回值
|
||||
无
|
||||
|
||||
### entire_url(url)
|
||||
获得参数URL的绝对url
|
||||
|
||||
#### 例子
|
||||
dspy
|
||||
```
|
||||
url = entire_url('insert.dspy')
|
||||
```
|
||||
template
|
||||
```
|
||||
{
|
||||
"widgettype":"urlwidget",
|
||||
"options":{
|
||||
"url":"{{entire_url('insert.dspy')}}"
|
||||
}
|
||||
}
|
||||
```
|
||||
#### 参数说明
|
||||
##### url
|
||||
相对路径或绝对路径
|
||||
可以上一下形式
|
||||
* http://.....
|
||||
* https://......
|
||||
* /abc/lerltg.tmpl
|
||||
* abc/gggg.html
|
||||
|
||||
##### 返回值
|
||||
带http的路径
|
||||
|
225
docs/cn/sql.md
Executable file
225
docs/cn/sql.md
Executable file
@ -0,0 +1,225 @@
|
||||
# sqlor
|
||||
|
||||
|
||||
sqlor为web应用提供数据库接口支持, 目前支持的数据库有:
|
||||
* sqlite3
|
||||
* mysql(mariadb)
|
||||
* postgresql
|
||||
* gauss
|
||||
* oracle
|
||||
* sql server
|
||||
|
||||
本模块支持异步DBAPI接口,目前测试过的有aiomysql, aiopg
|
||||
|
||||
## sqlor 优点
|
||||
|
||||
* 支持多种数据库
|
||||
* 支持异步,减少了thread的开销
|
||||
* 自动游标管理
|
||||
* 自动事务管理
|
||||
|
||||
## 典型的应用模式
|
||||
|
||||
## sor 方法
|
||||
|
||||
### C(tablename, ns)
|
||||
向添加表中添加一条记录
|
||||
#### 例子
|
||||
```
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.C(tablename, ns)
|
||||
```
|
||||
#### 参数说明
|
||||
|
||||
##### tablename
|
||||
数据库表名称
|
||||
|
||||
##### ns
|
||||
字典类型的数据,字典的key为自发段名,字典的value是字段值,至少表主键和必须项要求数据
|
||||
|
||||
#### 返回值
|
||||
无
|
||||
|
||||
### R(tablename, ns, filters=None)
|
||||
按要求检索数据,支持分页数据检索和全部数据检索
|
||||
#### 例子
|
||||
```
|
||||
db = DBPools()
|
||||
ns = params_kw.copy()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.R(tablename, ns)
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
##### tablename
|
||||
数据表名
|
||||
##### ns
|
||||
字典数据,参数
|
||||
##### filters
|
||||
缺省为None, 无过滤对象
|
||||
否则是一个字典类型的过滤定义对象
|
||||
#### 返回值
|
||||
* 当ns中没有page属性时,说明返回全部数据,返回数组类型的数据列表
|
||||
* 当ns中有page属性时,返回
|
||||
```
|
||||
{
|
||||
"total":记录数,
|
||||
"rows":[一页的数据记录]
|
||||
}
|
||||
```
|
||||
字典数据
|
||||
|
||||
### U(tablename, ns)
|
||||
修改数据记录,ns中的keys必须包含数据表的primary字段,按照primary字段数据检索并修改数据记录
|
||||
#### 例子
|
||||
```
|
||||
db = DBPools()
|
||||
ns = params_kw.copy()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.U(tablename, ns)
|
||||
```
|
||||
#### 参数说明
|
||||
|
||||
##### tablename
|
||||
数据表名
|
||||
##### ns
|
||||
字典类型的数据,字典的key为自发段名,字典的value是字段值,至少表主键和必须项要求数据
|
||||
#### 返回值
|
||||
无
|
||||
|
||||
### D(tablename, ns)
|
||||
删除数据记录,ns中的keys必须包含数据表的primary字段,按照primary字段数据删除数据表记录
|
||||
#### 例子
|
||||
```
|
||||
db = DBPools()
|
||||
ns = params_kw.copy()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.D(tablename, ns)
|
||||
```
|
||||
#### 参数说明
|
||||
|
||||
##### tablename
|
||||
数据表名
|
||||
##### ns
|
||||
字典类型的数据,字典的key为自发段名,字典的value是字段值,至少表主键数据
|
||||
#### 返回值
|
||||
无
|
||||
|
||||
### tables()
|
||||
|
||||
### I(tablename)
|
||||
获得表的建表信息
|
||||
#### 例子
|
||||
```
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.I(tablename)
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
##### tablename
|
||||
数据表名
|
||||
|
||||
### 返回值
|
||||
如果数据表不存在,返回None,否则返回字典类型,
|
||||
|
||||
返回值例子
|
||||
```
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "user",
|
||||
"title": "\u7528\u6237\u89d2\u8272",
|
||||
"primary": [
|
||||
"id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"dec": null,
|
||||
"nullable": "no",
|
||||
"title": "\u7528\u6237id",
|
||||
"table_name": "user"
|
||||
},
|
||||
{
|
||||
"name": "userid",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"dec": null,
|
||||
"nullable": "yes",
|
||||
"title": "\u7528\u6237id",
|
||||
"table_name": "user"
|
||||
},
|
||||
{
|
||||
"name": "rolerd",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"dec": null,
|
||||
"nullable": "yes",
|
||||
"title": "\u89d2\u8272id",
|
||||
"table_name": "user"
|
||||
}
|
||||
],
|
||||
"indexes": []
|
||||
}
|
||||
```
|
||||
|
||||
### sqlExe(sql,ns)
|
||||
执行SQL语句
|
||||
#### 例子
|
||||
```
|
||||
db = DBPools()
|
||||
sql = "select * from mytable id=${id} country=${country}"
|
||||
ns = params_kw.copy()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.sqlExe(sql, ns)
|
||||
```
|
||||
sql中支持使用参数,sqlor使用$[x}$引用变量,sql中的变量需要在ns中能够找到
|
||||
|
||||
#### 参数说明
|
||||
##### sql
|
||||
字符串, 一个合法的sql语句字符串,支持变量,变量用${x}$格式引用ns中的属性.
|
||||
|
||||
##### ns
|
||||
sql语句所需参数字典,如果在ns中找不到sql中引用的变量,将出错
|
||||
|
||||
#### 返回值
|
||||
如果是“select” 语句,返回数组变量,包含所有找到的数据记录
|
||||
其他类型的sql执行结果没有定义
|
||||
|
||||
如果出错,返回None
|
||||
|
||||
### sqlPaging(sql, ns)
|
||||
执行sql,并分页返回数据
|
||||
|
||||
#### 例子
|
||||
```
|
||||
db = DBPools()
|
||||
sql = "select * from mytable id=${id} country=${country}"
|
||||
ns = params_kw.copy()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.sqlPaging(sql, ns)
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
##### sql
|
||||
字符串, 一个合法的sql语句字符串,支持变量,变量用${x}$格式引用ns中的属性.
|
||||
|
||||
##### ns
|
||||
ns中必须包含一个sort属性,说明sql结果数据的排序字段,如果不指定,函数将出错,page参数如果不存在,自动设置为1
|
||||
|
||||
##### 返回值
|
||||
如果sql是一个select语句,返回如下结构数据
|
||||
```
|
||||
{
|
||||
"total":总记录数
|
||||
"rows":[一页的数据]
|
||||
}
|
||||
```
|
||||
|
0
docs/index.md
Executable file
0
docs/index.md
Executable file
4
macos/build.sh
Executable file
4
macos/build.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
pyinstaller -y --clean gadget.spec
|
||||
cp dist/gadget ~/bin
|
||||
# scp dist/gadget kimird.com:/opt/kymoz/macosx
|
48
macos/gadget.spec
Executable file
48
macos/gadget.spec
Executable file
@ -0,0 +1,48 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
import dataui
|
||||
duipath = os.path.dirname(dataui.__file__)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['../src/gadget.py'],
|
||||
pathex=['/Volumes/home/ymq/pydev/github/gadget/src'],
|
||||
binaries=[],
|
||||
datas=[
|
||||
(f'{duipath}/tmpl/bricks/*.*', 'dataui/tmpl/bricks')
|
||||
],
|
||||
hiddenimports=[
|
||||
'sqlite3',
|
||||
'aiopg',
|
||||
'aiomysql'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='gadget',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None )
|
10
requirements.txt
Executable file
10
requirements.txt
Executable file
@ -0,0 +1,10 @@
|
||||
ahserver
|
||||
sqlor
|
||||
apppublic
|
||||
aiohttp
|
||||
aiohttp-auth-autz
|
||||
openpyxl
|
||||
aiofiles
|
||||
aiohttp_session
|
||||
rsa
|
||||
cryptography
|
51
src/gadget.py
Executable file
51
src/gadget.py
Executable file
@ -0,0 +1,51 @@
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from dataui.crud_parser import BricksCRUDProcessor
|
||||
from ahserver.configuredServer import ConfiguredServer
|
||||
|
||||
from appPublic.registerfunction import RegisterFunction
|
||||
from appPublic.objectAction import ObjectAction
|
||||
from appPublic.app_logger import create_logger
|
||||
from appPublic.folderUtils import ProgramPath
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.i18n import getI18N
|
||||
|
||||
from ahserver.filedownload import path_encode
|
||||
from imgThumb import thumb
|
||||
from idfile import idFileDownload
|
||||
# from myauth import MyAuthAPI
|
||||
from rf import getPublicKey, getI18nMapping
|
||||
from loadplugins import load_plugins
|
||||
from version import __version__
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(prog="Gadget")
|
||||
parser.add_argument('-w', '--workdir')
|
||||
parser.add_argument('-p', '--port')
|
||||
args = parser.parse_args()
|
||||
workdir = args.workdir or os.getcwd()
|
||||
p = ProgramPath()
|
||||
print(workdir)
|
||||
config = getConfig(workdir, NS={'workdir':workdir, 'ProgramPath':p})
|
||||
if config.logger:
|
||||
logger = create_logger(config.logger.name or 'gadget',
|
||||
levelname=config.logger.levelname or 'debug',
|
||||
file=config.logger.logfile or None)
|
||||
else:
|
||||
logger = create_logger('gadget', levelname='debug')
|
||||
rf = RegisterFunction()
|
||||
rf.register('makeThumb',thumb)
|
||||
rf.register('idFileDownload',idFileDownload)
|
||||
rf.register('getPublicKey', getPublicKey)
|
||||
rf.register('getI18nMapping', getI18nMapping)
|
||||
|
||||
i18n = getI18N(path=workdir)
|
||||
# server = ConfiguredServer(auth_klass=MyAuthAPI, workdir=workdir)
|
||||
server = ConfiguredServer(workdir=workdir)
|
||||
load_plugins(workdir)
|
||||
logger.info(f'gadget version={__version__}')
|
||||
logger.debug(f'debug mode show ?')
|
||||
port = args.port or config.website.port or 8080
|
||||
port = int(port)
|
||||
server.run(port=port)
|
9
src/gadgetstart.sh
Normal file
9
src/gadgetstart.sh
Normal file
@ -0,0 +1,9 @@
|
||||
start_gadget()
|
||||
{
|
||||
while [ "1" = "1" ]
|
||||
do
|
||||
cd /d/gadget
|
||||
bin/gadget >gadget.out 2>&1
|
||||
done
|
||||
}
|
||||
start_gadget &
|
12
src/id2file.py
Executable file
12
src/id2file.py
Executable file
@ -0,0 +1,12 @@
|
||||
|
||||
from sqlor.dbpools import runSQL
|
||||
|
||||
async def getFilenameFromId(idstr:str) -> str:
|
||||
sql = "select * from kvobjects where id='%s'" % idstr
|
||||
recs = await runSQL('homedata',sql)
|
||||
if recs is None:
|
||||
return None
|
||||
if len(recs) == 0:
|
||||
return None
|
||||
return recs[0].name
|
||||
|
30
src/idfile.py
Normal file
30
src/idfile.py
Normal file
@ -0,0 +1,30 @@
|
||||
import os
|
||||
from PIL import Image, ExifTags
|
||||
from io import BytesIO
|
||||
from aiohttp.web_exceptions import (
|
||||
HTTPException,
|
||||
HTTPExpectationFailed,
|
||||
HTTPForbidden,
|
||||
HTTPMethodNotAllowed,
|
||||
HTTPNotFound,
|
||||
)
|
||||
from aiohttp.web_response import Response, StreamResponse
|
||||
|
||||
from appPublic.registerfunction import RegisterFunction
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from ahserver.filedownload import file_download
|
||||
from id2file import getFilenameFromId
|
||||
|
||||
def www_abspath(fp):
|
||||
if fp[0] == '/':
|
||||
fp = fp[1:]
|
||||
config = getConfig()
|
||||
return os.path.join(config.filesroot, fp)
|
||||
|
||||
async def idFileDownload(*args, **kw):
|
||||
print(f'idFileDownload(): {args=}, {kw=}')
|
||||
fname = kw.get('path', None)
|
||||
path = www_abspath(fname)
|
||||
print(f'{fname=}, {path=}')
|
||||
request = kw.get('request')
|
||||
return await file_download(request,path)
|
82
src/imgThumb.py
Executable file
82
src/imgThumb.py
Executable file
@ -0,0 +1,82 @@
|
||||
import os
|
||||
from PIL import Image, ExifTags
|
||||
from io import BytesIO
|
||||
from aiohttp.web_exceptions import (
|
||||
HTTPException,
|
||||
HTTPExpectationFailed,
|
||||
HTTPForbidden,
|
||||
HTTPMethodNotAllowed,
|
||||
HTTPNotFound,
|
||||
)
|
||||
from aiohttp.web_response import Response, StreamResponse
|
||||
from id2file import getFilenameFromId
|
||||
|
||||
|
||||
def imageUp(img):
|
||||
try:
|
||||
o = 'Orientation'
|
||||
exif=dict(img._getexif().items())
|
||||
if exif[o] == 3:
|
||||
img = img.rotate(180, expand=True)
|
||||
elif exif[o] == 6:
|
||||
img = img.rotate(270, expand=True)
|
||||
elif exif[o] == 8:
|
||||
img = img.rotate(90, expand=True)
|
||||
return img
|
||||
except (AttributeError, KeyError, IndexError):
|
||||
# cases: image don't have getexif
|
||||
return img
|
||||
|
||||
def imageThumb(imgfilepath,width=None,height=None):
|
||||
im = Image.open(imgfilepath)
|
||||
im = imageUp(im)
|
||||
mode = im.mode
|
||||
if mode not in ('L', 'RGB'):
|
||||
if mode == 'RGBA':
|
||||
alpha = im.split()[3]
|
||||
bgmask = alpha.point(lambda x: 255-x)
|
||||
im = im.convert('RGB')
|
||||
# paste(color, box, mask)
|
||||
im.paste((255,255,255), None, bgmask)
|
||||
else:
|
||||
im = im.convert('RGB')
|
||||
|
||||
w, h = im.size
|
||||
if not width and not height:
|
||||
width = 256
|
||||
if width:
|
||||
width = int(width)
|
||||
height = int(float(width) * float(h) / float(w))
|
||||
else:
|
||||
height = int(height)
|
||||
width = int(float(height) * float(w) / float(h))
|
||||
thumb = im.resize((width,height),Image.ANTIALIAS)
|
||||
f = BytesIO()
|
||||
thumb.save(f,format='jpeg',quality=60)
|
||||
im.close()
|
||||
v = f.getvalue()
|
||||
return v
|
||||
|
||||
async def thumb(*args, **kw):
|
||||
id = args[0]
|
||||
request = kw.get('request')
|
||||
xpath = request.path[len(options.leading):]
|
||||
if xpath == '':
|
||||
raise HTTPNotFound
|
||||
id = xpath[1:]
|
||||
imgpath = await getFilenameFromId(id)
|
||||
v = imageThumb(imgpath,width=options.width,height=options.height)
|
||||
response = Response(
|
||||
status=200,
|
||||
headers = {
|
||||
'Content-Disposition': 'attrachment;filename={}'.format(os.path.basename(imgpath)),
|
||||
'Content-Length':str(len(v))
|
||||
}
|
||||
)
|
||||
await response.prepare(request)
|
||||
await response.write(v)
|
||||
await response.write_eof()
|
||||
return response
|
||||
|
||||
if __name__ == '__main__':
|
||||
imageThumb("/home/ymq/media/pictures/2019-08/IMG_20190804_113014.jpg", width=256)
|
17
src/loadplugins.py
Executable file
17
src/loadplugins.py
Executable file
@ -0,0 +1,17 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from appPublic.folderUtils import listFile
|
||||
from appPublic.ExecFile import ExecFile
|
||||
|
||||
def load_plugins(p_dir):
|
||||
ef = ExecFile()
|
||||
pdir = os.path.join(p_dir, 'plugins')
|
||||
if not os.path.isdir(pdir):
|
||||
print('load_plugins:%s not exists' % pdir)
|
||||
return
|
||||
sys.path.append(pdir)
|
||||
for py in listFile(pdir, suffixs=['.py'], rescursive=True):
|
||||
ef.set('sys',sys)
|
||||
ef.run(py)
|
||||
|
37
src/myauth.py
Executable file
37
src/myauth.py
Executable file
@ -0,0 +1,37 @@
|
||||
from ahserver.auth_api import AuthAPI
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.registerfunction import getRegisterFunctionByName
|
||||
|
||||
class MyAuthAPI(AuthAPI):
|
||||
async def needAuth(self,path):
|
||||
return True
|
||||
|
||||
async def checkUserPermission(self, user, path):
|
||||
return True
|
||||
|
||||
async def checkUserPassword(self,user_id,password):
|
||||
config = getConfig()
|
||||
if config.users:
|
||||
for userid, passwd in config.users.items():
|
||||
if user_id == userid and password == passwd:
|
||||
print('******user passwd check OK****************')
|
||||
return True
|
||||
rf = getRegisterFunctionByName('user_password_check')
|
||||
if rf:
|
||||
return rf(user_id, password)
|
||||
|
||||
return False
|
||||
|
||||
async def getPermissionNeed(self,path):
|
||||
rf = getRegisterFunctionByName('get_need_permission')
|
||||
if rf:
|
||||
return rf(path)
|
||||
|
||||
return 'ok'
|
||||
|
||||
async def getUserPermissions(self,user):
|
||||
rf = getRegisterFunctionByName('get_user_permissions')
|
||||
if rf:
|
||||
return rf(user)
|
||||
|
||||
return ['ok']
|
16
src/rf.py
Executable file
16
src/rf.py
Executable file
@ -0,0 +1,16 @@
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.i18n import getI18N
|
||||
from ahserver.filedownload import file_download
|
||||
|
||||
async def getPublicKey(*args, **kw):
|
||||
config = getConfig()
|
||||
request = kw.get('request')
|
||||
pf = config.website.rsakey.publickey
|
||||
return await file_download(request,pf)
|
||||
|
||||
async def getI18nMapping(*args, **kw):
|
||||
lang = args[0]
|
||||
i18n = getI18N()
|
||||
mapping = i18n.getLangDict(lang)
|
||||
return mapping
|
||||
|
1
src/version.py
Executable file
1
src/version.py
Executable file
@ -0,0 +1 @@
|
||||
__version__ = '1.0.1'
|
48
test/conf/config.json
Executable file
48
test/conf/config.json
Executable file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"logger":{
|
||||
"name":"gadget",
|
||||
"levelname":"debug",
|
||||
"logfile":"$[workdir]$/logs/gadget.log"
|
||||
},
|
||||
"filesroot":"$[workdir]$/files",
|
||||
"website":{
|
||||
"paths":[
|
||||
["$[workdir]$/wwwroot",""]
|
||||
],
|
||||
"host":"0.0.0.0",
|
||||
"port":9080,
|
||||
"coding":"utf-8",
|
||||
"ssl_gg":{
|
||||
"crtfile":"$[workdir]$/conf/www.bsppo.com.pem",
|
||||
"keyfile":"$[workdir]$/conf/www.bsppo.com.key"
|
||||
},
|
||||
"indexes":[
|
||||
"index.html",
|
||||
"index.tmpl",
|
||||
"index.dspy",
|
||||
"index.md"
|
||||
],
|
||||
"processors":[
|
||||
[".proxy","proxy"],
|
||||
[".xlsxds","xlsxds"],
|
||||
[".sqlds","sqlds"],
|
||||
[".tmpl.js","tmpl"],
|
||||
[".tmpl.css","tmpl"],
|
||||
[".html.tmpl","tmpl"],
|
||||
[".tmpl","tmpl"],
|
||||
[".ui","tmpl"],
|
||||
[".dspy","dspy"],
|
||||
[".md","md"]
|
||||
],
|
||||
"rsakey":{
|
||||
"privatekey":"$[workdir]$/conf/rsa_private_key.pem",
|
||||
"publickey":"$[workdir]$/conf/rsa_public_key.pem"
|
||||
}
|
||||
},
|
||||
"langMapping":{
|
||||
"zh-Hans-CN":"zh-cn",
|
||||
"zh-CN":"zh-cn",
|
||||
"en-us":"en",
|
||||
"en-US":"en"
|
||||
}
|
||||
}
|
0
test/files/README.md
Executable file
0
test/files/README.md
Executable file
0
test/i18n/en_US/msg.txt
Executable file
0
test/i18n/en_US/msg.txt
Executable file
0
test/i18n/zh_CN/msg.txt
Executable file
0
test/i18n/zh_CN/msg.txt
Executable file
0
test/logs/README.md
Executable file
0
test/logs/README.md
Executable file
2
test/logs/gadget.log
Normal file
2
test/logs/gadget.log
Normal file
@ -0,0 +1,2 @@
|
||||
2023-02-23 15:51:57,018[gadget][INFO][gadget.py:60]gadget version=1.0.1
|
||||
2023-02-23 15:51:57,018[gadget][DEBUG][gadget.py:61]debug mode show ?
|
7
test/wwwroot/index.html
Executable file
7
test/wwwroot/index.html
Executable file
@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<p>Welcome to gadget</p>
|
||||
</body>
|
||||
</html>
|
1
ubuntu/build.sh
Executable file
1
ubuntu/build.sh
Executable file
@ -0,0 +1 @@
|
||||
pyinstaller -y --clean gadget.spec
|
15
ubuntu/gadget.service
Normal file
15
ubuntu/gadget.service
Normal file
@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Gadget Webserver base on aiothhp
|
||||
After=network.target mariadb.service
|
||||
|
||||
[Service]
|
||||
# EnvironmentFile=-/etc/default/gadget
|
||||
ExecStart=/d/gadget/bin/start.sh
|
||||
ExecStop=/usr/bin/killall gadget
|
||||
KillMode=process
|
||||
RuntimeDirectory=/d/gadget
|
||||
RuntimeDirectoryMode=0755
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Alias=gadget.service
|
48
ubuntu/gadget.spec
Executable file
48
ubuntu/gadget.spec
Executable file
@ -0,0 +1,48 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
import dataui
|
||||
duipath = os.path.dirname(dataui.__file__)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['../src/gadget.py'],
|
||||
pathex=['/Volumes/home/ymq/pydev/github/gadget/src'],
|
||||
binaries=[],
|
||||
datas=[
|
||||
(f'{duipath}/tmpl/bricks/*.*', 'dataui/tmpl/bricks')
|
||||
],
|
||||
hiddenimports=[
|
||||
'sqlite3',
|
||||
'aiopg',
|
||||
'aiomysql'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='gadget',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None )
|
3
windows/build.cmd
Executable file
3
windows/build.cmd
Executable file
@ -0,0 +1,3 @@
|
||||
pyinstaller -y --clean gadget.spec
|
||||
copy dist\gadget.exe c:\cmds
|
||||
|
48
windows/gadget.spec
Executable file
48
windows/gadget.spec
Executable file
@ -0,0 +1,48 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
import dataui
|
||||
duipath = os.path.dirname(dataui.__file__)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['../src/gadget.py'],
|
||||
pathex=['/Volumes/home/ymq/pydev/github/gadget/src'],
|
||||
binaries=[],
|
||||
datas=[
|
||||
(f'{duipath}/tmpl/bricks/*.*', 'dataui/tmpl/bricks')
|
||||
],
|
||||
hiddenimports=[
|
||||
'sqlite3',
|
||||
'aiopg',
|
||||
'aiomysql'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='gadget',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None )
|
Loading…
Reference in New Issue
Block a user