From 8996f1ce060e49f247cc2456dde9e48975331ddf Mon Sep 17 00:00:00 2001 From: kqjy Date: Tue, 24 Mar 2026 12:10:38 +0800 Subject: [PATCH] Fix folder selection not showing delete button in bucket browser --- app/ui.py | 39 +++++++++++++++++++++++++++------ static/js/bucket-detail-main.js | 29 ++++++++++++++++++++---- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/app/ui.py b/app/ui.py index 60d3917..29ffd05 100644 --- a/app/ui.py +++ b/app/ui.py @@ -1063,6 +1063,27 @@ def bulk_delete_objects(bucket_name: str): return _respond(False, f"A maximum of {MAX_KEYS} objects can be deleted per request", status_code=400) unique_keys = list(dict.fromkeys(cleaned)) + + folder_prefixes = [k for k in unique_keys if k.endswith("/")] + if folder_prefixes: + try: + client = get_session_s3_client() + for prefix in folder_prefixes: + unique_keys.remove(prefix) + paginator = client.get_paginator("list_objects_v2") + for page in paginator.paginate(Bucket=bucket_name, Prefix=prefix): + for obj in page.get("Contents", []): + if obj["Key"] not in unique_keys: + unique_keys.append(obj["Key"]) + except (ClientError, EndpointConnectionError, ConnectionClosedError) as exc: + if isinstance(exc, ClientError): + err, status = handle_client_error(exc) + return _respond(False, err["error"], status_code=status) + return _respond(False, "S3 API server is unreachable", status_code=502) + + if not unique_keys: + return _respond(False, "No objects found under the selected folders", status_code=400) + try: _authorize_ui(principal, bucket_name, "delete") except IamError as exc: @@ -1093,13 +1114,17 @@ def bulk_delete_objects(bucket_name: str): else: try: client = get_session_s3_client() - objects_to_delete = [{"Key": k} for k in unique_keys] - resp = client.delete_objects( - Bucket=bucket_name, - Delete={"Objects": objects_to_delete, "Quiet": False}, - ) - deleted = [d["Key"] for d in resp.get("Deleted", [])] - errors = [{"key": e["Key"], "error": e.get("Message", e.get("Code", "Unknown error"))} for e in resp.get("Errors", [])] + deleted = [] + errors = [] + for i in range(0, len(unique_keys), 1000): + batch = unique_keys[i:i + 1000] + objects_to_delete = [{"Key": k} for k in batch] + resp = client.delete_objects( + Bucket=bucket_name, + Delete={"Objects": objects_to_delete, "Quiet": False}, + ) + deleted.extend(d["Key"] for d in resp.get("Deleted", [])) + errors.extend({"key": e["Key"], "error": e.get("Message", e.get("Code", "Unknown error"))} for e in resp.get("Errors", [])) for key in deleted: _replication_manager().trigger_replication(bucket_name, key, action="delete") except (ClientError, EndpointConnectionError, ConnectionClosedError) as exc: diff --git a/static/js/bucket-detail-main.js b/static/js/bucket-detail-main.js index 2ade8e8..9b48d21 100644 --- a/static/js/bucket-detail-main.js +++ b/static/js/bucket-detail-main.js @@ -867,6 +867,11 @@ const checkbox = row.querySelector('[data-folder-select]'); checkbox?.addEventListener('change', (e) => { e.stopPropagation(); + if (checkbox.checked) { + selectedRows.set(folderPath, { key: folderPath, isFolder: true }); + } else { + selectedRows.delete(folderPath); + } const folderObjects = allObjects.filter(obj => obj.key.startsWith(folderPath)); folderObjects.forEach(obj => { if (checkbox.checked) { @@ -1351,8 +1356,11 @@ } if (selectAllCheckbox) { const filesInView = visibleItems.filter(item => item.type === 'file'); - const total = filesInView.length; - const visibleSelectedCount = filesInView.filter(item => selectedRows.has(item.data.key)).length; + const foldersInView = visibleItems.filter(item => item.type === 'folder'); + const total = filesInView.length + foldersInView.length; + const fileSelectedCount = filesInView.filter(item => selectedRows.has(item.data.key)).length; + const folderSelectedCount = foldersInView.filter(item => selectedRows.has(item.path)).length; + const visibleSelectedCount = fileSelectedCount + folderSelectedCount; selectAllCheckbox.disabled = total === 0; selectAllCheckbox.checked = visibleSelectedCount > 0 && visibleSelectedCount === total && total > 0; selectAllCheckbox.indeterminate = visibleSelectedCount > 0 && visibleSelectedCount < total; @@ -1374,8 +1382,12 @@ const keys = Array.from(selectedRows.keys()); bulkDeleteList.innerHTML = ''; if (bulkDeleteCount) { - const label = keys.length === 1 ? 'object' : 'objects'; - bulkDeleteCount.textContent = `${keys.length} ${label} selected`; + const folderCount = keys.filter(k => k.endsWith('/')).length; + const objectCount = keys.length - folderCount; + const parts = []; + if (folderCount) parts.push(`${folderCount} folder${folderCount !== 1 ? 's' : ''}`); + if (objectCount) parts.push(`${objectCount} object${objectCount !== 1 ? 's' : ''}`); + bulkDeleteCount.textContent = `${parts.join(' and ')} selected`; } if (!keys.length) { const empty = document.createElement('li'); @@ -3172,6 +3184,15 @@ } }); + const foldersInView = visibleItems.filter(item => item.type === 'folder'); + foldersInView.forEach(item => { + if (shouldSelect) { + selectedRows.set(item.path, { key: item.path, isFolder: true }); + } else { + selectedRows.delete(item.path); + } + }); + document.querySelectorAll('[data-folder-select]').forEach(cb => { cb.checked = shouldSelect; });