44import posixpath
55import tempfile
66import threading
7+ import time
78import warnings
89from datetime import datetime
910from datetime import timedelta
@@ -441,6 +442,10 @@ def get_default_settings(self):
441442 "use_threads" : setting ("AWS_S3_USE_THREADS" , True ),
442443 "transfer_config" : setting ("AWS_S3_TRANSFER_CONFIG" , None ),
443444 "client_config" : setting ("AWS_S3_CLIENT_CONFIG" , None ),
445+ "cloudfront_distribution_id" : setting ("AWS_CLOUDFRONT_DISTRIBUTION_ID" ),
446+ "cloudfront_invalidate_on_change" : setting (
447+ "AWS_CLOUDFRONT_INVALIDATE_ON_CHANGE" , False
448+ ),
444449 }
445450
446451 def __getstate__ (self ):
@@ -558,18 +563,28 @@ def _save(self, name, content):
558563
559564 obj = self .bucket .Object (name )
560565
566+ file_existed = None
567+ # Avoid HEAD request cost when invalidation is disabled.
568+ if self .cloudfront_invalidate_on_change :
569+ file_existed = self .exists (cleaned_name )
570+
561571 # Workaround file being closed errantly see: https://github.com/boto/s3transfer/issues/80
562572 original_close = content .close
563573 content .close = lambda : None
564574 try :
565575 obj .upload_fileobj (content , ExtraArgs = params , Config = self .transfer_config )
566576 finally :
567577 content .close = original_close
578+
579+ if should_invalidate and file_existed :
580+ self ._invalidate_cloudfront (name )
581+
568582 return cleaned_name
569583
570584 def delete (self , name ):
585+ name = self ._normalize_name (clean_name (name ))
586+
571587 try :
572- name = self ._normalize_name (clean_name (name ))
573588 self .bucket .Object (name ).delete ()
574589 except ClientError as err :
575590 if err .response ["ResponseMetadata" ]["HTTPStatusCode" ] == 404 :
@@ -579,6 +594,9 @@ def delete(self, name):
579594 # Some other error was encountered. Re-raise it
580595 raise
581596
597+ if self .cloudfront_invalidate_on_change :
598+ self ._invalidate_cloudfront (name )
599+
582600 def exists (self , name ):
583601 name = self ._normalize_name (clean_name (name ))
584602 params = _filter_download_params (self .get_object_parameters (name ))
@@ -699,6 +717,32 @@ def url(self, name, parameters=None, expire=None, http_method=None):
699717 )
700718 return url
701719
720+ def _invalidate_cloudfront (self , name ):
721+ if not self .cloudfront_distribution_id :
722+ raise ImproperlyConfigured (
723+ "AWS_CLOUDFRONT_DISTRIBUTION_ID must be set to invalidate files "
724+ "on CloudFront."
725+ )
726+
727+ invalidation_path = name if name .startswith ("/" ) else f"/{ name } "
728+
729+ cloudfront = self ._create_session ().client (
730+ "cloudfront" ,
731+ config = Config (
732+ region_name = self .region_name ,
733+ ),
734+ )
735+ cloudfront .create_invalidation (
736+ DistributionId = self .cloudfront_distribution_id ,
737+ InvalidationBatch = {
738+ "Paths" : {
739+ "Quantity" : 1 ,
740+ "Items" : [invalidation_path ],
741+ },
742+ "CallerReference" : f"{ name } -{ int (time .time () * 1000 )} " ,
743+ },
744+ )
745+
702746 def get_available_name (self , name , max_length = None ):
703747 """Overwrite existing file with the same name."""
704748 name = clean_name (name )
0 commit comments