v0.3.2: add GET /api/artifacts/{id}/raw for content read-back
Metadata (GET /api/artifacts/{id}) stays metadata-only; the full content now
has its own endpoint so an agent can verify exactly what it posted.
- New ArtifactRawView at /api/artifacts/{id}/raw: bearer-or-session auth,
owner-scoped. Serves exact source text (markdown/html_full/json/csv), file
bytes (image/pdf/binary), or 404 for render-only kinds (html_fragment/deck).
Reuses raw_artifact_path/safe_artifact_path.
- serialize_artifact exposes api_raw_url; /api/capabilities advertises the
route. (The old /a/{id}?raw= viewer stays session-only for browsers.)
- Docs + tests (exact content, owner-scoping, 403 unauth, 404 render-only).
Suite: 136 passing; verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2b901f7844
commit
f6f004c689
6 changed files with 102 additions and 15 deletions
|
|
@ -19,6 +19,7 @@ urlpatterns = [
|
|||
path("api/keys/<int:key_id>", views.ApiKeyDetailView.as_view(), name="api-key-detail"),
|
||||
path("api/artifacts", views.ArtifactListCreateView.as_view(), name="artifact-api"),
|
||||
path("api/artifacts/<str:artifact_id>", views.ArtifactDetailView.as_view(), name="artifact-detail-api"),
|
||||
path("api/artifacts/<str:artifact_id>/raw", views.ArtifactRawView.as_view(), name="artifact-raw-api"),
|
||||
path("settings/", views.settings_home, name="settings-home"),
|
||||
path("settings/account/", views.settings_account, name="settings-account"),
|
||||
path("settings/api-keys/", views.settings_api_keys, name="settings-api-keys"),
|
||||
|
|
|
|||
|
|
@ -303,6 +303,37 @@ class ArtifactDetailView(APIView):
|
|||
return Response(status=204)
|
||||
|
||||
|
||||
class ArtifactRawView(APIView):
|
||||
"""The artifact's full content, for read-back / verification. Same auth as
|
||||
the detail endpoint (bearer token or session), owner-scoped.
|
||||
|
||||
GET /api/artifacts/{id}/raw
|
||||
- markdown / html_full / json / csv -> exact posted source (text/plain)
|
||||
- image / pdf / binary -> the stored file bytes
|
||||
- html_fragment / deck -> 404 (render-only; no raw kept)
|
||||
"""
|
||||
|
||||
def get(self, request, artifact_id):
|
||||
user = resolve_request_user(request)
|
||||
if user is None:
|
||||
return Response({"detail": "Authentication required (session or bearer token)."}, status=403)
|
||||
artifact = get_object_or_404(Artifact, pk=artifact_id, owner=user)
|
||||
|
||||
raw_path = raw_artifact_path(artifact)
|
||||
if raw_path and raw_path.exists():
|
||||
return FileResponse(raw_path.open("rb"), content_type="text/plain; charset=utf-8")
|
||||
|
||||
if artifact.kind in {Artifact.IMAGE, Artifact.PDF, Artifact.BINARY}:
|
||||
path = safe_artifact_path(artifact.path)
|
||||
if path.exists():
|
||||
content_type, _ = mimetypes.guess_type(path.name)
|
||||
return FileResponse(path.open("rb"), content_type=content_type or "application/octet-stream")
|
||||
|
||||
return Response(
|
||||
{"detail": "No raw content available for this kind (render-only)."}, status=404
|
||||
)
|
||||
|
||||
|
||||
class CapabilitiesView(APIView):
|
||||
"""Static capability info. Reachable by a logged-in session or a valid API token,
|
||||
so agents can check connectivity before posting. Exposes no user data."""
|
||||
|
|
@ -314,6 +345,7 @@ class CapabilitiesView(APIView):
|
|||
methods = {
|
||||
"/api/artifacts": ["GET", "POST"],
|
||||
"/api/artifacts/{id}": ["GET", "PATCH", "DELETE"],
|
||||
"/api/artifacts/{id}/raw": ["GET"],
|
||||
"/api/assets": ["POST"],
|
||||
"/api/capabilities": ["GET"],
|
||||
"/api/skill": ["GET"],
|
||||
|
|
@ -1274,6 +1306,7 @@ def serialize_artifact(artifact, request):
|
|||
"url": request.build_absolute_uri(reverse("artifact", args=[artifact.id])),
|
||||
"api_url": request.build_absolute_uri(reverse("artifact-detail-api", args=[artifact.id])),
|
||||
"raw_url": request.build_absolute_uri(f"{reverse('artifact', args=[artifact.id])}?raw=1"),
|
||||
"api_raw_url": request.build_absolute_uri(reverse("artifact-raw-api", args=[artifact.id])),
|
||||
"raw_available": raw_artifact_path(artifact) is not None,
|
||||
"source": artifact.source,
|
||||
"title": artifact.title,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue