49
49
from trac .mimeview .api import RenderingContext , get_mimetype
50
50
from trac .resource import *
51
51
from trac .util import compat , get_reporter_id , presentation , get_pkginfo , \
52
- pathjoin , translation
52
+ lazy , pathjoin , sha1 , translation
53
53
from trac .util .html import escape , plaintext
54
54
from trac .util .text import pretty_size , obfuscate_email_address , \
55
55
shorten_line , unicode_quote_plus , to_unicode , \
@@ -141,10 +141,10 @@ def add_stylesheet(req, filename, mimetype='text/css', media=None):
141
141
elif filename .startswith ('common/' ) and 'htdocs_location' in req .chrome :
142
142
href = Href (req .chrome ['htdocs_location' ])(filename [7 :])
143
143
else :
144
- href = req . href
145
- if not filename . startswith ( '/' ):
146
- href = href . chrome
147
- href = href ( filename )
144
+ if filename . startswith ( '/' ):
145
+ href = req . href ( filename )
146
+ else :
147
+ href = req . href . chrome ( req . chrome [ 'static_hash' ], filename )
148
148
add_link (req , 'stylesheet' , href , mimetype = mimetype , media = media )
149
149
150
150
def add_script (req , filename , mimetype = 'text/javascript' , charset = 'utf-8' ,
@@ -164,10 +164,10 @@ def add_script(req, filename, mimetype='text/javascript', charset='utf-8',
164
164
elif filename .startswith ('common/' ) and 'htdocs_location' in req .chrome :
165
165
href = Href (req .chrome ['htdocs_location' ])(filename [7 :])
166
166
else :
167
- href = req . href
168
- if not filename . startswith ( '/' ):
169
- href = href . chrome
170
- href = href ( filename )
167
+ if filename . startswith ( '/' ):
168
+ href = req . href ( filename )
169
+ else :
170
+ href = req . href . chrome ( req . chrome [ 'static_hash' ], filename )
171
171
script = {'href' : href , 'type' : mimetype , 'charset' : charset ,
172
172
'prefix' : Markup ('<!--[if %s]>' % ie_if ) if ie_if else None ,
173
173
'suffix' : Markup ('<![endif]-->' ) if ie_if else None }
@@ -343,6 +343,22 @@ class Chrome(Component):
343
343
auto_reload = BoolOption ('trac' , 'auto_reload' , False ,
344
344
"""Automatically reload template files after modification.""" )
345
345
346
+ fingerprint_resources = ChoiceOption ('trac' , 'fingerprint_resources' ,
347
+ ['content' , 'meta' , 'disabled' ],
348
+ """Control the fingerprinting of static resources.
349
+
350
+ URLs to static resources below `/chrome` have the form
351
+ `/chrome/![0-9a-f]{8}/.*`, where the second element is a fingerprint
352
+ of the ''content'' of all resources (for "content") or their
353
+ ''metadata'' (size and mtime, for "meta"). This allows aggressive
354
+ caching of static resources on the browser, while still ensuring that
355
+ they are reloaded when they change.
356
+
357
+ Setting this option to "disabled" disables fingerprinting, and
358
+ reverts the URLs to static resources to `/chrome/.*`.
359
+
360
+ (''since 0.13'')""" )
361
+
346
362
genshi_cache_size = IntOption ('trac' , 'genshi_cache_size' , 128 ,
347
363
"""The maximum number of templates that the template loader will cache
348
364
in memory. The default value is 128. You may want to choose a higher
@@ -537,15 +553,20 @@ def upgrade_environment(self, db):
537
553
538
554
# IRequestHandler methods
539
555
556
+ _chrome_path_re = re .compile (r'/chrome/(?:(?P<hash>![0-9a-f]+)/)?'
557
+ r'(?P<prefix>[^/]+)/+(?P<filename>.+)' )
558
+
540
559
def match_request (self , req ):
541
- match = re .match (r'/chrome/(?P<prefix>[^/]+)/+(?P<filename>.+)' ,
542
- req .path_info )
560
+ match = self ._chrome_path_re .match (req .path_info )
543
561
if match :
562
+ req .args ['hash' ] = match .group ('hash' )
544
563
req .args ['prefix' ] = match .group ('prefix' )
545
564
req .args ['filename' ] = match .group ('filename' )
546
565
return True
547
566
548
567
def process_request (self , req ):
568
+ hash_matches = self .static_hash \
569
+ and req .args ['hash' ] == self .static_hash
549
570
prefix = req .args ['prefix' ]
550
571
filename = req .args ['filename' ]
551
572
@@ -558,7 +579,10 @@ def process_request(self, req):
558
579
path = os .path .normpath (os .path .join (dir , filename ))
559
580
assert os .path .commonprefix ([dir , path ]) == dir
560
581
if os .path .isfile (path ):
561
- req .send_file (path , get_mimetype (path ))
582
+ req .send_file (path , get_mimetype (path ),
583
+ expires = datetime .datetime .now (utc )
584
+ + datetime .timedelta (days = 365 )
585
+ if hash_matches else None )
562
586
563
587
self .log .warning ('File %s not found in any of %s' , filename , dirs )
564
588
raise HTTPNotFound ('File %s not found' , filename )
@@ -587,11 +611,42 @@ def get_link_resolvers(self):
587
611
588
612
def _format_link (self , formatter , ns , file , label ):
589
613
file , query , fragment = formatter .split_link (file )
590
- href = formatter .href .chrome ('site' , file ) + query + fragment
614
+ href = formatter .href .chrome (self .static_hash , 'site' , file ) + query \
615
+ + fragment
591
616
return tag .a (label , href = href )
592
617
593
618
# Public API methods
594
619
620
+ @lazy
621
+ def static_hash (self ):
622
+ """Return a hash of all available static resources."""
623
+ if self .fingerprint_resources == 'content' :
624
+ def update (path ):
625
+ with open (path , 'rb' ) as f :
626
+ while True :
627
+ data = f .read (65536 )
628
+ if not data :
629
+ break
630
+ hash .update (data )
631
+ elif self .fingerprint_resources == 'meta' :
632
+ def update (path ):
633
+ st = os .stat (path )
634
+ hash .update (str (st .st_size ) + str (st .st_mtime ))
635
+ else :
636
+ return None
637
+
638
+ all_dirs = [dir [1 ] for provider in self .template_providers
639
+ for dir in provider .get_htdocs_dirs () or []]
640
+ all_dirs .sort ()
641
+ hash = sha1 ()
642
+ for dir in all_dirs :
643
+ for path , dirs , files in os .walk (dir ):
644
+ dirs .sort ()
645
+ files .sort ()
646
+ for name in files :
647
+ update (os .path .join (path , name ))
648
+ return '!' + hash .hexdigest ()[:8 ]
649
+
595
650
def get_all_templates_dirs (self ):
596
651
"""Return a list of the names of all known templates directories."""
597
652
dirs = []
@@ -610,9 +665,11 @@ def prepare_request(self, req, handler=None):
610
665
611
666
chrome = {'metas' : [], 'links' : {}, 'scripts' : [], 'script_data' : {},
612
667
'ctxtnav' : [], 'warnings' : [], 'notices' : []}
613
- setattr ( req , ' chrome' , chrome )
668
+ req . chrome = chrome
614
669
615
- htdocs_location = self .htdocs_location or req .href .chrome ('common' )
670
+ chrome ['static_hash' ] = self .static_hash
671
+ htdocs_location = self .htdocs_location \
672
+ or req .href .chrome (self .static_hash , 'common' )
616
673
chrome ['htdocs_location' ] = htdocs_location .rstrip ('/' ) + '/'
617
674
618
675
# HTML <head> links
@@ -721,11 +778,14 @@ def get_icon_data(self, req):
721
778
if icon_src :
722
779
if not icon_src .startswith ('/' ) and icon_src .find ('://' ) == - 1 :
723
780
if '/' in icon_src :
724
- icon_abs_src = req .abs_href .chrome (icon_src )
725
- icon_src = req .href .chrome (icon_src )
781
+ icon_abs_src = req .abs_href .chrome (self .static_hash ,
782
+ icon_src )
783
+ icon_src = req .href .chrome (self .static_hash , icon_src )
726
784
else :
727
- icon_abs_src = req .abs_href .chrome ('common' , icon_src )
728
- icon_src = req .href .chrome ('common' , icon_src )
785
+ icon_abs_src = req .abs_href .chrome (self .static_hash ,
786
+ 'common' , icon_src )
787
+ icon_src = req .href .chrome (self .static_hash , 'common' ,
788
+ icon_src )
729
789
mimetype = get_mimetype (icon_src )
730
790
icon = {'src' : icon_src , 'abs_src' : icon_abs_src ,
731
791
'mimetype' : mimetype }
@@ -742,12 +802,13 @@ def get_logo_data(self, href, abs_href=None):
742
802
logo_src_abs = logo_src
743
803
elif '/' in logo_src :
744
804
# Like 'common/trac_banner.png' or 'site/my_banner.png'
745
- logo_src_abs = abs_href .chrome (logo_src )
746
- logo_src = href .chrome (logo_src )
805
+ logo_src_abs = abs_href .chrome (self . static_hash , logo_src )
806
+ logo_src = href .chrome (self . static_hash , logo_src )
747
807
else :
748
808
# Like 'trac_banner.png'
749
- logo_src_abs = abs_href .chrome ('common' , logo_src )
750
- logo_src = href .chrome ('common' , logo_src )
809
+ logo_src_abs = abs_href .chrome (self .static_hash , 'common' ,
810
+ logo_src )
811
+ logo_src = href .chrome (self .static_hash , 'common' , logo_src )
751
812
width = self .logo_width if self .logo_width > - 1 else None
752
813
height = self .logo_height if self .logo_height > - 1 else None
753
814
logo = {
0 commit comments