feat: Unterordner-Navigation in Ordner-Freigaben
Geteilte Ordner koennen jetzt komplett durchnavigiert werden: - Klick auf einen Unterordner oeffnet dessen Inhalt - Breadcrumb-Navigation: Root > Unterordner > Unter-Unterordner mit Klick zurueck auf jede Ebene - Ordner-Zeilen haben Hover-Effekt und Cursor-Pointer Backend: - GET /share/<token>/files?parent_id=X - Unterordner auflisten - Sicherheitspruefung: parent_id muss innerhalb des geteilten Ordners liegen (_is_inside_shared_folder traversiert den Baum) - Breadcrumb wird serverseitig berechnet - Download + Delete erlauben jetzt Dateien in jeder Tiefe (nicht nur direkte Kinder) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+50
-19
@@ -503,9 +503,21 @@ def share_info(token):
|
||||
}), 200
|
||||
|
||||
|
||||
def _is_inside_shared_folder(file_obj, shared_folder_id):
|
||||
"""Check if a file/folder is a descendant of the shared folder."""
|
||||
current = file_obj
|
||||
while current:
|
||||
if current.id == shared_folder_id:
|
||||
return True
|
||||
if current.parent_id is None:
|
||||
return False
|
||||
current = current.parent
|
||||
return False
|
||||
|
||||
|
||||
@api_bp.route('/share/<token>/files', methods=['GET'])
|
||||
def share_list_files(token):
|
||||
"""List files in a shared folder (read or write permission required)."""
|
||||
"""List files in a shared folder or subfolder."""
|
||||
link = ShareLink.query.filter_by(token=token).first()
|
||||
if not link:
|
||||
return jsonify({'error': 'Link nicht gefunden'}), 404
|
||||
@@ -516,27 +528,49 @@ def share_list_files(token):
|
||||
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:
|
||||
shared_folder = db.session.get(File, link.file_id)
|
||||
if not shared_folder.is_folder:
|
||||
return jsonify({'error': 'Kein Ordner'}), 400
|
||||
|
||||
files = File.query.filter_by(parent_id=f.id)\
|
||||
# Allow browsing subfolders via parent_id parameter
|
||||
parent_id = request.args.get('parent_id', None, type=int)
|
||||
if parent_id is None:
|
||||
parent_id = shared_folder.id
|
||||
else:
|
||||
# Verify the requested parent is inside the shared folder
|
||||
target_parent = db.session.get(File, parent_id)
|
||||
if not target_parent or not _is_inside_shared_folder(target_parent, shared_folder.id):
|
||||
return jsonify({'error': 'Zugriff verweigert'}), 403
|
||||
|
||||
files = File.query.filter_by(parent_id=parent_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
|
||||
# Build breadcrumb from current parent back to shared root
|
||||
breadcrumb = []
|
||||
if parent_id != shared_folder.id:
|
||||
current = db.session.get(File, parent_id)
|
||||
while current and current.id != shared_folder.id:
|
||||
breadcrumb.insert(0, {'id': current.id, 'name': current.name})
|
||||
current = current.parent
|
||||
|
||||
return jsonify({
|
||||
'files': [{
|
||||
'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],
|
||||
'breadcrumb': breadcrumb,
|
||||
'current_parent_id': parent_id,
|
||||
'shared_root_id': shared_folder.id,
|
||||
}), 200
|
||||
|
||||
|
||||
@api_bp.route('/share/<token>/files/<int:file_id>/download', methods=['GET'])
|
||||
@@ -557,14 +591,12 @@ def share_download_file(token, file_id):
|
||||
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:
|
||||
# Check file is inside shared folder (any depth)
|
||||
if not _is_inside_shared_folder(target_file, link.file_id):
|
||||
return jsonify({'error': 'Datei gehoert nicht zu diesem Ordner'}), 403
|
||||
|
||||
if target_file.is_folder:
|
||||
@@ -603,8 +635,7 @@ def share_delete_file(token, 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:
|
||||
if not _is_inside_shared_folder(target_file, link.file_id):
|
||||
return jsonify({'error': 'Datei gehoert nicht zu diesem Ordner'}), 403
|
||||
|
||||
# Delete from disk
|
||||
|
||||
Reference in New Issue
Block a user