"""File-browser + note-index endpoints (sandbox-enforced).""" import json import re import subprocess import sys from urllib.parse import parse_qs, urlparse import constants class FilesHandlers: """Mixin for /api/files, /api/refresh-index.""" def _resolve_sandboxed(self, path): """Resolve `path` against ALLOWED_WORKSPACES. Returns (target, workspace) or (None, None).""" allowed_dirs = constants.ALLOWED_WORKSPACES for base in allowed_dirs: try: candidate = (base / path).resolve() if any(str(candidate).startswith(str(d)) for d in allowed_dirs): return candidate, base except Exception: continue return None, None def handle_files_get(self): """List files or get file content.""" params = parse_qs(urlparse(self.path).query) path = params.get('path', [''])[0] action = params.get('action', ['list'])[0] target, workspace = self._resolve_sandboxed(path) if target is None: self.send_json({'error': 'Access denied'}, 403) return if action != 'list': self.send_json({'error': 'Unknown action'}, 400) return if not target.exists(): self.send_json({'error': 'Path not found'}, 404) return if target.is_file(): try: content = target.read_text(encoding='utf-8', errors='replace') self.send_json({ 'type': 'file', 'path': path, 'name': target.name, 'content': content[:100000], 'size': target.stat().st_size, 'truncated': target.stat().st_size > 100000, }) except Exception as e: self.send_json({'error': str(e)}, 500) else: items = [] try: for item in sorted(target.iterdir()): stat = item.stat() item_path = f"{path}/{item.name}" if path else item.name items.append({ 'name': item.name, 'type': 'dir' if item.is_dir() else 'file', 'size': stat.st_size if item.is_file() else None, 'mtime': stat.st_mtime, 'path': item_path, }) self.send_json({'type': 'dir', 'path': path, 'items': items}) except Exception as e: self.send_json({'error': str(e)}, 500) def handle_files_post(self): """Save file content.""" try: content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length).decode('utf-8') data = json.loads(post_data) path = data.get('path', '') content = data.get('content', '') target, workspace = self._resolve_sandboxed(path) if target is None: self.send_json({'error': 'Access denied'}, 403) return target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content, encoding='utf-8') self.send_json({'status': 'saved', 'path': path, 'size': len(content)}) except Exception as e: self.send_json({'error': str(e)}, 500) def handle_refresh_index(self): """Regenerate memory/kb/index.json by running tools/update_notes_index.py.""" try: script = constants.TOOLS_DIR / 'update_notes_index.py' result = subprocess.run( [sys.executable, str(script)], capture_output=True, text=True, timeout=30, ) if result.returncode == 0: output = result.stdout total_match = re.search(r'with (\d+) notes', output) total = int(total_match.group(1)) if total_match else 0 self.send_json({ 'success': True, 'message': f'Index regenerat cu {total} notițe', 'total': total, 'output': output, }) else: self.send_json({'success': False, 'error': result.stderr or 'Unknown error'}, 500) except subprocess.TimeoutExpired: self.send_json({'success': False, 'error': 'Timeout'}, 500) except Exception as e: self.send_json({'success': False, 'error': str(e)}, 500)