commit b47ca80edae593287b76765941a73ae3ee485a54 Author: yumoqing Date: Wed Aug 26 15:28:18 2026 +0800 feat: filesync 模块 — 从 FOMS 单体拆分出的独立模块 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d674a41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +py3/ +models/mysql.ddl.sql +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d5d763 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# filesync — 运行文件同步模块 + +FOMS 平台拆出的独立模块之一。松耦合:表 `cluster_id` 仅存字符串 ID,不建外键, +rsync 执行由 Agent 完成,管理中心只存配置、收结果。 + +| 功能 | 表 | 说明 | +|---|---|---| +| 文件目录同步 | `filesync_directories` | rsync 同步配置,原 `sync_directories` | + +端点: +- `/filesync/api/agent_report_dir_health.dspy` — Agent 上报 rsync 结果 + +加载: +```python +from filesync.init import load_filesync +load_filesync() +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c755674 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .init import load_filesync # noqa: F401 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..6278aa0 --- /dev/null +++ b/build.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# filesync 模块 build:生成 DDL + CRUD 页面(xls2ui) +set -e +cdir=$(pwd) + +if [ -d models ]; then + json2ddl mysql models/ > "$cdir/models/mysql.ddl.sql" 2>/dev/null || echo " skip ddl" +fi + +if [ -d json ]; then + cd "$cdir/json" + for f in *.json; do + echo " xls2ui $f" + xls2ui -m ../models -o ../wwwroot filesync "$f" 2>/dev/null || echo " skipped" + done +fi + +echo "=== filesync Build Complete ===" diff --git a/filesync/__init__.py b/filesync/__init__.py new file mode 100644 index 0000000..bfa96a9 --- /dev/null +++ b/filesync/__init__.py @@ -0,0 +1 @@ +# filesync 模块 — 运行文件同步管理 diff --git a/filesync/init.py b/filesync/init.py new file mode 100644 index 0000000..78f3926 --- /dev/null +++ b/filesync/init.py @@ -0,0 +1,97 @@ +"""filesync 模块 — 运行文件同步(rsync 目录同步)管理。 + +独立模块,松耦合设计: + · 表 cluster_id 仅存字符串 ID,不建外键(引用 foms 核心的 clusters 表)。 + · 不 import foms 核心代码,可独立部署、独立测试。 + · rsync 实际执行由 Agent 在被管主机完成,管理中心只存配置、收结果。 + +表: + filesync_directories 文件目录同步配置(原 sync_directories) +""" +import json +from appPublic.uniqueID import getID +from appPublic.timeUtils import curDateString +from sqlor.dbpools import DBPools, get_sor_context +from ahserver.serverenv import ServerEnv + +DBNAME = 'foms' + + +def _ns(params_kw): + ns = dict(params_kw) if hasattr(params_kw, 'items') else {} + for k in list(ns.keys()): + if k.endswith('_text'): + ns.pop(k) + return ns + + +def _get(params_kw, key, default=''): + return params_kw.get(key, default) if hasattr(params_kw, 'get') else default + + +# ═══════════════════════════════════════════ +# 目录同步 CRUD +# ═══════════════════════════════════════════ + +async def create_directory(request, params_kw): + env = ServerEnv() + async with get_sor_context(env, DBNAME) as sor: + ns = _ns(params_kw) + ns['id'] = getID() + ns['created_at'] = curDateString() + ns['updated_at'] = curDateString() + await sor.C('filesync_directories', ns) + return {'widgettype': 'Message', 'options': {'title': '成功', 'message': '同步配置创建成功', 'type': 'success'}} + + +async def update_directory(request, params_kw): + env = ServerEnv() + async with get_sor_context(env, DBNAME) as sor: + ns = _ns(params_kw) + ns['updated_at'] = curDateString() + await sor.U('filesync_directories', ns) + return {'widgettype': 'Message', 'options': {'title': '成功', 'message': '同步配置更新成功', 'type': 'success'}} + + +async def delete_directory(request, params_kw): + env = ServerEnv() + async with get_sor_context(env, DBNAME) as sor: + await sor.D('filesync_directories', {'id': _get(params_kw, 'id')}) + return {'widgettype': 'Message', 'options': {'title': '成功', 'message': '同步配置已删除', 'type': 'success'}} + + +# ═══════════════════════════════════════════ +# Agent 上报:同步结果(原 agent_report_health 的 dir 分支) +# ═══════════════════════════════════════════ + +async def agent_report_dir_health(request, params_kw): + """Agent 上报 rsync 同步结果""" + env = ServerEnv() + dir_statuses = _get(params_kw, 'dir_statuses', []) + if isinstance(dir_statuses, str): + dir_statuses = json.loads(dir_statuses) + async with get_sor_context(env, DBNAME) as sor: + for dir_stat in dir_statuses: + await sor.U('filesync_directories', { + 'id': dir_stat.get('id'), + 'last_sync_at': curDateString(), + 'last_sync_result': dir_stat.get('result'), + 'updated_at': curDateString(), + }) + return {'status': 'ok'} + + +# ═══════════════════════════════════════════ +# 注册 +# ═══════════════════════════════════════════ + +def load_filesync(): + env = ServerEnv() + env.create_directory = create_directory + env.create_filesync_directories = create_directory + env.update_directory = update_directory + env.update_filesync_directories = update_directory + env.delete_directory = delete_directory + env.delete_filesync_directories = delete_directory + env.agent_report_dir_health = agent_report_dir_health + return True diff --git a/json/filesync_directories_list.json b/json/filesync_directories_list.json new file mode 100644 index 0000000..22a9c55 --- /dev/null +++ b/json/filesync_directories_list.json @@ -0,0 +1,16 @@ +{ + "tblname": "filesync_directories", + "alias": "filesync_directories_list", + "title": "文件目录同步配置", + "params": { + "sortby": ["updated_at desc"], + "browserfields": { + "exclouded": ["id"], + "alters": { + "last_sync_result": {"uitype": "code"}, + "enabled": {"uitype": "code", "data": [{"value": "1", "text": "启用"}, {"value": "0", "text": "禁用"}]}, + "cluster_id": {"uitype": "code", "valueField": "id", "textField": "name"} + } + } + } +} diff --git a/models/filesync_directories.json b/models/filesync_directories.json new file mode 100644 index 0000000..e68c065 --- /dev/null +++ b/models/filesync_directories.json @@ -0,0 +1,33 @@ +{ + "summary": [ + { + "name": "filesync_directories", + "title": "文件目录同步配置", + "primary": ["id"], + "catelog": "entity" + } + ], + "fields": [ + {"name": "id", "title": "配置ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "cluster_id", "title": "所属集群", "type": "str", "length": 32, "nullable": "no"}, + {"name": "name", "title": "同步任务名称", "type": "str", "length": 128, "nullable": "no"}, + {"name": "source_host", "title": "源主机", "type": "str", "length": 255}, + {"name": "source_path", "title": "源目录路径", "type": "str", "length": 512, "nullable": "no"}, + {"name": "dest_host", "title": "目标主机", "type": "str", "length": 255}, + {"name": "dest_path", "title": "目标目录路径", "type": "str", "length": 512, "nullable": "no"}, + {"name": "sync_interval", "title": "同步间隔(秒)", "type": "int", "nullable": "no", "default": "60"}, + {"name": "exclude_patterns", "title": "排除模式", "type": "str", "length": 512}, + {"name": "last_sync_at", "title": "上次同步时间", "type": "datetime"}, + {"name": "last_sync_result", "title": "上次同步结果", "type": "str", "length": 32}, + {"name": "enabled", "title": "启用", "type": "str", "length": 1, "nullable": "no", "default": "1"}, + {"name": "created_at", "title": "创建时间", "type": "datetime"}, + {"name": "updated_at", "title": "更新时间", "type": "datetime"} + ], + "indexes": [ + {"name": "idx_filesync_dir_cluster", "idxtype": "index", "idxfields": ["cluster_id"]} + ], + "codes": [ + {"field": "last_sync_result", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='sync_result'"}, + {"field": "cluster_id", "table": "clusters", "valuefield": "id", "textfield": "name"} + ] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..037ec5d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "filesync" +version = "1.0.0" +description = "运行文件同步模块 — rsync 目录同步配置与状态管理" +requires-python = ">=3.8" +dependencies = [ + "sqlor", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["filesync*"] diff --git a/wwwroot/api/agent_report_dir_health.dspy b/wwwroot/api/agent_report_dir_health.dspy new file mode 100644 index 0000000..7ec0e30 --- /dev/null +++ b/wwwroot/api/agent_report_dir_health.dspy @@ -0,0 +1,3 @@ +# Agent 上报 rsync 同步结果 +result = await agent_report_dir_health(request, params_kw) +return json.dumps(result, ensure_ascii=False)