Skip to content

Commit 92290b2

Browse files
pavanputhraclaude
andauthored
fix(s3): resolve vcon path via lookup pointer to support date-based key structure (#145)
S3 stores vcons under YYYY/MM/DD/<uuid>.vcon but the get/delete interface only receives a UUID. Introduce a lookup pointer file at lookup/<uuid>.txt (written on save) that contains the date prefix, allowing get and delete to resolve the full key without any schema changes. - Add _date_prefix() helper to centralise YYYY/MM/DD formatting - Add _build_lookup_key() to build the lookup/<uuid>.txt path - Update _build_s3_key() to accept a pre-computed date_path instead of created_at - save(): write lookup pointer alongside the vcon object (2 puts total) - get(): fetch lookup pointer to resolve date path, then fetch vcon (2 gets total) - delete(): resolve via lookup pointer, delete both vcon and lookup objects - Add .txt extension to lookup files for easy viewing in AWS Console - 43 tests covering all helpers, save, get, and delete paths Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 02ba7f0 commit 92290b2

2 files changed

Lines changed: 231 additions & 65 deletions

File tree

server/storage/s3/__init__.py

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,40 @@ def _create_s3_client(opts: dict):
3636
return boto3.client("s3", **client_kwargs)
3737

3838

39-
def _build_s3_key(vcon_uuid: str, created_at: Optional[str] = None, s3_path: Optional[str] = None) -> str:
40-
"""Build the S3 object key for a vCon with date-based folder structure.
41-
39+
def _date_prefix(created_at: str) -> str:
40+
"""Return the YYYY/MM/DD folder prefix derived from an ISO timestamp."""
41+
return datetime.fromisoformat(created_at).strftime("%Y/%m/%d")
42+
43+
44+
def _build_lookup_key(vcon_uuid: str, s3_path: Optional[str] = None) -> str:
45+
"""Build the S3 key for the lookup pointer file.
46+
4247
Args:
4348
vcon_uuid: The vCon UUID
44-
created_at: ISO format timestamp from the vCon's created_at field.
45-
If provided, creates date-based folder structure (YYYY/MM/DD).
4649
s3_path: Optional prefix path in the S3 bucket
47-
50+
51+
Returns:
52+
S3 key in format: [s3_path/]lookup/<uuid>
53+
"""
54+
key = f"lookup/{vcon_uuid}.txt"
55+
if not s3_path:
56+
return key
57+
return f"{s3_path.rstrip('/')}/{key}"
58+
59+
60+
def _build_s3_key(vcon_uuid: str, date_path: Optional[str] = None, s3_path: Optional[str] = None) -> str:
61+
"""Build the S3 object key for a vCon.
62+
63+
Args:
64+
vcon_uuid: The vCon UUID
65+
date_path: Pre-computed YYYY/MM/DD prefix (from _date_prefix).
66+
If provided, creates date-based folder structure.
67+
s3_path: Optional prefix path in the S3 bucket
68+
4869
Returns:
4970
S3 key in format: [s3_path/][YYYY/MM/DD/]uuid.vcon
5071
"""
51-
if created_at:
52-
timestamp = datetime.fromisoformat(created_at).strftime("%Y/%m/%d")
53-
key = f"{timestamp}/{vcon_uuid}.vcon"
54-
else:
55-
key = f"{vcon_uuid}.vcon"
72+
key = f"{date_path}/{vcon_uuid}.vcon" if date_path else f"{vcon_uuid}.vcon"
5673
if not s3_path:
5774
return key
5875
return f"{s3_path.rstrip('/')}/{key}"
@@ -68,10 +85,16 @@ def save(
6885
vcon = vcon_redis.get_vcon(vcon_uuid)
6986
s3 = _create_s3_client(opts)
7087

71-
destination_directory = _build_s3_key(vcon_uuid, vcon.created_at, opts.get("s3_path"))
88+
date_path = _date_prefix(vcon.created_at)
89+
destination_directory = _build_s3_key(vcon_uuid, date_path, opts.get("s3_path"))
7290
s3.put_object(
7391
Bucket=opts["aws_bucket"], Key=destination_directory, Body=vcon.dumps()
7492
)
93+
94+
lookup_key = _build_lookup_key(vcon_uuid, opts.get("s3_path"))
95+
s3.put_object(
96+
Bucket=opts["aws_bucket"], Key=lookup_key, Body=date_path.encode()
97+
)
7598
logger.info(f"Finished S3 storage for vCon: {vcon_uuid}")
7699
except Exception as e:
77100
logger.error(
@@ -80,15 +103,49 @@ def save(
80103
raise e
81104

82105
def get(vcon_uuid: str, opts=default_options) -> Optional[dict]:
83-
"""Get a vCon from S3 by UUID."""
106+
"""Get a vCon from S3 by UUID.
107+
108+
Uses a lookup pointer file (lookup/<uuid>) to resolve the date-based path
109+
before fetching the actual vCon object.
110+
"""
84111
try:
85112
s3 = _create_s3_client(opts)
86113

87-
key = _build_s3_key(vcon_uuid, s3_path=opts.get("s3_path"))
114+
lookup_key = _build_lookup_key(vcon_uuid, opts.get("s3_path"))
115+
lookup_response = s3.get_object(Bucket=opts["aws_bucket"], Key=lookup_key)
116+
date_path = lookup_response['Body'].read().decode('utf-8').strip()
117+
118+
key = _build_s3_key(vcon_uuid, date_path, opts.get("s3_path"))
88119

89120
response = s3.get_object(Bucket=opts["aws_bucket"], Key=key)
90121
return json.loads(response['Body'].read().decode('utf-8'))
91-
122+
92123
except Exception as e:
93124
logger.error(f"s3 storage plugin: failed to get vCon: {vcon_uuid}, error: {e}")
94125
return None
126+
127+
128+
def delete(vcon_uuid: str, opts=default_options) -> bool:
129+
"""Delete a vCon and its lookup pointer from S3.
130+
131+
Returns True if deleted, False if not found or on error.
132+
"""
133+
try:
134+
s3 = _create_s3_client(opts)
135+
bucket = opts["aws_bucket"]
136+
s3_path = opts.get("s3_path")
137+
138+
lookup_key = _build_lookup_key(vcon_uuid, s3_path)
139+
lookup_response = s3.get_object(Bucket=bucket, Key=lookup_key)
140+
date_path = lookup_response['Body'].read().decode('utf-8').strip()
141+
142+
vcon_key = _build_s3_key(vcon_uuid, date_path, s3_path)
143+
s3.delete_object(Bucket=bucket, Key=vcon_key)
144+
s3.delete_object(Bucket=bucket, Key=lookup_key)
145+
146+
logger.info(f"s3 storage plugin: deleted vCon {vcon_uuid}")
147+
return True
148+
149+
except Exception as e:
150+
logger.error(f"s3 storage plugin: failed to delete vCon: {vcon_uuid}, error: {e}")
151+
return False

0 commit comments

Comments
 (0)