download archive zip
This commit is contained in:
@@ -68,6 +68,7 @@ History + RAG:
|
|||||||
|
|
||||||
Playback:
|
Playback:
|
||||||
- `GET /api/posts/<post_id>/audio-url?user_id=<id>` (required for private posts)
|
- `GET /api/posts/<post_id>/audio-url?user_id=<id>` (required for private posts)
|
||||||
|
- `GET /api/posts/<post_id>/archive.zip?user_id=<id>` (download archive package; required for private posts)
|
||||||
|
|
||||||
Post data:
|
Post data:
|
||||||
- `GET /api/posts`
|
- `GET /api/posts`
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -4,16 +4,18 @@ Includes auth, upload+transcription, history, and RAG search workflow.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import re
|
import re
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from faster_whisper import WhisperModel
|
from faster_whisper import WhisperModel
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request, send_file
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
@@ -23,6 +25,8 @@ from db_queries import (
|
|||||||
add_rag_chunks,
|
add_rag_chunks,
|
||||||
create_audio_post,
|
create_audio_post,
|
||||||
create_user,
|
create_user,
|
||||||
|
download_storage_object_by_stored_path,
|
||||||
|
get_archive_file_by_role,
|
||||||
get_archive_metadata,
|
get_archive_metadata,
|
||||||
get_original_audio_url,
|
get_original_audio_url,
|
||||||
get_archive_rights,
|
get_archive_rights,
|
||||||
@@ -529,6 +533,64 @@ def api_post_audio_url(post_id: int):
|
|||||||
return _error(str(e), 500)
|
return _error(str(e), 500)
|
||||||
|
|
||||||
|
|
||||||
|
@api.get("/posts/<int:post_id>/archive.zip")
|
||||||
|
def api_post_archive_zip(post_id: int):
|
||||||
|
"""
|
||||||
|
Download a complete archive zip for a post.
|
||||||
|
Private posts require owner user_id in query params.
|
||||||
|
"""
|
||||||
|
row = get_audio_post_by_id(post_id)
|
||||||
|
if not row:
|
||||||
|
return _error("Post not found.", 404)
|
||||||
|
|
||||||
|
visibility = row.get("visibility")
|
||||||
|
owner_id = row.get("user_id")
|
||||||
|
requester_id = request.args.get("user_id", type=int)
|
||||||
|
|
||||||
|
if visibility == "private" and requester_id != owner_id:
|
||||||
|
return _error("Not authorized to download this private archive.", 403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
bundle = get_post_bundle(post_id)
|
||||||
|
if not bundle:
|
||||||
|
return _error("Post bundle not found.", 404)
|
||||||
|
|
||||||
|
archive_buf = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(archive_buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
# Always include core JSON artifacts.
|
||||||
|
zf.writestr("post.json", json.dumps(bundle.get("post", {}), indent=2, default=str))
|
||||||
|
zf.writestr("metadata.json", json.dumps(bundle.get("metadata", {}), indent=2, default=str))
|
||||||
|
zf.writestr("rights.json", json.dumps(bundle.get("rights", {}), indent=2, default=str))
|
||||||
|
zf.writestr("rag_chunks.json", json.dumps(bundle.get("rag_chunks", []), indent=2, default=str))
|
||||||
|
zf.writestr("audit_log.json", json.dumps(bundle.get("audit_log", []), indent=2, default=str))
|
||||||
|
|
||||||
|
# Derive transcript from rag chunks.
|
||||||
|
chunks = bundle.get("rag_chunks", []) or []
|
||||||
|
transcript_text = " ".join(
|
||||||
|
(chunk.get("text") or "").strip() for chunk in chunks if chunk.get("text")
|
||||||
|
).strip()
|
||||||
|
if transcript_text:
|
||||||
|
zf.writestr("transcript.txt", transcript_text)
|
||||||
|
|
||||||
|
# Include original media from Supabase Storage if present.
|
||||||
|
original_file = get_archive_file_by_role(post_id, "original_audio")
|
||||||
|
if original_file and original_file.get("path"):
|
||||||
|
original_bytes = download_storage_object_by_stored_path(original_file["path"])
|
||||||
|
source_name = original_file["path"].split("/")[-1] or f"post_{post_id}_original.bin"
|
||||||
|
zf.writestr(f"original/{source_name}", original_bytes)
|
||||||
|
|
||||||
|
archive_buf.seek(0)
|
||||||
|
download_name = f"voicevault_post_{post_id}_archive.zip"
|
||||||
|
return send_file(
|
||||||
|
archive_buf,
|
||||||
|
mimetype="application/zip",
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=download_name,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return _error(f"Failed to build archive zip: {e}", 500)
|
||||||
|
|
||||||
|
|
||||||
@api.post("/posts/<int:post_id>/files")
|
@api.post("/posts/<int:post_id>/files")
|
||||||
def api_add_file(post_id: int):
|
def api_add_file(post_id: int):
|
||||||
payload = request.get_json(force=True, silent=False) or {}
|
payload = request.get_json(force=True, silent=False) or {}
|
||||||
|
|||||||
@@ -111,6 +111,30 @@ def get_original_audio_url(post_id: int, expires_in: int = 3600) -> Dict[str, An
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_archive_file_by_role(post_id: int, role: str) -> Optional[Dict[str, Any]]:
|
||||||
|
response = (
|
||||||
|
supabase.table("archive_files")
|
||||||
|
.select("*")
|
||||||
|
.eq("post_id", post_id)
|
||||||
|
.eq("role", role)
|
||||||
|
.limit(1)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
return _first(response)
|
||||||
|
|
||||||
|
|
||||||
|
def download_storage_object_by_stored_path(stored_path: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Download object bytes from a stored path like
|
||||||
|
'archives/user/uuid/original/file.mp4'.
|
||||||
|
"""
|
||||||
|
bucket, object_path = _parse_bucket_path(stored_path)
|
||||||
|
content = supabase.storage.from_(bucket).download(object_path)
|
||||||
|
if isinstance(content, (bytes, bytearray)):
|
||||||
|
return bytes(content)
|
||||||
|
raise RuntimeError("Failed to download storage object content.")
|
||||||
|
|
||||||
|
|
||||||
# ==================== Users ====================
|
# ==================== Users ====================
|
||||||
|
|
||||||
def create_user(payload: Dict[str, Any]) -> Dict[str, Any]:
|
def create_user(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
|||||||
Reference in New Issue
Block a user