Normal download streaming added.
This commit is contained in:
@@ -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
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user