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:
parent
1a831bfb04
commit
4b487974c6
|
|
@ -652,6 +652,40 @@ def share_download_file(token, file_id):
|
||||||
download_name=target_file.name)
|
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'])
|
@api_bp.route('/share/<token>/files/<int:file_id>', methods=['DELETE'])
|
||||||
def share_delete_file(token, file_id):
|
def share_delete_file(token, file_id):
|
||||||
"""Delete a file from a shared folder (write permission required)."""
|
"""Delete a file from a shared folder (write permission required)."""
|
||||||
|
|
|
||||||
|
|
@ -500,11 +500,7 @@ async function createFolder() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFile(data) {
|
function downloadFile(data) {
|
||||||
const url = filesStore.downloadUrl(data.id)
|
window.location.href = filesStore.downloadUrl(data.id)
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = data.name
|
|
||||||
a.click()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRename(data) {
|
function openRename(data) {
|
||||||
|
|
@ -574,6 +570,7 @@ async function shareWithUser() {
|
||||||
selectedShareUser.value = null
|
selectedShareUser.value = null
|
||||||
const res = await apiClient.get(`/files/${shareFile.value.id}/permissions`)
|
const res = await apiClient.get(`/files/${shareFile.value.id}/permissions`)
|
||||||
filePermissions.value = res.data
|
filePermissions.value = res.data
|
||||||
|
await filesStore.loadFiles(currentParentId())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
||||||
}
|
}
|
||||||
|
|
@ -584,6 +581,7 @@ async function removeUserShare(permId) {
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`/files/${shareFile.value.id}/permissions/${permId}`)
|
await apiClient.delete(`/files/${shareFile.value.id}/permissions/${permId}`)
|
||||||
filePermissions.value = filePermissions.value.filter(p => p.id !== permId)
|
filePermissions.value = filePermissions.value.filter(p => p.id !== permId)
|
||||||
|
await filesStore.loadFiles(currentParentId())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
||||||
}
|
}
|
||||||
|
|
@ -607,6 +605,7 @@ async function createShare() {
|
||||||
shareExpiry.value = ''
|
shareExpiry.value = ''
|
||||||
shareLinkPermission.value = 'read'
|
shareLinkPermission.value = 'read'
|
||||||
toast.add({ severity: 'success', summary: 'Link erstellt', life: 3000 })
|
toast.add({ severity: 'success', summary: 'Link erstellt', life: 3000 })
|
||||||
|
await filesStore.loadFiles(currentParentId())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('createShare error:', err)
|
console.error('createShare error:', err)
|
||||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error || String(err), life: 5000 })
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error || String(err), life: 5000 })
|
||||||
|
|
@ -624,6 +623,7 @@ async function removeShare(token) {
|
||||||
try {
|
try {
|
||||||
await filesStore.deleteShareLink(token)
|
await filesStore.deleteShareLink(token)
|
||||||
shareLinks.value = shareLinks.value.filter(l => l.token !== token)
|
shareLinks.value = shareLinks.value.filter(l => l.token !== token)
|
||||||
|
await filesStore.loadFiles(currentParentId())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,11 @@
|
||||||
<Button label="Herunterladen" icon="pi pi-download" size="large" @click="downloadFile" />
|
<Button label="Herunterladen" icon="pi pi-download" size="large" @click="downloadFile" />
|
||||||
</div>
|
</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 v-if="fileInfo.is_folder && fileInfo.permission !== 'upload_only'" class="folder-content">
|
||||||
<div class="folder-breadcrumb">
|
<div class="folder-breadcrumb">
|
||||||
<a class="crumb" @click="navigateToRoot">{{ fileInfo.name }}</a>
|
<a class="crumb" @click="navigateToRoot">{{ fileInfo.name }}</a>
|
||||||
|
|
@ -223,6 +227,14 @@ function downloadFile() {
|
||||||
window.location.href = url
|
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) {
|
function downloadFolderFile(file) {
|
||||||
let url = `/api/share/${token}/files/${file.id}/download`
|
let url = `/api/share/${token}/files/${file.id}/download`
|
||||||
if (fileInfo.value?.has_password && password.value) {
|
if (fileInfo.value?.has_password && password.value) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue