Skip to content

Commit e05c0c5

Browse files
WondermiqueLeoQuote
authored andcommitted
1、sql/templates/sqlsubmit.html
添加导出工单参数,默认为非导出工单 2、sql/templates/sqlquery.html 增加导出工单表单信息,并增加扫描行数检查 3、common/templates/config.html 增加导出工单相关配置表单 4、sql/views.py 传递相关页面所需值 5、sql/templates/sqlworkflow.html 增加工单页面,导出格式的显示 6、sql/templates/detail.html 增加下载按钮,与 offlinedownload.py 交互 7、common/check.py 增加config内oss、sftp及本地存储的检查 8、sql_api/serializers.py 传递相关参数 9、sql/utils/workflow_audit.py 取消导出工单的自动审核,正常情况下导出工单不应自动审核 10、sql/engines/offlinedownload.py 导出工单主要代码 11、sql/engines/goinception.py 增加导出工单类型 12、sql/engines/mysql.py 传递相关参数 13、sql/models.py (1)syntax_type新增(3,导出工单) (2)新增字段is_offline_export、export_format、file_name (3)permissions新增("offline_download", "离线下载权限") 涉及 sql: alter table sql_workflow add column export_format varchar(10) DEFAULT NULL, add column is_offline_export varchar(3) NOT NULL, add column file_name varchar(255) DEFAULT NULL; set @content_type_id=(select id from django_content_type where app_label='sql' and model='permission'); insert IGNORE INTO auth_permission (name, content_type_id, codename) VALUES('离线下载权限', @content_type_id, 'offline_download'); 14、sql/sql_workflow.py 增加导出格式参数 15、sql/urls.py 增加 offlinedownload 的路由 新增 sql 脚本: src/init_sql/v1.11.1_offlinedownload.sql 与上方 sql 内容一致,无需反复执行 新增依赖: sqlparse==0.4.4 paramiko==3.4.0 oss2==2.18.3 openpyxl==3.1.2
1 parent c544f10 commit e05c0c5

17 files changed

+1423
-49
lines changed

common/check.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import MySQLdb
66
import simplejson as json
77
from django.http import HttpResponse
8+
from paramiko import Transport, SFTPClient
9+
import oss2
10+
import os
811

