From f06820ced237a81a7f74c6dcf95124540edfdfb2 Mon Sep 17 00:00:00 2001 From: Gaumit Kauts <123269559+Gaumit-Kauts@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:52:26 -0700 Subject: [PATCH] download archive zip --- README.md | 1 + .../__pycache__/api_routes.cpython-311.pyc | Bin 36499 -> 41026 bytes .../__pycache__/db_queries.cpython-311.pyc | Bin 24869 -> 26271 bytes backend/api_routes.py | 64 +++++++++++++++++- backend/db_queries.py | 24 +++++++ 5 files changed, 88 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 35e59b9..da6cbb7 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ History + RAG: Playback: - `GET /api/posts//audio-url?user_id=` (required for private posts) +- `GET /api/posts//archive.zip?user_id=` (download archive package; required for private posts) Post data: - `GET /api/posts` diff --git a/backend/__pycache__/api_routes.cpython-311.pyc b/backend/__pycache__/api_routes.cpython-311.pyc index e9bccd33d754040edb8467ab4f6c571335c5e642..2a83a733e683b7f7e7d4863cc416d2ed8fccd427 100644 GIT binary patch delta 10523 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νsQ-`X$av&1VvGJ7LM_}y;`JifCq+0zN+;NjRdv6!9#sK6x4VZIgot- zKJ$4~t&e4%Y@KSJOU{oa=fjrlqReHPQ>*5zg)wX4tf`Rapeej%$ua*6h0_Vp?=FIW zr^^{^T#86YEqv~m;NkieP+?7n7qKR9d3xh`V#K;Qose)OF|uq)i`bUrA?bS<=;G(W zcv>VCrpk+?NCwQf3K}PwTRI#_!P1oG(81AQXh2m$gZFEJh%$I6qJ@T>xTJ+;vNR`F%@njkXI3GB~w;}B|@lx`rBJdy_oR4cURNq1h zeMU)iOhzGnR0!`zucPE1M3_kUdrCrK)eF1l3hHA8^>e1&iPn?t^R}FrZN)8{;~kr0 zX4jmpGG?p1W%Ivd^IzFFXWJO7vu&Jco3~_4m7Q%lUq4$|HD~d}ES}p5OkOdcmNnJ= z=+RS0XOrynDLJ#bo90rQV=2wEre^(HIdxKYFD}b>6WmA8N6-)N=XJ3jsLyYahmkm? zAJ4Z+)&ZL9Bp89yg@)u zyne=BoQNS_R!Y?nOPVM3dM!j5$urlSsS{8P1n*`L~?~h#xpT^4E}^1-r}1!r;BT zQV+R8EPb59Oc02bN8rPRTcNLk6^N5b*e~#8iYHEPr|DeHJMMM5|;x(ZD3S12L5$0 zSGwe9k$zYARf1$THWY4nReiEUTKMH4k1?&`D^EH@fkjY{Cr@l`lb`EaWDu@~W?YYm8 zd4NE?8?&+KIT-V|%IA?T)ua9bDf>HQu@v99Fz}bVD&-e&X0@)Y@tLxJKt>k{E)o2Y z;4%y~-siqR;xqzw%%?~Of~OJwt7GzSz@HmEtQm@$#$6(_(`5HQ>4`lNJMmq3rO7W} zMXNgfA59NQ1uv4r7ZWA!0zA3CRMt`K1Z7ROesg_EqFLnfH2if_6YB$Cb1*wfrmqsb zM(|65pTLir8=NAEHqx^Z+zT7^W0d129y%C~vjAFVw7BIP$aU-MTE?Zq>tw%_O%zo; z79t;qTWu}!n<$3#;>`|;U5D1K-%g`d?=Yu0z}q$&zsC6^`muJ8Wd13cUW2*1Ecn&-o}!#iYf$7n2)YQm z35E%%2XktxoSL1HF7-2`A3aMFy=9{WdU-}2bhV9Q=vo;S(OL6f5YXxH)wIo`Oiq=F zcP9Qah+nxoNsaqM>R3=)%m0S648P&`^k?qvk@|ZW{uImvZYSVyWw{uxN^GZMaWBLE$mt%(Jd@08{_}MBK_mtHS&J|2Z6`S delta 2051 zcmZWpX>3$g6z+X}_YHI?Q!I9#fmIf*! zq)5u5zzHA~v7iW1RPdpK+Gw!i5=e|uBZEugFB6TyUl0lpj4gz_EWj2)q2gg~IH>53+(9*zZREix zipPcT1-~LwGpD7| z%<_j(V~GK0F-8>_S?Di0tj=+kVhWEm-SDug^D?x(uu1hf{m6XM6Mzvl4I=6!_)Kv- z%Td?IM>s0bF0>1B&Ou%zeRH9i*ElNCE;g3H3%tx#g%g(;j~ID4^+V<~IKlO-B%>PD zQlnm~^Gt4*jTa0f+~=5&e!ty%+5*fLFd8twX3iLo!^ZY&RgM~roQ-w(m6^$^QH!zV zhSHj#FSTxKw_Vmd8384=1y@3}52sRTuNwAS1M$k*p2UopiT8L;D-0}K& zKT@#reWt>;z>t2751{ZCcnU`|DZ9?%DbYx*cTKp@i1e)4+!O1K>516n{lZed0|PEY zpva@q%-t$H^T2d#bI~rAvIT9il|a@M2BoAk)utr6333UxK~G7B>uI6{AK@1}2ceML zx>Ay<#!QJCe=F46@Zea>$Nkt9F-gr)&}13)TJ=>?R!b&rX(Q1Tj>fAO_$&fBDos zXvBGU)C_MDF3Ry7qzAklkbY^^1whg4<%XZjbJ!8MU!JZVrLZHAUE%f8y6Fx<8AUU} zDuUGnQwe0oHL$V5ryWE4w*>20#YAO_+-^B&C#s9ZF1R>1o$ZF}bA#G(RPHBOlPd2h zY#7v_8_^SNoOy`ytt426plkM|!!Q(F%uk}?w7w0dE2$?aVm!ZnoFH*tF+YW#bgN=s zKuvvxf?g#!L+~2GS=d&yS|1_07XkNlAMyPJ2M}V32yVAqOef`eV6~;J82q){by?X# zQvX+;TobtxFQFe^3}(PzwKMtaXk=T-3kQ_+b2RS1^^!IMr|a_hd8Ad=S9O(%nk?r4 zIO^xIB~Vu%O}#>49}s*}Y$dRURM(p*#8uJLAB(eY^mrO{{wZn;tk#C@ zO2#O~kIzima3@F{gx{Cd@oPvstT~MZ3LAyc^3yJw z6#>sfnD`ga*j$rz9jU=^`A$QyvDg}G{+nqR$bAuCfa!3nr8Cna^Af>(2r&iW-(`Lu zmbd!ZR(Phhnq7v`)<@aL;A$(-e>rU2RdtO={d>n7!dhG^VTe36Y^c zphw(<$sNTL-(Te?s4Jt37m{u`y7(3Kk5b5Z6Rx{f6F{kn2p@@yrh3pP&;iP7%=V3R-7DDRjk7 z{|lI1x%^jD&cVK}<26r_PZjbg6K`t5NNJ7Pqa^>FI1}w0>k;z4wX13~xKE}d?hT|b>;y9qj1b|PX%#F@qT0qk9vopXf3AqUL<@Cz=eu*lz36>P|)JVpcR<&i?`F CHtk6O diff --git a/backend/api_routes.py b/backend/api_routes.py index 40887ff..f6d474d 100644 --- a/backend/api_routes.py +++ b/backend/api_routes.py @@ -4,16 +4,18 @@ Includes auth, upload+transcription, history, and RAG search workflow. """ import hashlib +import io import json import os import uuid import re +import zipfile from pathlib import Path from typing import Any, List from dotenv import load_dotenv 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.utils import secure_filename @@ -23,6 +25,8 @@ from db_queries import ( add_rag_chunks, create_audio_post, create_user, + download_storage_object_by_stored_path, + get_archive_file_by_role, get_archive_metadata, get_original_audio_url, get_archive_rights, @@ -529,6 +533,64 @@ def api_post_audio_url(post_id: int): return _error(str(e), 500) +@api.get("/posts//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//files") def api_add_file(post_id: int): payload = request.get_json(force=True, silent=False) or {} diff --git a/backend/db_queries.py b/backend/db_queries.py index b7a1115..441e4fd 100644 --- a/backend/db_queries.py +++ b/backend/db_queries.py @@ -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 ==================== def create_user(payload: Dict[str, Any]) -> Dict[str, Any]: