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:
parent
73c36785e2
commit
11352d2ca5
2 changed files with 124 additions and 2 deletions
112
app.py
112
app.py
|
|
@ -1951,7 +1951,66 @@ def download_agent_file(agent_key, filename):
|
||||||
if not os.path.isfile(filepath):
|
if not os.path.isfile(filepath):
|
||||||
return jsonify({'error': 'Datei nicht gefunden'}), 404
|
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>')
|
@app.route('/files/email/view/<filename>')
|
||||||
|
|
@ -2032,6 +2091,57 @@ def view_project_file(filename):
|
||||||
return jsonify({'error': str(e)}), 500
|
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'])
|
@app.route('/emails', methods=['GET', 'POST'])
|
||||||
def emails():
|
def emails():
|
||||||
"""Email Management Interface"""
|
"""Email Management Interface"""
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,9 @@
|
||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
<button class="btn btn-sm btn-secondary"
|
<button class="btn btn-sm btn-secondary"
|
||||||
onclick="viewProjectFile(event, '{{ file.name }}')" title="Anzeigen">👁</button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -165,7 +168,11 @@
|
||||||
<div class="file-meta">{{ (file.size / 1024)|round(1) }} KB · {{ file.modified[:10] }}</div>
|
<div class="file-meta">{{ (file.size / 1024)|round(1) }} KB · {{ file.modified[:10] }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-actions">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -245,6 +252,11 @@ function viewProjectFile(event, name) {
|
||||||
openFileModal(name, '/files/project/view/' + encodeURIComponent(name) + '?json=1');
|
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) {
|
function editEmailFile(name) {
|
||||||
currentEditFile = name;
|
currentEditFile = name;
|
||||||
document.getElementById('editModalFilename').textContent = name;
|
document.getElementById('editModalFilename').textContent = name;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue