121 lines
4.5 KiB
Python
121 lines
4.5 KiB
Python
"""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)
|