From 7a26788ad22bca9f3efa4d709963534e6f46c13b Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sat, 11 Apr 2026 20:09:58 +0200 Subject: [PATCH] feat: Ordner-Freigaben zeigen Dateiliste + Download/Loeschen Share-Links fuer Ordner verhalten sich jetzt je nach Berechtigung: read (Nur Lesen): - Zeigt alle Dateien im Ordner mit Name, Groesse, Typ - Download-Button pro Datei - Kein Upload, kein Loeschen write (Lesen+Schreiben): - Zeigt alle Dateien im Ordner - Download-Button pro Datei - Loeschen-Button pro Datei - Upload-Zone (Drag & Drop + Button) - Nach Upload wird Dateiliste automatisch aktualisiert upload_only (Nur Upload): - Kein Dateilisting, kein Ordnername sichtbar - Nur Upload-Zone Backend-Endpunkte: - GET /share//files - Dateien im geteilten Ordner auflisten - GET /share//files//download - Einzeldatei herunterladen - DELETE /share//files/ - Datei loeschen (nur write) - Alle Endpunkte pruefen Passwort, Ablaufdatum und Berechtigung - Dateien muessen direkte Kinder des geteilten Ordners sein (kein Ausbruch) Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/files.py | 115 ++++++++++++++++++++++ frontend/src/views/ShareView.vue | 164 ++++++++++++++++++++++++------- 2 files changed, 246 insertions(+), 33 deletions(-) diff --git a/backend/app/api/files.py b/backend/app/api/files.py index 6e15ecc..5e2c463 100644 --- a/backend/app/api/files.py +++ b/backend/app/api/files.py @@ -503,6 +503,121 @@ def share_info(token): }), 200 +@api_bp.route('/share//files', methods=['GET']) +def share_list_files(token): + """List files in a shared folder (read or write permission required).""" + link = ShareLink.query.filter_by(token=token).first() + if not link: + return jsonify({'error': 'Link nicht gefunden'}), 404 + + if link.is_expired(): + return jsonify({'error': 'Link abgelaufen'}), 410 + + if link.permission == 'upload_only': + return jsonify({'error': 'Dieser Link erlaubt keinen Einblick'}), 403 + + # Check password via header + if link.password_hash: + password = request.args.get('password', '') or request.headers.get('X-Share-Password', '') + if not bcrypt.check_password_hash(link.password_hash, password): + return jsonify({'error': 'Passwort erforderlich'}), 401 + + f = db.session.get(File, link.file_id) + if not f.is_folder: + return jsonify({'error': 'Kein Ordner'}), 400 + + files = File.query.filter_by(parent_id=f.id)\ + .order_by(File.is_folder.desc(), File.name).all() + + return jsonify([{ + 'id': fi.id, + 'name': fi.name, + 'is_folder': fi.is_folder, + 'size': fi.size, + 'mime_type': fi.mime_type, + 'updated_at': fi.updated_at.isoformat() if fi.updated_at else None, + } for fi in files]), 200 + + +@api_bp.route('/share//files//download', methods=['GET']) +def share_download_file(token, file_id): + """Download a specific file from a shared folder.""" + link = ShareLink.query.filter_by(token=token).first() + if not link: + return jsonify({'error': 'Link nicht gefunden'}), 404 + + if link.is_expired(): + return jsonify({'error': 'Link abgelaufen'}), 410 + + if link.permission not in ('read', 'write'): + return jsonify({'error': 'Download nicht erlaubt'}), 403 + + if link.password_hash: + password = request.args.get('password', '') or request.headers.get('X-Share-Password', '') + if not bcrypt.check_password_hash(link.password_hash, password): + return jsonify({'error': 'Passwort erforderlich'}), 401 + + # Verify file belongs to the shared folder + target_file = db.session.get(File, file_id) + if not target_file: + return jsonify({'error': 'Datei nicht gefunden'}), 404 + + # Check file is inside shared folder (direct child) + shared_folder = db.session.get(File, link.file_id) + if target_file.parent_id != shared_folder.id: + return jsonify({'error': 'Datei gehoert nicht zu diesem Ordner'}), 403 + + if target_file.is_folder: + return jsonify({'error': 'Ordner koennen nicht heruntergeladen werden'}), 400 + + filepath = Path(current_app.config['UPLOAD_PATH']) / str(target_file.owner_id) / target_file.storage_path + if not filepath.exists(): + return jsonify({'error': 'Datei nicht gefunden'}), 404 + + link.download_count += 1 + db.session.commit() + + return send_file(str(filepath), mimetype=target_file.mime_type, as_attachment=True, + download_name=target_file.name) + + +@api_bp.route('/share//files/', methods=['DELETE']) +def share_delete_file(token, file_id): + """Delete a file from a shared folder (write permission required).""" + link = ShareLink.query.filter_by(token=token).first() + if not link: + return jsonify({'error': 'Link nicht gefunden'}), 404 + + if link.is_expired(): + return jsonify({'error': 'Link abgelaufen'}), 410 + + if link.permission != 'write': + return jsonify({'error': 'Loeschen nicht erlaubt'}), 403 + + if link.password_hash: + password = request.headers.get('X-Share-Password', '') + if not bcrypt.check_password_hash(link.password_hash, password): + return jsonify({'error': 'Passwort erforderlich'}), 401 + + target_file = db.session.get(File, file_id) + if not target_file: + return jsonify({'error': 'Datei nicht gefunden'}), 404 + + shared_folder = db.session.get(File, link.file_id) + if target_file.parent_id != shared_folder.id: + return jsonify({'error': 'Datei gehoert nicht zu diesem Ordner'}), 403 + + # Delete from disk + if target_file.storage_path: + filepath = Path(current_app.config['UPLOAD_PATH']) / str(target_file.owner_id) / target_file.storage_path + if filepath.exists(): + filepath.unlink() + + db.session.delete(target_file) + db.session.commit() + return jsonify({'message': 'Datei geloescht'}), 200 + + @api_bp.route('/share//verify', methods=['POST']) def share_verify(token): link = ShareLink.query.filter_by(token=token).first() diff --git a/frontend/src/views/ShareView.vue b/frontend/src/views/ShareView.vue index ed78bc1..95c4ec4 100644 --- a/frontend/src/views/ShareView.vue +++ b/frontend/src/views/ShareView.vue @@ -1,6 +1,6 @@