summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Apolloner <florian@apolloner.eu>2022-12-29 16:52:56 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-01-02 09:53:52 +0100
commitafa2e28205fe708334ad463b6d3b0e9960b945a6 (patch)
treedd04d81ebdaca8ab8c38f37f5c221923e67cf7cf
parent75500feecddcb27b6ab65c9057e7317024cef761 (diff)
Fixed #34235 -- Added ManifestFilesMixin.manifest_hash attribute.
This adds ManifestFilesMixin.manifest_hash attribute exposing a "hash" of the full manifest. This allows applications to determine when their static files have changed.
-rw-r--r--django/contrib/staticfiles/storage.py19
-rw-r--r--docs/ref/contrib/staticfiles.txt8
-rw-r--r--docs/releases/4.2.txt4
-rw-r--r--tests/staticfiles_tests/project/documents/staticfiles_v1.json6
-rw-r--r--tests/staticfiles_tests/test_storage.py31
5 files changed, 58 insertions, 10 deletions
diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
index eae25ba737..c09f01e446 100644
--- a/django/contrib/staticfiles/storage.py
+++ b/django/contrib/staticfiles/storage.py
@@ -439,7 +439,7 @@ class HashedFilesMixin:
class ManifestFilesMixin(HashedFilesMixin):
- manifest_version = "1.0" # the manifest format standard
+ manifest_version = "1.1" # the manifest format standard
manifest_name = "staticfiles.json"
manifest_strict = True
keep_intermediate_files = False
@@ -449,7 +449,7 @@ class ManifestFilesMixin(HashedFilesMixin):
if manifest_storage is None:
manifest_storage = self
self.manifest_storage = manifest_storage
- self.hashed_files = self.load_manifest()
+ self.hashed_files, self.manifest_hash = self.load_manifest()
def read_manifest(self):
try:
@@ -461,15 +461,15 @@ class ManifestFilesMixin(HashedFilesMixin):
def load_manifest(self):
content = self.read_manifest()
if content is None:
- return {}
+ return {}, ""
try:
stored = json.loads(content)
except json.JSONDecodeError:
pass
else:
version = stored.get("version")
- if version == "1.0":
- return stored.get("paths", {})
+ if version in ("1.0", "1.1"):
+ return stored.get("paths", {}), stored.get("hash", "")
raise ValueError(
"Couldn't load manifest '%s' (version %s)"
% (self.manifest_name, self.manifest_version)
@@ -482,7 +482,14 @@ class ManifestFilesMixin(HashedFilesMixin):
self.save_manifest()
def save_manifest(self):
- payload = {"paths": self.hashed_files, "version": self.manifest_version}
+ self.manifest_hash = self.file_hash(
+ None, ContentFile(json.dumps(sorted(self.hashed_files.items())).encode())
+ )
+ payload = {
+ "paths": self.hashed_files,
+ "version": self.manifest_version,
+ "hash": self.manifest_hash,
+ }
if self.manifest_storage.exists(self.manifest_name):
self.manifest_storage.delete(self.manifest_name)
contents = json.dumps(payload).encode()
diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt
index 0f0e8d8001..7ca3584c33 100644
--- a/docs/ref/contrib/staticfiles.txt
+++ b/docs/ref/contrib/staticfiles.txt
@@ -336,6 +336,14 @@ argument. For example::
Support for finding paths to JavaScript modules in ``import`` and
``export`` statements was added.
+.. attribute:: storage.ManifestStaticFilesStorage.manifest_hash
+
+.. versionadded:: 4.2
+
+This attribute provides a single hash that changes whenever a file in the
+manifest changes. This can be useful to communicate to SPAs that the assets on
+the server have changed (due to a new deployment).
+
.. attribute:: storage.ManifestStaticFilesStorage.max_post_process_passes
Since static files might reference other static files that need to have their
diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt
index 826545f444..6d2fb32644 100644
--- a/docs/releases/4.2.txt
+++ b/docs/releases/4.2.txt
@@ -201,6 +201,10 @@ Minor features
replaces paths to JavaScript modules in ``import`` and ``export`` statements
with their hashed counterparts.
+* The new :attr:`.ManifestStaticFilesStorage.manifest_hash` attribute provides
+ a hash over all files in the manifest and changes whenever one of the files
+ changes.
+
:mod:`django.contrib.syndication`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/staticfiles_tests/project/documents/staticfiles_v1.json b/tests/staticfiles_tests/project/documents/staticfiles_v1.json
new file mode 100644
index 0000000000..4f85945e3f
--- /dev/null
+++ b/tests/staticfiles_tests/project/documents/staticfiles_v1.json
@@ -0,0 +1,6 @@
+{
+ "version": "1.0",
+ "paths": {
+ "dummy.txt": "dummy.txt"
+ }
+}
diff --git a/tests/staticfiles_tests/test_storage.py b/tests/staticfiles_tests/test_storage.py
index 077d14bcc4..f2f1899aac 100644
--- a/tests/staticfiles_tests/test_storage.py
+++ b/tests/staticfiles_tests/test_storage.py
@@ -436,7 +436,7 @@ class TestCollectionManifestStorage(TestHashedFiles, CollectionTestCase):
# The in-memory version of the manifest matches the one on disk
# since a properly created manifest should cover all filenames.
if hashed_files:
- manifest = storage.staticfiles_storage.load_manifest()
+ manifest, _ = storage.staticfiles_storage.load_manifest()
self.assertEqual(hashed_files, manifest)
def test_manifest_exists(self):
@@ -463,7 +463,7 @@ class TestCollectionManifestStorage(TestHashedFiles, CollectionTestCase):
def test_parse_cache(self):
hashed_files = storage.staticfiles_storage.hashed_files
- manifest = storage.staticfiles_storage.load_manifest()
+ manifest, _ = storage.staticfiles_storage.load_manifest()
self.assertEqual(hashed_files, manifest)
def test_clear_empties_manifest(self):
@@ -476,7 +476,7 @@ class TestCollectionManifestStorage(TestHashedFiles, CollectionTestCase):
hashed_files = storage.staticfiles_storage.hashed_files
self.assertIn(cleared_file_name, hashed_files)
- manifest_content = storage.staticfiles_storage.load_manifest()
+ manifest_content, _ = storage.staticfiles_storage.load_manifest()
self.assertIn(cleared_file_name, manifest_content)
original_path = storage.staticfiles_storage.path(cleared_file_name)
@@ -491,7 +491,7 @@ class TestCollectionManifestStorage(TestHashedFiles, CollectionTestCase):
hashed_files = storage.staticfiles_storage.hashed_files
self.assertNotIn(cleared_file_name, hashed_files)
- manifest_content = storage.staticfiles_storage.load_manifest()
+ manifest_content, _ = storage.staticfiles_storage.load_manifest()
self.assertNotIn(cleared_file_name, manifest_content)
def test_missing_entry(self):
@@ -535,6 +535,29 @@ class TestCollectionManifestStorage(TestHashedFiles, CollectionTestCase):
2,
)
+ def test_manifest_hash(self):
+ # Collect the additional file.
+ self.run_collectstatic()
+
+ _, manifest_hash_orig = storage.staticfiles_storage.load_manifest()
+ self.assertNotEqual(manifest_hash_orig, "")
+ self.assertEqual(storage.staticfiles_storage.manifest_hash, manifest_hash_orig)
+ # Saving doesn't change the hash.
+ storage.staticfiles_storage.save_manifest()
+ self.assertEqual(storage.staticfiles_storage.manifest_hash, manifest_hash_orig)
+ # Delete the original file from the app, collect with clear.
+ os.unlink(self._clear_filename)
+ self.run_collectstatic(clear=True)
+ # Hash is changed.
+ _, manifest_hash = storage.staticfiles_storage.load_manifest()
+ self.assertNotEqual(manifest_hash, manifest_hash_orig)
+
+ def test_manifest_hash_v1(self):
+ storage.staticfiles_storage.manifest_name = "staticfiles_v1.json"
+ manifest_content, manifest_hash = storage.staticfiles_storage.load_manifest()
+ self.assertEqual(manifest_hash, "")
+ self.assertEqual(manifest_content, {"dummy.txt": "dummy.txt"})
+
@override_settings(STATICFILES_STORAGE="staticfiles_tests.storage.NoneHashStorage")
class TestCollectionNoneHashStorage(CollectionTestCase):