api_app.py 16 KB

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