feat: Share-Link Typ 'Nur Upload' (upload_only)

Dritter Link-Typ neben read und write:
- upload_only: Nur Dateien hochladen, kein Download, kein Ordnerinhalt
  sichtbar, Ordnername wird nicht angezeigt

Backend-Absicherung:
- GET /share/<token>/download gibt 403 bei upload_only
- POST /share/<token>/upload erlaubt upload_only + write
- GET /share/<token>/info gibt download_allowed zurueck

Frontend Share-Dialog:
- Drei Optionen: Nur Lesen / Lesen+Hochladen / Nur Upload
- Bestehende Links zeigen Typ an

Frontend ShareView:
- upload_only: Zeigt nur Upload-Zone, kein Dateiname, kein Download
- Hinweistext 'Dieser Link erlaubt nur das Hochladen von Dateien'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-11 18:44:02 +02:00
parent 116c33a7dc
commit e1eb6a83ae
3 changed files with 28 additions and 13 deletions

View File

@ -421,8 +421,8 @@ def create_share_link(file_id):
max_downloads = data.get('max_downloads') max_downloads = data.get('max_downloads')
permission = data.get('permission', 'read') permission = data.get('permission', 'read')
if permission not in ('read', 'write'): if permission not in ('read', 'write', 'upload_only'):
return jsonify({'error': 'Berechtigung muss "read" oder "write" sein'}), 400 return jsonify({'error': 'Berechtigung muss "read", "write" oder "upload_only" sein'}), 400
token = secrets.token_urlsafe(32) token = secrets.token_urlsafe(32)
password_hash = None password_hash = None
@ -498,7 +498,8 @@ def share_info(token):
'mime_type': f.mime_type, 'mime_type': f.mime_type,
'has_password': bool(link.password_hash), 'has_password': bool(link.password_hash),
'permission': link.permission, 'permission': link.permission,
'upload_allowed': f.is_folder and link.permission == 'write', 'upload_allowed': f.is_folder and link.permission in ('write', 'upload_only'),
'download_allowed': link.permission in ('read', 'write'),
}), 200 }), 200
@ -531,6 +532,9 @@ def share_download(token):
if not link: if not link:
return jsonify({'error': 'Link nicht gefunden'}), 404 return jsonify({'error': 'Link nicht gefunden'}), 404
if link.permission == 'upload_only':
return jsonify({'error': 'Dieser Link erlaubt nur Upload, keinen Download'}), 403
if link.is_expired(): if link.is_expired():
return jsonify({'error': 'Link abgelaufen'}), 410 return jsonify({'error': 'Link abgelaufen'}), 410
@ -576,9 +580,9 @@ def share_upload(token):
if link.is_expired(): if link.is_expired():
return jsonify({'error': 'Link abgelaufen'}), 410 return jsonify({'error': 'Link abgelaufen'}), 410
# Check write permission # Check write/upload permission
if link.permission != 'write': if link.permission not in ('write', 'upload_only'):
return jsonify({'error': 'Dieser Link erlaubt nur Lesen'}), 403 return jsonify({'error': 'Dieser Link erlaubt keinen Upload'}), 403
# Check password if set # Check password if set
if link.password_hash: if link.password_hash:

View File

