Files

94 lines
4.6 KiB
Python
Raw Permalink Normal View History

"""Refresh reviewed sources atomically without resetting saved options or history."""
import argparse
import copy
import json
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from console import catalog, native_plans
from console.settings import Settings
from console.store import Store
def refresh(store, backup, apply=False):
original = {s['id']: s for s in store.scripts()}
class Draft:
settings = store.settings
items = copy.deepcopy(original)
def scripts(self): return list(self.items.values())
def save_script(self, item): self.items[item['id']] = item
draft = Draft()
scan = catalog.discover(draft)
for item in draft.items.values():
profile = native_plans.profile_for(store.settings, item)
if not profile:
continue
if item['id'] not in original:
item['config'] = native_plans.validate_config(store.settings, item,
{'native_options': {}, 'timeout_seconds': profile.get('timeout_seconds', 3600)})
item['enabled'] = profile.get('available', True)
if not profile.get('available', True):
item.update(enabled=False, blocked=True, reason=profile.get('unavailable_reason', '原环境待配置'))
if item['enabled']:
item.update(status='ready', reason='')
elif item['status'] != 'missing':
item['status'] = 'needs_configuration'
changes = []
for identity, value in draft.items.items():
before = original.get(identity)
if before and {k:v for k,v in before.items() if k != 'updated_at'} == {k:v for k,v in value.items() if k != 'updated_at'}:
continue
changes.append((identity, before, value))
if not apply:
return {'scan': scan, 'changed': len(changes), 'applied': False}
with store.connect(write=True) as db:
if db.execute("SELECT 1 FROM runs WHERE state IN ('queued','running')").fetchone():
raise ValueError('请先等待排队和运行任务结束')
records = []
for identity, before, after in changes:
row = db.execute('SELECT data FROM scripts WHERE id=?', (identity,)).fetchone()
if (json.loads(row[0]) if row else None) != before:
raise ValueError('配置有并发修改,本次刷新未写入')
records.append({'id': identity, 'before': row[0] if row else None, 'after': json.dumps(after)})
if not records:
return {'scan': scan, 'changed': 0, 'applied': True}
backup.parent.mkdir(parents=True, exist_ok=True)
with backup.open('x', encoding='utf-8') as handle:
json.dump({'scripts': records}, handle, ensure_ascii=False, indent=2)
backup.chmod(0o600)
for record in records:
db.execute('INSERT OR REPLACE INTO scripts VALUES(?,?)', (record['id'], record['after']))
return {'scan': scan, 'changed': len(changes), 'applied': True}
def restore(store, backup):
records = json.loads(backup.read_text(encoding='utf-8'))['scripts']
with store.connect(write=True) as db:
if db.execute("SELECT 1 FROM runs WHERE state IN ('queued','running')").fetchone():
raise ValueError('请先等待排队和运行任务结束')
for record in records:
row = db.execute('SELECT data FROM scripts WHERE id=?', (record['id'],)).fetchone()
current = row[0] if row else None
if current not in (record['before'], record['after']):
raise ValueError('刷新后配置已修改,请人工核对,禁止覆盖')
if record['before'] is None and db.execute('SELECT 1 FROM runs WHERE script_id=?', (record['id'],)).fetchone():
raise ValueError('新增项目已有运行历史,不能删除登记,请人工恢复')
for record in records:
if record['before'] is None:
db.execute('DELETE FROM scripts WHERE id=?', (record['id'],))
else:
db.execute('UPDATE scripts SET data=? WHERE id=?', (record['before'], record['id']))
return {'restored': len(records), 'history_preserved': True}
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--apply', action='store_true')
parser.add_argument('--restore', action='store_true')
parser.add_argument('--backup', type=Path, required=True)
args = parser.parse_args()
if args.apply and args.restore: parser.error('选择刷新或恢复之一')
store = Store(Settings.from_env())
print(json.dumps(restore(store, args.backup) if args.restore else refresh(store, args.backup, args.apply)))