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 import APIRouter, Depends, HTTPException, UploadFile, File, Form, Header
from fastapi.responses import PlainTextResponse, Response, JSONResponse from fastapi.responses import PlainTextResponse, Response, JSONResponse, StreamingResponse
from typing import List, Optional from typing import List, Optional
from datetime import datetime from datetime import datetime
from uuid import UUID from uuid import UUID
@@ -9,6 +9,7 @@ from traceback import format_exc
import base64 import base64
import traceback import traceback
from pydantic import BaseModel, model_validator from pydantic import BaseModel, model_validator
import re
from packetserver.http.dependencies import get_current_http_user from packetserver.http.dependencies import get_current_http_user
from packetserver.http.auth import HttpUser from packetserver.http.auth import HttpUser
@@ -487,3 +488,64 @@ async def get_object_binary(
created_at=obj.created_at, created_at=obj.created_at,
modified_at=obj.modified_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
)