@ -184,7 +184,7 @@
<div class="link-info"> <div class="link-info">
<code>{{ currentOrigin }}/share/{{ link.token }}</code> <code>{{ currentOrigin }}/share/{{ link.token }}</code>
<small> <small>
{{ link.permission === 'write' ? 'Lesen+Schreiben' : 'Nur Lesen' }} {{ {read: 'Nur Lesen', write: 'Lesen+Schreiben', upload_only: 'Nur Upload'}[link.permission] || link.permission }}
| {{ link.download_count }} Downloads | {{ link.download_count }} Downloads
<template v-if="link.expires_at"> | Bis {{ formatDate(link.expires_at) }}</template> <template v-if="link.expires_at"> | Bis {{ formatDate(link.expires_at) }}</template>
<template v-if="link.has_password"> | Passwortgeschuetzt</template> <template v-if="link.has_password"> | Passwortgeschuetzt</template>
@ -255,7 +255,11 @@ const selectedShareUser = ref(null)
const shareUserPermission = ref('read') const shareUserPermission = ref('read')
const userSearchResults = ref([]) const userSearchResults = ref([])
const userPermOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Schreiben', value: 'write' }, { label: 'Admin', value: 'admin' }] const userPermOptions = [{ label: 'Lesen', value: 'read' }, { label: 'Schreiben', value: 'write' }, { label: 'Admin', value: 'admin' }]
const linkPermOptions = [{ label: 'Nur Lesen (Download)', value: 'read' }, { label: 'Lesen + Hochladen (nur Ordner)', value: 'write' }] const linkPermOptions = [
{ label: 'Nur Lesen (Download)', value: 'read' },
{ label: 'Lesen + Hochladen (nur Ordner)', value: 'write' },
{ label: 'Nur Upload (Ordner, kein Einblick)', value: 'upload_only' },
]
const shareLinkPermission = ref('read') const shareLinkPermission = ref('read')
const currentOrigin = window.location.origin const currentOrigin = window.location.origin
const shareLoading = ref(false) const shareLoading = ref(false)

View File

@ -14,9 +14,10 @@
</div> </div>
<div v-else-if="fileInfo" class="share-info"> <div v-else-if="fileInfo" class="share-info">
<h2>{{ fileInfo.name }}</h2> <h2 v-if="fileInfo.permission !== 'upload_only'">{{ fileInfo.name }}</h2>
<p class="file-size" v-if="fileInfo.size && !fileInfo.is_folder">{{ formatSize(fileInfo.size) }}</p> <h2 v-else>Datei-Upload</h2>
<Tag v-if="fileInfo.is_folder" value="Ordner" severity="info" /> <p class="file-size" v-if="fileInfo.size && !fileInfo.is_folder && fileInfo.permission !== 'upload_only'">{{ formatSize(fileInfo.size) }}</p>
<Tag v-if="fileInfo.is_folder && fileInfo.permission !== 'upload_only'" value="Ordner" severity="info" />
<div v-if="fileInfo.has_password && !authenticated" class="password-form"> <div v-if="fileInfo.has_password && !authenticated" class="password-form">
<p>Diese Freigabe ist passwortgeschuetzt.</p> <p>Diese Freigabe ist passwortgeschuetzt.</p>
@ -28,8 +29,8 @@
</div> </div>
<div v-else class="actions-section"> <div v-else class="actions-section">
<!-- Download (files only) --> <!-- Download (files only, not upload_only) -->
<div v-if="!fileInfo.is_folder" class="action-block"> <div v-if="!fileInfo.is_folder && fileInfo.download_allowed" class="action-block">
<Button <Button
label="Herunterladen" label="Herunterladen"
icon="pi pi-download" icon="pi pi-download"
@ -38,6 +39,11 @@
/> />
</div> </div>
<!-- Upload-only hint -->
<p v-if="fileInfo.permission === 'upload_only'" class="upload-only-hint">
Dieser Link erlaubt nur das Hochladen von Dateien.
</p>
<!-- Upload (folders only) --> <!-- Upload (folders only) -->
<div v-if="fileInfo.upload_allowed" class="action-block"> <div v-if="fileInfo.upload_allowed" class="action-block">
<div class="upload-area" <div class="upload-area"
@ -216,4 +222,5 @@ onMounted(loadInfo)
.upload-area p { margin: 0.5rem 0; color: var(--p-text-muted-color); font-size: 0.9rem; } .upload-area p { margin: 0.5rem 0; color: var(--p-text-muted-color); font-size: 0.9rem; }
.upload-progress { margin-top: 1rem; } .upload-progress { margin-top: 1rem; }
.upload-progress p { font-size: 0.85rem; color: var(--p-text-muted-color); margin-top: 0.5rem; } .upload-progress p { font-size: 0.85rem; color: var(--p-text-muted-color); margin-top: 0.5rem; }
.upload-only-hint { color: var(--p-text-muted-color); font-size: 0.9rem; margin-bottom: 1rem; }
</style> </style>