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 @@