MyFSIO v0.4.1 Release #34
39
app/ui.py
39
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)
|
return _respond(False, f"A maximum of {MAX_KEYS} objects can be deleted per request", status_code=400)
|
||||||
|
|
||||||
unique_keys = list(dict.fromkeys(cleaned))
|
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:
|
try:
|
||||||
_authorize_ui(principal, bucket_name, "delete")
|
_authorize_ui(principal, bucket_name, "delete")
|
||||||
except IamError as exc:
|
except IamError as exc:
|
||||||
@@ -1093,13 +1114,17 @@ def bulk_delete_objects(bucket_name: str):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
client = get_session_s3_client()
|
client = get_session_s3_client()
|
||||||
objects_to_delete = [{"Key": k} for k in unique_keys]
|
deleted = []
|
||||||
resp = client.delete_objects(
|
errors = []
|
||||||
Bucket=bucket_name,
|
for i in range(0, len(unique_keys), 1000):
|
||||||
Delete={"Objects": objects_to_delete, "Quiet": False},
|
batch = unique_keys[i:i + 1000]
|
||||||
)
|
objects_to_delete = [{"Key": k} for k in batch]
|
||||||
deleted = [d["Key"] for d in resp.get("Deleted", [])]
|
resp = client.delete_objects(
|
||||||
errors = [{"key": e["Key"], "error": e.get("Message", e.get("Code", "Unknown error"))} for e in resp.get("Errors", [])]
|
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:
|
for key in deleted:
|
||||||
_replication_manager().trigger_replication(bucket_name, key, action="delete")
|
_replication_manager().trigger_replication(bucket_name, key, action="delete")
|
||||||
except (ClientError, EndpointConnectionError, ConnectionClosedError) as exc:
|
except (ClientError, EndpointConnectionError, ConnectionClosedError) as exc:
|
||||||
|
|||||||
@@ -867,6 +867,11 @@
|
|||||||
const checkbox = row.querySelector('[data-folder-select]');
|
const checkbox = row.querySelector('[data-folder-select]');
|
||||||
checkbox?.addEventListener('change', (e) => {
|
checkbox?.addEventListener('change', (e) => {
|
||||||
e.stopPropagation();
|
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));
|
const folderObjects = allObjects.filter(obj => obj.key.startsWith(folderPath));
|
||||||
folderObjects.forEach(obj => {
|
folderObjects.forEach(obj => {
|
||||||
if (checkbox.checked) {
|
if (checkbox.checked) {
|
||||||
@@ -1351,8 +1356,11 @@
|
|||||||
}
|
}
|
||||||
if (selectAllCheckbox) {
|
if (selectAllCheckbox) {
|
||||||
const filesInView = visibleItems.filter(item => item.type === 'file');
|
const filesInView = visibleItems.filter(item => item.type === 'file');
|
||||||
const total = filesInView.length;
|
const foldersInView = visibleItems.filter(item => item.type === 'folder');
|
||||||
const visibleSelectedCount = filesInView.filter(item => selectedRows.has(item.data.key)).length;
|
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.disabled = total === 0;
|
||||||
selectAllCheckbox.checked = visibleSelectedCount > 0 && visibleSelectedCount === total && total > 0;
|
selectAllCheckbox.checked = visibleSelectedCount > 0 && visibleSelectedCount === total && total > 0;
|
||||||
selectAllCheckbox.indeterminate = visibleSelectedCount > 0 && visibleSelectedCount < total;
|
selectAllCheckbox.indeterminate = visibleSelectedCount > 0 && visibleSelectedCount < total;
|
||||||
@@ -1374,8 +1382,12 @@
|
|||||||
const keys = Array.from(selectedRows.keys());
|
const keys = Array.from(selectedRows.keys());
|
||||||
bulkDeleteList.innerHTML = '';
|
bulkDeleteList.innerHTML = '';
|
||||||
if (bulkDeleteCount) {
|
if (bulkDeleteCount) {
|
||||||
const label = keys.length === 1 ? 'object' : 'objects';
|
const folderCount = keys.filter(k => k.endsWith('/')).length;
|
||||||
bulkDeleteCount.textContent = `${keys.length} ${label} selected`;
|
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) {
|
if (!keys.length) {
|
||||||
const empty = document.createElement('li');
|
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 => {
|
document.querySelectorAll('[data-folder-select]').forEach(cb => {
|
||||||
cb.checked = shouldSelect;
|
cb.checked = shouldSelect;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user