Browse Source

feat: full backup endpoint + manual trigger button in settings

nonvioce 3 months ago
parent
commit
e68491f925

+ 46 - 0
tiku-client/lib/pages/settings/settings_page.dart

@@ -68,6 +68,24 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
     _webdavConfigured = false; _showWebdavForm = false;
   }
 
+  Future<void> _triggerFullBackup() async {
+    final api = ref.read(apiServiceProvider);
+    try {
+      await api.triggerFullBackup();
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          const SnackBar(content: Text('全量备份已触发')),
+        );
+      }
+    } catch (e) {
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('备份失败: $e')),
+        );
+      }
+    }
+  }
+
   Future<void> _saveWebDAV() async {
     final api = ref.read(apiServiceProvider);
     try {
@@ -239,6 +257,34 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
             ),
           ],
 
+          // 全量备份
+          if (isConfigured) ...[
+            const SizedBox(height: 16),
+            Card(
+              child: Padding(
+                padding: const EdgeInsets.all(16),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Text('数据备份', style: Theme.of(context).textTheme.titleMedium),
+                    const SizedBox(height: 8),
+                    Text('备份 bank.db + 全部 docx 到本地和 WebDAV',
+                        style: Theme.of(context).textTheme.bodySmall),
+                    const SizedBox(height: 12),
+                    SizedBox(
+                      width: double.infinity,
+                      child: OutlinedButton.icon(
+                        onPressed: _triggerFullBackup,
+                        icon: const Icon(Icons.backup),
+                        label: const Text('全量备份'),
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ],
+
           const SizedBox(height: 24),
           // 关于
           Card(

+ 4 - 0
tiku-client/lib/services/api_service.dart

@@ -139,6 +139,10 @@ class ApiService {
         options: _authOptions);
   }
 
+  Future<void> triggerFullBackup() async {
+    await _dio.post('/api/admin/full-backup', options: _authOptions);
+  }
+
   // ── 题目详情 ──────────────────────────────────────
 
   Future<Map<String, dynamic>> problemDetail(String id) async {

+ 15 - 1
tiku-server/app/routers/admin.py

@@ -37,7 +37,7 @@ def set_backup_config(request: Request, body: dict, _=Depends(verify_token)):
 
 @router.post("/admin/backup/trigger")
 def trigger_backup(request: Request, _=Depends(verify_token)):
-    """手动触发备份"""
+    """手动触发增量备份"""
     config = request.app.state.config
     try:
         auto_backup(config)
@@ -48,6 +48,20 @@ def trigger_backup(request: Request, _=Depends(verify_token)):
         })
 
 
+@router.post("/admin/full-backup")
+def trigger_full_backup(request: Request, _=Depends(verify_token)):
+    """手动触发全量备份(bank.db + 全部 docx)"""
+    config = request.app.state.config
+    try:
+        from app.services.backup import full_backup
+        backup_dir = full_backup(config)
+        return {"ok": True, "path": str(backup_dir.name)}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={
+            "error": {"code": "full_backup_failed", "message": str(e)}
+        })
+
+
 @router.get("/admin/backup/history")
 def backup_history(request: Request, _=Depends(verify_token)):
     """备份历史"""

+ 30 - 0
tiku-server/app/services/backup.py

@@ -8,6 +8,36 @@ from pathlib import Path
 from app.database import get_connection
 
 
+def full_backup(config) -> Path:
+    """全量备份:bank.db + 全部 docx 文件"""
+    import shutil
+    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
+    backup_dir = config.backups_dir / ts
+    backup_dir.mkdir(parents=True, exist_ok=True)
+
+    # bank.db
+    if config.db_path.exists():
+        shutil.copy2(config.db_path, backup_dir / "bank.db")
+
+    # 全部 docx
+    if config.problems_dir.exists():
+        dst_dir = backup_dir / "problems"
+        dst_dir.mkdir(parents=True, exist_ok=True)
+        for docx in config.problems_dir.glob("*.docx"):
+            shutil.copy2(docx, dst_dir / docx.name)
+
+    # 推送到 WebDAV
+    if config.webdav_enabled:
+        try:
+            _push_to_webdav(backup_dir, config)
+        except Exception as e:
+            print(f"⚠ WebDAV 推送失败: {e}")
+
+    _cleanup_old_backups(config)
+    print(f"✓ 全量备份: {backup_dir}")
+    return backup_dir
+
+
 def manual_backup(config, remote: bool = False):
     """手动备份"""
     backup_dir = _create_backup(config)