1
+ import os
2
+ import posixpath
3
+ import re
4
+
5
+ from urllib .parse import unquote ,urldefrag
6
+
1
7
from django .conf import settings
2
8
from django .contrib .staticfiles .storage import ManifestFilesMixin ,StaticFilesStorage
9
+ from django .contrib .staticfiles .utils import matches_patterns
10
+ from django .core .files .base import ContentFile
3
11
4
12
from pipeline .storage import PipelineMixin
5
13
from storages .backends .s3boto3 import S3Boto3Storage
@@ -11,11 +19,152 @@ class MediaStorage(S3Boto3Storage):
11
19
12
20
class PipelineManifestStorage (PipelineMixin ,ManifestFilesMixin ,StaticFilesStorage ):
13
21
"""
14
- Override the replacement patterns to match URL-encoded quotations.
22
+ Applys patches from https://github.com/django/django/pull/11241 to ignore
23
+ imports in comments. Ref: https://code.djangoproject.com/ticket/21080
15
24
"""
16
- patterns = (
17
- ("*.css" , (
18
- r"""(url\((?:['"]|%22|%27){0,1}\s*(.*?)(?:['"]|%22|%27){0,1}\))""" ,
19
- (r"""(@import\s*["']\s*(.*?)["'])""" ,"""@import url("%s")""" ),
20
- )),
21
- )
25
+
26
+ def get_comment_blocks (self ,content ):
27
+ """
28
+ Return a list of (start, end) tuples for each comment block.
29
+ """
30
+ return [
31
+ (match .start (),match .end ())
32
+ for match in re .finditer (r"\/\*.*?\*\/" ,content ,flags = re .DOTALL )
33
+ ]
34
+
35
+ def url_converter (self ,name ,hashed_files ,template = None ,comment_blocks = []):
36
+ """
37
+ Return the custom URL converter for the given file name.
38
+ """
39
+ if template is None :
40
+ template = self .default_template
41
+
42
+ def converter (matchobj ):
43
+ """
44
+ Convert the matched URL to a normalized and hashed URL.
45
+ This requires figuring out which files the matched URL resolves
46
+ to and calling the url() method of the storage.
47
+ """
48
+ matched ,url = matchobj .groups ()
49
+
50
+ # Ignore URLs in comments.
51
+ if self .is_in_comment (matchobj .start (),comment_blocks ):
52
+ return matched
53
+
54
+ # Ignore absolute/protocol-relative and data-uri URLs.
55
+ if re .match (r'^[a-z]+:' ,url ):
56
+ return matched
57
+
58
+ # Ignore absolute URLs that don't point to a static file (dynamic
59
+ # CSS / JS?). Note that STATIC_URL cannot be empty.
60
+ if url .startswith ('/' )and not url .startswith (settings .STATIC_URL ):
61
+ return matched
62
+
63
+ # Strip off the fragment so a path-like fragment won't interfere.
64
+ url_path ,fragment = urldefrag (url )
65
+
66
+ if url_path .startswith ('/' ):
67
+ # Otherwise the condition above would have returned prematurely.
68
+ assert url_path .startswith (settings .STATIC_URL )
69
+ target_name = url_path [len (settings .STATIC_URL ):]
70
+ else :
71
+ # We're using the posixpath module to mix paths and URLs conveniently.
72
+ source_name = name if os .sep == '/' else name .replace (os .sep ,'/' )
73
+ target_name = posixpath .join (posixpath .dirname (source_name ),url_path )
74
+
75
+ # Determine the hashed name of the target file with the storage backend.
76
+ hashed_url = self ._url (
77
+ self ._stored_name ,unquote (target_name ),
78
+ force = True ,hashed_files = hashed_files ,
79
+ )
80
+
81
+ transformed_url = '/' .join (url_path .split ('/' )[:- 1 ]+ hashed_url .split ('/' )[- 1 :])
82
+
83
+ # Restore the fragment that was stripped off earlier.
84
+ if fragment :
85
+ transformed_url += ('?#' if '?#' in url else '#' )+ fragment
86
+
87
+ # Return the hashed version to the file
88
+ return template % unquote (transformed_url )
89
+
90
+ return converter
91
+
92
+ def is_in_comment (self ,pos ,comments ):
93
+ for start ,end in comments :
94
+ if start < pos and pos < end :
95
+ return True
96
+ if pos < start :
97
+ return False
98
+ return False
99
+
100
+ def _post_process (self ,paths ,adjustable_paths ,hashed_files ):
101
+ # Sort the files by directory level
102
+ def path_level (name ):
103
+ return len (name .split (os .sep ))
104
+
105
+ for name in sorted (paths ,key = path_level ,reverse = True ):
106
+ substitutions = True
107
+ # use the original, local file, not the copied-but-unprocessed
108
+ # file, which might be somewhere far away, like S3
109
+ storage ,path = paths [name ]
110
+ with storage .open (path )as original_file :
111
+ cleaned_name = self .clean_name (name )
112
+ hash_key = self .hash_key (cleaned_name )
113
+
114
+ # generate the hash with the original content, even for
115
+ # adjustable files.
116
+ if hash_key not in hashed_files :
117
+ hashed_name = self .hashed_name (name ,original_file )
118
+ else :
119
+ hashed_name = hashed_files [hash_key ]
120
+
121
+ # then get the original's file content..
122
+ if hasattr (original_file ,'seek' ):
123
+ original_file .seek (0 )
124
+
125
+ hashed_file_exists = self .exists (hashed_name )
126
+ processed = False
127
+
128
+ # ..to apply each replacement pattern to the content
129
+ if name in adjustable_paths :
130
+ old_hashed_name = hashed_name
131
+ content = original_file .read ().decode (settings .FILE_CHARSET )
132
+ for extension ,patterns in self ._patterns .items ():
133
+ if matches_patterns (path , (extension ,)):
134
+ comment_blocks = self .get_comment_blocks (content )
135
+ for pattern ,template in patterns :
136
+ converter = self .url_converter (name ,hashed_files ,template ,comment_blocks )
137
+ try :
138
+ content = pattern .sub (converter ,content )
139
+ except ValueError as exc :
140
+ yield name ,None ,exc ,False
141
+ if hashed_file_exists :
142
+ self .delete (hashed_name )
143
+ # then save the processed result
144
+ content_file = ContentFile (content .encode ())
145
+ # Save intermediate file for reference
146
+ saved_name = self ._save (hashed_name ,content_file )
147
+ hashed_name = self .hashed_name (name ,content_file )
148
+
149
+ if self .exists (hashed_name ):
150
+ self .delete (hashed_name )
151
+
152
+ saved_name = self ._save (hashed_name ,content_file )
153
+ hashed_name = self .clean_name (saved_name )
154
+ # If the file hash stayed the same, this file didn't change
155
+ if old_hashed_name == hashed_name :
156
+ substitutions = False
157
+ processed = True
158
+
159
+ if not processed :
160
+ # or handle the case in which neither processing nor
161
+ # a change to the original file happened
162
+ if not hashed_file_exists :
163
+ processed = True
164
+ saved_name = self ._save (hashed_name ,original_file )
165
+ hashed_name = self .clean_name (saved_name )
166
+
167
+ # and then set the cache accordingly
168
+ hashed_files [hash_key ]= hashed_name
169
+
170
+ yield name ,hashed_name ,processed ,substitutions