@@ -296,24 +296,23 @@ def get_valid_name(self, name: str) -> str:
296
296
return super ().get_valid_name (name )
297
297
298
298
def _normalize_name (self , name : str ) -> str :
299
- """Create tenant-aware S3 key"""
300
- try :
301
- name = self .get_valid_name (name )
302
- normalized = safe_join (self .location , self .get_tenant_path (name ))
303
- LOGGER .debug ("Normalized S3 key" , original = name , normalized = normalized )
304
- return normalized
305
- except ValueError as e :
306
- LOGGER .error (
307
- "Invalid S3 key path detected" , name = name , tenant = self .tenant_prefix , error = str (e )
308
- )
309
- raise SuspiciousOperation (f"Invalid path: { name } " ) from e
299
+ """Normalize file path for S3 storage"""
300
+ if ".." in name or name .startswith ("/" ):
301
+ raise SuspiciousOperation (f"Suspicious path: { name } " )
302
+ normalized = self .get_tenant_path (name )
303
+ LOGGER .debug (
304
+ "Normalized S3 key" ,
305
+ original = name ,
306
+ normalized = normalized ,
307
+ )
308
+ return normalized
310
309
311
310
def _randomize_filename (self , filename : str ) -> str :
312
- """Generate random filename"""
313
- stem = uuid . uuid4 (). hex
314
- suffix = Path ( filename ). suffix . lower ()
315
- tenant_hash = uuid . uuid5 (uuid .NAMESPACE_DNS , self . tenant_prefix ). hex [: 8 ]
316
- randomized = f"{ tenant_hash } _{ stem } { suffix } "
311
+ """Generate a randomized filename while preserving extension """
312
+ name , ext = os . path . splitext ( filename )
313
+ tenant_hash = str ( uuid . uuid5 ( uuid . NAMESPACE_DNS , self . tenant_prefix ))[: 8 ]
314
+ random_uuid = str (uuid .uuid4 ())
315
+ randomized = f"{ tenant_hash } _{ random_uuid } { ext . lower () } "
317
316
LOGGER .debug (
318
317
"Randomized filename" ,
319
318
original = filename ,
@@ -323,51 +322,57 @@ def _randomize_filename(self, filename: str) -> str:
323
322
return randomized
324
323
325
324
def _save (self , name : str , content ) -> str :
326
- """Save file with secure random name and handle old file cleanup"""
327
- try :
328
- name = self .get_valid_name (name )
329
- randomized_name = self ._randomize_filename (name )
330
- normalized_name = self ._normalize_name (randomized_name )
331
-
332
- self ._file_mapping [name ] = normalized_name
333
-
334
- LOGGER .info (
335
- "Saving file to S3" ,
336
- original_name = name ,
337
- randomized_name = randomized_name ,
338
- normalized_name = normalized_name ,
339
- tenant = self .tenant_prefix ,
340
- )
325
+ """Save file to S3 with tenant isolation and random filename"""
326
+ randomized_name = self ._randomize_filename (name )
327
+ normalized_name = self ._normalize_name (randomized_name )
328
+
329
+ LOGGER .info (
330
+ "Saving file to S3" ,
331
+ original_name = name ,
332
+ randomized_name = randomized_name ,
333
+ normalized_name = normalized_name ,
334
+ tenant = self .tenant_prefix ,
335
+ )
341
336
342
- result = super ()._save (randomized_name , content )
337
+ try :
338
+ # Upload file to S3
339
+ self .bucket .Object (normalized_name ).upload_fileobj (content )
343
340
341
+ # Verify upload
344
342
try :
345
343
self .bucket .Object (normalized_name ).load ()
346
- LOGGER .debug (
347
- "File saved successfully to S3" , key = normalized_name , tenant = self .tenant_prefix
348
- )
349
344
except ClientError as e :
345
+ error_code = e .response .get ("Error" , {}).get ("Code" , "Unknown" )
346
+ error_message = e .response .get ("Error" , {}).get ("Message" , "Unknown error" )
350
347
LOGGER .error (
351
348
"Failed to verify S3 upload" ,
352
349
key = normalized_name ,
353
- error_code = e . response [ "Error" ][ "Code" ] ,
354
- message = e . response [ "Error" ][ "Message" ] ,
350
+ error_code = error_code ,
351
+ message = error_message ,
355
352
tenant = self .tenant_prefix ,
356
353
)
357
- self ._delete_file (normalized_name )
354
+ # Clean up failed upload
355
+ try :
356
+ self .bucket .Object (normalized_name ).delete ()
357
+ except Exception as cleanup_error :
358
+ LOGGER .error (
359
+ "Failed to clean up failed upload" ,
360
+ key = normalized_name ,
361
+ error = str (cleanup_error ),
362
+ tenant = self .tenant_prefix ,
363
+ )
358
364
raise
359
365
360
- return result
366
+ # Store mapping of original name to normalized name
367
+ self ._file_mapping [name ] = normalized_name
361
368
362
- except ClientError as e :
363
- LOGGER .error (
364
- "S3 upload failed" ,
365
- error_code = e .response ["Error" ]["Code" ],
366
- message = e .response ["Error" ]["Message" ],
369
+ LOGGER .debug (
370
+ "File saved successfully to S3" ,
367
371
key = normalized_name ,
368
372
tenant = self .tenant_prefix ,
369
373
)
370
- raise
374
+ return normalized_name
375
+
371
376
except Exception as e :
372
377
LOGGER .error (
373
378
"Unexpected error saving file to S3" ,
@@ -377,6 +382,34 @@ def _save(self, name: str, content) -> str:
377
382
)
378
383
raise
379
384
385
+ def delete (self , name : str ) -> None :
386
+ """Delete file from S3"""
387
+ try :
388
+ # Get normalized name from mapping or normalize original name
389
+ normalized_name = self ._file_mapping .get (name , self ._normalize_name (name ))
390
+ self .bucket .Object (normalized_name ).delete ()
391
+ # Remove from mapping if exists
392
+ self ._file_mapping .pop (name , None )
393
+ LOGGER .debug (
394
+ "File deleted from S3" ,
395
+ key = normalized_name ,
396
+ tenant = self .tenant_prefix ,
397
+ )
398
+ except ClientError as e :
399
+ if e .response .get ("Error" , {}).get ("Code" ) != "404" :
400
+ LOGGER .error (
401
+ "Failed to delete file from S3" ,
402
+ name = name ,
403
+ error = str (e ),
404
+ tenant = self .tenant_prefix ,
405
+ )
406
+ raise
407
+ LOGGER .debug (
408
+ "File not found during delete" ,
409
+ name = name ,
410
+ tenant = self .tenant_prefix ,
411
+ )
412
+
380
413
def url (self , name : str , ** kwargs ) -> str :
381
414
"""Generate URL without signing parameters when using custom domain"""
382
415
try :
0 commit comments