Skip to content

Commit 3216303

Browse files
committed
[s3] Add option to invalidate CloudFront cache on file change
Invalidates CloudFront cache automatically when files are overwritten or deleted for users who don't use random-based filenames.
1 parent ca89a94 commit 3216303

File tree

1 file changed

+45
-1
lines changed

1 file changed

+45
-1
lines changed

storages/backends/s3.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import posixpath
55
import tempfile
66
import threading
7+
import time
78
import warnings
89
from datetime import datetime
910
from 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

Comments
 (0)