4 changed files with 639 additions and 0 deletions
-
53client-file-collector/README.md
-
100client-file-collector/build_exe.bat
-
484client-file-collector/client_file_collector.py
-
2client-file-collector/requirements.txt
@ -0,0 +1,53 @@ |
|||||
|
# 文件采集客户端(支持最多3行配置) |
||||
|
|
||||
|
客户端按轮询秒定时将本地目录文件上传到 `xujie-sys`,服务端统一落盘到: |
||||
|
|
||||
|
- `D:\ckp-file\<equipmentNo>\` |
||||
|
|
||||
|
## 当前需求对应关系 |
||||
|
|
||||
|
- 客户端只需录入: |
||||
|
- 系统地址 |
||||
|
- 轮询秒 |
||||
|
- 最多3行配置(`site + buNo + equipmentNo + 本地目录`) |
||||
|
- 双击打开 EXE 后,录入并保存配置即可按轮询秒自动同步。 |
||||
|
- 服务端收到每行配置上传的文件后,会自动写入 `D:\ckp-file\equipmentNo`。 |
||||
|
|
||||
|
## 服务端接口 |
||||
|
|
||||
|
- `POST /collector/client/upload` |
||||
|
- form-data: |
||||
|
- `file` |
||||
|
- `site` |
||||
|
- `buNo` |
||||
|
- `equipmentNo` |
||||
|
|
||||
|
## 客户端行为说明 |
||||
|
|
||||
|
- 每轮轮询会扫描每行配置的本地目录。 |
||||
|
- 仅同步新增或修改过的文件(避免重复上传同一文件)。 |
||||
|
- 不会删除本地文件。 |
||||
|
- Windows 下首次启动 `QMSFileCollector.exe` 时,会自动写入当前用户自启动项(优先写入 `Startup` 启动文件夹,失败时回退注册表 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`)。 |
||||
|
|
||||
|
## 打包步骤 |
||||
|
|
||||
|
1. 安装 Python 3.10+。 |
||||
|
2. 在当前目录执行: |
||||
|
|
||||
|
```bash |
||||
|
build_exe.bat |
||||
|
``` |
||||
|
|
||||
|
3. 生成文件: |
||||
|
|
||||
|
- `dist/QMSFileCollector.exe` |
||||
|
|
||||
|
## 常见问题 |
||||
|
|
||||
|
- 找不到 `dist/QMSFileCollector.exe` |
||||
|
- 请先看脚本输出是否有 `Python3 interpreter not found`。 |
||||
|
- 若机器上 `python --version` 是 `2.7`,不会执行成功。 |
||||
|
|
||||
|
- `pip` 下载超时 |
||||
|
- 脚本已内置镜像重试(清华 -> 阿里 -> 官方)。 |
||||
|
- 网络不稳定时可重试执行 `build_exe.bat`。 |
||||
@ -0,0 +1,100 @@ |
|||||
|
@echo off |
||||
|
chcp 65001 >nul |
||||
|
setlocal EnableDelayedExpansion |
||||
|
|
||||
|
set "PY_EXE=" |
||||
|
set "PY_ARGS=" |
||||
|
set "PIP_INSTALL_ARGS=--disable-pip-version-check --default-timeout 180 --retries 10 --trusted-host pypi.org --trusted-host files.pythonhosted.org --trusted-host pypi.tuna.tsinghua.edu.cn --trusted-host mirrors.aliyun.com" |
||||
|
|
||||
|
where py >nul 2>nul |
||||
|
if not errorlevel 1 ( |
||||
|
py -3 -c "import sys;sys.exit(0 if sys.version_info[0]>=3 else 1)" >nul 2>nul |
||||
|
if not errorlevel 1 ( |
||||
|
set "PY_EXE=py" |
||||
|
set "PY_ARGS=-3" |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
if not defined PY_EXE ( |
||||
|
where python3 >nul 2>nul |
||||
|
if not errorlevel 1 ( |
||||
|
python3 -c "import sys;sys.exit(0 if sys.version_info[0]>=3 else 1)" >nul 2>nul |
||||
|
if not errorlevel 1 ( |
||||
|
set "PY_EXE=python3" |
||||
|
set "PY_ARGS=" |
||||
|
) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
if not defined PY_EXE ( |
||||
|
python -c "import sys;sys.exit(0 if sys.version_info[0]>=3 else 1)" >nul 2>nul |
||||
|
if not errorlevel 1 ( |
||||
|
set "PY_EXE=python" |
||||
|
set "PY_ARGS=" |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
if not defined PY_EXE ( |
||||
|
for %%D in ("%LocalAppData%\Programs\Python\Python312\python.exe" "%LocalAppData%\Programs\Python\Python311\python.exe" "%LocalAppData%\Programs\Python\Python310\python.exe" "C:\Program Files\Python312\python.exe" "C:\Program Files\Python311\python.exe" "C:\Program Files\Python310\python.exe") do ( |
||||
|
if exist %%~D ( |
||||
|
%%~D -c "import sys;sys.exit(0 if sys.version_info[0]>=3 else 1)" >nul 2>nul |
||||
|
if not errorlevel 1 ( |
||||
|
set "PY_EXE=%%~D" |
||||
|
set "PY_ARGS=" |
||||
|
goto :py_found |
||||
|
) |
||||
|
) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
:py_found |
||||
|
if not defined PY_EXE ( |
||||
|
echo Python3 interpreter not found. |
||||
|
echo Current python version: |
||||
|
python --version |
||||
|
echo Please install Python 3.10+ and retry. |
||||
|
echo Download URL https://www.python.org/downloads/windows/ |
||||
|
exit /b 1 |
||||
|
) |
||||
|
|
||||
|
echo Using interpreter: |
||||
|
call "%PY_EXE%" %PY_ARGS% --version |
||||
|
|
||||
|
echo [1/3] Install dependencies... |
||||
|
call :install_requirements |
||||
|
if errorlevel 1 ( |
||||
|
echo Dependency install failed after trying all indexes |
||||
|
exit /b 1 |
||||
|
) |
||||
|
|
||||
|
echo [2/3] Build EXE... |
||||
|
if exist "dist\QMSFileCollector.exe" ( |
||||
|
del /f /q "dist\QMSFileCollector.exe" >nul 2>nul |
||||
|
if exist "dist\QMSFileCollector.exe" ( |
||||
|
echo Existing dist\QMSFileCollector.exe is locked. |
||||
|
echo Please close running QMSFileCollector process and retry. |
||||
|
exit /b 1 |
||||
|
) |
||||
|
) |
||||
|
call "%PY_EXE%" %PY_ARGS% -m PyInstaller --noconfirm --clean --onefile --windowed --name QMSFileCollector client_file_collector.py |
||||
|
if errorlevel 1 ( |
||||
|
echo EXE build failed |
||||
|
exit /b 1 |
||||
|
) |
||||
|
|
||||
|
echo [3/3] Build completed |
||||
|
echo Output: dist\QMSFileCollector.exe |
||||
|
endlocal |
||||
|
exit /b 0 |
||||
|
|
||||
|
:install_requirements |
||||
|
for %%I in (https://pypi.tuna.tsinghua.edu.cn/simple https://mirrors.aliyun.com/pypi/simple/ https://pypi.org/simple) do ( |
||||
|
echo Trying pip index: %%I |
||||
|
call "%PY_EXE%" %PY_ARGS% -m pip install %PIP_INSTALL_ARGS% -i %%I -r requirements.txt |
||||
|
if not errorlevel 1 ( |
||||
|
echo Dependencies installed from %%I |
||||
|
exit /b 0 |
||||
|
) |
||||
|
echo Install failed from %%I, try next index... |
||||
|
) |
||||
|
exit /b 1 |
||||
@ -0,0 +1,484 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
import json |
||||
|
import os |
||||
|
import queue |
||||
|
import sys |
||||
|
import threading |
||||
|
import time |
||||
|
from datetime import datetime |
||||
|
from pathlib import Path |
||||
|
from tkinter import Tk, Label, Entry, Button, StringVar, END, DISABLED, NORMAL, filedialog, messagebox, simpledialog |
||||
|
from tkinter.scrolledtext import ScrolledText |
||||
|
|
||||
|
import requests |
||||
|
|
||||
|
|
||||
|
def get_runtime_dir(): |
||||
|
if getattr(sys, "frozen", False): |
||||
|
return Path(sys.executable).resolve().parent |
||||
|
return Path(__file__).resolve().parent |
||||
|
|
||||
|
|
||||
|
def _ensure_windows_run_registry(entry_name, exe_path): |
||||
|
import winreg |
||||
|
run_key_path = r"Software\Microsoft\Windows\CurrentVersion\Run" |
||||
|
target_value = "\"%s\"" % exe_path |
||||
|
|
||||
|
try: |
||||
|
with winreg.OpenKey( |
||||
|
winreg.HKEY_CURRENT_USER, |
||||
|
run_key_path, |
||||
|
0, |
||||
|
winreg.KEY_READ | winreg.KEY_SET_VALUE |
||||
|
) as key: |
||||
|
try: |
||||
|
old_value, _ = winreg.QueryValueEx(key, entry_name) |
||||
|
except FileNotFoundError: |
||||
|
old_value = None |
||||
|
|
||||
|
if old_value and str(old_value).strip().strip("\"").lower() == exe_path.lower(): |
||||
|
return False, "系统自启动项已存在,无需重复写入。" |
||||
|
|
||||
|
winreg.SetValueEx(key, entry_name, 0, winreg.REG_SZ, target_value) |
||||
|
if old_value: |
||||
|
return True, "检测到自启动路径变化,已自动更新。" |
||||
|
return True, "首次启动已写入系统自启动项,下次开机会自动运行。" |
||||
|
except Exception as e: |
||||
|
return False, "写入系统自启动项失败: %s" % e |
||||
|
|
||||
|
|
||||
|
def _cleanup_legacy_startup_items(legacy_entry_name, app_data): |
||||
|
if app_data: |
||||
|
startup_dir = Path(app_data) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" |
||||
|
legacy_script = startup_dir / (legacy_entry_name + ".vbs") |
||||
|
try: |
||||
|
if legacy_script.exists(): |
||||
|
legacy_script.unlink() |
||||
|
except Exception: |
||||
|
pass |
||||
|
|
||||
|
try: |
||||
|
import winreg |
||||
|
run_key_path = r"Software\Microsoft\Windows\CurrentVersion\Run" |
||||
|
with winreg.OpenKey( |
||||
|
winreg.HKEY_CURRENT_USER, |
||||
|
run_key_path, |
||||
|
0, |
||||
|
winreg.KEY_SET_VALUE |
||||
|
) as key: |
||||
|
try: |
||||
|
winreg.DeleteValue(key, legacy_entry_name) |
||||
|
except FileNotFoundError: |
||||
|
pass |
||||
|
except Exception: |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
def ensure_windows_startup(): |
||||
|
if os.name != "nt": |
||||
|
return False, "当前系统非Windows,跳过自启动注册。" |
||||
|
|
||||
|
if not getattr(sys, "frozen", False): |
||||
|
return False, "当前为源码运行,跳过自启动注册。" |
||||
|
|
||||
|
exe_path = str(Path(sys.executable).resolve()) |
||||
|
entry_name = "QMSFileCollector" |
||||
|
|
||||
|
app_data = os.getenv("APPDATA", "").strip() |
||||
|
_cleanup_legacy_startup_items("CKPClientFileCollector", app_data) |
||||
|
if app_data: |
||||
|
startup_dir = Path(app_data) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" |
||||
|
startup_script = startup_dir / (entry_name + ".vbs") |
||||
|
script_content = ( |
||||
|
"Set WshShell = CreateObject(\"WScript.Shell\")\n" |
||||
|
"WshShell.Run Chr(34) & \"%s\" & Chr(34), 0\n" |
||||
|
"Set WshShell = Nothing\n" |
||||
|
) % exe_path.replace("\"", "\"\"") |
||||
|
|
||||
|
try: |
||||
|
startup_dir.mkdir(parents=True, exist_ok=True) |
||||
|
old_content = "" |
||||
|
if startup_script.exists(): |
||||
|
old_content = startup_script.read_text(encoding="utf-8") |
||||
|
|
||||
|
if old_content == script_content: |
||||
|
return False, "启动文件夹自启动项已存在,无需重复写入。" |
||||
|
|
||||
|
startup_script.write_text(script_content, encoding="utf-8") |
||||
|
if old_content: |
||||
|
return True, "检测到启动文件夹自启动路径变化,已自动更新。" |
||||
|
return True, "首次启动已写入启动文件夹自启动项,下次开机会自动运行。" |
||||
|
except Exception as startup_error: |
||||
|
try: |
||||
|
changed, reg_msg = _ensure_windows_run_registry(entry_name, exe_path) |
||||
|
return changed, "写入启动文件夹失败,已回退注册表方式: %s" % reg_msg |
||||
|
except Exception as reg_error: |
||||
|
return False, "写入启动文件夹和注册表均失败: %s; %s" % (startup_error, reg_error) |
||||
|
|
||||
|
try: |
||||
|
return _ensure_windows_run_registry(entry_name, exe_path) |
||||
|
except Exception as e: |
||||
|
return False, "未获取到APPDATA且注册表写入失败: %s" % e |
||||
|
|
||||
|
|
||||
|
class CollectorApp: |
||||
|
def __init__(self, root): |
||||
|
self.root = root |
||||
|
self.root.title("文件采集客户端") |
||||
|
self.root.geometry("1180x720") |
||||
|
self.root.resizable(True, True) |
||||
|
|
||||
|
self.config_path = get_runtime_dir() / "collector_config.json" |
||||
|
self.log_queue = queue.Queue() |
||||
|
self.running = False |
||||
|
self.worker_thread = None |
||||
|
self.active_rows = [] |
||||
|
self.upload_state = {} |
||||
|
self.notice_ts = {} |
||||
|
|
||||
|
self.base_url_var = StringVar() |
||||
|
self.poll_seconds_var = StringVar(value="10") |
||||
|
|
||||
|
self.row_vars = [] |
||||
|
for _ in range(3): |
||||
|
self.row_vars.append({ |
||||
|
"site": StringVar(), |
||||
|
"bu_no": StringVar(), |
||||
|
"equipment_no": StringVar(), |
||||
|
"source_path": StringVar(), |
||||
|
}) |
||||
|
|
||||
|
self._build_ui() |
||||
|
self._init_windows_startup() |
||||
|
self._load_config() |
||||
|
self.root.after(200, self._flush_logs) |
||||
|
|
||||
|
def _init_windows_startup(self): |
||||
|
changed, message = ensure_windows_startup() |
||||
|
if changed: |
||||
|
self.log(message) |
||||
|
return |
||||
|
|
||||
|
if getattr(sys, "frozen", False) and ("失败" in message): |
||||
|
self.log(message) |
||||
|
|
||||
|
def _build_ui(self): |
||||
|
Label(self.root, text="系统地址").place(x=20, y=20) |
||||
|
Entry(self.root, textvariable=self.base_url_var, width=92).place(x=85, y=20) |
||||
|
Button(self.root, text="弹框设置地址", command=self.popup_set_base_url).place(x=860, y=16) |
||||
|
Button(self.root, text="保存配置", command=self.save_config).place(x=970, y=16) |
||||
|
|
||||
|
Label(self.root, text="轮询秒").place(x=20, y=58) |
||||
|
Entry(self.root, textvariable=self.poll_seconds_var, width=10).place(x=85, y=58) |
||||
|
Button(self.root, text="开始同步", command=self.start_collect).place(x=180, y=54) |
||||
|
Button(self.root, text="停止同步", command=self.stop_collect).place(x=265, y=54) |
||||
|
|
||||
|
Label(self.root, text="配置说明:最多3行,每行需填写 site + buNo + equipmentNo + 本地目录").place(x=360, y=58) |
||||
|
|
||||
|
Label(self.root, text="行").place(x=20, y=100) |
||||
|
Label(self.root, text="Site").place(x=80, y=100) |
||||
|
Label(self.root, text="BuNo").place(x=230, y=100) |
||||
|
Label(self.root, text="EquipmentNo").place(x=380, y=100) |
||||
|
Label(self.root, text="本地目录").place(x=560, y=100) |
||||
|
|
||||
|
for idx, row in enumerate(self.row_vars): |
||||
|
y = 130 + idx * 40 |
||||
|
Label(self.root, text=str(idx + 1)).place(x=26, y=y) |
||||
|
Entry(self.root, textvariable=row["site"], width=16).place(x=80, y=y) |
||||
|
Entry(self.root, textvariable=row["bu_no"], width=16).place(x=230, y=y) |
||||
|
Entry(self.root, textvariable=row["equipment_no"], width=18).place(x=380, y=y) |
||||
|
Entry(self.root, textvariable=row["source_path"], width=56).place(x=560, y=y) |
||||
|
Button(self.root, text="选择目录", command=lambda x=idx: self.choose_source_dir(x)).place(x=1010, y=y - 4) |
||||
|
|
||||
|
self.log_text = ScrolledText(self.root, width=165, height=23) |
||||
|
self.log_text.place(x=20, y=280) |
||||
|
self.log_text.configure(state=DISABLED) |
||||
|
|
||||
|
def popup_set_base_url(self): |
||||
|
new_url = simpledialog.askstring( |
||||
|
"设置目标地址", |
||||
|
"请输入xujie-sys地址,例如:http://172.26.58.88:8080", |
||||
|
initialvalue=self.base_url_var.get().strip() |
||||
|
) |
||||
|
if new_url is not None: |
||||
|
self.base_url_var.set(new_url.strip()) |
||||
|
self.log("已设置目标地址: %s" % new_url.strip()) |
||||
|
|
||||
|
def choose_source_dir(self, row_index): |
||||
|
path = filedialog.askdirectory(title="选择第%d行本地目录" % (row_index + 1)) |
||||
|
if path: |
||||
|
self.row_vars[row_index]["source_path"].set(path) |
||||
|
|
||||
|
def _load_config(self): |
||||
|
if not self.config_path.exists(): |
||||
|
self.log("未找到本地配置,请先录入并保存。") |
||||
|
return |
||||
|
try: |
||||
|
config = json.loads(self.config_path.read_text(encoding="utf-8")) |
||||
|
self.base_url_var.set(config.get("base_url", "")) |
||||
|
self.poll_seconds_var.set(str(config.get("poll_seconds", "10"))) |
||||
|
|
||||
|
rows = config.get("rows") |
||||
|
# 兼容旧版本单行配置 |
||||
|
if not isinstance(rows, list): |
||||
|
rows = [{ |
||||
|
"site": config.get("site", ""), |
||||
|
"bu_no": config.get("bu_no", ""), |
||||
|
"equipment_no": config.get("equipment_no", ""), |
||||
|
"source_path": config.get("source_path", ""), |
||||
|
}] |
||||
|
|
||||
|
for idx in range(min(3, len(rows))): |
||||
|
item = rows[idx] if isinstance(rows[idx], dict) else {} |
||||
|
self.row_vars[idx]["site"].set(str(item.get("site", "")).strip()) |
||||
|
self.row_vars[idx]["bu_no"].set(str(item.get("bu_no", "")).strip()) |
||||
|
self.row_vars[idx]["equipment_no"].set(str(item.get("equipment_no", "")).strip()) |
||||
|
self.row_vars[idx]["source_path"].set(str(item.get("source_path", "")).strip()) |
||||
|
|
||||
|
self.log("配置加载完成。") |
||||
|
if self._can_auto_start(): |
||||
|
self.start_collect(auto_mode=True) |
||||
|
except Exception as e: |
||||
|
self.log("读取配置失败: %s" % e) |
||||
|
|
||||
|
def save_config(self): |
||||
|
try: |
||||
|
rows = self._get_valid_rows(require_complete=True, raise_on_empty=False) |
||||
|
except ValueError as e: |
||||
|
messagebox.showwarning("提示", str(e)) |
||||
|
return |
||||
|
|
||||
|
data = { |
||||
|
"base_url": self.base_url_var.get().strip(), |
||||
|
"poll_seconds": self.poll_seconds_var.get().strip(), |
||||
|
"rows": rows, |
||||
|
} |
||||
|
try: |
||||
|
self.config_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") |
||||
|
self.log("配置已保存: %s" % self.config_path) |
||||
|
except Exception as e: |
||||
|
messagebox.showerror("错误", "保存配置失败: %s" % e) |
||||
|
return |
||||
|
|
||||
|
if self.running: |
||||
|
self.active_rows = rows |
||||
|
self.log("采集配置已更新,下一轮轮询自动生效。") |
||||
|
elif self._can_auto_start(): |
||||
|
self.start_collect(auto_mode=True) |
||||
|
|
||||
|
def _can_auto_start(self): |
||||
|
if not self._base_url(): |
||||
|
return False |
||||
|
if not self._is_poll_seconds_valid(): |
||||
|
return False |
||||
|
try: |
||||
|
rows = self._get_valid_rows(require_complete=True, raise_on_empty=True) |
||||
|
return len(rows) > 0 |
||||
|
except Exception: |
||||
|
return False |
||||
|
|
||||
|
def _is_poll_seconds_valid(self): |
||||
|
try: |
||||
|
return int(self.poll_seconds_var.get().strip()) > 0 |
||||
|
except Exception: |
||||
|
return False |
||||
|
|
||||
|
def _base_url(self): |
||||
|
return self.base_url_var.get().strip().rstrip("/") |
||||
|
|
||||
|
def _get_valid_rows(self, require_complete, raise_on_empty): |
||||
|
rows = [] |
||||
|
for idx, row in enumerate(self.row_vars): |
||||
|
site = row["site"].get().strip() |
||||
|
bu_no = row["bu_no"].get().strip() |
||||
|
equipment_no = row["equipment_no"].get().strip() |
||||
|
source_path = row["source_path"].get().strip() |
||||
|
|
||||
|
filled_count = 0 |
||||
|
for value in (site, bu_no, equipment_no, source_path): |
||||
|
if value: |
||||
|
filled_count += 1 |
||||
|
|
||||
|
if filled_count == 0: |
||||
|
continue |
||||
|
|
||||
|
if require_complete and filled_count < 4: |
||||
|
raise ValueError("第%d行配置未填写完整,请补全site、buNo、equipmentNo、本地目录。" % (idx + 1)) |
||||
|
|
||||
|
if filled_count == 4: |
||||
|
rows.append({ |
||||
|
"row_index": idx + 1, |
||||
|
"site": site, |
||||
|
"bu_no": bu_no, |
||||
|
"equipment_no": equipment_no, |
||||
|
"source_path": source_path, |
||||
|
}) |
||||
|
|
||||
|
if raise_on_empty and len(rows) == 0: |
||||
|
raise ValueError("请至少填写一行完整采集配置。") |
||||
|
return rows |
||||
|
|
||||
|
def start_collect(self, auto_mode=False): |
||||
|
if self.running: |
||||
|
if not auto_mode: |
||||
|
self.log("采集任务已在运行中。") |
||||
|
return |
||||
|
|
||||
|
if not self._base_url(): |
||||
|
if auto_mode: |
||||
|
self.log("自动启动失败:系统地址为空。") |
||||
|
else: |
||||
|
messagebox.showwarning("提示", "请先设置系统地址") |
||||
|
return |
||||
|
|
||||
|
try: |
||||
|
poll_seconds = int(self.poll_seconds_var.get().strip() or "10") |
||||
|
if poll_seconds <= 0: |
||||
|
raise ValueError("轮询秒必须大于0") |
||||
|
except Exception: |
||||
|
if auto_mode: |
||||
|
self.log("自动启动失败:轮询秒无效。") |
||||
|
else: |
||||
|
messagebox.showwarning("提示", "轮询秒必须是大于0的整数") |
||||
|
return |
||||
|
|
||||
|
try: |
||||
|
rows = self._get_valid_rows(require_complete=True, raise_on_empty=True) |
||||
|
except ValueError as e: |
||||
|
if auto_mode: |
||||
|
self.log("自动启动失败:%s" % e) |
||||
|
else: |
||||
|
messagebox.showwarning("提示", str(e)) |
||||
|
return |
||||
|
|
||||
|
self.active_rows = rows |
||||
|
self.running = True |
||||
|
self.worker_thread = threading.Thread(target=self._worker_loop, daemon=True) |
||||
|
self.worker_thread.start() |
||||
|
if auto_mode: |
||||
|
self.log("配置有效,已自动启动轮询同步。") |
||||
|
else: |
||||
|
self.log("采集任务已启动。") |
||||
|
|
||||
|
def stop_collect(self): |
||||
|
if not self.running: |
||||
|
self.log("采集任务未运行。") |
||||
|
return |
||||
|
self.running = False |
||||
|
self.log("正在停止采集任务...") |
||||
|
|
||||
|
def _worker_loop(self): |
||||
|
while self.running: |
||||
|
rows = list(self.active_rows) |
||||
|
for row in rows: |
||||
|
if not self.running: |
||||
|
break |
||||
|
self._sync_one_row(row) |
||||
|
|
||||
|
try: |
||||
|
poll_seconds = int(self.poll_seconds_var.get().strip() or "10") |
||||
|
except Exception: |
||||
|
poll_seconds = 10 |
||||
|
|
||||
|
for _ in range(max(1, poll_seconds)): |
||||
|
if not self.running: |
||||
|
break |
||||
|
time.sleep(1) |
||||
|
self.log("采集任务已停止。") |
||||
|
|
||||
|
def _sync_one_row(self, row): |
||||
|
row_index = row["row_index"] |
||||
|
source_path = row["source_path"] |
||||
|
path_key = "row%d_missing" % row_index |
||||
|
empty_key = "row%d_empty" % row_index |
||||
|
|
||||
|
if not os.path.isdir(source_path): |
||||
|
self._log_with_interval(path_key, "第%d行目录不存在: %s" % (row_index, source_path), 60) |
||||
|
return |
||||
|
|
||||
|
files = [] |
||||
|
for name in os.listdir(source_path): |
||||
|
file_path = Path(source_path) / name |
||||
|
if file_path.is_file(): |
||||
|
files.append(file_path) |
||||
|
|
||||
|
if not files: |
||||
|
self._log_with_interval(empty_key, "第%d行目录为空,等待新文件..." % row_index, 60) |
||||
|
return |
||||
|
|
||||
|
files.sort(key=lambda p: p.stat().st_mtime) |
||||
|
for file_path in files: |
||||
|
if not self.running: |
||||
|
return |
||||
|
signature = self._file_signature(file_path) |
||||
|
if signature is None: |
||||
|
continue |
||||
|
|
||||
|
state_key = "%d|%s" % (row_index, str(file_path).lower()) |
||||
|
if self.upload_state.get(state_key) == signature: |
||||
|
continue |
||||
|
|
||||
|
if self._upload_file(row, file_path): |
||||
|
self.upload_state[state_key] = signature |
||||
|
|
||||
|
def _file_signature(self, file_path): |
||||
|
try: |
||||
|
stat = file_path.stat() |
||||
|
return stat.st_mtime_ns, stat.st_size |
||||
|
except Exception as e: |
||||
|
self.log("读取文件状态失败: %s, 原因: %s" % (file_path, e)) |
||||
|
return None |
||||
|
|
||||
|
def _upload_file(self, row, file_path): |
||||
|
url = self._base_url() + "/collector/client/upload" |
||||
|
data = { |
||||
|
"site": row["site"], |
||||
|
"buNo": row["bu_no"], |
||||
|
"equipmentNo": row["equipment_no"], |
||||
|
} |
||||
|
|
||||
|
try: |
||||
|
with open(file_path, "rb") as fp: |
||||
|
files = {"file": (file_path.name, fp, "application/octet-stream")} |
||||
|
resp = requests.post(url, data=data, files=files, timeout=180) |
||||
|
|
||||
|
resp.raise_for_status() |
||||
|
result = resp.json() |
||||
|
if result.get("code") == 0: |
||||
|
response_data = result.get("data") or {} |
||||
|
server_path = response_data.get("savedFullPath", "") |
||||
|
self.log("第%d行上传成功: %s -> %s" % (row["row_index"], file_path.name, server_path)) |
||||
|
return True |
||||
|
|
||||
|
self.log("第%d行上传失败: %s, 原因: %s" % ( |
||||
|
row["row_index"], file_path.name, result.get("msg"))) |
||||
|
return False |
||||
|
except Exception as e: |
||||
|
self.log("第%d行上传异常: %s, 原因: %s" % (row["row_index"], file_path.name, e)) |
||||
|
return False |
||||
|
|
||||
|
def _log_with_interval(self, key, message, seconds): |
||||
|
now = time.time() |
||||
|
last = self.notice_ts.get(key, 0) |
||||
|
if now - last >= seconds: |
||||
|
self.notice_ts[key] = now |
||||
|
self.log(message) |
||||
|
|
||||
|
def log(self, message): |
||||
|
self.log_queue.put("[%s] %s" % (datetime.now().strftime("%H:%M:%S"), message)) |
||||
|
|
||||
|
def _flush_logs(self): |
||||
|
while not self.log_queue.empty(): |
||||
|
line = self.log_queue.get() |
||||
|
self.log_text.configure(state=NORMAL) |
||||
|
self.log_text.insert(END, line + "\n") |
||||
|
self.log_text.see(END) |
||||
|
self.log_text.configure(state=DISABLED) |
||||
|
self.root.after(200, self._flush_logs) |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
app_root = Tk() |
||||
|
app = CollectorApp(app_root) |
||||
|
app_root.mainloop() |
||||
@ -0,0 +1,2 @@ |
|||||
|
requests |
||||
|
pyinstaller |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue