This commit is contained in:
yumoqing 2023-12-05 11:33:44 +08:00
parent a8104356b7
commit 7ef6a33056
31 changed files with 1126 additions and 0 deletions

47
README.md Normal file → Executable file
View 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
View 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
* 数据库支持sqlite3mysql mariadb postgresqlsqlserver 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用户idNone代表没有登录
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
View 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
View 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
View File

@ -0,0 +1,77 @@
# 环境
gadget服务器给处理器提供了一个执行环境处理器可以引用环境提供的模块变量函数等资源来完成处理器等处理任务
## 请求相关环境
### request
服务器接收到的服务请求对象,具体内容请参考[AioHttp](https://github.com/aiohttp/aiohttp)
### params_kw
服务器接收到浏览器发送过来的参数支持各种https方法过来的数据
### 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
View File

@ -0,0 +1,225 @@
# sqlor
sqlor为web应用提供数据库接口支持 目前支持的数据库有:
* sqlite3
* mysqlmariadb
* 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
View File

4
macos/build.sh Executable file
View 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
View 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
View File

@ -0,0 +1,10 @@
ahserver
sqlor
apppublic
aiohttp
aiohttp-auth-autz
openpyxl
aiofiles
aiohttp_session
rsa
cryptography

51
src/gadget.py Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
__version__ = '1.0.1'

48
test/conf/config.json Executable file
View 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
View File

0
test/i18n/en_US/msg.txt Executable file
View File

0
test/i18n/zh_CN/msg.txt Executable file
View File

0
test/logs/README.md Executable file
View File

2
test/logs/gadget.log Normal file
View 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
View File

@ -0,0 +1,7 @@
<html>
<head>
</head>
<body>
<p>Welcome to gadget</p>
</body>
</html>

1
ubuntu/build.sh Executable file
View File

@ -0,0 +1 @@
pyinstaller -y --clean gadget.spec

15
ubuntu/gadget.service Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
pyinstaller -y --clean gadget.spec
copy dist\gadget.exe c:\cmds

48
windows/gadget.spec Executable file
View 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 )