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/<token>/download-zip mit Passwort + Ablauf-Check
- Benachrichtigung an Ersteller bei ZIP-Download

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-11 20:35:30 +02:00
parent 1a831bfb04
commit 4b487974c6
3 changed files with 52 additions and 6 deletions

View File

@ -652,6 +652,40 @@ def share_download_file(token, file_id):
download_name=target_file.name)
@api_bp.route('/share/<token>/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/<token>/files/<int:file_id>', methods=['DELETE'])
def share_delete_file(token, file_id):
"""Delete a file from a shared folder (write permission required)."""

View File

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

View File

@ -36,7 +36,11 @@
<Button label="Herunterladen" icon="pi pi-download" size="large" @click="downloadFile" />
</div>
<!-- Folder: file listing (read + write) -->
<!-- Folder: ZIP download + file listing (read + write) -->
<div v-if="fileInfo.is_folder && fileInfo.download_allowed" class="action-block">
<Button label="Ganzen Ordner als ZIP herunterladen" icon="pi pi-file-zip" outlined @click="downloadFolderZip" />
</div>
<div v-if="fileInfo.is_folder && fileInfo.permission !== 'upload_only'" class="folder-content">
<div class="folder-breadcrumb">
<a class="crumb" @click="navigateToRoot">{{ fileInfo.name }}</a>
@ -223,6 +227,14 @@ function downloadFile() {
window.location.href = url
}
function downloadFolderZip() {
let url = `/api/share/${token}/download-zip`
if (fileInfo.value?.has_password && password.value) {
url += `?password=${encodeURIComponent(password.value)}`
}
window.location.href = url
}
function downloadFolderFile(file) {
let url = `/api/share/${token}/files/${file.id}/download`
if (fileInfo.value?.has_password && password.value) {