feat: .tsb backup import/restore functionality
This commit is contained in:
@@ -369,6 +369,37 @@ async def api_export_backup():
|
|||||||
headers={"Content-Disposition": "attachment; filename=turbosu_backup.tsb"})
|
headers={"Content-Disposition": "attachment; filename=turbosu_backup.tsb"})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/backup/import")
|
||||||
|
async def api_import_backup(file: UploadFile = File(...)):
|
||||||
|
if not file.filename or not file.filename.endswith('.tsb'):
|
||||||
|
raise HTTPException(400, "Only .tsb files are accepted")
|
||||||
|
|
||||||
|
import io, zipfile, shutil
|
||||||
|
zip_data = await file.read()
|
||||||
|
base = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf:
|
||||||
|
for name in zf.namelist():
|
||||||
|
if name.endswith('/') or '__pycache__' in name:
|
||||||
|
continue
|
||||||
|
dest = base / name
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest.write_bytes(zf.read(name))
|
||||||
|
|
||||||
|
from config.settings import reload_config
|
||||||
|
reload_config()
|
||||||
|
from models.dashboard import dashboard_manager
|
||||||
|
import importlib
|
||||||
|
importlib.reload(__import__('models.dashboard', fromlist=['dashboard_manager']))
|
||||||
|
from server.game_manager import game_plugin_manager
|
||||||
|
game_plugin_manager.reload()
|
||||||
|
|
||||||
|
return {"ok": True, "message": "备份已恢复,请重启服务"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
# ---- Data Forwarding ----
|
# ---- Data Forwarding ----
|
||||||
@router.get("/forward")
|
@router.get("/forward")
|
||||||
async def api_list_forward_targets():
|
async def api_list_forward_targets():
|
||||||
|
|||||||
@@ -100,6 +100,17 @@ const PageSettings = {
|
|||||||
a.click();
|
a.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-import-backup')?.addEventListener('click', () => {
|
||||||
|
const input = document.createElement('input'); input.type = 'file'; input.accept = '.tsb';
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = e.target.files[0]; if (!file) return;
|
||||||
|
const fd = new FormData(); fd.append('file', file);
|
||||||
|
const res = await fetch('/api/backup/import', { method: 'POST', body: fd });
|
||||||
|
const data = await res.json();
|
||||||
|
Toast.show(data.ok ? data.message || '恢复成功' : '恢复失败: ' + data.message, data.ok ? 'success' : 'error');
|
||||||
|
}; input.click();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('settings-test-mode')?.addEventListener('change', async (e) => {
|
document.getElementById('settings-test-mode')?.addEventListener('change', async (e) => {
|
||||||
const on = e.target.checked;
|
const on = e.target.checked;
|
||||||
await API.updateConfig({ ui_test_mode: on });
|
await API.updateConfig({ ui_test_mode: on });
|
||||||
@@ -173,7 +184,7 @@ const PageSettings = {
|
|||||||
|
|
||||||
'<div class="settings-section"><h3>游戏插件管理</h3><div style="display:flex;gap:8px;margin-bottom:16px"><button id="btn-import-plugin" class="btn btn-secondary btn-sm">导入 .tsp</button><button id="btn-reload-plugins" class="btn btn-secondary btn-sm">重新加载</button></div><div id="settings-game-list">' + this._gamePluginListHtml(games) + '</div></div>' +
|
'<div class="settings-section"><h3>游戏插件管理</h3><div style="display:flex;gap:8px;margin-bottom:16px"><button id="btn-import-plugin" class="btn btn-secondary btn-sm">导入 .tsp</button><button id="btn-reload-plugins" class="btn btn-secondary btn-sm">重新加载</button></div><div id="settings-game-list">' + this._gamePluginListHtml(games) + '</div></div>' +
|
||||||
|
|
||||||
'<div class="settings-section"><h3>数据备份</h3><div class="settings-row"><div><div class="settings-label">导出全量备份</div><div class="settings-desc">打包仪表盘+场景+配置为 .tsb 文件</div></div><div class="settings-control"><button id="btn-export-backup" class="btn btn-secondary btn-sm">下载 .tsb</button></div></div></div>' +
|
'<div class="settings-section"><h3>数据备份</h3><div class="settings-row"><div><div class="settings-label">导出全量备份</div><div class="settings-desc">打包仪表盘+场景+配置为 .tsb</div></div><div class="settings-control"><button id="btn-export-backup" class="btn btn-secondary btn-sm">下载 .tsb</button></div></div><div class="settings-row"><div><div class="settings-label">恢复备份</div><div class="settings-desc">从 .tsb 文件恢复,需重启</div></div><div class="settings-control"><button id="btn-import-backup" class="btn btn-secondary btn-sm">导入 .tsb</button></div></div></div>' +
|
||||||
|
|
||||||
'<div class="settings-section"><h3>开发工具</h3><div class="settings-row"><div><div class="settings-label">UI 测试模式</div><div class="settings-desc">本地随机模拟遥测数据</div></div><div class="settings-control"><label class="switch-label"><input type="checkbox" id="settings-test-mode" ' + (cfg.ui_test_mode?'checked':'') + '><span class="switch-track"></span></label></div></div></div>' +
|
'<div class="settings-section"><h3>开发工具</h3><div class="settings-row"><div><div class="settings-label">UI 测试模式</div><div class="settings-desc">本地随机模拟遥测数据</div></div><div class="settings-control"><label class="switch-label"><input type="checkbox" id="settings-test-mode" ' + (cfg.ui_test_mode?'checked':'') + '><span class="switch-track"></span></label></div></div></div>' +
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user