diff --git a/portal/templates/portal/shared_space/home.html b/portal/templates/portal/shared_space/home.html index 5041db2..0fd2cf4 100644 --- a/portal/templates/portal/shared_space/home.html +++ b/portal/templates/portal/shared_space/home.html @@ -67,6 +67,43 @@ border-color: var(--error); } + .result-item.duplicate { + border-color: #f59e0b; + } + + .duplicate-actions { + display: flex; + gap: 0.5rem; + } + + .duplicate-actions button { + border: none; + padding: 0.35rem 0.85rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.8rem; + font-weight: 500; + } + + .btn-overwrite { + background: var(--primary); + color: white; + } + + .btn-overwrite:hover { + background: var(--primary-hover); + } + + .btn-skip { + background: var(--border); + color: var(--text); + } + + .btn-skip:hover { + background: var(--text-muted); + color: white; + } + .result-info { flex: 1; } @@ -249,9 +286,7 @@ function formatBytes(bytes) { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } -function uploadFile(file) { - const itemId = 'upload-' + Date.now() + Math.random().toString(36).substr(2, 9); - +function uploadFile(file, action) { // Check file size before upload (skip if unlimited) if (maxSize > 0 && file.size > maxSize) { const item = document.createElement('div'); @@ -266,10 +301,15 @@ function uploadFile(file) { return; } - // Create result item - const item = document.createElement('div'); - item.className = 'result-item'; - item.id = itemId; + // Create or reuse result item + let item = action ? document.getElementById('dup-' + file.name) : null; + if (!item) { + item = document.createElement('div'); + item.className = 'result-item'; + results.insertBefore(item, results.firstChild); + } else { + item.className = 'result-item'; + } item.innerHTML = `

${file.name}

@@ -277,11 +317,10 @@ function uploadFile(file) {
`; - results.insertBefore(item, results.firstChild); - // Upload const formData = new FormData(); formData.append('file', file); + if (action) formData.append('action', action); const xhr = new XMLHttpRequest(); @@ -306,6 +345,32 @@ function uploadFile(file) { `; + } else if (xhr.status === 409) { + const data = JSON.parse(xhr.responseText); + item.id = 'dup-' + file.name; + item.classList.add('duplicate'); + const existingSize = data.existing_file.size ? formatBytes(data.existing_file.size) : 'unknown size'; + item.innerHTML = ` +
+

${file.name}

+
"${data.existing_file.title}" already exists (${existingSize})
+
+
+ + +
+ `; + item.querySelector('.btn-overwrite').addEventListener('click', () => uploadFile(file, 'overwrite')); + item.querySelector('.btn-skip').addEventListener('click', () => { + item.classList.remove('duplicate'); + item.classList.add('error'); + item.innerHTML = ` +
+

${file.name}

+
Skipped (duplicate)
+
+ `; + }); } else { let errorMsg = 'Upload failed'; try { @@ -314,14 +379,16 @@ function uploadFile(file) { } catch(e) {} item.classList.add('error'); item.querySelector('.meta').textContent = errorMsg; - item.querySelector('.progress-bar').remove(); + const pb = item.querySelector('.progress-bar'); + if (pb) pb.remove(); } }); xhr.addEventListener('error', () => { item.classList.add('error'); item.querySelector('.meta').textContent = 'Upload failed - network error'; - item.querySelector('.progress-bar').remove(); + const pb = item.querySelector('.progress-bar'); + if (pb) pb.remove(); }); xhr.open('POST', '{% url "shared_space_upload" %}'); diff --git a/portal/views_shared_space.py b/portal/views_shared_space.py index 5ca8d84..8eed557 100644 --- a/portal/views_shared_space.py +++ b/portal/views_shared_space.py @@ -71,6 +71,32 @@ class SharedSpaceUploadAPIView(View): 'error': f'File too large. Maximum size is {space.max_file_size_mb}MB' }, status=400) + action = request.POST.get('action', '') + + # Check for duplicate filename in this space + existing = MediaFile.objects.filter( + shared_space=space, + original_filename=uploaded_file.name, + ).first() + + if existing and action != 'overwrite': + existing_share = existing.public_shares.filter(is_active=True).first() + return JsonResponse({ + 'duplicate': True, + 'existing_file': { + 'id': str(existing.id), + 'title': existing.title, + 'filename': existing.original_filename, + 'size': existing.file_size, + 'share_url': existing_share.get_public_url() if existing_share else None, + }, + }, status=409) + + # Overwrite: delete the old file and its shares + if existing and action == 'overwrite': + existing.file.delete(save=False) + existing.delete() + title = request.POST.get('title', '') or uploaded_file.name description = request.POST.get('description', '')