HR-ATS-Portal/backend/s3/views.py

111 lines
4.1 KiB
Python

"""S3 storage service — private objects; auth download / short-lived open URLs."""
from pathlib import Path
from fastapi import HTTPException,UploadFile
from fastapi.responses import Response
from s3.plugins import S3,S3ServiceError,assert_pdf
from s3.serializers import serialize_delete,serialize_health,serialize_open,serialize_upload
class S3Storage:
"""No DB session — pure object storage against the configured private bucket."""
def __init__(self):
self.s3=S3()
def _map(self,exc:S3ServiceError):
raise HTTPException(status_code=exc.status_code,detail=exc.message)
async def health_check(self):
try:
return serialize_health(self.s3.head_bucket())
except S3ServiceError as e:
self._map(e)
async def upload_for_record(self,file:UploadFile,source,record_id,owner_id):
"""PDF gate → PutObject under {source}/{record_id}/{owner_id}/{name}.pdf."""
if file is None:
raise HTTPException(status_code=422,detail="file is required")
filename=(file.filename or "").strip() or "resume.pdf"
try:
assert_pdf(filename,file.content_type)
except S3ServiceError as e:
self._map(e)
body=await file.read()
if not body:
raise HTTPException(status_code=422,detail="file is empty")
try:
result=self.s3.upload_for_record(
body,
filename,
source=source,
record_id=record_id,
owner_id=owner_id,
content_type=file.content_type,
)
return serialize_upload(result)
except S3ServiceError as e:
self._map(e)
async def upload_bytes_for_record(self,body,filename,source,record_id,owner_id,content_type=None):
try:
assert_pdf(filename or "resume.pdf",content_type)
result=self.s3.upload_for_record(
body,
filename or "resume.pdf",
source=source,
record_id=record_id,
owner_id=owner_id,
content_type=content_type,
)
return serialize_upload(result)
except S3ServiceError as e:
self._map(e)
async def delete_file(self,key):
if not key or not str(key).strip():
raise HTTPException(status_code=422,detail="key is required")
try:
return serialize_delete(self.s3.delete_object(str(key).strip()))
except S3ServiceError as e:
self._map(e)
async def object_url(self,key):
"""Stable DB identity address (private — not for anonymous open)."""
if not key or not str(key).strip():
raise HTTPException(status_code=422,detail="key is required")
try:
raw=str(key).strip()
object_key=self.s3.key_from_url(raw)
url=self.s3.object_url(object_key)
return {"key":object_key,"url":url,"access":"private"}
except S3ServiceError as e:
self._map(e)
async def open_url(self,key,expires_in=None):
"""Short-lived presigned GET — use this when a recruiter needs to open the CV."""
if not key or not str(key).strip():
raise HTTPException(status_code=422,detail="key is required")
try:
return serialize_open(self.s3.presigned_get_url(str(key).strip(),expires_in=expires_in))
except S3ServiceError as e:
self._map(e)
async def download_file(self,key):
"""Authenticated stream of a private PDF (no public bucket needed)."""
if not key or not str(key).strip():
raise HTTPException(status_code=422,detail="key is required")
try:
object_key=self.s3.key_from_url(str(key).strip())
body=self.s3.download_bytes(object_key)
name=Path(object_key).name or "resume.pdf"
return Response(
content=body,
media_type="application/pdf",
headers={"Content-Disposition":f'inline; filename="{name}"'},
)
except S3ServiceError as e:
self._map(e)