Skip to content

Commit 72a4ce4

Browse files
authored
feat: serve demo from S3 via /blob (lazy seed) (#58)
Demo doesn't go through the worker, so the previous S3 PR (#57) didn't help it — /demo/data was still ~25s. - Move demo handling into /blob (the new endpoint), not /data (the endpoint we're deprecating). - /blob/demo: no auth, lazy-seeds S3 from generate_demo_database() on the first call after deploy, returns a presigned URL with iv=null. - /blob/<real-package>: unchanged (auth + DB row + presigned URL with iv). - /data left fully untouched. - blob_storage.upload() replaces the misleadingly-named upload_encrypted; worker call site updated.
1 parent a37cbbc commit 72a4ce4

3 files changed

Lines changed: 35 additions & 25 deletions

File tree

src/app.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -235,51 +235,60 @@ def get_package_data(package_id):
235235

236236
@app.route('/process/<package_id>/blob', methods=['GET'])
237237
def get_package_blob(package_id):
238-
"""Return a presigned S3 URL the client can fetch the encrypted blob from.
238+
"""Return a presigned S3 URL the client can fetch the package blob from.
239239
240-
Pairs with /data: same auth, same lookup, but instead of streaming the
241-
decrypted SQLite through API Gateway (slow + 6MB cap), the client
242-
downloads encrypted bytes directly from S3 and decrypts them locally
240+
Replaces /data. The client downloads bytes directly from S3 (fast, no
241+
API Gateway middleman) and — for real packages — decrypts them locally
243242
using its UPN.
244243
245-
Response: {"url": <presigned GET>, "iv": <hex>, "ttl": <seconds>}
246-
Errors: 401 (bad bearer), 404 (no row), 409 (row exists but no S3 blob).
244+
Response: {"url": <presigned GET>, "iv": <hex|null>, "ttl": <seconds>}
245+
iv is null for the demo (the demo blob is unencrypted).
246+
247+
Errors: 401 (bad bearer), 404 (no row / no S3 blob), 501 (S3 not wired).
247248
"""
248249
import os
249250
import blob_storage
250251
from db import SavedPackageData
251252

253+
if not blob_storage.is_enabled():
254+
# /blob requires S3. Local dev should use /data.
255+
return jsonify({'errorMessageCode': 'S3_NOT_CONFIGURED'}), 501
256+
257+
ttl = int(os.getenv('PACKAGE_DATA_PRESIGNED_URL_TTL_SECONDS', '300'))
258+
259+
# Demo: unauthenticated, lazy-seed S3 from generate_demo_database() on the
260+
# very first call after deploy, then serve from S3 forever after.
261+
if package_id == 'demo':
262+
if not blob_storage.exists('demo'):
263+
blob_storage.upload('demo', generate_demo_database())
264+
return jsonify({
265+
'url': blob_storage.presigned_url('demo', ttl_seconds=ttl),
266+
'iv': None,
267+
'ttl': ttl,
268+
}), 200
269+
252270
(is_auth, _) = check_authorization_bearer(request, package_id)
253271
if not is_auth:
254272
return make_response('', 401)
255273

256-
if not blob_storage.is_enabled():
257-
# /blob is meaningless without S3 — fall through so callers can
258-
# detect and switch back to /data.
259-
return jsonify({'errorMessageCode': 'S3_NOT_CONFIGURED'}), 501
260-
261274
session = Session()
262275
row = session.query(SavedPackageData).filter_by(package_id=package_id).order_by(
263276
SavedPackageData.created_at.desc()
264277
).first()
265278
session.close()
266279

267-
if not row:
280+
if not row or not blob_storage.exists(package_id):
268281
return make_response('', 404)
269282

270-
if not blob_storage.exists(package_id):
271-
# Row exists in DB but worker hadn't migrated yet (or upload failed).
272-
# Caller can fall back to /data.
273-
return jsonify({'errorMessageCode': 'BLOB_NOT_IN_S3'}), 409
274-
275-
ttl = int(os.getenv('PACKAGE_DATA_PRESIGNED_URL_TTL_SECONDS', '300'))
276-
url = blob_storage.presigned_url(package_id, ttl_seconds=ttl)
277-
278283
iv_value = row.iv
279284
if isinstance(iv_value, (bytes, bytearray)):
280285
iv_value = iv_value.hex()
281286

282-
return jsonify({'url': url, 'iv': iv_value, 'ttl': ttl}), 200
287+
return jsonify({
288+
'url': blob_storage.presigned_url(package_id, ttl_seconds=ttl),
289+
'iv': iv_value,
290+
'ttl': ttl,
291+
}), 200
283292

284293

285294
@app.route('/process/<package_id>', methods=['DELETE'])

src/blob_storage.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ def _key(package_id: str) -> str:
2424
return f"packages/{package_id}.bin"
2525

2626

27-
def upload_encrypted(package_id: str, encrypted_bytes: bytes) -> None:
28-
"""Push the encrypted blob to S3 under a deterministic key."""
27+
def upload(package_id: str, body: bytes) -> None:
28+
"""Push a blob to S3 under a deterministic key. Same operation for the
29+
worker's encrypted SQLite and the demo's unencrypted one."""
2930
import boto3
3031

3132
boto3.client("s3").put_object(
3233
Bucket=_bucket(),
3334
Key=_key(package_id),
34-
Body=encrypted_bytes,
35+
Body=body,
3536
ContentType="application/octet-stream",
3637
)
3738

src/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ def read_analytics_file(package_status_id, package_id, link, session):
882882
import blob_storage
883883
if blob_storage.is_enabled():
884884
try:
885-
blob_storage.upload_encrypted(package_id, data)
885+
blob_storage.upload(package_id, data)
886886
except Exception as e:
887887
print(f'WARN: S3 blob upload failed for {package_id}: {e}')
888888

0 commit comments

Comments
 (0)