Skip to content
Merged
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
2 changes: 2 additions & 0 deletions app/api/wps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from fastapi.responses import Response, StreamingResponse

from app.core.drawings import DrawingsService, DrawingsServiceDep
from app.core.s3 import CACHE_CONTROL_NO_STORE
from app.schemas.drawings import DrawingsCreateResponse, DrawingsUpdateResponse
from app.schemas.errors import ErrorResponse
from app.settings import get_settings
Expand Down Expand Up @@ -94,6 +95,7 @@ async def get_drawing(
media_type=DrawingsService.KMZ_CONTENT_TYPE,
headers={
"Content-Disposition": f'attachment; filename="{drawing_id}.kmz"',
"Cache-Control": CACHE_CONTROL_NO_STORE,
},
)

Expand Down
3 changes: 3 additions & 0 deletions app/core/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

logger = logging.getLogger(__name__)

CACHE_CONTROL_NO_STORE = "no-store, max-age=0"


class S3Service:
"""Async S3 client wrapper for KMZ drawing storage."""
Expand Down Expand Up @@ -48,6 +50,7 @@ async def upload_drawing(
Key=key,
ExtraArgs={
"ContentType": content_type,
"CacheControl": CACHE_CONTROL_NO_STORE,
"Metadata": metadata,
},
)
Expand Down
1 change: 1 addition & 0 deletions app/tests/test_drawings_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def test_get_drawing_existing(client: TestClient, valid_kmz_bytes: bytes):
assert response.status_code == 200
assert response.headers["content-type"] == "application/vnd.google-earth.kmz"
assert "attachment" in response.headers["content-disposition"]
assert response.headers["cache-control"] == "no-store, max-age=0"
assert response.content == valid_kmz_bytes


Expand Down
1 change: 1 addition & 0 deletions app/tests/test_s3_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ async def test_upload_drawing_success(settings, s3_client) -> None:
response = s3_client.head_object(Bucket=settings.aws_s3_bucket_name, Key=key)
assert response["ContentLength"] == len(data)
assert response["Metadata"] == metadata
assert response["CacheControl"] == "no-store, max-age=0"


@pytest.mark.asyncio
Expand Down
25 changes: 19 additions & 6 deletions scripts/test_s3_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

Usage:
make start-moto # ensure moto is running
uv run python3 scripts/test_s3_local.py
uv run python scripts/test_s3_local.py ./france.kmz
"""

import argparse
import asyncio
import hashlib
import io
Expand All @@ -26,18 +27,29 @@

ENDPOINT = os.environ.get("AWS_S3_ENDPOINT_URL", "http://localhost:5000")
BUCKET = os.environ.get("AWS_S3_BUCKET_NAME", "service-drawings-local")
TEST_FILE = "France.kmz"


async def main() -> None: # noqa: PLR0915
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Smoke-test S3Service against a local moto server."
)
parser.add_argument(
"file",
help="Path to the KMZ file to upload",
)
return parser.parse_args()


async def main(file_path: str) -> None: # noqa: PLR0915
import aioboto3 # noqa: PLC0415

session = aioboto3.Session()
async with session.client("s3", endpoint_url=ENDPOINT) as client: # type: ignore # noqa: PGH003
svc = S3Service(client=client, bucket=BUCKET)

# Read test KMZ
with open(TEST_FILE, "rb") as f:
with open(file_path, "rb") as f:
data = f.read()

sha256 = hashlib.sha256(data).hexdigest()
Expand All @@ -46,7 +58,7 @@ async def main() -> None: # noqa: PLR0915

print(f"Bucket: {BUCKET}")
print(f"Endpoint: {ENDPOINT}")
print(f"File: {TEST_FILE} ({len(data):,} bytes)")
print(f"File: {file_path} ({len(data):,} bytes)")
print(f"SHA-256: {sha256}")
print(f"Key: {key}")
print()
Expand Down Expand Up @@ -112,4 +124,5 @@ async def main() -> None: # noqa: PLR0915


if __name__ == "__main__":
asyncio.run(main())
args = parse_args()
asyncio.run(main(args.file))