feat: Ordner als ZIP herunterladen + Share-Status visuell anzeigen
Ordner-Download: - Ordner koennen jetzt als ZIP heruntergeladen werden (rekursiv mit allen Unterordnern und Dateien) - ZIP-Icon statt Download-Icon bei Ordnern in der Dateiliste - Backend erstellt ZIP mit ZIP_DEFLATED und allowZip64 fuer grosse Ordner Share-Status visuell: - Share-Button zeigt gruenes Personen-Icon (pi-users) wenn die Datei/ der Ordner bereits Freigaben hat (Links oder Benutzer-Berechtigungen) - Normales Share-Icon (pi-share-alt) wenn keine Freigaben existieren - Backend liefert has_shares und has_permissions pro Datei mit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6515b3a256
commit
1a831bfb04
|
|
@ -1,12 +1,15 @@
|
|||
import io
|
||||
import os
|
||||
import uuid
|
||||
import hashlib
|
||||
import secrets
|
||||
import mimetypes
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from flask import request, jsonify, send_file, current_app
|
||||
from flask import request, jsonify, send_file, current_app, Response
|
||||
|
||||
from app.api import api_bp
|
||||
from app.api.auth import token_required
|
||||
|
|
@ -74,7 +77,12 @@ def list_files():
|
|||
File.parent_id.is_(None)
|
||||
).order_by(File.is_folder.desc(), File.name).all()
|
||||
|
||||
result = [f.to_dict() for f in files]
|
||||
result = []
|
||||
for f in files:
|
||||
d = f.to_dict()
|
||||
d['has_shares'] = ShareLink.query.filter_by(file_id=f.id).count() > 0
|
||||
d['has_permissions'] = FilePermission.query.filter_by(file_id=f.id).count() > 0
|
||||
result.append(d)
|
||||
for f in shared:
|
||||
d = f.to_dict()
|
||||
d['shared'] = True
|
||||
|
|
@ -240,8 +248,9 @@ def download_file(file_id):
|
|||
f, err = _get_file_or_403(file_id, user, 'read')
|
||||
if err:
|
||||
return err
|
||||
|
||||
if f.is_folder:
|
||||
return jsonify({'error': 'Ordner koennen nicht heruntergeladen werden'}), 400
|
||||
return _download_folder_as_zip(f)
|
||||
|
||||
filepath = Path(current_app.config['UPLOAD_PATH']) / str(f.owner_id) / f.storage_path
|
||||
if not filepath.exists():
|
||||
|
|
@ -251,6 +260,36 @@ def download_file(file_id):
|
|||
download_name=f.name)
|
||||
|
||||
|
||||
def _download_folder_as_zip(folder):
|
||||
"""Stream a folder as ZIP download."""
|
||||
upload_path = Path(current_app.config['UPLOAD_PATH'])
|
||||
|
||||
def _add_folder_to_zip(zf, folder_obj, prefix):
|
||||
children = File.query.filter_by(parent_id=folder_obj.id).all()
|
||||
for child in children:
|
||||
if child.is_folder:
|
||||
_add_folder_to_zip(zf, child, f'{prefix}{child.name}/')
|
||||
else:
|
||||
filepath = upload_path / str(child.owner_id) / child.storage_path
|
||||
if filepath.exists():
|
||||
zf.write(str(filepath), f'{prefix}{child.name}')
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
|
||||
tmp_path = tmp.name
|
||||
tmp.close()
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True) as zf:
|
||||
_add_folder_to_zip(zf, folder, '')
|
||||
|
||||
return send_file(tmp_path, mimetype='application/zip', as_attachment=True,
|
||||
download_name=f'{folder.name}.zip')
|
||||
finally:
|
||||
# Cleanup after response is sent (Flask handles this)
|
||||
import atexit
|
||||
atexit.register(lambda: os.path.exists(tmp_path) and os.unlink(tmp_path))
|
||||
|
||||
|
||||
# --- Rename / Move ---
|
||||
|
||||
@api_bp.route('/files/<int:file_id>', methods=['PUT'])
|
||||
|
|
|
|||
|
|
@ -78,14 +78,16 @@
|
|||
<template #body="{ data }">
|
||||
<div class="row-actions">
|
||||
<Button
|
||||
v-if="!data.is_folder"
|
||||
icon="pi pi-download"
|
||||
:icon="data.is_folder ? 'pi pi-file-zip' : 'pi pi-download'"
|
||||
text rounded size="small"
|
||||
:title="data.is_folder ? 'Als ZIP herunterladen' : 'Herunterladen'"
|
||||
@click.stop="downloadFile(data)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-share-alt"
|
||||
:icon="(data.has_shares || data.has_permissions) ? 'pi pi-users' : 'pi pi-share-alt'"
|
||||
text rounded size="small"
|
||||
:severity="(data.has_shares || data.has_permissions) ? 'success' : undefined"
|
||||
:title="(data.has_shares || data.has_permissions) ? 'Freigaben verwalten' : 'Teilen'"
|
||||
@click.stop="openShare(data)"
|
||||
/>
|
||||
<Button
|
||||
|
|
|
|||
Loading…
Reference in New Issue