feat: Enhance media cleanup functionality with loading indicator and detailed results
This commit is contained in:
@@ -43,6 +43,7 @@ def test_media_cleanup_page_for_manager(client, cid_manager_user):
|
|||||||
response = client.get(reverse("media_cleanup"))
|
response = client.get(reverse("media_cleanup"))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"Unused Media Cleanup" in response.content
|
assert b"Unused Media Cleanup" in response.content
|
||||||
|
assert b"Cleanup in progress" in response.content
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -108,3 +109,4 @@ def test_media_cleanup_live_run_disables_dry_run(client, cid_manager_user, monke
|
|||||||
assert captured["kwargs"]["interactive"] is False
|
assert captured["kwargs"]["interactive"] is False
|
||||||
assert b"cleanup ok" in response.content
|
assert b"cleanup ok" in response.content
|
||||||
assert b"Recovered space" in response.content
|
assert b"Recovered space" in response.content
|
||||||
|
assert b"removed" in response.content
|
||||||
|
|||||||
+16
-10
@@ -109,21 +109,21 @@ import psutil
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _directory_size_bytes(path: str) -> int:
|
def _media_file_inventory(path: str) -> dict[str, int]:
|
||||||
"""Return recursive directory size in bytes, skipping unreadable files."""
|
"""Return a mapping of file path to byte size for files under a directory."""
|
||||||
if not path or not os.path.exists(path):
|
if not path or not os.path.exists(path):
|
||||||
return 0
|
return {}
|
||||||
|
|
||||||
total = 0
|
inventory: dict[str, int] = {}
|
||||||
for root, _, files in os.walk(path):
|
for root, _, files in os.walk(path):
|
||||||
for filename in files:
|
for filename in files:
|
||||||
file_path = os.path.join(root, filename)
|
file_path = os.path.join(root, filename)
|
||||||
try:
|
try:
|
||||||
total += os.path.getsize(file_path)
|
inventory[file_path] = os.path.getsize(file_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return total
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
def feedback_checker(user):
|
def feedback_checker(user):
|
||||||
@@ -474,11 +474,10 @@ def media_cleanup(request):
|
|||||||
command_kwargs["exclude"] = exclude
|
command_kwargs["exclude"] = exclude
|
||||||
|
|
||||||
error_message = ""
|
error_message = ""
|
||||||
media_size_before = _directory_size_bytes(settings.MEDIA_ROOT)
|
media_inventory_before = _media_file_inventory(settings.MEDIA_ROOT)
|
||||||
media_size_after = media_size_before
|
media_inventory_after = media_inventory_before
|
||||||
try:
|
try:
|
||||||
call_command("cleanup_unused_media", **command_kwargs)
|
call_command("cleanup_unused_media", **command_kwargs)
|
||||||
media_size_after = _directory_size_bytes(settings.MEDIA_ROOT)
|
|
||||||
except CommandError as exc:
|
except CommandError as exc:
|
||||||
success = False
|
success = False
|
||||||
error_message = str(exc)
|
error_message = str(exc)
|
||||||
@@ -486,8 +485,14 @@ def media_cleanup(request):
|
|||||||
success = False
|
success = False
|
||||||
error_message = str(exc)
|
error_message = str(exc)
|
||||||
|
|
||||||
|
media_inventory_after = _media_file_inventory(settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
media_size_before = sum(media_inventory_before.values())
|
||||||
|
media_size_after = sum(media_inventory_after.values())
|
||||||
|
removed_files = set(media_inventory_before) - set(media_inventory_after)
|
||||||
|
|
||||||
output = out.getvalue().strip()
|
output = out.getvalue().strip()
|
||||||
recovered_size = max(0, media_size_before - media_size_after)
|
recovered_size = sum(media_inventory_before[path] for path in removed_files)
|
||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
"ran": True,
|
"ran": True,
|
||||||
@@ -502,6 +507,7 @@ def media_cleanup(request):
|
|||||||
"media_size_before": media_size_before,
|
"media_size_before": media_size_before,
|
||||||
"media_size_after": media_size_after,
|
"media_size_after": media_size_after,
|
||||||
"recovered_size": recovered_size,
|
"recovered_size": recovered_size,
|
||||||
|
"removed_file_count": len(removed_files),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
<style>
|
||||||
|
.media-cleanup-loading.htmx-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.media-cleanup-form.htmx-request .media-cleanup-loading.htmx-indicator {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.media-cleanup-form.htmx-request button,
|
||||||
|
.media-cleanup-form.htmx-request input,
|
||||||
|
.media-cleanup-form.htmx-request textarea {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
||||||
@@ -17,11 +35,13 @@
|
|||||||
<div class="card border-0 shadow-sm mb-4">
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form
|
<form
|
||||||
|
id="media-cleanup-form"
|
||||||
method="post"
|
method="post"
|
||||||
hx-post="{% url 'media_cleanup' %}"
|
hx-post="{% url 'media_cleanup' %}"
|
||||||
hx-target="#media-cleanup-result"
|
hx-target="#media-cleanup-result"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
class="row g-3"
|
hx-indicator="#media-cleanup-loading"
|
||||||
|
class="row g-3 media-cleanup-form"
|
||||||
>
|
>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
@@ -85,6 +105,11 @@
|
|||||||
<i class="bi bi-trash me-1"></i> Delete unused files
|
<i class="bi bi-trash me-1"></i> Delete unused files
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="media-cleanup-loading" class="col-12 media-cleanup-loading htmx-indicator align-items-center gap-2 text-info">
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></div>
|
||||||
|
<span>Cleanup in progress. This can take a while for large media directories.</span>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<div class="border rounded p-2 small">
|
<div class="border rounded p-2 small">
|
||||||
<div class="text-muted">Recovered space</div>
|
<div class="text-muted">Recovered space</div>
|
||||||
<div><strong>{{ recovered_size|filesizeformat }}</strong></div>
|
<div><strong>{{ recovered_size|filesizeformat }}</strong></div>
|
||||||
|
<div class="text-muted">{{ removed_file_count|default:0 }} file{{ removed_file_count|default:0|pluralize }} removed</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user