diff --git a/app/run.py b/app/run.py index 782b396..ffa44a3 100644 --- a/app/run.py +++ b/app/run.py @@ -1,4 +1,4 @@ -# !/usr/bin/env python3 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from flask import ( json, @@ -25,11 +25,22 @@ import base64 import sys import os import re +import io +import threading parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, parent_dir) from quark_auto_save import Quark, Config, MagicRename +# 用于存储当前运行的日志 +current_log = io.StringIO() +log_lock = threading.Lock() +# 添加一个标志来跟踪任务是否正在运行 +task_running = False +# 存储当前运行的进程 +current_process = None +process_lock = threading.Lock() + def get_app_ver(): BUILD_SHA = os.environ.get("BUILD_SHA", "") @@ -188,6 +199,27 @@ def run_script_now(): f">>> 手动运行任务 [{tasklist[0].get('taskname') if len(tasklist)>0 else 'ALL'}] 开始执行..." ) + # 清空当前日志 + with log_lock: + current_log.seek(0) + current_log.truncate(0) + current_log.write("任务开始执行...\n") + + # 设置任务运行状态 + global task_running, current_process + task_running = True + + # 确保之前的进程已经终止 + with process_lock: + if current_process is not None: + try: + if current_process.poll() is None: + current_process.terminate() + current_process.wait(timeout=1) + except Exception as e: + logging.error(f"终止旧进程失败: {str(e)}") + current_process = None + def generate_output(): # 设置环境变量 process_env = os.environ.copy() @@ -202,24 +234,46 @@ def run_script_now(): ) if tasklist: process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False) - process = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - encoding="utf-8", - errors="replace", - bufsize=1, - env=process_env, - ) + + # 创建进程并保存引用 + global current_process + with process_lock: + current_process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + encoding="utf-8", + errors="replace", + bufsize=1, + env=process_env, + ) + try: - for line in iter(process.stdout.readline, ""): + for line in iter(current_process.stdout.readline, ""): logging.info(line.strip()) + # 将日志存储到全局变量中 + with log_lock: + current_log.write(line) yield f"data: {line}\n\n" + + # 检查进程退出状态 + exit_code = current_process.wait() + if exit_code != 0: + error_msg = f"任务异常结束,退出码: {exit_code}\n" + logging.error(error_msg) + with log_lock: + current_log.write(error_msg) + yield f"data: {error_msg}\n\n" + yield "data: [DONE]\n\n" finally: - process.stdout.close() - process.wait() + if current_process: + if current_process.poll() is None: + current_process.stdout.close() + current_process.wait() + # 检查进程状态 + check_process_status() return Response( stream_with_context(generate_output()), @@ -227,6 +281,54 @@ def run_script_now(): ) +# 添加一个函数来检查进程状态 +def check_process_status(): + global task_running, current_process + with process_lock: + if current_process is not None: + # 检查进程是否仍在运行 + poll_result = current_process.poll() + if poll_result is None: + # 进程仍在运行 + logging.debug(">>> 检查进程状态:进程仍在运行") + task_running = True + else: + # 进程已结束,记录退出码 + if poll_result != 0: + error_msg = f">>> 检查进程状态:进程异常结束,退出码 {poll_result}" + logging.error(error_msg) + # 将错误信息添加到日志中 + with log_lock: + if not current_log.getvalue().endswith( + f"任务异常结束,退出码: {poll_result}\n" + ): + current_log.write(f"任务异常结束,退出码: {poll_result}\n") + else: + logging.info(f">>> 检查进程状态:进程正常结束,退出码 {poll_result}") + task_running = False + current_process = None + else: + if task_running: + logging.info(">>> 检查进程状态:无进程引用但状态为运行中,重置状态") + task_running = False + else: + logging.debug(">>> 检查进程状态:无进程运行") + + +# 获取当前运行的日志 +@app.route("/get_current_log") +def get_current_log(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + # 检查进程状态 + check_process_status() + + with log_lock: + log_content = current_log.getvalue() + return jsonify({"success": True, "log": log_content, "running": task_running}) + + @app.route("/task_suggestions") def get_task_suggestions(): if not is_login(): @@ -299,6 +401,7 @@ def get_share_detail(): def preview_regex(data): task = request.json.get("task", {}) magic_regex = request.json.get("magic_regex", {}) + global_regex = request.json.get("global_regex", {}) mr = MagicRename(magic_regex) mr.set_taskname(task.get("taskname", "")) account = Quark(config_data["cookie"][0]) @@ -310,34 +413,64 @@ def get_share_detail(): dir_file_list = [] dir_filename_list = [] - pattern, replace = mr.magic_regex_conv( + # 获取全局正则和任务正则 + global_regex_enabled = global_regex.get("enabled", False) + global_pattern = global_regex.get("pattern", "") + global_replace = global_regex.get("replace", "") + + task_pattern, task_replace = mr.magic_regex_conv( task.get("pattern", ""), task.get("replace", "") ) + for share_file in data["list"]: - search_pattern = ( - task["update_subdir"] - if share_file["dir"] and task.get("update_subdir") - else pattern - ) - if re.search(search_pattern, share_file["file_name"]): - # 文件名重命名,目录不重命名 - file_name_re = ( - share_file["file_name"] - if share_file["dir"] - else mr.sub(pattern, replace, share_file["file_name"]) - ) + if share_file["dir"]: + search_pattern = task.get("update_subdir", "") + if re.search(search_pattern, share_file["file_name"]): + # 目录不进行重命名 + share_file["file_name_re"] = share_file["file_name"] + continue + + # 初始化重命名后的文件名为原始文件名 + renamed_name = share_file["file_name"] + applied_global_regex = False + applied_task_regex = False + + # 应用全局正则(如果启用) + if global_regex_enabled and global_pattern: + if re.search(global_pattern, renamed_name): + global_renamed_name = re.sub( + global_pattern, global_replace, renamed_name + ) + renamed_name = global_renamed_name + applied_global_regex = True + + # 应用任务正则(如果存在) + if task_pattern: + if re.search(task_pattern, renamed_name): + task_renamed_name = mr.sub(task_pattern, task_replace, renamed_name) + renamed_name = task_renamed_name + applied_task_regex = True + + # 检查是否应用了任何正则 + if applied_global_regex or applied_task_regex: if file_name_saved := mr.is_exists( - file_name_re, + renamed_name, dir_filename_list, (task.get("ignore_extension") and not share_file["dir"]), ): share_file["file_name_saved"] = file_name_saved else: - share_file["file_name_re"] = file_name_re + share_file["file_name_re"] = renamed_name # 文件列表排序 - if re.search(r"\{I+\}", replace): - mr.set_dir_file_list(dir_file_list, replace) + replace_for_i = "" + if task.get("replace"): + replace_for_i = task_replace + elif global_regex_enabled and global_replace: + replace_for_i = global_replace + + if replace_for_i and re.search(r"\{I+\}", replace_for_i): + mr.set_dir_file_list(dir_file_list, replace_for_i) mr.sort_file_list(data["list"]) if request.json.get("task"): @@ -375,8 +508,9 @@ def get_savepath_detail(): return jsonify({"success": False, "data": {"error": "获取fid失败"}}) else: fid = request.args.get("fid", "0") + file_list = { - "list": account.ls_dir(fid)["data"]["list"], + "list": account.ls_dir(fid), "paths": paths, } return jsonify({"success": True, "data": file_list}) @@ -471,8 +605,57 @@ def init(): # 读取配置 config_data = Config.read_json(CONFIG_PATH) Config.breaking_change_update(config_data) + + # --- 开始进行配置项向后兼容更新 --- + + # 1. 确保 magic_regex 存在 if not config_data.get("magic_regex"): config_data["magic_regex"] = MagicRename().magic_regex + logging.info("配置升级:已补全 'magic_regex' 默认配置。") + + # 2. 确保 global_regex 存在 + if not config_data.get("global_regex"): + config_data["global_regex"] = {"enabled": False, "pattern": "", "replace": ""} + logging.info("配置升级:已补全 'global_regex' 默认配置。") + + # 3. 确保 file_blacklist 存在 + if "file_blacklist" not in config_data: + config_data["file_blacklist"] = [] + logging.info("配置升级:已补全 'file_blacklist' 默认配置。") + + # 4. 确保 source 存在 + if "source" not in config_data: + config_data["source"] = { + "cloudsaver": {"server": "", "username": "", "password": "", "token": ""} + } + logging.info("配置升级:已补全 'source' 默认配置。") + + # 5. 确保 shortcuts 及其所有子项存在 + if "shortcuts" not in config_data: + config_data["shortcuts"] = { + "saveEnabled": True, + "runEnabled": False, + "autoSaveEnabled": True, + } + logging.info("配置升级:已补全 'shortcuts' 默认配置。") + else: + if "saveEnabled" not in config_data["shortcuts"]: + config_data["shortcuts"]["saveEnabled"] = True + if "runEnabled" not in config_data["shortcuts"]: + config_data["shortcuts"]["runEnabled"] = False + if "autoSaveEnabled" not in config_data["shortcuts"]: + config_data["shortcuts"]["autoSaveEnabled"] = True + logging.info("配置升级:为 'shortcuts' 已补全 'autoSaveEnabled' 子项。") + + # 6. 确保 tasklist 中每个任务都包含新参数 (例如 ignore_extension) + if config_data.get("tasklist") and isinstance(config_data["tasklist"], list): + for task in config_data["tasklist"]: + if "ignore_extension" not in task: + task["ignore_extension"] = False + if "addition" not in task: + task["addition"] = {} + + # --- 配置项向后兼容更新结束 --- # 默认管理账号 config_data["webui"] = { @@ -491,10 +674,42 @@ def init(): plugins_config_default.update(config_data.get("plugins", {})) config_data["plugins"] = plugins_config_default - # 更新配置 + # 更新配置,将所有补全的默认值写回文件 Config.write_json(CONFIG_PATH, config_data) +# 添加一个重置任务状态的路由 +@app.route("/reset_task_status", methods=["POST"]) +def reset_task_status(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + global task_running, current_process + + # 如果有正在运行的进程,尝试终止它 + with process_lock: + if current_process is not None: + try: + if current_process.poll() is None: # 进程仍在运行 + current_process.terminate() + current_process.wait(timeout=5) # 等待最多5秒 + current_process = None + except Exception as e: + logging.error(f"终止进程失败: {str(e)}") + + # 重置任务状态 + task_running = False + + # 清空日志 + with log_lock: + current_log.seek(0) + current_log.truncate(0) + current_log.write("任务状态已重置\n") + + logging.info(">>> 任务状态已手动重置") + return jsonify({"success": True, "message": "任务状态已重置"}) + + if __name__ == "__main__": init() reload_tasks()