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:
89
sync.py
Normal file
89
sync.py
Normal 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
|
||||
Reference in New Issue
Block a user