912
from common.utils.permission import superuser_required
1013
from sql.engines import get_engine
@@ -131,3 +134,84 @@ def instance(request):
131134
result["msg"] = "无法连接实例,\n{}".format(str(e))
132135
# 返回结果
133136
return HttpResponse(json.dumps(result), content_type="application/json")
137+
138+
139+
@superuser_required
140+
def file_storage_connect(request):
141+
result = {"status": 0, "msg": "ok", "data": []}
142+
storage_type = request.POST.get("storage_type")
143+
# 检查是否存在该变量
144+
max_export_rows = request.POST.get("max_export_rows", '10000')
145+
max_export_exec_time = request.POST.get("max_export_exec_time", '60')
146+
files_expire_with_days = request.POST.get("files_expire_with_days", '0')
147+
# 若变量已经定义,检查是否为空
148+
max_export_rows = max_export_rows if max_export_rows else '10000'
149+
max_export_exec_time = max_export_exec_time if max_export_exec_time else '60'
150+
files_expire_with_days = files_expire_with_days if files_expire_with_days else '0'
151+
check_list = {"max_export_rows": max_export_rows,
152+
"max_export_exec_time": max_export_exec_time,
153+
"files_expire_with_days": files_expire_with_days}
154+
try:
155+
# if not isinstance(files_expire_with_days, int):
156+
# 遍历字典,判断是否只有数字
157+
for key, value in check_list.items():
158+
print(value)
159+
if not value.isdigit():
160+
raise TypeError(f"Value: {key} \nmust be an integer.")
161+
except TypeError as e:
162+
result["status"] = 1
163+
result["msg"] = "参数类型错误,\n{}".format(str(e))
164+
165+
if storage_type == 'sftp':
166+
sftp_host = request.POST.get("sftp_host")
167+
sftp_port = int(request.POST.get("sftp_port"))
168+
sftp_user = request.POST.get("sftp_user")
169+
sftp_password = request.POST.get("sftp_password")
170+
sftp_path = request.POST.get("sftp_path")
171+
172+
try:
173+
with Transport((sftp_host, sftp_port)) as transport:
174+
transport.connect(username=sftp_user, password=sftp_password)
175+
# 创建 SFTPClient
176+
sftp = SFTPClient.from_transport(transport)
177+
remote_path = sftp_path
178+
try:
179+
sftp.listdir(remote_path)
180+
# files = sftp.listdir(remote_path)
181+
# print(f"SFTP 远程路径 '{remote_path}' 存在,包含文件/文件夹: {files}")
182+
except FileNotFoundError:
183+
raise Exception(f"SFTP 远程路径 '{remote_path}' 不存在")
184+
185+
except Exception as e:
186+
result["status"] = 1
187+
result["msg"] = "无法连接,\n{}".format(str(e))
188+
elif storage_type == 'oss':
189+
access_key_id = request.POST.get("access_key_id")
190+
access_key_secret = request.POST.get("access_key_secret")
191+
endpoint = request.POST.get("endpoint")
192+
bucket_name = request.POST.get("bucket_name")
193+
try:
194+
# 创建 OSS 认证
195+
auth = oss2.Auth(access_key_id, access_key_secret)
196+
# 创建 OSS Bucket 对象
197+
bucket = oss2.Bucket(auth, endpoint, bucket_name)
198+
199+
# 判断配置的 Bucket 是否存在
200+
try:
201+
bucket.get_bucket_info()
202+
except oss2.exceptions.NoSuchBucket:
203+
raise Exception(f"OSS 存储桶 '{bucket_name}' 不存在")
204+
205+
except Exception as e:
206+
result["status"] = 1
207+
result["msg"] = "无法连接,\n{}".format(str(e))
208+
elif storage_type == 'local':
209+
local_path = r'{}'.format(request.POST.get("local_path"))
210+
try:
211+
if not os.path.exists(local_path):
212+
raise FileNotFoundError(f"Destination directory '{local_path}' not found.")
213+
except Exception as e:
214+
result["status"] = 1
215+
result["msg"] = "本地路径不存在,\n{}".format(str(e))
216+
217+
return HttpResponse(json.dumps(result), content_type="application/json")

common/templates/config.html

Lines changed: 303 additions & 0 deletions
Large diffs are not rendered by default.

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ mozilla-django-oidc==3.0.0
4242
django-auth-dingding==0.0.3
4343
django-cas-ng==4.3.0
4444
cassandra-driver
45+
sqlparse==0.4.4
46+
paramiko==3.4.0
47+
oss2==2.18.3
48+
openpyxl==3.1.2

sql/engines/goinception.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def escape_string(self, value: str) -> str:
6868
"""字符串参数转义"""
6969
return pymysql.escape_string(value)
7070

71-
def execute_check(self, instance=None, db_name=None, sql=""):
71+
def execute_check(self, instance=None, db_name=None, sql="", is_offline_export=None):
7272
"""inception check"""
7373
# 判断如果配置了隧道则连接隧道
7474
host, port, user, password = self.remote_instance_conn(instance)
@@ -99,6 +99,8 @@ def execute_check(self, instance=None, db_name=None, sql=""):
9999
if check_result.syntax_type == 2:
100100
if get_syntax_type(r[5], parser=False, db_type="mysql") == "DDL":
101101
check_result.syntax_type = 1
102+
if is_offline_export == "yes":
103+
check_result.syntax_type = 3
102104
check_result.column_list = inception_result.column_list
103105
check_result.checked = True
104106
check_result.error = inception_result.error

sql/engines/mysql.py

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .models import ResultSet, ReviewResult, ReviewSet
1818
from sql.utils.data_masking import data_masking
1919
from common.config import SysConfig
20+
from sql.engines.offlinedownload import OffLineDownLoad
2021

2122
logger = logging.getLogger("default")
2223

@@ -71,6 +72,7 @@ def __init__(self, instance=None):
7172
super().__init__(instance=instance)
7273
self.config = SysConfig()
7374
self.inc_engine = GoInceptionEngine()
75+
self.sql_export = OffLineDownLoad()
7476

