feat: Files-Seite verbessert - View, Download & Delete für alle Dateitypen

UI Improvements:
- Agent Work Files: View-, Download- und Delete-Buttons hinzugefügt
- Projektdokumente: Download- und Delete-Buttons hinzugefügt
- Konsistentes UI über alle Datei-Kategorien
- View-Modal für Agent-Dateien (wie Projektdokumente)

Backend:
- /files/agent/<agent_key>/view/<filename> - Agent-Datei anzeigen
- /files/agent/<agent_key>/delete/<filename> - Agent-Datei löschen
- /files/agent/<agent_key>/<filename>?download=1 - Force Download
- /files/project/<filename>?download=1 - Projektdatei Download
- /files/project/delete/<filename> - Projektdatei löschen

Security:
- Path traversal protection für alle Routes
- Whitelist-basierte Dateityp-Validierung
- Agent-Zugriff nur auf eigene work-Verzeichnisse

Features:
- 👁 View: Datei im Modal anzeigen (Markdown, TXT)
- ↓ Download: Force download statt Browser-Ansicht
- ✕ Delete: Datei löschen mit Bestätigung
This commit is contained in:
pdyde 2026-02-21 13:25:37 +01:00
parent 73c36785e2
commit 11352d2ca5
2 changed files with 124 additions and 2 deletions

112
app.py
View file

@ -1951,7 +1951,66 @@ def download_agent_file(agent_key, filename):
if not os.path.isfile(filepath):
return jsonify({'error': 'Datei nicht gefunden'}), 404
return send_from_directory(work_dir, filename, as_attachment=False)
# Force download wenn download=1 Parameter
as_attachment = request.args.get('download') == '1'
return send_from_directory(work_dir, filename, as_attachment=as_attachment)
@app.route('/files/agent/<agent_key>/view/<filename>')
def view_agent_file(agent_key, filename):
"""Gibt Inhalt einer Agent-Datei als JSON zurück."""
if agent_key not in AGENTS:
return jsonify({'error': 'Agent nicht gefunden'}), 404
dirs = ensure_agent_structure(agent_key)
work_dir = dirs['work_dir']
filepath = os.path.join(work_dir, filename)
# Security: Stelle sicher, dass die Datei im work_dir ist
if not os.path.abspath(filepath).startswith(os.path.abspath(work_dir)):
return jsonify({'error': 'Zugriff verweigert'}), 403
if not os.path.isfile(filepath):
return jsonify({'error': 'Datei nicht gefunden'}), 404
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
if request.args.get('json'):
return jsonify({'content': content})
return content
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/files/agent/<agent_key>/delete/<filename>')
def delete_agent_file(agent_key, filename):
"""Löscht eine Datei aus dem Work-Ordner eines Agenten."""
if agent_key not in AGENTS:
flash('Agent nicht gefunden', 'danger')
return redirect(url_for('files_page'))
dirs = ensure_agent_structure(agent_key)
work_dir = dirs['work_dir']
filepath = os.path.join(work_dir, filename)
# Security: Stelle sicher, dass die Datei im work_dir ist
if not os.path.abspath(filepath).startswith(os.path.abspath(work_dir)):
flash('Zugriff verweigert', 'danger')
return redirect(url_for('files_page'))
if not os.path.isfile(filepath):
flash('Datei nicht gefunden', 'warning')
return redirect(url_for('files_page'))
try:
os.remove(filepath)
flash(f'Agent-Datei "{filename}" gelöscht', 'success')
except Exception as e:
flash(f'Fehler beim Löschen: {str(e)}', 'danger')
return redirect(url_for('files_page'))
@app.route('/files/email/view/<filename>')
@ -2032,6 +2091,57 @@ def view_project_file(filename):
return jsonify({'error': str(e)}), 500
@app.route('/files/project/<filename>')
def download_project_file(filename):
"""Liefert eine Projektdatei zum Download."""
base_dir = os.path.dirname(__file__)
filepath = os.path.join(base_dir, filename)
# Security: stay in base dir
if os.path.dirname(os.path.abspath(filepath)) != os.path.abspath(base_dir):
return jsonify({'error': 'Zugriff verweigert'}), 403
allowed_ext = ('.md', '.txt', '.docx')
if not filename.lower().endswith(allowed_ext):
return jsonify({'error': 'Dateityp nicht unterstützt'}), 400
if not os.path.isfile(filepath):
return jsonify({'error': 'Datei nicht gefunden'}), 404
# Force download wenn download=1 Parameter
as_attachment = request.args.get('download') == '1'
return send_from_directory(base_dir, filename, as_attachment=as_attachment)
@app.route('/files/project/delete/<filename>')
def delete_project_file(filename):
"""Löscht eine Projektdatei."""
base_dir = os.path.dirname(__file__)
filepath = os.path.join(base_dir, filename)
# Security: stay in base dir
if os.path.dirname(os.path.abspath(filepath)) != os.path.abspath(base_dir):
flash('Zugriff verweigert', 'danger')
return redirect(url_for('files_page'))
allowed_ext = ('.md', '.txt', '.docx')
if not filename.lower().endswith(allowed_ext):
flash('Dateityp nicht unterstützt', 'warning')
return redirect(url_for('files_page'))
if not os.path.isfile(filepath):
flash('Datei nicht gefunden', 'warning')
return redirect(url_for('files_page'))
try:
os.remove(filepath)
flash(f'Projektdokument "{filename}" gelöscht', 'success')
except Exception as e:
flash(f'Fehler beim Löschen: {str(e)}', 'danger')
return redirect(url_for('files_page'))
@app.route('/emails', methods=['GET', 'POST'])
def emails():
"""Email Management Interface"""

View file

@ -137,6 +137,9 @@
<div class="file-actions">
<button class="btn btn-sm btn-secondary"
onclick="viewProjectFile(event, '{{ file.name }}')" title="Anzeigen">👁</button>
<a href="/files/project/{{ file.name }}?download=1" class="btn btn-sm btn-primary" title="Herunterladen" download></a>
<a href="/files/project/delete/{{ file.name }}" class="btn btn-sm btn-danger"
onclick="return confirm('Projektdokument löschen?')" title="Löschen">✕</a>
</div>
</div>
{% endfor %}
@ -165,7 +168,11 @@
<div class="file-meta">{{ (file.size / 1024)|round(1) }} KB · {{ file.modified[:10] }}</div>
</div>
<div class="file-actions">
<a href="/files/agent/{{ agent_key }}/{{ file.name }}" class="btn btn-sm btn-secondary" title="Herunterladen"></a>
<button class="btn btn-sm btn-secondary"
onclick="viewAgentFile(event, '{{ agent_key }}', '{{ file.name }}')" title="Anzeigen">👁</button>
<a href="/files/agent/{{ agent_key }}/{{ file.name }}?download=1" class="btn btn-sm btn-primary" title="Herunterladen" download></a>
<a href="/files/agent/{{ agent_key }}/delete/{{ file.name }}" class="btn btn-sm btn-danger"
onclick="return confirm('Agent-Datei löschen?')" title="Löschen">✕</a>
</div>
</div>
{% endfor %}
@ -245,6 +252,11 @@ function viewProjectFile(event, name) {
openFileModal(name, '/files/project/view/' + encodeURIComponent(name) + '?json=1');
}
function viewAgentFile(event, agentKey, name) {
event.preventDefault();
openFileModal(`${agentKey}/${name}`, `/files/agent/${agentKey}/view/${encodeURIComponent(name)}?json=1`);
}
function editEmailFile(name) {
currentEditFile = name;
document.getElementById('editModalFilename').textContent = name;