diff --git a/app/replication.py b/app/replication.py index 884bbc3..4703bf9 100644 --- a/app/replication.py +++ b/app/replication.py @@ -306,6 +306,12 @@ class ReplicationManager: if self._shutdown: return + # Re-check if rule is still enabled (may have been paused after task was submitted) + current_rule = self.get_rule(bucket_name) + if not current_rule or not current_rule.enabled: + logger.debug(f"Replication skipped for {bucket_name}/{object_key}: rule disabled or removed") + return + if ".." in object_key or object_key.startswith("/") or object_key.startswith("\\"): logger.error(f"Invalid object key in replication (path traversal attempt): {object_key}") return diff --git a/app/storage.py b/app/storage.py index 13b751f..702d5bb 100644 --- a/app/storage.py +++ b/app/storage.py @@ -1052,9 +1052,10 @@ class ObjectStorage: pass shutil.rmtree(upload_root, ignore_errors=True) - + self._invalidate_bucket_stats_cache(bucket_id) - + self._invalidate_object_cache(bucket_id) + stat = destination.stat() return ObjectMeta( key=safe_key.as_posix(), diff --git a/templates/bucket_detail.html b/templates/bucket_detail.html index 6db7a77..f9832e1 100644 --- a/templates/bucket_detail.html +++ b/templates/bucket_detail.html @@ -3801,41 +3801,40 @@ selectAllCheckbox?.addEventListener('change', (event) => { const shouldSelect = Boolean(event.target?.checked); - - if (hasFolders()) { - const objectsInCurrentView = allObjects.filter(obj => obj.key.startsWith(currentPrefix)); - objectsInCurrentView.forEach(obj => { - const checkbox = obj.element.querySelector('[data-object-select]'); - if (checkbox && !checkbox.disabled) { - checkbox.checked = shouldSelect; - } - toggleRowSelection(obj.element, shouldSelect); - }); + // Get all file items in the current view (works with virtual scrolling) + const filesInView = visibleItems.filter(item => item.type === 'file'); - document.querySelectorAll('[data-folder-select]').forEach(cb => { - cb.checked = shouldSelect; - }); - } else { + // Update selectedRows directly using object keys (not DOM elements) + filesInView.forEach(item => { + if (shouldSelect) { + selectedRows.set(item.data.key, item.data); + } else { + selectedRows.delete(item.data.key); + } + }); - document.querySelectorAll('[data-object-row]').forEach((row) => { - if (row.style.display === 'none') return; - const checkbox = row.querySelector('[data-object-select]'); - if (!checkbox || checkbox.disabled) { - return; - } + // Update folder checkboxes in DOM (folders are always rendered) + document.querySelectorAll('[data-folder-select]').forEach(cb => { + cb.checked = shouldSelect; + }); + + // Update any currently rendered object checkboxes + document.querySelectorAll('[data-object-row]').forEach((row) => { + const checkbox = row.querySelector('[data-object-select]'); + if (checkbox) { checkbox.checked = shouldSelect; - toggleRowSelection(row, shouldSelect); - }); - } + } + }); + + updateBulkDeleteState(); setTimeout(updateBulkDownloadState, 0); }); bulkDownloadButton?.addEventListener('click', async () => { if (!bulkDownloadEndpoint) return; - const selected = Array.from(document.querySelectorAll('[data-object-select]:checked')).map( - (cb) => cb.closest('tr').dataset.key - ); + // Use selectedRows which tracks all selected objects (not just rendered ones) + const selected = Array.from(selectedRows.keys()); if (selected.length === 0) return; bulkDownloadButton.disabled = true;