From 6585beed20304c66a289faeb00a113725d955b4e Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Sun, 22 Mar 2026 12:08:44 +0000 Subject: [PATCH] Add download functionality for items and images with presigned URLs --- docs/PHASE_2_EMBEDDING_PIPELINE.md | 2 +- .../templates/library/collection_detail.html | 1 + .../templates/library/item_detail.html | 18 +++++-- mnemosyne/library/urls.py | 3 ++ mnemosyne/library/views.py | 50 +++++++++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/docs/PHASE_2_EMBEDDING_PIPELINE.md b/docs/PHASE_2_EMBEDDING_PIPELINE.md index a69eb73..3589909 100644 --- a/docs/PHASE_2_EMBEDDING_PIPELINE.md +++ b/docs/PHASE_2_EMBEDDING_PIPELINE.md @@ -493,6 +493,6 @@ All tests use Django `TestCase`. External services (LLM APIs, Neo4j) are mocked. - [ ] Celery tasks handle async embedding with progress tracking - [ ] Re-embedding works (delete old chunks, re-process) - [ ] Content hash prevents redundant re-embedding -- [ ] Prometheus metrics exposed at `/metrics` for pipeline monitoring +- [x] Prometheus metrics exposed at `/metrics` for pipeline monitoring - [ ] All tests pass with mocked LLM/embedding APIs - [ ] Bedrock embedding works via Bearer token HTTP (no boto3) diff --git a/mnemosyne/library/templates/library/collection_detail.html b/mnemosyne/library/templates/library/collection_detail.html index 3c83b31..44e5cc8 100644 --- a/mnemosyne/library/templates/library/collection_detail.html +++ b/mnemosyne/library/templates/library/collection_detail.html @@ -56,6 +56,7 @@ {{ item.file_type|default:"-" }} View + {% if item.s3_key %}↓ Download{% endif %} Edit diff --git a/mnemosyne/library/templates/library/item_detail.html b/mnemosyne/library/templates/library/item_detail.html index 0be6645..9e57be4 100644 --- a/mnemosyne/library/templates/library/item_detail.html +++ b/mnemosyne/library/templates/library/item_detail.html @@ -15,6 +15,11 @@ {% if item.file_type %}
{{ item.file_type }}
{% endif %}
+ {% if item.s3_key %} + + ↓ Download + + {% endif %} Edit
{% csrf_token %} @@ -94,15 +99,22 @@

Images ({{ images|length }})

diff --git a/mnemosyne/library/urls.py b/mnemosyne/library/urls.py index 4959253..4b3d281 100644 --- a/mnemosyne/library/urls.py +++ b/mnemosyne/library/urls.py @@ -50,7 +50,10 @@ urlpatterns = [ path("items//", views.item_detail, name="item-detail"), path("items//edit/", views.item_edit, name="item-edit"), path("items//reembed/", views.item_reembed, name="item-reembed"), + path("items//download/", views.item_download, name="item-download"), path("items//delete/", views.item_delete, name="item-delete"), + # Image views + path("images//view/", views.image_serve, name="image-serve"), # DRF API path("api/", include("library.api.urls")), ] diff --git a/mnemosyne/library/views.py b/mnemosyne/library/views.py index 518ebe1..97f56e6 100644 --- a/mnemosyne/library/views.py +++ b/mnemosyne/library/views.py @@ -14,6 +14,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.files.base import ContentFile from django.core.files.storage import default_storage +from django.http import Http404 from django.shortcuts import redirect, render from .content_types import get_library_type_config @@ -485,6 +486,55 @@ def item_delete(request, uid): return render(request, "library/item_confirm_delete.html", {"item": item}) +@login_required +def item_download(request, uid): + """Redirect to a presigned/storage URL for the item's original file.""" + try: + from .models import Item + + item = Item.nodes.get(uid=uid) + except Exception: + raise Http404("Item not found.") + + if not item.s3_key: + raise Http404("No file available for this item.") + + try: + url = default_storage.url(item.s3_key) + except Exception as e: + logger.error("Failed to generate download URL for %s: %s", item.s3_key, e) + raise Http404("File not accessible.") + + return redirect(url) + + +# --------------------------------------------------------------------------- +# Image views +# --------------------------------------------------------------------------- + + +@login_required +def image_serve(request, uid): + """Redirect to a presigned/storage URL for an image file.""" + try: + from .models import Image + + img = Image.nodes.get(uid=uid) + except Exception: + raise Http404("Image not found.") + + if not img.s3_key: + raise Http404("No file available for this image.") + + try: + url = default_storage.url(img.s3_key) + except Exception as e: + logger.error("Failed to generate image URL for %s: %s", img.s3_key, e) + raise Http404("Image not accessible.") + + return redirect(url) + + # --------------------------------------------------------------------------- # Embedding Pipeline Dashboard # ---------------------------------------------------------------------------