api_app.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. import datetime
  2. import gc
  3. import logging
  4. import os
  5. import shutil
  6. import socket
  7. import pythoncom
  8. import pywintypes
  9. import requests
  10. import win32com.client
  11. from flask import Flask, jsonify, request, current_app, send_from_directory, Response
  12. from flask_cors import CORS
  13. from requests_toolbelt import MultipartEncoder
  14. from win10toast import ToastNotifier
  15. from config import file_type_map
  16. from tools.check import is_file_open_in_wps
  17. from tools.file_manager import get_file_md5
  18. from tools.logger_handle import logger
  19. from tools.oss_client import oss_handle
  20. socket.getfqdn = lambda name=None: 'localhost'
  21. toaster = ToastNotifier()
  22. app = Flask(__name__, static_folder='../static')
  23. CORS(app, resources=r'/*')
  24. TARGET_URL = 'http://120.195.49.22:7215'
  25. class InterceptHandler(logging.Handler):
  26. def emit(self, record):
  27. # 将标准日志记录转为 loguru
  28. try:
  29. level = logger.level(record.levelname).name
  30. except ValueError:
  31. level = record.levelno
  32. logger.opt(depth=6, exception=record.exc_info).log(level, record.getMessage())
  33. logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO)
  34. for name in ("flask", "werkzeug", "watchdog"):
  35. logging.getLogger(name).handlers = [InterceptHandler()]
  36. logging.getLogger(name).propagate = False
  37. @app.route('/')
  38. def index():
  39. return send_from_directory(app.static_folder, 'index.html')
  40. # 代理静态文件(如 css, js, images 等)
  41. @app.route('/<path:path>')
  42. def static_proxy(path):
  43. return send_from_directory(app.static_folder, path)
  44. @logger.catch()
  45. @app.route('/pyapi/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
  46. def proxy(path):
  47. url = f'{TARGET_URL}/{path}'
  48. logger.info(f'接收到请求{url}')
  49. # 获取 headers(排除 Host 避免冲突)
  50. headers = {key: value for key, value in request.headers if key.lower() != 'host'}
  51. headers[
  52. 'User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
  53. try:
  54. if request.files:
  55. # 构造 Multipart 表单
  56. fields = {}
  57. # 添加普通字段
  58. for key, value in request.form.items():
  59. fields[key] = value
  60. # 添加文件字段
  61. for key, file in request.files.items():
  62. fields[key] = (file.filename, file.stream, file.mimetype)
  63. m = MultipartEncoder(fields=fields)
  64. headers['Content-Type'] = m.content_type
  65. resp = requests.request(
  66. method=request.method,
  67. url=url,
  68. headers=headers,
  69. params=request.args,
  70. data=m,
  71. cookies=request.cookies,
  72. allow_redirects=False
  73. )
  74. else:
  75. json_data = None
  76. form_data = None
  77. if request.content_type and 'application/json' in request.content_type.lower():
  78. json_data = request.get_json(silent=True)
  79. else:
  80. form_data = request.form.to_dict()
  81. resp = requests.request(
  82. method=request.method,
  83. url=url,
  84. headers=headers,
  85. params=request.args,
  86. data=form_data,
  87. json=json_data,
  88. cookies=request.cookies,
  89. allow_redirects=False
  90. )
  91. except Exception as e:
  92. logger.error(e)
  93. return jsonify({'code': -1})
  94. # 构建 Flask 的响应
  95. excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
  96. response_headers = [(k, v) for k, v in resp.raw.headers.items() if k.lower() not in excluded_headers]
  97. return Response(resp.content, resp.status_code, response_headers)
  98. @logger.catch()
  99. def open_file_by_wps(file_path):
  100. ext = file_path.split(".")[-1]
  101. shutil.rmtree(win32com.client.gencache.GetGeneratePath())
  102. if ext not in file_type_map:
  103. return jsonify({'code': 3001, 'msg': '不支持该类型的文件'})
  104. if not os.path.exists(file_path):
  105. return jsonify({'code': 3006, 'msg': '文件不存在'})
  106. pythoncom.CoInitialize()
  107. try:
  108. wps = win32com.client.DispatchEx(file_type_map[ext][0])
  109. wps.Visible = True
  110. if ext in ['csv', 'xlsx', 'xls']:
  111. getattr(wps, file_type_map[ext][1]).Open(os.path.abspath(file_path))
  112. else:
  113. getattr(wps, file_type_map[ext][1]).open(os.path.abspath(file_path))
  114. except pywintypes.com_error as e:
  115. logger.exception(e)
  116. pass
  117. finally:
  118. del wps
  119. gc.collect()
  120. pythoncom.CoUninitialize()
  121. return jsonify({'code': 1000, 'msg': '操作完成'})
  122. @logger.catch()
  123. @app.route('/download_and_open_file', methods=['POST'])
  124. def download_and_open_with_wps():
  125. '''
  126. 1000: 操作完成
  127. 2001: 检测到文档正在被编辑,无法下载
  128. 2002: 检测到远程文件已经更新本地文件也发生改动,终止操作
  129. 3001: 不支持该类型的文件
  130. 3002: 下载文件失败
  131. :return:
  132. '''
  133. file_id = request.get_json()['file_id']
  134. is_template = request.get_json()['is_template']
  135. # 判断是否为exe模式执行
  136. if is_template:
  137. file_info = app.config['serve_client'].get_template_file_info(file_id)
  138. time_str = datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S')
  139. local_path = os.path.join(current_app.config['work_path'],
  140. f'{file_info["name"]}({time_str}).{file_info["nameSuffix"]}')
  141. if app.config['serve_client'].download_template(file_info['fileLink'], local_path):
  142. return open_file_by_wps(local_path)
  143. else:
  144. return jsonify({'code': 3006, 'msg': '文件不存在'})
  145. file_info = app.config['serve_client'].get_file_info(file_id)
  146. if not file_info:
  147. return jsonify({'code': 3006, 'msg': '文件不存在'})
  148. local_file = os.path.join(current_app.config['work_path'], file_info['filePath'].replace('/', '_'))
  149. # 判断本地是否存在已下载的同名文件
  150. if not os.path.exists(local_file) or not os.path.exists(local_file + '.metadata.json'):
  151. app.config['serve_client'].download_file(file_info,
  152. current_app.config['work_path'])
  153. return open_file_by_wps(local_file)
  154. # 判断本地文件是否被打开
  155. if is_file_open_in_wps(local_file):
  156. return jsonify({'code': 1000, 'msg': '检测到文件已被打开'})
  157. # 判断本地文件和线上文件是否一致
  158. local_file_metadata = oss_handle.load_metadata(local_file)
  159. if local_file_metadata['update_time'] == file_info['updateTime']:
  160. return open_file_by_wps(local_file)
  161. if local_file_metadata['md5'] == app.config['serve_client'].get_file_md5(local_file):
  162. app.config['serve_client'].download_file(file_info, current_app.config['work_path'])
  163. return open_file_by_wps(local_file)
  164. return jsonify({'code': 2002, 'msg': '检测到远程文件已经更新本地文件也发生改动,终止操作'})
  165. @logger.catch()
  166. @app.route('/upload_local_file', methods=['POST'])
  167. def upload_local_file():
  168. file_name = request.get_json()['file_name']
  169. if is_file_open_in_wps(os.path.join(current_app.config['work_path'], file_name)):
  170. return jsonify({'code': 3004, 'msg': '操作失败,文件已被打开无法操作'})
  171. metadata = app.config['serve_client'].load_metadata(os.path.join(current_app.config['work_path'], file_name))
  172. code = app.config['serve_client'].upload_file(os.path.join(current_app.config['work_path'], file_name))
  173. if not code:
  174. return jsonify({'code': 3003, 'msg': '操作失败,上传文件失败'})
  175. try:
  176. os.remove(os.path.join(current_app.config['work_path'], file_name))
  177. os.remove(os.path.join(current_app.config['work_path'], file_name + '.metadata'))
  178. file_info = app.config['serve_client'].get_file_info(metadata['file_id'])
  179. if not file_info:
  180. return jsonify({'code': 3006, 'msg': '文件不存在'})
  181. app.config['serve_client'].download_file(file_info, current_app.config['work_path'])
  182. except Exception as e:
  183. logger.exception(e)
  184. return jsonify({'code': 1000, 'msg': '文件上传成功,本地文件清除失败'})
  185. return jsonify({'code': 1000, 'msg': '操作成功'})
  186. @logger.catch()
  187. @app.route('/update_local_file', methods=['POST'])
  188. def update_local_file():
  189. file_name = request.get_json()['file_name']
  190. if is_file_open_in_wps(os.path.join(current_app.config['work_path'], file_name)):
  191. return jsonify({'code': 3004, 'msg': '操作失败,文件已被打开无法操作'})
  192. try:
  193. metadata = oss_handle.load_metadata(os.path.join(current_app.config['work_path'], file_name))
  194. os.remove(os.path.join(current_app.config['work_path'], file_name))
  195. os.remove(os.path.join(current_app.config['work_path'], file_name + '.metadata'))
  196. except Exception as e:
  197. logger.exception(e)
  198. return jsonify({'code': 3005, 'msg': '操作失败,无权限操作本地文件'})
  199. try:
  200. file_info = app.config['serve_client'].get_file_info(metadata['file_id'])
  201. if not app.config['serve_client'].download_file(file_info, current_app.config['work_path']):
  202. return jsonify({'code': 3005, 'msg': '文件下载失败'})
  203. except Exception as e:
  204. logger.exception(e)
  205. return jsonify({'code': 3005, 'msg': '文件下载失败'})
  206. return jsonify({'code': 1000, 'msg': '操作成功'})
  207. @logger.catch()
  208. @app.route('/file_state_list', methods=['GET'])
  209. def file_state_list():
  210. '''
  211. state : 0 正常, 1: 云端有更新 2: 本地有变动且无冲突可以上传 3: 本地文件和云端文件有冲突
  212. :return:
  213. '''
  214. file_info_list = []
  215. for file_name in os.listdir(current_app.config['work_path']):
  216. suffer = file_name.split('.')[-1]
  217. if suffer not in file_type_map:
  218. continue
  219. if not os.path.exists(os.path.join(current_app.config['work_path'], file_name + '.metadata')):
  220. continue
  221. metadata = oss_handle.load_metadata(os.path.join(current_app.config['work_path'], file_name))
  222. file_info = {
  223. 'file_name': file_name,
  224. 'show_name': metadata['file_name'],
  225. 'cloud_update_time': metadata['update_time'],
  226. 'source_md5': metadata['md5'],
  227. 'state': 0
  228. }
  229. cloud_file_info = current_app.config['serve_client'].get_file_info(metadata['file_id'])
  230. if cloud_file_info == 2:
  231. file_info['state'] = 4
  232. file_info_list.append(file_info)
  233. continue
  234. if not cloud_file_info:
  235. file_info_list.append(file_info)
  236. continue
  237. if cloud_file_info['updateTime'] != metadata['update_time']:
  238. file_info['state'] = 1
  239. try:
  240. file_md5 = get_file_md5(os.path.join(current_app.config['work_path'], file_name))
  241. if file_md5 != metadata['md5']:
  242. if file_info['state'] == 1:
  243. file_info['state'] = 3
  244. else:
  245. file_info['state'] = 2
  246. except Exception as e:
  247. logger.exception(e)
  248. pass
  249. file_info_list.append(file_info)
  250. return jsonify({'code': 1000, 'msg': '操作成功', 'data': file_info_list})
  251. @logger.catch()
  252. @app.route('/remove_file', methods=['POST'])
  253. def remove_file():
  254. file_name = request.get_json()['file_name']
  255. file_path = os.path.join(current_app.config['work_path'], file_name)
  256. if not os.path.exists(file_path):
  257. return jsonify({'code': '3006', 'msg': '文件不存在'})
  258. try:
  259. os.remove(file_path)
  260. os.remove(f'{file_path}.metadata')
  261. except Exception as e:
  262. logger.exception(e)
  263. return jsonify({'code': 3005, 'msg': '操作失败,无权限操作本地文件'})
  264. return jsonify({'code': 1000, 'msg': '操作成功'})
  265. @logger.catch()
  266. @app.route('/start_wps_server', methods=['POST'])
  267. def start_wps_server():
  268. url = f"ksowpscloudsvr://start=RelayHttpServer&serverId=aef5ac0d-d5a3-49ee-b02f-c31eeb063f9b"
  269. os.startfile(url)
  270. return jsonify({'code': 1000, 'msg': '操作成功'})
  271. @logger.catch()
  272. @app.route('/open_file', methods=['POST'])
  273. def open_file():
  274. file_name = request.get_json()['file_name']
  275. file_path = os.path.join(current_app.config['work_path'], file_name)
  276. if not os.path.exists(file_path):
  277. return jsonify({'code': '3006', 'msg': '文件不存在'})
  278. if is_file_open_in_wps(file_path):
  279. return jsonify({'code': 1000, 'msg': '检测到文件已被打开'})
  280. return open_file_by_wps(file_path)
  281. @logger.catch()
  282. @app.route('/create_cloud_file', methods=['POST'])
  283. def create_cloud_file():
  284. params = request.get_json()
  285. flag = app.config['serve_client'].create_cloud_file(local_file=params['local_file'], folder_id=params['folder_id'])
  286. return jsonify({'code': 1000, 'msg': '文件上传成功' if flag else '网络错误,上传失败。'})
  287. @logger.catch()
  288. @app.route('/get_folder_tree', methods=['POST'])
  289. def get_base_path():
  290. return jsonify(app.config['serve_client'].get_folder_tree())
  291. @logger.catch()
  292. @app.route('/get_token', methods=['POST'])
  293. def get_token():
  294. return jsonify(app.config['serve_client'].login_reply)
  295. @logger.catch()
  296. @app.route('/refresh_token', methods=['POST'])
  297. def refresh_token():
  298. app.config['serve_client'].login()
  299. return jsonify(app.config['serve_client'].login_reply)
  300. @logger.catch()
  301. @app.route('/download_cloud_file', methods=['POST'])
  302. def download_cloud_file():
  303. '''
  304. 1000: 操作完成
  305. 2001: 检测到文档正在被编辑,无法下载
  306. 2002: 检测到远程文件已经更新本地文件也发生改动,终止操作
  307. 3001: 不支持该类型的文件
  308. 3002: 下载文件失败
  309. :return:
  310. '''
  311. file_id = request.get_json()['file_id']
  312. is_template = request.get_json()['is_template']
  313. # 判断是否为exe模式执行
  314. if is_template:
  315. file_info = app.config['serve_client'].get_template_file_info(file_id)
  316. time_str = datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S')
  317. local_path = os.path.join(current_app.config['work_path'],
  318. f'{file_info["name"]}({time_str}).{file_info["nameSuffix"]}')
  319. if app.config['serve_client'].download_template(file_info['fileLink'], local_path):
  320. return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_path})
  321. else:
  322. return jsonify({'code': 3006, 'msg': '文件不存在'})
  323. file_info = app.config['serve_client'].get_file_info(file_id)
  324. if not file_info:
  325. return jsonify({'code': 3006, 'msg': '文件不存在'})
  326. local_file = os.path.join(current_app.config['work_path'], file_info['filePath'].replace('/', '_'))
  327. # 判断本地是否存在已下载的同名文件
  328. if not os.path.exists(local_file) or not os.path.exists(local_file + '.metadata.json'):
  329. try:
  330. app.config['serve_client'].download_file(file_info,
  331. current_app.config['work_path'])
  332. except Exception as e:
  333. logger.exception(e)
  334. return jsonify({'code': 3002, 'msg': '下载文件失败'})
  335. return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file})
  336. # 判断本地文件是否被打开
  337. if is_file_open_in_wps(local_file):
  338. return jsonify({'code': 2001, 'msg': '检测到文档正在被编辑,无法下载'})
  339. # 判断本地文件和线上文件是否一致
  340. local_file_metadata = oss_handle.load_metadata(local_file)
  341. if local_file_metadata['update_time'] == file_info['updateTime']:
  342. return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file})
  343. if local_file_metadata['md5'] == app.config['serve_client'].get_file_md5(local_file):
  344. app.config['serve_client'].download_file(file_info, current_app.config['work_path'])
  345. return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file})
  346. return jsonify({'code': 2002, 'msg': '检测到远程文件已经更新本地文件也发生改动,终止操作'})
  347. def start_flask(serve_client, work_path):
  348. app.config['serve_client'] = serve_client
  349. app.config['work_path'] = work_path
  350. app.run(host='127.0.0.1', port=5855)