feat: .tsb backup import/restore functionality

This commit is contained in:
2026-07-26 18:31:04 +08:00
parent 02ffa33298
commit 2daa4d48f1
2 changed files with 43 additions and 1 deletions
+31
View File
@@ -369,6 +369,37 @@ async def api_export_backup():
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 ----
@router.get("/forward")
async def api_list_forward_targets():
+12 -1
View File
@@ -100,6 +100,17 @@ const PageSettings = {
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) => {
const on = e.target.checked;
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 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>' +