7577
def get_connection(self, db_name=None):
7678
# https://stackoverflow.com/questions/19256155/python-mysqldb-returning-x01-for-bit-values
@@ -621,12 +623,14 @@ def query_masking(self, db_name=None, sql="", resultset=None):
621623
mask_result = resultset
622624
return mask_result
623625

624-
def execute_check(self, db_name=None, sql=""):
626+
def execute_check(self, db_name=None, sql="", offline_data=None):
625627
"""上线单执行前的检查, 返回Review set"""
628+
# 获取离线导出工单参数
629+
offline_exp = offline_data["is_offline_export"] if offline_data is not None else "0"
626630
# 进行Inception检查,获取检测结果
627631
try:
628632
check_result = self.inc_engine.execute_check(
629-
instance=self.instance, db_name=db_name, sql=sql
633+
instance=self.instance, db_name=db_name, sql=sql, is_offline_export=offline_exp
630634
)
631635
except Exception as e:
632636
logger.debug(
@@ -659,10 +663,11 @@ def execute_check(self, db_name=None, sql=""):
659663
syntax_type = get_syntax_type(statement, parser=False, db_type="mysql")
660664
# 禁用语句
661665
if re.match(r"^select", statement.lower()):
662-
check_result.error_count += 1
663-
row.stagestatus = "驳回不支持语句"
664-
row.errlevel = 2
665-
row.errormessage = "仅支持DML和DDL语句,查询语句请使用SQL查询功能!"
666+
if offline_exp != "yes":
667+
check_result.error_count += 1
668+
row.stagestatus = "驳回不支持语句"
669+
row.errlevel = 2
670+
row.errormessage = "仅支持DML和DDL语句,查询语句请使用SQL查询功能!"
666671
# 高危语句
667672
elif critical_ddl_regex and p.match(statement.strip().lower()):
668673
check_result.error_count += 1
@@ -681,28 +686,31 @@ def execute_check(self, db_name=None, sql=""):
681686

682687
def execute_workflow(self, workflow):
683688
"""执行上线单,返回Review set"""
684-
# 判断实例是否只读
685-
read_only = self.query(sql="SELECT @@global.read_only;").rows[0][0]
686-
if read_only in (1, "ON"):
687-
result = ReviewSet(
688-
full_sql=workflow.sqlworkflowcontent.sql_content,
689-
rows=[
690-
ReviewResult(
691-
id=1,
692-
errlevel=2,
693-
stagestatus="Execute Failed",
694-
errormessage="实例read_only=1,禁止执行变更语句!",
695-
sql=workflow.sqlworkflowcontent.sql_content,
696-
)
697-
],
698-
)
699-
result.error = ("实例read_only=1,禁止执行变更语句!",)
700-
return result
701-
# TODO 原生执行
702-
# if workflow.is_manual == 1:
703-
# return self.execute(db_name=workflow.db_name, sql=workflow.sqlworkflowcontent.sql_content)
704-
# inception执行
705-
return self.inc_engine.execute(workflow)
689+
if workflow.is_offline_export == "yes":
690+
return self.sql_export.execute_offline_download(workflow)
691+
else:
692+
# 判断实例是否只读
693+
read_only = self.query(sql="SELECT @@global.read_only;").rows[0][0]
694+
if read_only in (1, "ON"):
695+
result = ReviewSet(
696+
full_sql=workflow.sqlworkflowcontent.sql_content,
697+
rows=[
698+
ReviewResult(
699+
id=1,
700+
errlevel=2,
701+
stagestatus="Execute Failed",
702+
errormessage="实例read_only=1,禁止执行变更语句!",
703+
sql=workflow.sqlworkflowcontent.sql_content,
704+
)
705+
],
706+
)
707+
result.error = ("实例read_only=1,禁止执行变更语句!",)
708+
return result
709+
# TODO 原生执行
710+
# if workflow.is_manual == 1:
711+
# return self.execute(db_name=workflow.db_name, sql=workflow.sqlworkflowcontent.sql_content)
712+
# inception执行
713+
return self.inc_engine.execute(workflow)
706714

707715
def execute(self, db_name=None, sql="", close_conn=True, parameters=None):
708716
"""原生执行语句"""

0 commit comments

Comments
 (0)