Normal download streaming added.

This commit is contained in:
Michael Woods
2025-12-26 00:16:58 -05:00
parent 7d01d24196
commit 0c75e9ebbc

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from fastapi.responses import PlainTextResponse, Response, JSONResponse
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Header
from fastapi.responses import PlainTextResponse, Response, JSONResponse, StreamingResponse
from typing import List, Optional
from datetime import datetime
from uuid import UUID
@@ -9,6 +9,7 @@ from traceback import format_exc
import base64
import traceback
from pydantic import BaseModel, model_validator
import re
from packetserver.http.dependencies import get_current_http_user
from packetserver.http.auth import HttpUser
@@ -486,4 +487,65 @@ async def get_object_binary(
private=obj.private,
created_at=obj.created_at,
modified_at=obj.modified_at
)
# Helper to sanitize filename for Content-Disposition
def sanitize_filename(filename: str) -> str:
# Remove path separators and control chars
filename = re.sub(r'[<>:"/\\|?*\x00-\x1F]', '_', filename)
return filename or "download"
@router.get("/objects/{uuid}/download")
async def download_object(
uuid: UUID,
db: DbDependency,
current_user: HttpUser = Depends(get_current_http_user),
accept: str = Header(None) # Optional: for future content negotiation if needed
):
username = current_user.username
try:
with db.transaction() as conn:
root = conn.root()
obj = Object.get_object_by_uuid(uuid, root)
if not obj:
raise HTTPException(status_code=404, detail="Object not found")
# Authorization check for private objects
if obj.private:
user = User.get_user_by_username(username, root)
if not user or user.uuid != obj.owner:
raise HTTPException(status_code=403, detail="Not authorized to access this private object")
# Get content as bytes
content_bytes = obj.data_bytes
# Guess content type
content_type, _ = mimetypes.guess_type(obj.name)
if content_type is None:
content_type = "application/octet-stream" if obj.binary else "text/plain"
# Sanitize filename for header
safe_filename = sanitize_filename(obj.name)
# Headers for download
headers = {
"Content-Disposition": f'attachment; filename="{safe_filename}"',
"Content-Length": str(obj.size),
}
logging.info(f"User {username} downloaded object {uuid} ({obj.name}) via streaming")
except HTTPException:
raise
except Exception as e:
logging.error(f"Download failed for {username} on {uuid}: {e}\n{traceback.format_exc()}")
raise HTTPException(status_code=500, detail="Failed to stream object")
# Stream the bytes directly (efficient, no full load in memory beyond ZODB)
return StreamingResponse(
iter([content_bytes]), # single chunk since ZODB objects are usually small-ish
media_type=content_type,
headers=headers
)