Skip to content

Commit 282e50b

Browse files
refactor: add shared utilities and remove code duplication
- Add s3proxy/utils.py with shared helper functions: - get_query_param() for safe query parameter extraction - format_http_date() for HTTP date formatting (RFC 7231) - format_iso8601() for S3 API datetime formatting - Add crypto.ENCRYPTION_OVERHEAD constant (replaces magic number 28) - Remove duplicate format_http_date() from handlers/objects.py - Use format_iso8601() for consistent datetime formatting
1 parent 44cf3c2 commit 282e50b

4 files changed

Lines changed: 56 additions & 8 deletions

File tree

s3proxy/crypto.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# Constants
1717
NONCE_SIZE = 12 # 96 bits for AES-GCM
1818
TAG_SIZE = 16 # 128 bits authentication tag
19+
ENCRYPTION_OVERHEAD = NONCE_SIZE + TAG_SIZE # 28 bytes added per encrypted chunk
1920
DEK_SIZE = 32 # 256 bits for AES-256
2021
PART_SIZE = 64 * 1024 * 1024 # 64 MB default part size for internal multipart uploads
2122
MIN_PART_SIZE = 5 * 1024 * 1024 # 5 MB minimum (S3 requirement for all parts except last)

s3proxy/handlers/multipart_ops.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
save_multipart_metadata,
2727
)
2828
from ..s3client import S3Credentials
29+
from ..utils import format_iso8601
2930
from ..xml_utils import find_elements, get_element_text
3031
from .base import BaseHandler
3132
from .objects import decode_aws_chunked, decode_aws_chunked_stream
@@ -946,7 +947,7 @@ async def handle_upload_part_copy(self, request: Request, creds: S3Credentials)
946947
)
947948

948949
# Return CopyPartResult
949-
last_modified = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z")
950+
last_modified = format_iso8601(datetime.now(UTC))
950951

951952
return Response(
952953
content=xml_responses.upload_part_copy_result(

s3proxy/handlers/objects.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
save_multipart_metadata,
2727
)
2828
from ..s3client import S3Client, S3Credentials
29+
from ..utils import format_http_date, format_iso8601
2930
from ..xml_utils import find_element, find_elements
3031
from .base import BaseHandler
3132

@@ -108,11 +109,6 @@ def chunked(data: bytes, size: int) -> Iterator[tuple[int, bytes]]:
108109
yield i // size + 1, data[i : i + size]
109110

110111

111-
def format_http_date(dt: datetime | None) -> str | None:
112-
"""Format datetime as HTTP date string."""
113-
return dt.strftime("%a, %d %b %Y %H:%M:%S GMT") if dt else None
114-
115-
116112
class ObjectHandlerMixin(BaseHandler):
117113
"""Mixin for object operations."""
118114

@@ -339,7 +335,7 @@ async def stream():
339335
ciphertext = await resp["Body"].read()
340336

341337
expected_size = internal_ct_end - internal_ct_offset + 1
342-
if len(ciphertext) < 28 or len(ciphertext) != expected_size:
338+
if len(ciphertext) < crypto.ENCRYPTION_OVERHEAD or len(ciphertext) != expected_size:
343339
logger.error(
344340
"Invalid ciphertext size - metadata mismatch",
345341
bucket=bucket,
@@ -946,7 +942,7 @@ async def handle_copy_object(self, request: Request, creds: S3Credentials) -> Re
946942
)
947943

948944
# Return CopyObjectResult
949-
last_modified = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z")
945+
last_modified = format_iso8601(datetime.now(UTC))
950946

951947
return Response(
952948
content=xml_responses.copy_object_result(etag, last_modified),

s3proxy/utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Shared utilities for S3Proxy."""
2+
3+
from datetime import datetime
4+
from urllib.parse import parse_qs
5+
6+
# HTTP date format per RFC 7231
7+
HTTP_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
8+
9+
# ISO 8601 format for S3 API responses
10+
ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
11+
12+
13+
def get_query_param(query: str | dict[str, list[str]], key: str, default: str = "") -> str:
14+
"""Get a single query parameter value with safe default.
15+
16+
Handles both raw query strings and pre-parsed dicts from parse_qs().
17+
"""
18+
if isinstance(query, str):
19+
query = parse_qs(query, keep_blank_values=True)
20+
values = query.get(key, [default])
21+
return values[0] if values else default
22+
23+
24+
def get_query_param_int(query: str | dict[str, list[str]], key: str, default: int) -> int:
25+
"""Get a query parameter as integer with safe default."""
26+
value = get_query_param(query, key, "")
27+
if not value:
28+
return default
29+
try:
30+
return int(value)
31+
except ValueError:
32+
return default
33+
34+
35+
def format_http_date(dt: datetime | str | None) -> str | None:
36+
"""Format datetime as HTTP date string (RFC 7231)."""
37+
if dt is None:
38+
return None
39+
if isinstance(dt, str):
40+
return dt
41+
if hasattr(dt, "strftime"):
42+
return dt.strftime(HTTP_DATE_FORMAT)
43+
return str(dt)
44+
45+
46+
def format_iso8601(dt: datetime | None) -> str:
47+
"""Format datetime as ISO 8601 for S3 API responses."""
48+
if dt is None:
49+
return datetime.utcnow().strftime(ISO8601_FORMAT)
50+
return dt.strftime(ISO8601_FORMAT)

0 commit comments

Comments
 (0)