Update to v7.57.0

This commit is contained in:
aapanel.com
2025-11-21 17:19:27 +08:00
parent b6e4ab8b35
commit eb21fc65dd
1241 changed files with 22299 additions and 5531 deletions
+17 -1
View File
@@ -1481,7 +1481,23 @@ class ajax:
tmp = public.ExecShell(php_bin + ' -c {} /www/server/panel/class/php_info.php'.format(php_ini))[0]
if tmp.find('Warning: JIT is incompatible') != -1:
tmp = tmp.strip().split('\n')[-1]
result = json.loads(tmp)
try:
result = json.loads(tmp)
result['phpinfo'] = {}
if "modules" not in result:
result['modules'] = []
if 'php_version' in result:
result['phpinfo']['php_version'] = result['php_version']
except Exception:
result = {
'php_version': php_version,
'phpinfo': {},
'modules': [],
'ini': ''
}
# result = json.loads(tmp)
result['phpinfo'] = {}
result['phpinfo']['php_version'] = result['php_version']
result['phpinfo']['php_path'] = php_path
+1 -1
View File
@@ -48,7 +48,7 @@ class panelSetup:
if ua.find('spider') != -1 or g.ua.find('bot') != -1:
return abort(403)
g.version = '7.55.0'
g.version = '7.57.0'
g.title = public.GetConfigValue('title')
g.uri = request.path
g.debug = os.path.exists('data/debug.pl')
+28 -8
View File
@@ -1285,6 +1285,10 @@ class config:
if os.path.exists('/etc/redhat-release'):
filename = '/usr/local/lsws/lsphp' + get.version + '/etc/php.ini'
phpini = public.readFile(filename)
if not isinstance(phpini, str):
return public.return_msg_gettext(False, 'Failed to read PHP configuration file, it may not exist: {}'.format(filename))
rep = r'session.save_handler\s*=\s*([0-9A-Za-z_& ~]+)(\s*;?|\r?\n)'
save_handler = re.search(rep, phpini)
if save_handler:
@@ -1860,7 +1864,11 @@ class config:
if not os.path.exists(session_path):
os.makedirs(session_path)
public.ExecShell('chown www.www {}'.format(session_path))
run_path = panelSite.panelSite().GetSiteRunPath(get)["runPath"]
run_path_data = panelSite.panelSite().GetSiteRunPath(get)
if not run_path_data:
return public.return_msg_gettext(False, 'Failed to get site runtime path!')
run_path = run_path_data.get('runPath')
user_ini_file = "{site_path}{run_path}/.user.ini".format(site_path=site_info["path"], run_path=run_path)
conf = "session.save_path={}/\nsession.save_handler = files".format(session_path)
if get.act == "1":
@@ -3015,11 +3023,26 @@ class config:
settings = '{}/BTPanel/languages/settings.json'.format(public.get_panel_path())
custom = '{}/BTPanel/static/vite/lang/my-MY'.format(public.get_panel_path())
if not os.path.exists(settings):
data = public.default_languages_config()
public.writeFile(settings, json.dumps(data))
default_data = public.default_languages_config()
file_content = public.readFile(settings)
if not file_content:
public.writeFile(settings, json.dumps(default_data))
data = default_data
else:
try:
data = json.loads(file_content)
except json.JSONDecodeError:
public.writeFile(settings, json.dumps(default_data))
data = default_data
setlang = "/www/server/panel/BTPanel/languages/language.pl"
if os.path.exists(setlang):
olang = public.ReadFile(setlang)
if olang:
data['default'] = olang
data = json.loads(public.readFile(settings))
if os.path.exists(custom):
data['languages'].append({
"name": "my",
@@ -3028,10 +3051,7 @@ class config:
"cn": "自定义"
})
# public.print_log(data)
return data
# 设置语言偏好
def set_language(self, args):
+4 -2
View File
@@ -545,8 +545,10 @@ class data:
id = get.id
SQL = db.Sql().table(tableName)
where = "id=?"
retuls = SQL.where(where,(id,)).getField(keyName)
return public.xsssec(retuls)
result = SQL.where(where,(id,)).getField(keyName)
if type(result) == str:
result = public.xsssec(result)
return result
'''
* 获取数据与分页
+10 -3
View File
@@ -288,7 +288,7 @@ session.save_handler = files'''.format(path, sess_path, sess_path)
if args.f_name.find('./') != -1 or args.f_path.find('./') != -1:
return public.return_msg_gettext(False, public.lang("Wrong parameter"))
if not os.path.exists(args.f_path):
os.makedirs(args.f_path, 493)
os.makedirs(args.f_path, 493, True)
if not 'dir_mode' in args or not 'file_mode' in args:
self.set_mode(args.f_path)
@@ -325,6 +325,8 @@ session.save_handler = files'''.format(path, sess_path, sess_path)
os.remove(new_name)
except:
public.ExecShell("rm -f %s" % new_name)
if os.path.isdir(new_name):
return public.returnMsg(False, "If the destination path already has a directory with the same name, change the file name")
os.renames(save_path, new_name)
if 'dir_mode' in args and 'file_mode' in args:
mode_tmp1 = args.dir_mode.split(',')
@@ -1150,7 +1152,7 @@ session.save_handler = files'''.format(path, sess_path, sess_path)
ps_body = public.xssencode2(args.ps_body)
ps_path = public.get_panel_path() + '/data/files_ps'
if not os.path.exists(ps_path):
os.makedirs(ps_path,384)
os.makedirs(ps_path,384, True)
if ps_type == 1:
f_name = os.path.basename(filename)
else:
@@ -1969,7 +1971,7 @@ session.save_handler = files'''.format(path, sess_path, sess_path)
if get.path.find('rewrite') == -1:
return public.return_msg_gettext(False, public.lang("Configuration file not exist"))
public.writeFile(get.path,'')
if self.__get_ext(get.path) in ['gz','zip','rar','exe','db','pdf','doc','xls','docx','xlsx','ppt','pptx','7z','bz2','png','gif','jpg','jpeg','bmp','icon','ico','pyc','class','so','pyd']:
if self.__get_ext(get.path) in ['gz','zip','rar','exe','db','pdf','doc','xls','docx','xlsx','ppt','pptx','7z','bz2','png','gif','jpg','jpeg','bmp','icon','ico','pyc','class','so','pyd','sock']:
return public.return_msg_gettext(False, public.lang("The file format does not support online editing!"))
# if os.path.getsize(get.path) > 3145928:
# return public.return_msg_gettext(False, public.lang("Cannot edit files larger than 2MB online!"))
@@ -2328,6 +2330,11 @@ session.save_handler = files'''.format(path, sess_path, sess_path)
# 文件压缩
def Zip(self, get):
if not hasattr(get, 'dfile') or not get.dfile.strip():
return public.return_msg_gettext(False, public.lang("The target compressed file cannot be empty!"))
dir_name = os.path.dirname(get.dfile)
if dir_name and not os.path.exists(dir_name):
os.makedirs(dir_name, exist_ok=True)
if not 'z_type' in get:
get.z_type = 'rar'
+1 -1
View File
@@ -939,7 +939,7 @@ class main(Base):
'ports=? and address=? and protocol=? and types=? and chain=?',
(get.port, get.address, get.protocol, get.strategy, get.chain)
).find()
if query_result:
if query_result and isinstance(query_result, dict):
if get.protocol == "tcp/udp" and self._isFirewalld:
self.remove_port_db(get, "tcp", query_result['addtime'], query_result['domain'])
self.remove_port_db(get, "udp", query_result['addtime'], query_result['domain'])
+47 -3
View File
@@ -7,9 +7,17 @@
# | Author: hwliang <hwl@aapanel.com>
# +-------------------------------------------------------------------
from public.hook_import import hook_import
hook_import()
import time,public,db,os,sys,json,re,shutil
import os
import re
import sys
import json
import time
import public
import db
os.chdir('/www/server/panel')
if not 'class/' in sys.path:
@@ -30,6 +38,7 @@ def control_init_now():
def control_init_delay():
delay_list = [
(install_packages,),
(rep_websocket_conf,),
(clear_other_files,),
(remove_tty1,),
@@ -43,8 +52,6 @@ def control_init_delay():
(remove_other,),
(upgrade_polkit,),
(deb_bashrc,),
(install_pycountry,),
(install_pyroute2,),
(update_pubsuffix_dat,),
(upgrade_fastcgi_cache_conf_format,),
(clean_max_log, '/www/server/panel/plugin/rsync/lsyncd.log'),
@@ -56,6 +63,7 @@ def control_init_delay():
for task in delay_list:
try:
if len(task) >= 2:
# noinspection PyArgumentList
task[0](*task[1:])
else:
task[0]()
@@ -111,6 +119,41 @@ def control_init():
install_pyroute2()
upgrade_fastcgi_cache_conf_format()
def install_packages():
try:
import dns.resolver
except ImportError:
public.ExecShell(f"{public.get_panel_path()}/pyenv/bin/pip3 install dnspython")
except Exception as e:
public.print_log("install pysqlite3 error:", str(e))
try:
import pysqlite3
except ImportError:
public.ExecShell(f"{public.get_panel_path()}/pyenv/bin/pip3 install pysqlite3-binary")
except Exception as e:
public.print_log("install pysqlite3 error:", str(e))
try:
import jsonpath
except ImportError:
public.ExecShell(f"{public.get_panel_path()}/pyenv/bin/pip3 install jsonpath")
except Exception as e:
public.print_log("install_packages error:", str(e))
try:
install_pycountry()
except Exception as e:
public.print_log("install pycountry error:", str(e))
try:
install_pyroute2()
except Exception as e:
public.print_log("install pyroute2 error:", str(e))
def rm_apache_cgi_test():
'''
@name 删除apache测试cgi文件
@@ -923,6 +966,7 @@ def test_ping():
if os.path.exists(_f): os.remove(_f)
try:
import panelPing
# noinspection PyUnresolvedReferences
panelPing.Test().create_token()
except:
pass
+1 -1
View File
@@ -94,7 +94,7 @@ class nginx:
try:
v = int(v)
except ValueError:
pass
v = 0
kv = {"name":k, "value": int(v), "unit":u,"ps":psstr}
conflist.append(kv)
+2
View File
@@ -24,6 +24,8 @@ class ols:
"""
keys = ["enableGzipCompress","gzipCompressLevel","rollingSize","maxConnections","maxSSLConnections","connTimeout","maxKeepAliveReq"]
conf = public.readFile(self._main_conf_path)
if not conf:
return {}
data = {}
for k in keys:
rep = r'{}\s+(\w+)'.format(k)
+5 -2
View File
@@ -62,8 +62,11 @@ class panelApi:
public.WriteFile(self.save_path,json.dumps(data))
public.ExecShell("chmod 600 " + self.save_path)
tmp = public.ReadFile(self.save_path)
data = json.loads(tmp)
try:
data = json.loads(tmp)
except json.JSONDecodeError:
default_config = {"open": False, "token": "", "limit_addr": []}
return default_config
is_save = False
if not 'binds' in data:
data['binds'] = []
+139 -5
View File
@@ -19,11 +19,13 @@ import requests
os.chdir("/www/server/panel")
sys.path.insert(0, "class/")
sys.path.insert(0, "class_v2/")
import public
from public.exceptions import HintException
__all__ = [
"aaPanelDns",
"NameSiloDns",
"NameCheapDns",
"CloudFlareDns",
@@ -94,7 +96,7 @@ class BaseDns(object):
) -> Optional[dict]:
raise NotImplementedError("create_org_record method must be implemented.")
def remove_record(self, domain_name: str, record: str, record_type: str) -> Optional[dict]:
def remove_record(self, domain_name: str, record: str, record_type: str, **kwargs) -> Optional[dict]:
raise NotImplementedError("remove_record method must be implemented.")
def update_record(self, domain_name: str, record: dict, new_record: dict, **kwargs) -> Optional[dict]:
@@ -117,6 +119,138 @@ class BaseDns(object):
return True
class aaPanelDns(BaseDns):
"""
遵循 ssl v2入参, 转发dnsmanager
"""
dns_provider_name = "aapanel"
kw_prefix = {
"priority": "priority"
}
def __init__(self, api_user: str = None, api_key: str = None, **kwargs):
super().__init__()
self.api_user = api_user
self.api_key = api_key
from ssl_dnsV2.dns_manager import DnsManager
self.manager = DnsManager()
# ============== acme ======================
def create_dns_record(self, domain_name, domain_dns_value):
domain_name = domain_name.lstrip("*.")
_, _, acme_txt = extract_zone(domain_name)
self.create_org_record(
domain_name=domain_name,
record=acme_txt,
record_value=domain_dns_value,
record_type="TXT",
ttl=600,
)
def delete_dns_record(self, domain_name, domain_dns_value) -> None:
domain_name = domain_name.lstrip("*.")
root, _, acme_txt = extract_zone(domain_name)
self.remove_record(root, acme_txt, "TXT", record_value=domain_dns_value)
# =============== 域名管理 ====================
def get_domains(self) -> list:
return self.manager.get_domains()
def get_dns_record(self, domain_name: str) -> list:
records = []
for x in self.manager.parser.get_zones_records(domain_name):
try:
# todo 更多类型特俗处理
if x.get("type") == "SOA":
continue
if x.get("type") == "MX":
priority = re.findall(r"^\s*(\d+)\s+", x.get("value"))
if priority:
x["priority"] = int(priority[0])
record = {
"record": x.get("name"),
"record_type": x.get("type"),
"record_value": x.get("value"),
"ttl": x.get("ttl"),
"proxy": x.get("proxy", -1),
"priority": x.get("priority", -1),
}
records.append(record)
except Exception as e:
public.print_log(f"aaPanelDns get_dns_record error: {e}")
continue
return records
def create_org_record(self, domain_name, record, record_value, record_type, ttl=600, **kwargs):
root, _, _ = extract_zone(domain_name)
body = {
"name": record,
"type": record_type.upper(),
"value": record_value,
"ttl": ttl,
"proxy": kwargs.get("proxy", -1),
"priority": kwargs.get("priority", -1),
}
body = white_kwargs(body, self.kw_prefix, kwargs)
try:
self.manager.add_record(
domain=root, **body
)
return {"status": True, "msg": "Success"}
except HintException as he:
return {"status": False, "msg": str(he)}
except Exception as e:
return {"status": False, "msg": str(e)}
def remove_record(self, domain_name, record, record_type="TXT", **kwargs) -> dict:
root, _, _ = extract_zone(domain_name)
body = {
"name": record,
"type": record_type.upper(),
}
if kwargs.get("record_value"):
body["value"] = kwargs.get("record_value")
try:
self.manager.delete_record(
domain=root, **body
)
return {"status": True, "msg": "Success"}
except HintException as he:
return {"status": False, "msg": str(he)}
except Exception as e:
return {"status": False, "msg": str(e)}
def update_record(self, domain_name: str, record: dict, new_record: dict, **kwargs):
domain, _, _ = extract_zone(domain_name)
body = {
"name": record.get("record"),
"type": record.get("record_type").upper(),
"value": record.get("record_value"),
"new_record": {
"name": new_record.get("record"),
"type": new_record.get("record_type").upper(),
"value": new_record.get("record_value"),
"ttl": new_record.get("ttl", 600),
"proxy": new_record.get("proxy", -1),
"priority": new_record.get("priority", -1),
}
}
try:
self.manager.update_record(
domain=domain, **body
)
return {"status": True, "msg": "Success"}
except HintException as he:
return {"status": False, "msg": str(he)}
except Exception as e:
return {"status": False, "msg": str(e)}
def verify(self) -> bool:
if os.path.exists(public.get_panel_path() + "/class_v2/ssl_dnsV2/aadns.pl"):
return True
return False
# noinspection PyUnusedLocal
class NameCheapDns(BaseDns):
dns_provider_name = "namecheap"
@@ -369,7 +503,7 @@ class NameCheapDns(BaseDns):
return self.__set_hosts_with_params(domain_name, params)
# 删除record
def remove_record(self, domain_name, record, record_type="TXT") -> dict:
def remove_record(self, domain_name, record, record_type="TXT", **kwargs) -> dict:
domain_name, _, _ = extract_zone(domain_name)
hosts_info = self._get_hosts(domain_name)
new_hosts = [
@@ -626,7 +760,7 @@ class CloudFlareDns(BaseDns):
return {"status": False, "msg": str(e)}
# 删除record
def remove_record(self, domain_name, record, record_type="TXT") -> dict:
def remove_record(self, domain_name, record, record_type="TXT", **kwargs) -> dict:
domain_name, _, _ = extract_zone(domain_name)
self.find_dns_zone(domain_name)
headers = self._get_auth_headers()
@@ -819,7 +953,7 @@ class PorkBunDns(BaseDns):
except Exception as err:
return {"status": False, "msg": err}
def remove_record(self, domain_name, record, record_type="TXT") -> dict:
def remove_record(self, domain_name, record, record_type="TXT", **kwargs) -> dict:
# record 跟cf一样, 需要带上域名
try:
domain, _, _ = extract_zone(domain_name)
@@ -1043,7 +1177,7 @@ class NameSiloDns(BaseDns):
except Exception as e:
return {"status": False, "msg": str(e)}
def remove_record(self, domain_name: str, record: str, record_type: str) -> dict:
def remove_record(self, domain_name: str, record: str, record_type: str, **kwargs) -> dict:
domain_name, _, _ = extract_zone(domain_name)
try:
rrid = self._find_rrid(domain_name, {"record": record, "record_type": record_type})
+20 -2
View File
@@ -74,7 +74,11 @@ class panelPlugin:
self.__tasks = []
if not os.path.exists(self.__tmp_path):
os.makedirs(self.__tmp_path, 0o755)
try:
os.makedirs(self.__tmp_path, 0o755)
except OSError as e:
# 当文件系统为只读时
public.print_log("Failed to create temporary directory: {}".format(e))
# 检查依赖
def check_deps(self,get):
@@ -662,12 +666,16 @@ class panelPlugin:
def check_setup_task(self,sName):
if not self.__tasks:
self.__tasks = public.M('tasks').where("status!=?",('1',)).field('status,name').select()
if not isinstance(self.__tasks, list):
return '1'
if sName.find('php-') != -1:
tmp = sName.split('-')
sName = tmp[0]
version = tmp[1]
isTask = '1'
for task in self.__tasks:
if not isinstance(task, dict): continue
tmpt = public.getStrBetween('[',']',task['name'])
if not tmpt:continue
tmp1 = tmpt.split('-')
@@ -688,6 +696,8 @@ class panelPlugin:
#构造本地插件信息
def get_local_plugin_info(self,info):
if 'versions' not in info:
return None
m_version = info['versions'].split(".")
if len(m_version) < 2: return None
if len(m_version) > 2:
@@ -1673,6 +1683,13 @@ class panelPlugin:
self.__tasks = public.M('tasks').where("status!=?",('1',)).field('status,name').select()
isTask = '1'
for task in self.__tasks:
if not isinstance(task, dict):
continue
if 'name' not in task:
continue
tmpt = public.getStrBetween('[',']',task['name'])
if not tmpt:
continue
tmpt = public.getStrBetween('[',']',task['name'])
if not tmpt:continue
tmp1 = tmpt.split('-')
@@ -2319,7 +2336,8 @@ class panelPlugin:
#导入插件包
def input_zip(self,get):
if not os.path.exists(get.tmp_path): return public.return_msg_gettext(False, public.lang("Temporary file does NOT exist, please re-upload!"))
if not hasattr(get, 'tmp_path') or not os.path.exists(get.tmp_path):
return public.return_msg_gettext(False, public.lang("Temporary file does NOT exist, please re-upload!"))
plugin_path = '/www/server/panel/plugin/' + get.plugin_name
if not os.path.exists(plugin_path): os.makedirs(plugin_path)
public.ExecShell(r"\cp -a -r " + get.tmp_path + '/* ' + plugin_path + '/')
+30 -7
View File
@@ -1372,7 +1372,20 @@ listener Default%s{
sql = public.M('domain')
id = get['id']
port = get.port
find = sql.where("pid=? AND name=?", (get.id, get.domain)).field('id,name').find()
domain_data = sql.where("pid=? AND name=?", (get.id, get.domain)).field('id,name').find()
if isinstance(domain_data, list):
if not domain_data:
return public.return_message(-1, 0, public.lang("Domain record not found"))
domain_data = domain_data[0]
if not isinstance(domain_data, dict) or not domain_data.get('id'):
return public.return_message(-1, 0, public.lang("Domain record not found"))
domain_count = sql.table('domain').where("pid=?", (id,)).count()
if domain_count <= 1: return public.return_message(-1, 0, public.lang("Last domain cannot be deleted!"))
domain_count = sql.table('domain').where("pid=?", (id,)).count()
if domain_count == 1: return public.return_msg_gettext(False, public.lang("Last domain cannot be deleted!"))
@@ -1382,10 +1395,14 @@ listener Default%s{
if conf:
# 删除域名
rep = r"server_name\s+(.+);"
tmp = re.search(rep, conf).group()
newServerName = tmp.replace(' ' + get['domain'] + ';', ';')
newServerName = newServerName.replace(' ' + get['domain'] + ' ', ' ')
conf = conf.replace(tmp, newServerName)
match = re.search(rep, conf)
if match:
tmp = match.group()
newServerName = tmp.replace(' ' + get['domain'] + ';', ';')
newServerName = newServerName.replace(' ' + get['domain'] + ' ', ' ')
conf = conf.replace(tmp, newServerName)
else:
public.WriteLog("Site manager", f"No server_name found in the Nginx configuration, the domain {get.domain} is only removed from the database")
# 删除端口
rep = r"listen.*[\s:]+(\d+).*;"
@@ -1425,7 +1442,7 @@ listener Default%s{
# openlitespeed
self._del_ols_domain(get)
sql.table('domain').where("id=?", (find['id'],)).delete()
sql.table('domain').where("id=?", (domain_data['id'],)).delete()
public.write_log_gettext('Site manager', 'Site [{}] deleted domain [{}] successfully!', (get.webname, get.domain))
if not multiple:
public.serviceReload()
@@ -5290,8 +5307,12 @@ location %s
def GetSecurity(self, get):
file = '/www/server/panel/vhost/nginx/' + get.name + '.conf'
conf = public.readFile(file)
data = {}
if type(conf) == bool: return public.return_msg_gettext(False, public.lang("Configuration file not exist"))
if not isinstance(conf, str) or not conf.strip():
return public.return_msg_gettext(False, public.lang("Configuration file not exist"))
data = {}
if conf.find('SECURITY-START') != -1:
rep = "#SECURITY-START(\n|.)+#SECURITY-END"
tmp = re.search(rep, conf).group()
@@ -5474,6 +5495,8 @@ RewriteRule \.(BTPFILE)$ /404.html [R,NC]
# 取网站分类
def get_site_types(self, get):
data = public.M("site_types").field("id,name").order("id asc").select()
if not isinstance(data, list):
data = []
data.insert(0, {"id": 0, "name": public.lang("Default category")})
for i in data:
i['name']=public.xss_version(i['name'])
+22
View File
@@ -196,6 +196,26 @@ class bt_task:
self.modify_task(id, 'status', 1)
self.modify_task(id, 'endtime', int(time.time()))
# 执行任务
def start_task_new(self):
tip_file = '/dev/shm/.start_task.pl'
try:
public.writeFile(tip_file, str(int(time.time())))
if not os.path.exists(self.__task_tips):
return
if os.path.exists(self.__task_tips):
os.remove(self.__task_tips)
public.M(self.__table).where(
'status=?', ('-1',)
).setField('status', 0)
task_list = self.get_task_list(0)
for task_info in task_list:
self.execute_task(
task_info['id'], task_info['type'], task_info['shell'], task_info['other']
)
except Exception as e:
raise e
# 开始检测任务
def start_task(self):
noe = False
@@ -343,6 +363,8 @@ class bt_task:
else:
return public.return_msg_gettext(False, public.lang("Specified compression format is not supported!"))
if not os.path.exists(dfile):
return public.return_msg_gettext(False, public.lang("Compression failed: The target file {} is not generated", dfile))
self.set_file_accept(dfile)
#public.WriteLog("TYPE_FILE", 'Compression succeeded!', (sfiles, dfile),not_web = self.not_web)
public.write_log_gettext("File manager", 'Compressed file [ {} ] to [ {} ] success', (sfiles, dfile))
+10 -1
View File
@@ -1106,6 +1106,9 @@ export PATH
#s_pid = int(public.readFile(pid_file))
data = public.readFile(pid_file)
if isinstance(data,str) and data:
data = data.strip()
if not data.isdigit():
return public.return_message(0,0,[])
s_pid = int(data)
else:
return []
@@ -1661,7 +1664,13 @@ cd {}
@param project_info<dict> 项目信息
@return list
'''
project_info['project_config'] = json.loads(project_info['project_config'])
project_info['project_config'] = {}
if isinstance(project_info, dict) and 'project_config' in project_info:
config_str = project_info['project_config']
if isinstance(config_str, str):
project_info['project_config'] = json.loads(config_str)
# project_info['project_config'] = json.loads(project_info['project_config'])
project_info['run'] = self.get_project_run_state(project_name = project_info['name'])
# project_info['run'] = True
project_info['load_info'] = {}
+1 -1
View File
@@ -3,7 +3,7 @@ from .fields import *
from .manager import Q
from .model import aaModel
__version__ = "1.1.3"
__version__ = "1.2.0"
__all__ = [
"__version__",
+167 -72
View File
@@ -1,11 +1,12 @@
# coding: utf-8
import copy
import itertools
import json
import time
from collections.abc import Callable
from dataclasses import dataclass, field as dataclass_field
from datetime import datetime
from typing import Any, TypeVar, List, Optional
from typing import Any, TypeVar, List, Optional, Iterable
from public.exceptions import HintException
@@ -39,99 +40,145 @@ def json_func(v_type: type, value: Any, forward: bool = True):
raise e
def marks_dirty(func: Callable) -> Callable:
"""wrapper"""
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
if hasattr(self, "_mark_dirty"):
self._mark_dirty()
return result
return wrapper
def _wrap_value(value, on_change_callback, field_name):
"""wrap Tracked"""
if isinstance(value, list) and not isinstance(value, TrackedList):
return TrackedList(value, on_change=on_change_callback, field_name=field_name)
if isinstance(value, dict) and not isinstance(value, TrackedDict):
return TrackedDict(value, on_change=on_change_callback, field_name=field_name)
return value
class TrackedList(list):
"""override list, track fields dirty"""
__slots__ = ("_tracker", "_field_name", "_batch")
__slots__ = ("_on_change", "_field_name")
def __init__(self, *args, tracker=None, field_name=None, **kwargs):
super().__init__(*args, **kwargs)
self._tracker = tracker
def __init__(self, iterable: Iterable = (), *, on_change: Callable = None, field_name: str | None = None):
self._on_change = on_change
self._field_name = field_name
self._batch = False
wrapped = [
_wrap_value(v, on_change, field_name) for v in (iterable or [])
]
super().__init__(wrapped)
def _mark_dirty(self):
if self._tracker and self._field_name and self._batch is False:
self._tracker._mark_dirty(self._field_name)
def _notify_change(self):
"""call back"""
if self._on_change and self._field_name:
self._on_change(self._field_name)
def __deepcopy__(self, memo):
return list(copy.deepcopy(item, memo) for item in self)
def __setitem__(self, key, value):
if isinstance(key, slice):
value = [_wrap_value(v, self._on_change, self._field_name) for v in value]
else:
value = _wrap_value(value, self._on_change, self._field_name)
super().__setitem__(key, value)
self._notify_change()
@marks_dirty
def append(self, item):
super().append(item)
super().append(_wrap_value(item, self._on_change, self._field_name))
self._notify_change()
def insert(self, index: int, item: Any):
super().insert(index, _wrap_value(item, self._on_change, self._field_name))
self._notify_change()
@marks_dirty
def remove(self, item):
super().remove(item)
self._notify_change()
@marks_dirty
def __setitem__(self, key, value):
super().__setitem__(key, value)
@marks_dirty
def pop(self, *args, **kwargs):
return super().pop(*args, **kwargs)
result = super().pop(*args, **kwargs)
self._notify_change()
return result
@marks_dirty
def clear(self):
super().clear()
self._notify_change()
def extend(self, iterable):
self._batch = True
try:
super().extend(iterable)
finally:
self._batch = False
self._mark_dirty()
if not iterable:
return
wrapped_iterable = [
_wrap_value(v, self._on_change, self._field_name) for v in iterable
]
super().extend(wrapped_iterable)
self._notify_change()
def sort(self, *args, **kwargs):
super().sort(*args, **kwargs)
self._notify_change()
def reverse(self):
super().reverse()
self._notify_change()
class TrackedDict(dict):
"""override dict, track fields dirty"""
__slots__ = ("_tracker", "_field_name", "_batch")
__slots__ = ("_on_change", "_field_name")
def __init__(self, *args, tracker=None, field_name=None, **kwargs):
def __init__(self, *args, on_change: Callable = None, field_name: str | None = None, **kwargs):
super().__init__(*args, **kwargs)
self._tracker = tracker
self._on_change = on_change
self._field_name = field_name
self._batch = False
items_to_update = {}
for key, value in list(self.items()):
# list() to avoid "dictionary changed size during iteration"
items_to_update[key] = _wrap_value(value, self._on_change, self._field_name)
super().update(items_to_update)
def _mark_dirty(self):
if self._tracker and self._field_name and self._batch is False:
self._tracker._mark_dirty(self._field_name)
def _notify_change(self):
"""call back"""
if self._on_change and self._field_name:
self._on_change(self._field_name)
def __deepcopy__(self, memo):
return {k: copy.deepcopy(v, memo) for k, v in self.items()}
@marks_dirty
def __setitem__(self, key, value):
super().__setitem__(key, value)
wrapped_value = _wrap_value(value, self._on_change, self._field_name)
super().__setitem__(key, wrapped_value)
self._notify_change()
@marks_dirty
def __delitem__(self, key):
super().__delitem__(key)
self._notify_change()
@marks_dirty
def pop(self, key, *args, **kwargs):
return super().pop(key, *args, **kwargs)
def pop(self, *args, **kwargs):
result = super().pop(*args, **kwargs)
self._notify_change()
return result
def popitem(self):
result = super().popitem()
self._notify_change()
return result
@marks_dirty
def clear(self):
super().clear()
self._notify_change()
def update(self, *args, **kwargs):
self._batch = True
try:
# noinspection PyArgumentList
super().update(*args, **kwargs)
finally:
self._batch = False
self._mark_dirty()
other = dict(*args, **kwargs)
if not other:
return
wrapped_other = {}
for key, value in other.items():
wrapped_other[key] = _wrap_value(value, self._on_change, self._field_name)
super().update(wrapped_other)
self._notify_change()
def setdefault(self, key, default=None):
if key not in self:
wrapped_default = _wrap_value(default, self._on_change, self._field_name)
result = super().setdefault(key, wrapped_default)
self._notify_change()
else:
result = super().get(key)
return result
@dataclass
@@ -166,17 +213,19 @@ class aaField(object):
def __get__(self, instance: object, owner):
if instance is None:
return self
try:
return instance.__dict__[self.field_name]
except KeyError:
raise AttributeError(f'{self.field_name} is not set')
return instance.__dict__.get(self.field_name, self.get_default_val())
def __set__(self, instance: M, value: Any):
# base type field, check new set value
if self.field_name not in instance.__dict__ or instance.__dict__[self.field_name] != value:
instance.__dict__[self.field_name] = value
if hasattr(instance, "_mark_dirty"):
instance._mark_dirty(self.field_name)
current_value = instance.__dict__.get(self.field_name)
if current_value is value:
# base type field, not Tracker
return
if hasattr(instance, "_mark_dirty"):
instance._mark_dirty(self.field_name)
instance.__dict__[self.field_name] = value
def __delete__(self, instance):
try:
@@ -325,7 +374,6 @@ class BlobField(aaField):
py_type: type = bytes
@dataclass
class ListField(aaField):
"""
@@ -336,13 +384,37 @@ class ListField(aaField):
def __get__(self, instance: M, owner):
if instance is None:
return self
value = instance.__dict__.get(self.field_name, self.default or [])
value: Iterable[Any] = instance.__dict__.get(self.field_name)
if value is None:
value = self.get_default_val()
# init default val for the first time
instance.__dict__[self.field_name] = value
if not isinstance(value, TrackedList):
# generate tracker
value = TrackedList(value, tracker=instance, field_name=self.field_name)
# generate call back
value = TrackedList(
value,
on_change=instance._mark_dirty,
field_name=self.field_name,
)
instance.__dict__[self.field_name] = value # update instance's attr
return value
def __set__(self, instance: M, value: Any):
"""override, other update handled by TrackedList"""
if not isinstance(value, list):
raise TypeError(f"Field '{self.field_name}' expects a list, but got {type(value).__name__}")
tracked_value = TrackedList(
value,
on_change=instance._mark_dirty,
field_name=self.field_name,
)
instance._mark_dirty(self.field_name)
super().__set__(instance, tracked_value)
@staticmethod
def _serialized(value: list | str, forward: bool = True) -> list | Any:
return json_func(list, value, forward)
@@ -367,16 +439,39 @@ class DictField(aaField):
Dict field
"""
def __get__(self, instance, owner):
def __get__(self, instance: M, owner):
if instance is None:
return self
value = instance.__dict__.get(self.field_name, self.default or {})
value: dict = instance.__dict__.get(self.field_name)
if value is None:
value = self.get_default_val()
instance.__dict__[self.field_name] = value
if not isinstance(value, TrackedDict):
# generate tracker
value = TrackedDict(value, tracker=instance, field_name=self.field_name)
# generate tracker call back
value = TrackedDict(
value,
on_change=instance._mark_dirty,
field_name=self.field_name,
)
instance.__dict__[self.field_name] = value # update instance's attr
return value
def __set__(self, instance: M, value: Any):
"""override, other update handled by TrackedDict"""
if not isinstance(value, dict):
raise TypeError(f"Field '{self.field_name}' expects a dict, but got {type(value).__name__}")
tracked_value = TrackedDict(
value,
on_change=instance._mark_dirty,
field_name=self.field_name,
)
instance._mark_dirty(self.field_name)
super().__set__(instance, tracked_value)
@staticmethod
def _serialized(value: dict | str, forward: bool = True) -> dict | Any:
return json_func(dict, value, forward)
+29 -11
View File
@@ -5,19 +5,22 @@ import sqlite3 as Engine
import uuid
from functools import reduce
from itertools import chain
from typing import Optional, TypeVar, Generic, Any, List, Dict, Generator, Iterable
from typing import Optional, TypeVar, Generic, Any, List, Dict, Generator, Iterable, TYPE_CHECKING, Type
from public.aaModel.fields import COMPARE
from public.exceptions import HintException, PanelError
from public.sqlite_easy import Db
if TYPE_CHECKING:
from .model import aaModel
__all__ = ["aaManager", "Q"]
M = TypeVar("M", bound="aaModel")
# ==================== Patch ==================
def _builtin(check_engine: Engine = None) -> bool:
def _builtin(check_engine: Any = None) -> bool:
if not check_engine:
check_engine = Engine
try:
@@ -31,7 +34,7 @@ def _builtin(check_engine: Engine = None) -> bool:
return False
def _get_engine() -> tuple[bool, Engine]:
def _get_engine() -> tuple[bool, Any]:
try:
import pysqlite3 as engine
flag = True
@@ -67,7 +70,7 @@ class Operator:
"like": "%{}%", "startswith": "{}%", "endswith": "%{}",
}
def __init__(self, model_class: M, query: Db.query):
def __init__(self, model_class: M, query: "Db.query"):
self._model_class: M = model_class
self._query = query
self._tb = self._model_class.__table_name__
@@ -245,6 +248,7 @@ class Operator:
return f"{key} {op} ({placeholders})", val
self._q_error(key, compare, val, sp_compare)
return None, None
def __compare_reducer(self, key: str, compare: str, road: list, val: Any):
sp_compare = getattr(self._fields.get(key), "compare")
@@ -330,7 +334,7 @@ class Operator:
return f"{self._tb}.{key} = ?", [val]
return f"{self._tb}.{key} IS NULL", []
if self._flag is True:
if self._flag:
path = self.__generate_road(road)
if val is not None:
if is_json:
@@ -472,8 +476,8 @@ class QuerySet(Generic[M]):
查询集
"""
def __init__(self, model_class: M, query: Db.query):
self._model_class: M = model_class
def __init__(self, model_class: Type[M], query: "Db.query"):
self._model_class: Type[M] = model_class
self._tb = self._model_class.__table_name__
self._query = query
self._cache = None
@@ -536,7 +540,7 @@ class QuerySet(Generic[M]):
return chain(self.__execute() or [], other.__execute() or [])
@property
def _clone_q(self) -> Db.query:
def _clone_q(self) -> "Db.query":
return self._query.fork()
def _gen_M(self, data) -> M:
@@ -738,7 +742,7 @@ class aaObjects(Generic[M]):
cls.__m_map__[args.__table_name__] = aaMigrate(args).run_migrate()
return super(aaObjects, cls).__new__(cls)
def __init__(self, model: M):
def __init__(self, model: Type[M]):
self._model = model
self.__q = None
@@ -752,7 +756,7 @@ class aaObjects(Generic[M]):
return self._queryset_class(self._model, self._query.fork())
@property
def _query(self) -> Db.query:
def _query(self) -> "Db.query":
if not self.__q:
q = Db(
db_name=self._model.__db_name__,
@@ -764,6 +768,20 @@ class aaObjects(Generic[M]):
def _insert(self, val_data) -> int:
return self._query.insert(val_data)
def _update(self, cdt: dict, val_data: dict) -> int:
if not cdt or not val_data:
return 0
q = self._query.fork()
conditions = []
params = []
for k, v in cdt.items():
conditions.append(f"`{k}` = ?")
params.append(v)
q.where(" AND ".join(conditions), params)
return q.update(val_data)
def insert(self, data: Dict[str, Any], raise_exp: bool = True) -> dict:
"""
插入单条数据
@@ -1012,7 +1030,7 @@ class aaManager:
self._queryset_class = qs_cls
self._cache = {}
def __get__(self, instance, cls: M):
def __get__(self, instance, cls: Type[M]):
if instance is not None:
raise RuntimeError(
f"object manager can't accessible from '{cls.__name__}' instances"
+49 -21
View File
@@ -86,10 +86,10 @@ class aaMetaClass(type):
class aaCusModel(metaclass=aaMetaClass):
__abstract__ = True
objects = aaManager()
_dirty_fields: set = None
_dirty_fields: Optional[set] = None
def __init__(self, **kwargs):
if self.__abstract__ is True:
if self.__abstract__:
raise RuntimeError(f'{self.__class__.__name__} class can not be init')
self._field_filter = kwargs.pop("_field_filter", None)
for f, v in self._generate_init(kwargs, all_flag=True):
@@ -98,15 +98,15 @@ class aaCusModel(metaclass=aaMetaClass):
self._dirty_fields = set()
def _mark_dirty(self, field_name: str):
if self._dirty_fields is not None:
self._dirty_fields.add(field_name)
if self._dirty_fields is None:
return
self._dirty_fields.add(field_name)
def _generate_init(self, val_data: dict, all_flag: bool = False) -> Generator:
fields_map = self._get_fields() if all_flag else {
k: v for k, v in self._get_fields().items()
if k in val_data or (hasattr(v, "dynamic") and v.auto_now is True)
}
for name, field in fields_map.items():
default_val = field.get_default_val()
val = val_data.get(name, default_val)
@@ -158,6 +158,13 @@ class aaModel(aaCusModel):
index = ["status"] 索引
"""
__db_name__: str
__table_name__: str
__fields__: dict
__primary_key__: str
__serializes__: dict
__index_keys__: list
__abstract__: bool = True
__destroyed: bool = False
id: int = None
@@ -242,40 +249,61 @@ class aaModel(aaCusModel):
@check_destroyed
def save(self, raise_exp: bool = True) -> Optional[Self]:
"""
模型数据, 不存在则 保存 , 存在则 更新
模型数据, 不存在则 保存 , 存在则 更新, 仅更新变动字段
:raise_exp 抛异常
:return: model object 字段类型异常等问题返回 None
"""
if self.__class__.__abstract__ is True:
if self.__class__.__abstract__:
raise RuntimeError(f'{self.__class__.__name__} class can not be save')
try:
cls = self.__class__
primary_key = cls.__primary_key__
pk = int(self.__dict__.get(primary_key, 0))
dirtys = {f: self.__dict__[f] for f in self._dirty_fields}
if dirtys:
validate = self._validate(target=dirtys, raise_exp=raise_exp)
else:
# not changed & not insert.
if not self._dirty_fields and pk != 0:
return self
dirtys = {
k: v for k, v in self.__dict__.items() if k in self._dirty_fields
}
if pk == 0:
# insert, all fields default
validate = self._validate(raise_exp=raise_exp)
else:
if "update_time" in cls._get_fields() and "update_time" not in dirtys:
dirtys["update_time"] = None
# for field_name, field_obj in cls._get_fields().items():
# if hasattr(field_obj, "auto_now") and field_obj.auto_now:
# dirtys[field_name] = None
# update, olnly validate dirty fields
validate = self._validate(target=dirtys, raise_exp=raise_exp)
if not validate:
if raise_exp:
raise HintException("validate error")
return None
q = cls.objects._query
if pk != 0: # update
if pk == 0: # insert
self._before_save()
new_id = cls.objects._insert(validate)
if not new_id:
if raise_exp:
raise HintException("insert failed")
return None
self.__dict__[primary_key] = new_id
self._after_save()
else: # update
self._before_update()
if q.where(f"{primary_key}=?", (pk,)).update(validate) == 1:
if cls.objects._update({primary_key: pk}, validate) == 1:
self._after_update()
else: # update failed
if raise_exp:
raise HintException("update failed")
return None
else: # save
self._before_save()
new_id = q.insert(validate)
if new_id:
setattr(self, primary_key, new_id)
self._after_save()
# reset in finally block
return self
# return self if not new_id else cls(**{primary_key: new_id, **self.__dict__})
except (TypeError, AttributeError) as t:
if raise_exp:
raise t
+98 -5
View File
@@ -1006,6 +1006,8 @@ def GetLocalIp():
else:
ipaddress = match_ipv4.match(m_str).group(0)
WriteFile(filename, ipaddress)
if isinstance(ipaddress, str):
ipaddress = ipaddress.strip()
c_ip = check_ip(ipaddress)
if not c_ip: return GetHost()
return ipaddress
@@ -9336,11 +9338,12 @@ def ensure_unique_db_name(db_name: str) -> str:
# 优先使用新名称
def ensure_unique_db_name2(db_name: str) -> str:
import random
chars = string.ascii_lowercase + string.digits
while True:
new_db_name = '{}_{}'.format(
db_name[:9],
GetRandomString(6)
)
prefix = db_name[:9].lower()
suffix = ''.join(random.choices(chars, k=6))
new_db_name = f'{prefix}_{suffix}'
if not S('databases').where('name', new_db_name).exists():
return new_db_name
@@ -9629,7 +9632,7 @@ def get_multi_webservice_status():
apacheBin = '{}/apache/bin/apachectl'.format(get_setup_path())
olsBin = '/usr/local/lsws/bin/lswsctrl'
if os.path.exists(nginxSbin) and (os.path.exists(apacheBin) or os.path.exists(olsBin)):
if os.path.exists(nginxSbin) and os.path.exists(apacheBin) and os.path.exists(olsBin):
return True
return False
@@ -9737,3 +9740,93 @@ def cp_dir(src: str, dst: str, ignores: List[str] | Set[str] = None, overwrite:
cp_dir(src_item, dst_item, ignores, overwrite)
else:
_copy2(src_item, dst_item)
# 解压到指定目录下
def extract_archive_to_target(archive_path, target_dir):
"""
解压压缩包到指定目标目录支持多种格式并自动处理目录结构
:param archive_path: 压缩包文件路径 /path/to/file.zip
:param target_dir: 目标解压目录 /path/to/target
:return: (bool, str) 成功返回 (True, '')失败返回 (False, 错误信息)
"""
import tempfile
import zipfile
import tarfile
# 校验输入
if not os.path.exists(archive_path):
return False, f"The compressed package does not exist: {archive_path}"
if not os.path.isfile(archive_path):
return False, f"Not a valid file: {archive_path}"
# 确保目标目录存在
os.makedirs(target_dir, exist_ok=True)
# 获取文件后缀
file_ext = os.path.splitext(archive_path)[1].lower()
supported_ext = ['.zip', '.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.gz']
if file_ext not in supported_ext:
return False, f"Unsupported formats are supported: {', '.join(supported_ext)}"
# 创建临时目录处理目录结构
temp_dir = tempfile.mkdtemp()
try:
# 解压到临时目录
try:
if file_ext == '.zip':
with zipfile.ZipFile(archive_path, 'r') as zf:
zf.extractall(temp_dir)
else:
mode = 'r'
if file_ext in ['.tar.gz', '.tgz', '.gz']:
mode = 'r:gz'
elif file_ext in ['.tar.bz2', '.tbz2']:
mode = 'r:bz2'
with tarfile.open(archive_path, mode) as tf:
tf.extractall(temp_dir)
except Exception as e:
return False, f"Decompression failed: {str(e)}"
# 检查临时目录是否为空
temp_items = os.listdir(temp_dir)
if not temp_items:
return False, 'The content of the compressed package is empty.'
# 统计临时目录内的目录和文件数量,判断结构
dir_count = 0
file_count = 0
first_dir = None
for item in temp_items:
item_path = os.path.join(temp_dir, item)
if os.path.isdir(item_path):
dir_count += 1
if first_dir is None:
first_dir = item_path
else:
file_count += 1
# 处理单层根目录结构
if dir_count == 1 and file_count == 0:
# 只有一个目录 取该目录内容
content_dir = first_dir
else:
# 取临时目录全部内容
content_dir = temp_dir
# 复制内容到目标目录
for item in os.listdir(content_dir):
src = os.path.join(content_dir, item)
dst = os.path.join(target_dir, item)
try:
if os.path.isdir(src):
shutil.copytree(src, dst, dirs_exist_ok=True)
else:
shutil.copy2(src, dst)
except Exception as e:
return False, f"The file copy failed: {str(e)}"
return True, ""
finally:
# 清理临时目录
shutil.rmtree(temp_dir, ignore_errors=True)