diff --git a/archivebox/base_models/models.py b/archivebox/base_models/models.py index 296bf741..b50bf521 100755 --- a/archivebox/base_models/models.py +++ b/archivebox/base_models/models.py @@ -284,16 +284,20 @@ class ModelWithOutputDir(ModelWithUUID): elif path.is_dir(): shutil.rmtree(path, ignore_errors=True) + def schedule_delete_cleanup(self, *, using: str | None = None) -> None: + """Capture output paths before DB deletion and remove them after commit.""" + paths = self.validate_output_paths_for_delete(self.output_paths_for_delete()) + transaction.on_commit(lambda: self.delete_output_paths(paths), using=using) + @classmethod def register_delete_signal(cls) -> None: if cls._delete_signal_registered: return - def schedule_output_dir_cleanup(sender, instance, **kwargs): + def schedule_output_dir_cleanup(sender, instance, using, **kwargs): if not isinstance(instance, ModelWithOutputDir): return - paths = instance.validate_output_paths_for_delete(instance.output_paths_for_delete()) - transaction.on_commit(lambda paths=paths: instance.delete_output_paths(paths)) + instance.schedule_delete_cleanup(using=using) pre_delete.connect( schedule_output_dir_cleanup, diff --git a/archivebox/core/models.py b/archivebox/core/models.py index 231afe61..22e730f9 100644 --- a/archivebox/core/models.py +++ b/archivebox/core/models.py @@ -3300,7 +3300,7 @@ class Snapshot(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithConfig, ModelW fallback_ts = ts_to_date_str(self.downloaded_at or self.created_at) for root, root_entries in grouped_hash_outputs.items(): fallback_path = ArchiveResult._fallback_output_file_path(list(root_entries.keys()), root, root_entries) - if not fallback_path: + if not fallback_path or not (snap_dir / root / fallback_path).exists(): continue fallback_meta = root_entries.get(fallback_path, {}) outputs.append( @@ -4112,12 +4112,19 @@ class ArchiveResult(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithNotes): ), ) - def delete(self, *args, **kwargs): + def schedule_delete_cleanup(self, *, using: str | None = None) -> None: + """Remove shared plugin output and refresh persisted Snapshot metadata after commit.""" snapshot_id = self.snapshot_id - deleted = super().delete(*args, **kwargs) - if snapshot_id: - transaction.on_commit(lambda: type(self).refresh_snapshot_output_sizes({snapshot_id})) - return deleted + paths = self.validate_output_paths_for_delete(self.output_paths_for_delete()) + + def cleanup() -> None: + type(self).delete_output_paths(paths) + type(self).refresh_snapshot_output_sizes({snapshot_id}) + snapshot = Snapshot.objects.filter(pk=snapshot_id).first() + if snapshot: + snapshot.write_index_jsonl() + + transaction.on_commit(cleanup, using=using) @staticmethod def refresh_snapshot_output_sizes(snapshot_ids): diff --git a/archivebox/core/views.py b/archivebox/core/views.py index dc6557bf..71d47228 100644 --- a/archivebox/core/views.py +++ b/archivebox/core/views.py @@ -353,10 +353,15 @@ class SnapshotView(View): if (out.get("size") or 0) > 0 and out.get("name") not in hidden_card_plugins ] archiveresults = {} + result_ids_by_name = {} for output in outputs: + if output.get("result"): + result_ids_by_name.setdefault(output["name"], []).append(str(output["result"].id)) current = archiveresults.get(output["name"]) if current is None or (output.get("size") or 0) > (current.get("size") or 0): archiveresults[output["name"]] = output + for name, output in archiveresults.items(): + output["result_ids"] = ",".join(result_ids_by_name.get(name, ())) hash_index = snapshot.hashes_index loose_items, failed_items = snapshot.get_detail_page_auxiliary_items( outputs, @@ -479,6 +484,7 @@ class SnapshotView(View): "related_years": related_years, "loose_items": loose_items, "failed_items": failed_items, + "can_delete_outputs": bool(request.user.is_authenticated and request.user.is_active and request.user.is_superuser), "title_tags": [{"name": tag.name, "style": tag_widget._tag_style(tag.name)} for tag in sorted(tags, key=lambda tag: tag.name)], } return render(template_name="core/snapshot.html", request=request, context=context) diff --git a/archivebox/templates/core/snapshot.html b/archivebox/templates/core/snapshot.html index cbf6449e..80fdbedf 100644 --- a/archivebox/templates/core/snapshot.html +++ b/archivebox/templates/core/snapshot.html @@ -1117,7 +1117,8 @@ line-height: 1; opacity: 0.8; } - .thumb-actions a { + .thumb-actions a, + .thumb-actions button { display: inline-flex; align-items: center; justify-content: center; @@ -1127,11 +1128,20 @@ background: #e7ebef; text-decoration: none; color: inherit; + border: 0; + cursor: pointer; } - .thumb-actions a:hover { + .thumb-actions a:hover, + .thumb-actions button:hover { background: #d9e0e7; opacity: 1; } + .thumb-actions .delete-pending { + width: auto; + min-width: 22px; + color: #b91c1c; + font-weight: 700; + } .thumb-card .thumb-body > a:not(.thumb-actions a), .thumb-card .thumb-body > h4 { grid-column: 1; @@ -1246,11 +1256,15 @@ font-size: 12px; max-height: 24px; } - .thumb-card:has([data-compact]) .thumb-actions a { + .thumb-card:has([data-compact]) .thumb-actions a, + .thumb-card:has([data-compact]) .thumb-actions button { width: 18px; height: 18px; border-radius: 4px; } + .thumb-card:has([data-compact]) .thumb-actions .delete-pending { + width: auto; + } .thumb-card:has([data-compact]) .thumb-body h4 { font-size: 0.9em; margin-bottom: 0px; @@ -1697,6 +1711,9 @@ {% if display_path %} ⬇️ {% endif %} + {% if can_delete_outputs and result.result %} + + {% endif %} {% if display_path %} @@ -1782,12 +1799,80 @@ + {% if can_delete_outputs %}{% endif %}