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', '')