feat: add scheduled sync daemon and TRUNCATE migration mode

- Add sync.py for LAN file sync with atomic writes and mtime-based change detection
- Add watch.py daemon that periodically syncs files and migrates only changed tables
- Add TRUNCATE mode to migrate.py (--truncate flag) to preserve table structure
- Update config.yaml schema with check_interval_minutes and lan_sources
- Update README with new features documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-06-01 13:29:23 +08:00
parent 07ab64bf38
commit 7edbd987f0
5 changed files with 390 additions and 10 deletions

89
sync.py Normal file
View File

@@ -0,0 +1,89 @@
"""
LAN file sync module.
Detects file changes, copies from LAN UNC paths to local, and determines which tables need migration.
"""
import os
import shutil
def sync_file(local_path, lan_path):
"""Sync a single file from LAN to local.
Returns:
(status, error_msg)
status: 'copied' | 'updated' | 'skipped' | 'error'
"""
try:
if not os.path.exists(lan_path):
return 'error', f'LAN path not accessible: {lan_path}'
lan_mtime = os.path.getmtime(lan_path)
local_existed = os.path.exists(local_path)
if local_existed:
local_mtime = os.path.getmtime(local_path)
if local_mtime == lan_mtime:
return 'skipped', None
# Ensure parent directory exists
os.makedirs(os.path.dirname(local_path), exist_ok=True)
# Atomic copy: write to .tmp first, then os.replace
tmp_path = local_path + '.tmp'
try:
shutil.copy2(lan_path, tmp_path)
os.replace(tmp_path, local_path)
except Exception:
# Clean up temp file on failure
if os.path.exists(tmp_path):
os.remove(tmp_path)
raise
if not local_existed:
return 'copied', None
else:
return 'updated', None
except Exception as e:
return 'error', str(e)
def sync_all_files(cfg, excel_dir):
"""Sync all LAN source files to local.
Returns:
{rel_path: (status, error_msg)}
"""
lan_sources = cfg.get('lan_sources', {})
results = {}
for rel_path, lan_path in lan_sources.items():
local_path = os.path.join(excel_dir, rel_path)
status, error_msg = sync_file(local_path, lan_path)
results[rel_path] = (status, error_msg)
return results
def determine_tables_to_migrate(cfg, sync_results):
"""Determine which tables need migration based on sync results.
A table needs migration if ANY of its source files were copied or updated.
Returns:
set of table names that need migration
"""
tables_to_migrate = set()
for table_cfg in cfg.get('tables', []):
table_name = table_cfg['table']
for src in table_cfg['sources']:
file_rel = src['file']
if file_rel in sync_results:
status, _ = sync_results[file_rel]
if status in ('copied', 'updated'):
tables_to_migrate.add(table_name)
break
return tables_to_migrate