lockbot 2.9.3__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.
- {lockbot-2.9.3/python/lockbot.egg-info → lockbot-2.9.4}/PKG-INFO +1 -1
- {lockbot-2.9.3 → lockbot-2.9.4}/pyproject.toml +1 -1
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/admin/router.py +87 -63
- lockbot-2.9.4/python/lockbot/backend/app/backup/service.py +618 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/bots/models.py +1 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/bots/router.py +2 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/bots/schemas.py +1 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/main.py +26 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/base_bot.py +9 -3
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/i18n/en.py +2 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/i18n/zh.py +2 -0
- {lockbot-2.9.3 → lockbot-2.9.4/python/lockbot.egg-info}/PKG-INFO +1 -1
- lockbot-2.9.3/python/lockbot/backend/app/backup/service.py +0 -191
- {lockbot-2.9.3 → lockbot-2.9.4}/LICENSE +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/MANIFEST.in +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/README.md +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/admin/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/audit/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/audit/models.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/audit/router.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/audit/service.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/auth/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/auth/dependencies.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/auth/models.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/auth/router.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/auth/schemas.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/backup/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/backup/router.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/backup/scheduler.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/bots/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/bots/encryption.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/bots/manager.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/bots/webhook_handler.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/config.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/database.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/logs/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/rate_limit.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/settings/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/settings/models.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/backend/app/settings/router.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/bot_instance.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/config.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/device_bot.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/device_usage_alert.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/device_usage_utils.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/entry.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/env.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/handler.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/i18n/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/io.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/message_adapter.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/msg_utils.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/node_bot.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/platforms/__init__.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/platforms/infoflow.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/queue_bot.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/request.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/scheduler.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot/core/utils.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot.egg-info/SOURCES.txt +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot.egg-info/dependency_links.txt +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot.egg-info/requires.txt +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/python/lockbot.egg-info/top_level.txt +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/setup.cfg +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/tools/create_super_admin.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/tools/gen_keys.py +0 -0
- {lockbot-2.9.3 → lockbot-2.9.4}/tools/reset_super_admin_password.py +0 -0
|
@@ -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
|
|
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
|
|
304
|
-
from lockbot.backend.app.
|
|
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
|
-
|
|
319
|
-
except
|
|
320
|
-
|
|
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 =
|
|
308
|
+
filename = zip_path.name
|
|
328
309
|
return FileResponse(
|
|
329
|
-
path=
|
|
310
|
+
path=str(zip_path),
|
|
330
311
|
filename=filename,
|
|
331
|
-
media_type="application/
|
|
332
|
-
background=BackgroundTask(
|
|
312
|
+
media_type="application/zip",
|
|
313
|
+
background=BackgroundTask(_cleanup_backup_tmp, zip_path),
|
|
333
314
|
)
|
|
334
315
|
|
|
335
316
|
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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")
|