Add duplicate file detection with skip/overwrite options
When uploading a file that already exists in a shared space (same filename), the UI now shows the existing file info with Overwrite and Skip buttons instead of silently creating a duplicate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
de38c8e1a3
commit
d1b44e9b1e
|
|
@ -67,6 +67,43 @@
|
||||||
border-color: var(--error);
|
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 {
|
.result-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -249,9 +286,7 @@ function formatBytes(bytes) {
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadFile(file) {
|
function uploadFile(file, action) {
|
||||||
const itemId = 'upload-' + Date.now() + Math.random().toString(36).substr(2, 9);
|
|
||||||
|
|
||||||
// Check file size before upload (skip if unlimited)
|
// Check file size before upload (skip if unlimited)
|
||||||
if (maxSize > 0 && file.size > maxSize) {
|
if (maxSize > 0 && file.size > maxSize) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
|
|
@ -266,10 +301,15 @@ function uploadFile(file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create result item
|
// Create or reuse result item
|
||||||
const item = document.createElement('div');
|
let item = action ? document.getElementById('dup-' + file.name) : null;
|
||||||
item.className = 'result-item';
|
if (!item) {
|
||||||
item.id = itemId;
|
item = document.createElement('div');
|
||||||
|
item.className = 'result-item';
|
||||||
|
results.insertBefore(item, results.firstChild);
|
||||||
|
} else {
|
||||||
|
item.className = 'result-item';
|
||||||
|
}
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="result-info">
|
<div class="result-info">
|
||||||
<h3>${file.name}</h3>
|
<h3>${file.name}</h3>
|
||||||
|
|
@ -277,11 +317,10 @@ function uploadFile(file) {
|
||||||
<div class="progress-bar"><div class="progress" style="width: 0%"></div></div>
|
<div class="progress-bar"><div class="progress" style="width: 0%"></div></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
results.insertBefore(item, results.firstChild);
|
|
||||||
|
|
||||||
// Upload
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
if (action) formData.append('action', action);
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
|
@ -306,6 +345,32 @@ function uploadFile(file) {
|
||||||
<button class="copy-btn" onclick="copyLink(this, '${data.share.url}')">Copy</button>
|
<button class="copy-btn" onclick="copyLink(this, '${data.share.url}')">Copy</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
} 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 = `
|
||||||
|
<div class="result-info">
|
||||||
|
<h3>${file.name}</h3>
|
||||||
|
<div class="meta">"${data.existing_file.title}" already exists (${existingSize})</div>
|
||||||
|
</div>
|
||||||
|
<div class="duplicate-actions">
|
||||||
|
<button class="btn-overwrite" data-action="overwrite">Overwrite</button>
|
||||||
|
<button class="btn-skip" data-action="skip">Skip</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<div class="result-info">
|
||||||
|
<h3>${file.name}</h3>
|
||||||
|
<div class="meta">Skipped (duplicate)</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
let errorMsg = 'Upload failed';
|
let errorMsg = 'Upload failed';
|
||||||
try {
|
try {
|
||||||
|
|
@ -314,14 +379,16 @@ function uploadFile(file) {
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
item.classList.add('error');
|
item.classList.add('error');
|
||||||
item.querySelector('.meta').textContent = errorMsg;
|
item.querySelector('.meta').textContent = errorMsg;
|
||||||
item.querySelector('.progress-bar').remove();
|
const pb = item.querySelector('.progress-bar');
|
||||||
|
if (pb) pb.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener('error', () => {
|
xhr.addEventListener('error', () => {
|
||||||
item.classList.add('error');
|
item.classList.add('error');
|
||||||
item.querySelector('.meta').textContent = 'Upload failed - network 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" %}');
|
xhr.open('POST', '{% url "shared_space_upload" %}');
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,32 @@ class SharedSpaceUploadAPIView(View):
|
||||||
'error': f'File too large. Maximum size is {space.max_file_size_mb}MB'
|
'error': f'File too large. Maximum size is {space.max_file_size_mb}MB'
|
||||||
}, status=400)
|
}, 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
|
title = request.POST.get('title', '') or uploaded_file.name
|
||||||
description = request.POST.get('description', '')
|
description = request.POST.get('description', '')
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue