diff --git a/backend/main.py b/backend/main.py index 5ccc1d9..87e947c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3400,6 +3400,123 @@ async def api_backups_purge( return {"deleted": 0} + +@app.get("/api/backups/content") +async def api_backups_content( + path: str = Query(..., description="Full path to backup file"), + current_user=Depends(require_auth), +): + """Return the content of a specific backup file.""" + try: + fpath = Path(path) + if ".obsigate-backup" not in str(fpath): + raise HTTPException(status_code=403, detail="Access denied") + if not fpath.exists() or not fpath.is_file(): + raise HTTPException(status_code=404, detail="Backup not found") + content = fpath.read_text(encoding="utf-8", errors="replace") + # Truncate large files to 100KB + if len(content) > 102400: + content = content[:102400] + "\n\n... (tronque a 100 Ko)" + return {"content": content, "name": fpath.name, "size": len(content)} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/backups/compress") +async def api_backups_compress( + body: dict = Body(...), + current_user=Depends(require_auth), +): + """Compress backups older than N days. Body: {older_than_days: 30, dry_run: false}""" + import gzip as gz_mod + older_than = body.get("older_than_days", 30) + dry_run = body.get("dry_run", False) + cutoff = time.time() - (older_than * 86400) + compressed = 0 + saved_bytes = 0 + + for vault_name in index.keys(): + if not check_vault_access(vault_name, current_user): + continue + vd = get_vault_data(vault_name) + if not vd: + continue + vault_root = Path(vd["path"]) + backup_root = Path(os.environ.get("OBSIGATE_BACKUP_DIR", ".obsigate-backup")) + if not backup_root.is_absolute(): + backup_root = vault_root / backup_root + vault_dir = backup_root / vault_name + if not vault_dir.exists(): + continue + for fpath in vault_dir.rglob("*.bak"): + if not fpath.is_file(): + continue + if fpath.name.endswith(".bak.gz"): + continue + mtime = fpath.stat().st_mtime + if mtime > cutoff: + continue + if not dry_run: + try: + gz_path = fpath.with_suffix(fpath.suffix + ".gz") + data = fpath.read_bytes() + with gz_mod.open(str(gz_path), "wb", compresslevel=6) as gzf: + gzf.write(data) + orig_size = len(data) + gz_size = gz_path.stat().st_size + if gz_size < orig_size: + fpath.unlink() + saved_bytes += (orig_size - gz_size) + else: + gz_path.unlink() # compression didn't help + compressed += 1 + except Exception as e: + logger.warning(f"Failed to compress {fpath}: {e}") + else: + compressed += 1 + + return {"compressed": compressed, "saved_bytes": saved_bytes, "dry_run": dry_run} + + +@app.post("/api/backups/auto") +async def api_backups_auto( + body: dict = Body(...), + current_user=Depends(require_auth), +): + """Create backups for files modified since a given time. Body: {since_hours: 24}""" + since_hours = body.get("since_hours", 24) + cutoff = time.time() - (since_hours * 3600) + backed_up = 0 + + for vault_name in index.keys(): + if not check_vault_access(vault_name, current_user): + continue + vd = get_vault_data(vault_name) + if not vd: + continue + vault_root = Path(vd["path"]) + for fpath in vault_root.rglob("*"): + if not fpath.is_file(): + continue + if fpath.name.startswith('.'): + continue + if any(p.startswith('.') or p in {'.obsidian', '.trash', '.git', '.obsigate-backup', '__pycache__', 'node_modules'} for p in fpath.relative_to(vault_root).parts): + continue + mtime = fpath.stat().st_mtime + if mtime < cutoff: + continue + try: + rel = str(fpath.relative_to(vault_root)).replace("\\", "/") + _backup_file(fpath, vault_name, rel) + backed_up += 1 + except Exception as e: + logger.warning(f"Auto-backup failed for {rel}: {e}") + + return {"backed_up": backed_up, "since_hours": since_hours} + + # --------------------------------------------------------------------------- # Configuration API # --------------------------------------------------------------------------- diff --git a/frontend/js/backup-manager.js b/frontend/js/backup-manager.js index 84ecd43..d9c8e26 100644 --- a/frontend/js/backup-manager.js +++ b/frontend/js/backup-manager.js @@ -1,14 +1,14 @@ /** * Backup Manager module for ObsiGate. * - * Provides a full-page view to browse, inspect, and delete all backups - * across all vaults. Accessible from the backup/diff viewer. + * Full-page view to browse, preview, restore, and delete backups across vaults. + * Accessible from the backup/diff viewer footer. */ import { api } from './auth.js'; import { showToast } from './ui.js'; import { safeCreateIcons } from './utils.js'; -var state = { backups: [], filter: '' }; +var state = { backups: [], filter: '', preview: null }; export function openBackupManager() { var area = document.getElementById('content-area'); @@ -28,9 +28,14 @@ export function openBackupManager() { '' + '' + '
' + - '