feat(dashboard): endpoint de download fișiere binare + tool epub-to-audio

- dashboard/handlers/files.py: handle_files_download() servește mp3/wav/zip
  din WORKSPACE_DIR cu Content-Disposition attachment, resolve dedicat
  (nu _resolve_sandboxed, care nu ajunge niciodată la WORKSPACE_DIR)
- dashboard/api.py: rutează /api/files/download
- tools/epub_to_audio.py: tool nou de conversie EPUB → audio
- cron/jobs.json, memory/kb/index.json: stare auto-generată (job runs,
  regenerare index KB)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 17:50:27 +00:00
parent 164ff32031
commit bf62d6fb4b
5 changed files with 207 additions and 23 deletions

View File

@@ -72,6 +72,42 @@ class FilesHandlers:
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_files_download(self):
"""Stream a binary file (audio, zip, etc.) from WORKSPACE_DIR as a download.
Dedicated resolution (not `_resolve_sandboxed`, which always resolves
against ALLOWED_WORKSPACES[0]/BASE_DIR and never reaches WORKSPACE_DIR)
so this stays isolated from the shared file-browser sandbox logic.
"""
params = parse_qs(urlparse(self.path).query)
path = params.get('path', [''])[0]
try:
target = (constants.WORKSPACE_DIR / path).resolve()
target.relative_to(constants.WORKSPACE_DIR.resolve())
except (ValueError, OSError):
self.send_json({'error': 'Access denied'}, 403)
return
if not target.is_file():
self.send_json({'error': 'Not found'}, 404)
return
ext = target.suffix.lstrip('.').lower()
ctype = {
'mp3': 'audio/mpeg',
'wav': 'audio/wav',
'zip': 'application/zip',
}.get(ext, 'application/octet-stream')
data = target.read_bytes()
self.send_response(200)
self.send_header('Content-Type', ctype)
self.send_header('Content-Length', str(len(data)))
self.send_header('Content-Disposition', f'attachment; filename="{target.name}"')
self.send_header('Cache-Control', 'public, max-age=3600')
self.end_headers()
self.wfile.write(data)
def handle_files_post(self):
"""Save file content."""
try: