From 4b487974c6cd11e1ba3b23c443f75e975987335d Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sat, 11 Apr 2026 20:35:30 +0200 Subject: [PATCH] fix: ZIP-Download, Share-Status live aktualisieren, Ordner-ZIP bei Share-Links ZIP-Download fix: - window.location.href statt a.download fuer API-Downloads (a.download funktioniert nicht mit authentifizierten API-Routen) Share-Status live: - Dateiliste wird nach jeder Share-Aenderung automatisch neu geladen (Link erstellen, Link loeschen, Benutzer-Freigabe setzen/entfernen) - Gruenes Share-Icon aktualisiert sich sofort ohne F5 Ordner-ZIP bei Share-Links: - "Ganzen Ordner als ZIP herunterladen" Button bei read/write Ordner-Shares - Backend: GET /share//download-zip mit Passwort + Ablauf-Check - Benachrichtigung an Ersteller bei ZIP-Download Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/files.py | 34 ++++++++++++++++++++++++++++++++ frontend/src/views/FilesView.vue | 10 +++++----- frontend/src/views/ShareView.vue | 14 ++++++++++++- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/backend/app/api/files.py b/backend/app/api/files.py index cf1b2ad..22278c5 100644 --- a/backend/app/api/files.py +++ b/backend/app/api/files.py @@ -652,6 +652,40 @@ def share_download_file(token, file_id): download_name=target_file.name) +@api_bp.route('/share//download-zip', methods=['GET']) +def share_download_zip(token): + """Download the entire shared folder as ZIP.""" + 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 + + f = db.session.get(File, link.file_id) + if not f.is_folder: + return jsonify({'error': 'Kein Ordner'}), 400 + + link.download_count += 1 + db.session.commit() + + try: + from app.services.system_mail import notify_share_link_accessed + notify_share_link_accessed(link, f.name, request.remote_addr) + except Exception: + pass + + return _download_folder_as_zip(f) + + @api_bp.route('/share//files/', methods=['DELETE']) def share_delete_file(token, file_id): """Delete a file from a shared folder (write permission required).""" diff --git a/frontend/src/views/FilesView.vue b/frontend/src/views/FilesView.vue index 8fd1981..5b2a124 100644 --- a/frontend/src/views/FilesView.vue +++ b/frontend/src/views/FilesView.vue @@ -500,11 +500,7 @@ async function createFolder() { } function downloadFile(data) { - const url = filesStore.downloadUrl(data.id) - const a = document.createElement('a') - a.href = url - a.download = data.name - a.click() + window.location.href = filesStore.downloadUrl(data.id) } function openRename(data) { @@ -574,6 +570,7 @@ async function shareWithUser() { selectedShareUser.value = null const res = await apiClient.get(`/files/${shareFile.value.id}/permissions`) filePermissions.value = res.data + await filesStore.loadFiles(currentParentId()) } catch (err) { toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 }) } @@ -584,6 +581,7 @@ async function removeUserShare(permId) { try { await apiClient.delete(`/files/${shareFile.value.id}/permissions/${permId}`) filePermissions.value = filePermissions.value.filter(p => p.id !== permId) + await filesStore.loadFiles(currentParentId()) } catch (err) { toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 }) } @@ -607,6 +605,7 @@ async function createShare() { shareExpiry.value = '' shareLinkPermission.value = 'read' toast.add({ severity: 'success', summary: 'Link erstellt', life: 3000 }) + await filesStore.loadFiles(currentParentId()) } catch (err) { console.error('createShare error:', err) toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error || String(err), life: 5000 }) @@ -624,6 +623,7 @@ async function removeShare(token) { try { await filesStore.deleteShareLink(token) shareLinks.value = shareLinks.value.filter(l => l.token !== token) + await filesStore.loadFiles(currentParentId()) } catch (err) { toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 }) } diff --git a/frontend/src/views/ShareView.vue b/frontend/src/views/ShareView.vue index 00005fe..32cc2f3 100644 --- a/frontend/src/views/ShareView.vue +++ b/frontend/src/views/ShareView.vue @@ -36,7 +36,11 @@