Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ def sqlachmey_uri(cls, v: str) -> str:
S3_ENDPOINT_URL: str = os.environ["S3_ENDPOINT_URL"]
S3_PROXY_URL: str = os.environ.get("S3_PROXY_URL", "")
S3_URL_EXPIRATION: int = int(os.environ.get("S3_URL_EXPIRATION") or 24 * 3600)
# Comma-separated browser origins allowed to fetch bucket objects cross-origin (the frontend
# platform URLs). Applied as a CORS policy at bucket creation so the frontend can fetch()
# presigned image URLs (e.g. the "download all" buttons). Deployments MUST set this to the
# real frontend origins; the localhost default only serves local dev.
S3_CORS_ORIGINS: str = os.environ.get("S3_CORS_ORIGINS", "http://localhost:5173")

# Sequence handling: three windows gate sequence behaviour, each on its own timescale.
# All three are tuned against the cameras' frame interval (time between two frames of the
Expand Down
24 changes: 24 additions & 0 deletions src/app/services/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,35 @@ def create_bucket(self, bucket_name: str) -> bool:
else {"CreateBucketConfiguration": {"LocationConstraint": self._s3.meta.region_name}}
)
self._s3.create_bucket(Bucket=bucket_name, **config_)
self._put_bucket_cors(bucket_name)
return True
except ClientError as e:
logger.warning(e)
return False

def _put_bucket_cors(self, bucket_name: str) -> None:
"""Apply the CORS policy so browsers can fetch() presigned URLs cross-origin.

Allows the frontend origins (settings.S3_CORS_ORIGINS) to GET bucket objects, which the
platform's "download all" buttons rely on (native fetch triggers CORS, unlike a plain
<img> or <a download>).
"""
origins = [origin.strip() for origin in settings.S3_CORS_ORIGINS.split(",") if origin.strip()]
self._s3.put_bucket_cors(
Bucket=bucket_name,
CORSConfiguration={
"CORSRules": [
{
"AllowedOrigins": origins,
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["Content-Length", "Content-Type"],
"MaxAgeSeconds": 3000,
}
]
},
)

def get_bucket(self, bucket_name: str) -> S3Bucket:
"""Get an existing bucket in S3 storage"""
return S3Bucket(self._s3, bucket_name, self.proxy_url)
Expand Down
6 changes: 6 additions & 0 deletions src/tests/services/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ async def test_s3_service(region, endpoint_url, access_key, secret_key, proxy_ur
# Create random bucket
bucket_name = "dummy-bucket"
service.create_bucket(bucket_name)
# The CORS policy is applied at creation so the frontend can fetch() presigned URLs
cors_rules = service._s3.get_bucket_cors(Bucket=bucket_name)["CORSRules"]
assert cors_rules[0]["AllowedMethods"] == ["GET", "HEAD"]
assert cors_rules[0]["AllowedOrigins"] == [
origin.strip() for origin in settings.S3_CORS_ORIGINS.split(",") if origin.strip()
]
# Delete the bucket
await service.delete_bucket(bucket_name)
else:
Expand Down