lockbot 2.9.2__tar.gz → 2.9.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {lockbot-2.9.2/python/lockbot.egg-info → lockbot-2.9.4}/PKG-INFO +17 -2
  2. {lockbot-2.9.2 → lockbot-2.9.4}/README.md +16 -1
  3. {lockbot-2.9.2 → lockbot-2.9.4}/pyproject.toml +1 -1
  4. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/admin/router.py +87 -63
  5. lockbot-2.9.4/python/lockbot/backend/app/backup/service.py +618 -0
  6. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/models.py +1 -0
  7. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/router.py +2 -0
  8. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/schemas.py +1 -0
  9. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/main.py +26 -0
  10. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/base_bot.py +9 -3
  11. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/i18n/en.py +2 -0
  12. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/i18n/zh.py +2 -0
  13. {lockbot-2.9.2 → lockbot-2.9.4/python/lockbot.egg-info}/PKG-INFO +17 -2
  14. lockbot-2.9.2/python/lockbot/backend/app/backup/service.py +0 -191
  15. {lockbot-2.9.2 → lockbot-2.9.4}/LICENSE +0 -0
  16. {lockbot-2.9.2 → lockbot-2.9.4}/MANIFEST.in +0 -0
  17. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/__init__.py +0 -0
  18. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/__init__.py +0 -0
  19. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/__init__.py +0 -0
  20. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/admin/__init__.py +0 -0
  21. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/audit/__init__.py +0 -0
  22. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/audit/models.py +0 -0
  23. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/audit/router.py +0 -0
  24. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/audit/service.py +0 -0
  25. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/__init__.py +0 -0
  26. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/dependencies.py +0 -0
  27. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/models.py +0 -0
  28. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/router.py +0 -0
  29. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/schemas.py +0 -0
  30. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/backup/__init__.py +0 -0
  31. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/backup/router.py +0 -0
  32. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/backup/scheduler.py +0 -0
  33. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/__init__.py +0 -0
  34. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/encryption.py +0 -0
  35. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/manager.py +0 -0
  36. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/webhook_handler.py +0 -0
  37. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/config.py +0 -0
  38. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/database.py +0 -0
  39. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/logs/__init__.py +0 -0
  40. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/rate_limit.py +0 -0
  41. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/settings/__init__.py +0 -0
  42. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/settings/models.py +0 -0
  43. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/settings/router.py +0 -0
  44. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/__init__.py +0 -0
  45. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/bot_instance.py +0 -0
  46. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/config.py +0 -0
  47. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/device_bot.py +0 -0
  48. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/device_usage_alert.py +0 -0
  49. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/device_usage_utils.py +0 -0
  50. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/entry.py +0 -0
  51. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/env.py +0 -0
  52. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/handler.py +0 -0
  53. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/i18n/__init__.py +0 -0
  54. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/io.py +0 -0
  55. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/message_adapter.py +0 -0
  56. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/msg_utils.py +0 -0
  57. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/node_bot.py +0 -0
  58. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/platforms/__init__.py +0 -0
  59. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/platforms/infoflow.py +0 -0
  60. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/queue_bot.py +0 -0
  61. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/request.py +0 -0
  62. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/scheduler.py +0 -0
  63. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/utils.py +0 -0
  64. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot.egg-info/SOURCES.txt +0 -0
  65. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot.egg-info/dependency_links.txt +0 -0
  66. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot.egg-info/requires.txt +0 -0
  67. {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot.egg-info/top_level.txt +0 -0
  68. {lockbot-2.9.2 → lockbot-2.9.4}/setup.cfg +0 -0
  69. {lockbot-2.9.2 → lockbot-2.9.4}/tools/create_super_admin.py +0 -0
  70. {lockbot-2.9.2 → lockbot-2.9.4}/tools/gen_keys.py +0 -0
  71. {lockbot-2.9.2 → lockbot-2.9.4}/tools/reset_super_admin_password.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockbot
3
- Version: 2.9.2
3
+ Version: 2.9.4
4
4
  Summary: Cluster resource management bot for IM platforms
5
5
  Author-email: Jianbang Yang <yangjianbang112@gmail.com>
6
6
  License: MIT
@@ -150,10 +150,25 @@ See `python/lockbot/core/config.py` for the full configuration reference.
150
150
 
151
151
  ## Development
152
152
 
153
+ ### Dev Server
154
+
153
155
  ```bash
154
- # Install dev dependencies
156
+ # Install dependencies
155
157
  pip install -e ".[dev]"
158
+ cd frontend && npm install && cd ..
159
+
160
+ # Start backend (port 8002, hot-reload)
161
+ bash scripts/dev/backend.sh
162
+
163
+ # Start frontend (port 8001, proxies /api/* -> localhost:8002)
164
+ bash scripts/dev/frontend.sh
165
+ ```
156
166
 
167
+ Both servers support hot-reload — code changes take effect automatically.
168
+
169
+ ### Testing
170
+
171
+ ```bash
157
172
  # Run tests
158
173
  pytest
159
174
 
@@ -107,10 +107,25 @@ See `python/lockbot/core/config.py` for the full configuration reference.
107
107
 
108
108
  ## Development
109
109
 
110
+ ### Dev Server
111
+
110
112
  ```bash
111
- # Install dev dependencies
113
+ # Install dependencies
112
114
  pip install -e ".[dev]"
115
+ cd frontend && npm install && cd ..
116
+
117
+ # Start backend (port 8002, hot-reload)
118
+ bash scripts/dev/backend.sh
119
+
120
+ # Start frontend (port 8001, proxies /api/* -> localhost:8002)
121
+ bash scripts/dev/frontend.sh
122
+ ```
113
123
 
124
+ Both servers support hot-reload — code changes take effect automatically.
125
+
126
+ ### Testing
127
+
128
+ ```bash
114
129
  # Run tests
115
130
  pytest
116
131
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lockbot"
7
- version = "2.9.2"
7
+ version = "2.9.4"
8
8
  description = "Cluster resource management bot for IM platforms"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -3,16 +3,10 @@ Admin API routes.
3
3
  """
4
4
 
5
5
  import contextlib
6
- import io
7
- import json
8
6
  import os
9
- import shutil
10
- import tempfile
11
- import zipfile
12
- from datetime import datetime
13
7
 
14
- from fastapi import APIRouter, Depends, HTTPException, Request
15
- from fastapi.responses import FileResponse, Response
8
+ from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
9
+ from fastapi.responses import FileResponse
16
10
  from sqlalchemy.orm import Session
17
11
  from starlette.background import BackgroundTask
18
12
 
@@ -300,78 +294,108 @@ def download_backup(
300
294
  _admin: User = Depends(require_super_admin),
301
295
  db: Session = Depends(get_db),
302
296
  ):
303
- """Download full SQLite database backup (super_admin only)."""
304
- from lockbot.backend.app.config import DATABASE_URL
297
+ """Download full backup archive (DB + bot states + manifest) as zip (super_admin only)."""
298
+ from lockbot.backend.app.backup.service import create_backup_archive
305
299
 
306
- db_url = DATABASE_URL
307
- if not db_url.startswith("sqlite:///"):
308
- raise HTTPException(status_code=400, detail="Backup only supported for SQLite")
309
-
310
- db_path = db_url[len("sqlite:///") :]
311
- if not os.path.exists(db_path):
312
- raise HTTPException(status_code=404, detail="Database file not found")
313
-
314
- # Copy to temp file to avoid sending a mid-write database
315
- fd, tmp_path = tempfile.mkstemp(suffix=".db")
316
- os.close(fd)
317
300
  try:
318
- shutil.copy2(db_path, tmp_path)
319
- except BaseException:
320
- with contextlib.suppress(OSError):
321
- os.unlink(tmp_path)
322
- raise
301
+ zip_path = create_backup_archive(password=None)
302
+ except RuntimeError as e:
303
+ raise HTTPException(status_code=400, detail=str(e)) from None
323
304
 
324
305
  write_audit_log(db, _admin, "admin.backup", ip=request.client.host if request.client else None)
325
306
  db.commit()
326
307
 
327
- filename = f"lockbot_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db"
308
+ filename = zip_path.name
328
309
  return FileResponse(
329
- path=tmp_path,
310
+ path=str(zip_path),
330
311
  filename=filename,
331
- media_type="application/x-sqlite3",
332
- background=BackgroundTask(os.unlink, tmp_path),
312
+ media_type="application/zip",
313
+ background=BackgroundTask(_cleanup_backup_tmp, zip_path),
333
314
  )
334
315
 
335
316
 
336
- _DEFAULT_DATA_DIR = os.environ.get("DATA_DIR", "/data")
317
+ def _cleanup_backup_tmp(zip_path):
337
318
 
319
+ with contextlib.suppress(OSError):
320
+ zip_path.unlink(missing_ok=True)
321
+ with contextlib.suppress(OSError):
322
+ zip_path.parent.rmdir()
323
+
324
+
325
+ @router.post("/backup/restore/preview")
326
+ async def preview_restore_backup(
327
+ file: UploadFile = File(...),
328
+ password: str | None = Form(default=None),
329
+ source_secret_key: str | None = Form(default=None),
330
+ _admin: User = Depends(require_super_admin),
331
+ db: Session = Depends(get_db),
332
+ ):
333
+ """Preview restoring bots from a unified backup zip (super_admin only)."""
334
+ from lockbot.backend.app.backup.service import restore_preview
338
335
 
339
- @router.get("/bot-states")
340
- def download_all_bot_states(
336
+ content = await file.read()
337
+ if not content:
338
+ raise HTTPException(status_code=400, detail="Backup file is empty")
339
+ try:
340
+ return restore_preview(content, password or None, source_secret_key or None, _admin.id, db)
341
+ except ValueError as e:
342
+ err = str(e)
343
+ if err == "BACKUP_PASSWORD_REQUIRED":
344
+ raise HTTPException(status_code=422, detail="BACKUP_PASSWORD_REQUIRED") from None
345
+ if err == "BACKUP_PASSWORD_WRONG":
346
+ raise HTTPException(status_code=422, detail="BACKUP_PASSWORD_WRONG") from None
347
+ raise HTTPException(status_code=400, detail=err) from None
348
+
349
+
350
+ @router.post("/backup/restore/apply")
351
+ async def apply_restore_backup(
341
352
  request: Request,
353
+ file: UploadFile = File(...),
354
+ password: str | None = Form(default=None),
355
+ source_secret_key: str | None = Form(default=None),
356
+ selected_uuids: str = Form(...),
342
357
  _admin: User = Depends(require_super_admin),
343
358
  db: Session = Depends(get_db),
344
359
  ):
345
- """Download all bot state files as a zip archive (super_admin only)."""
346
- bots = db.query(Bot).filter(Bot.is_deleted.is_(False)).all()
360
+ """Apply restore for selected bots from a backup zip (super_admin only).
347
361
 
348
- buf = io.BytesIO()
349
- with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
350
- for bot in bots:
351
- # Determine data dir from config_overrides or default
352
- data_dir = _DEFAULT_DATA_DIR
353
- try:
354
- overrides = json.loads(bot.config_overrides or "{}")
355
- data_dir = overrides.get("DATA_DIR") or _DEFAULT_DATA_DIR
356
- except (json.JSONDecodeError, AttributeError):
357
- pass
358
- state_file = os.path.join(data_dir, "bots", str(bot.id), "bot_state.json")
359
- folder_name = f"bot_{bot.id}_{bot.name}"
360
- if os.path.exists(state_file):
361
- zf.write(state_file, f"{folder_name}/bot_state.json")
362
- else:
363
- # Create an empty placeholder so the admin knows the bot has no state
364
- zf.writestr(f"{folder_name}/bot_state.json", "{}")
365
-
366
- buf.seek(0)
367
- content = buf.read()
368
-
369
- write_audit_log(db, _admin, "admin.bot_states", ip=request.client.host if request.client else None)
370
- db.commit()
362
+ selected_uuids: JSON array of bot UUIDs to restore, e.g. '["uuid1","uuid2"]'
363
+ """
364
+ import json as _json
371
365
 
372
- filename = f"lockbot_states_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
373
- return Response(
374
- content=content,
375
- media_type="application/zip",
376
- headers={"Content-Disposition": f'attachment; filename="{filename}"'},
366
+ from lockbot.backend.app.backup.service import restore_apply
367
+
368
+ content = await file.read()
369
+ if not content:
370
+ raise HTTPException(status_code=400, detail="Backup file is empty")
371
+
372
+ try:
373
+ uuids = _json.loads(selected_uuids)
374
+ except (ValueError, TypeError):
375
+ raise HTTPException(status_code=422, detail="selected_uuids must be a JSON array") from None
376
+
377
+ if not isinstance(uuids, list) or not uuids:
378
+ raise HTTPException(status_code=422, detail="selected_uuids must be a non-empty array")
379
+
380
+ try:
381
+ result = restore_apply(content, password or None, source_secret_key or None, uuids, _admin.id, db)
382
+ except ValueError as e:
383
+ err = str(e)
384
+ if err == "BACKUP_PASSWORD_REQUIRED":
385
+ raise HTTPException(status_code=422, detail="BACKUP_PASSWORD_REQUIRED") from None
386
+ if err == "BACKUP_PASSWORD_WRONG":
387
+ raise HTTPException(status_code=422, detail="BACKUP_PASSWORD_WRONG") from None
388
+ raise HTTPException(status_code=400, detail=err) from None
389
+
390
+ write_audit_log(
391
+ db,
392
+ _admin,
393
+ "admin.restore",
394
+ detail={"created": result["created"], "overwritten": result["overwritten"]},
395
+ ip=request.client.host if request.client else None,
377
396
  )
397
+ db.commit()
398
+ return result
399
+
400
+
401
+ _DEFAULT_DATA_DIR = os.environ.get("DATA_DIR", "/data")