sprockets 2.3.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sprockets might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE +2 -2
- data/README.md +332 -115
- data/bin/sprockets +8 -0
- data/lib/rake/sprocketstask.rb +25 -13
- data/lib/sprockets/asset.rb +143 -205
- data/lib/sprockets/autoload/closure.rb +7 -0
- data/lib/sprockets/autoload/coffee_script.rb +7 -0
- data/lib/sprockets/autoload/eco.rb +7 -0
- data/lib/sprockets/autoload/ejs.rb +7 -0
- data/lib/sprockets/autoload/sass.rb +7 -0
- data/lib/sprockets/autoload/uglifier.rb +7 -0
- data/lib/sprockets/autoload/yui.rb +7 -0
- data/lib/sprockets/autoload.rb +11 -0
- data/lib/sprockets/base.rb +49 -257
- data/lib/sprockets/bower.rb +58 -0
- data/lib/sprockets/bundle.rb +65 -0
- data/lib/sprockets/cache/file_store.rb +165 -14
- data/lib/sprockets/cache/memory_store.rb +66 -0
- data/lib/sprockets/cache/null_store.rb +46 -0
- data/lib/sprockets/cache.rb +234 -0
- data/lib/sprockets/cached_environment.rb +69 -0
- data/lib/sprockets/closure_compressor.rb +53 -0
- data/lib/sprockets/coffee_script_processor.rb +25 -0
- data/lib/sprockets/coffee_script_template.rb +6 -0
- data/lib/sprockets/compressing.rb +74 -0
- data/lib/sprockets/configuration.rb +83 -0
- data/lib/sprockets/context.rb +125 -131
- data/lib/sprockets/dependencies.rb +73 -0
- data/lib/sprockets/digest_utils.rb +156 -0
- data/lib/sprockets/directive_processor.rb +209 -211
- data/lib/sprockets/eco_processor.rb +32 -0
- data/lib/sprockets/eco_template.rb +3 -35
- data/lib/sprockets/ejs_processor.rb +31 -0
- data/lib/sprockets/ejs_template.rb +3 -34
- data/lib/sprockets/encoding_utils.rb +258 -0
- data/lib/sprockets/engines.rb +45 -38
- data/lib/sprockets/environment.rb +17 -67
- data/lib/sprockets/erb_processor.rb +30 -0
- data/lib/sprockets/erb_template.rb +6 -0
- data/lib/sprockets/errors.rb +6 -13
- data/lib/sprockets/file_reader.rb +15 -0
- data/lib/sprockets/http_utils.rb +115 -0
- data/lib/sprockets/jst_processor.rb +35 -19
- data/lib/sprockets/legacy.rb +314 -0
- data/lib/sprockets/legacy_proc_processor.rb +35 -0
- data/lib/sprockets/legacy_tilt_processor.rb +29 -0
- data/lib/sprockets/loader.rb +176 -0
- data/lib/sprockets/manifest.rb +179 -98
- data/lib/sprockets/manifest_utils.rb +45 -0
- data/lib/sprockets/mime.rb +114 -32
- data/lib/sprockets/path_dependency_utils.rb +85 -0
- data/lib/sprockets/path_digest_utils.rb +47 -0
- data/lib/sprockets/path_utils.rb +282 -0
- data/lib/sprockets/paths.rb +81 -0
- data/lib/sprockets/processing.rb +157 -189
- data/lib/sprockets/processor_utils.rb +103 -0
- data/lib/sprockets/resolve.rb +208 -0
- data/lib/sprockets/sass_cache_store.rb +19 -15
- data/lib/sprockets/sass_compressor.rb +59 -0
- data/lib/sprockets/sass_functions.rb +2 -0
- data/lib/sprockets/sass_importer.rb +2 -29
- data/lib/sprockets/sass_processor.rb +285 -0
- data/lib/sprockets/sass_template.rb +4 -44
- data/lib/sprockets/server.rb +109 -84
- data/lib/sprockets/transformers.rb +145 -0
- data/lib/sprockets/uglifier_compressor.rb +63 -0
- data/lib/sprockets/uri_utils.rb +190 -0
- data/lib/sprockets/utils.rb +193 -44
- data/lib/sprockets/version.rb +1 -1
- data/lib/sprockets/yui_compressor.rb +65 -0
- data/lib/sprockets.rb +144 -53
- metadata +248 -238
- data/lib/sprockets/asset_attributes.rb +0 -126
- data/lib/sprockets/bundled_asset.rb +0 -79
- data/lib/sprockets/caching.rb +0 -96
- data/lib/sprockets/charset_normalizer.rb +0 -41
- data/lib/sprockets/index.rb +0 -99
- data/lib/sprockets/processed_asset.rb +0 -152
- data/lib/sprockets/processor.rb +0 -32
- data/lib/sprockets/safety_colons.rb +0 -28
- data/lib/sprockets/scss_template.rb +0 -13
- data/lib/sprockets/static_asset.rb +0 -57
- data/lib/sprockets/trail.rb +0 -90
data/lib/sprockets/base.rb
CHANGED
@@ -1,89 +1,24 @@
|
|
1
|
-
require 'sprockets/
|
2
|
-
require 'sprockets/
|
3
|
-
require 'sprockets/
|
4
|
-
require 'sprockets/
|
5
|
-
require 'sprockets/
|
1
|
+
require 'sprockets/asset'
|
2
|
+
require 'sprockets/bower'
|
3
|
+
require 'sprockets/cache'
|
4
|
+
require 'sprockets/configuration'
|
5
|
+
require 'sprockets/digest_utils'
|
6
|
+
require 'sprockets/errors'
|
7
|
+
require 'sprockets/loader'
|
8
|
+
require 'sprockets/path_digest_utils'
|
9
|
+
require 'sprockets/path_dependency_utils'
|
10
|
+
require 'sprockets/path_utils'
|
11
|
+
require 'sprockets/resolve'
|
6
12
|
require 'sprockets/server'
|
7
|
-
require 'sprockets/static_asset'
|
8
|
-
require 'sprockets/trail'
|
9
|
-
require 'pathname'
|
10
13
|
|
11
14
|
module Sprockets
|
12
|
-
# `Base` class for `Environment` and `
|
15
|
+
# `Base` class for `Environment` and `Cached`.
|
13
16
|
class Base
|
14
|
-
include
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
attr_reader :digest_class
|
20
|
-
|
21
|
-
# Assign a `Digest` implementation class. This maybe any Ruby
|
22
|
-
# `Digest::` implementation such as `Digest::MD5` or
|
23
|
-
# `Digest::SHA1`.
|
24
|
-
#
|
25
|
-
# environment.digest_class = Digest::SHA1
|
26
|
-
#
|
27
|
-
def digest_class=(klass)
|
28
|
-
expire_index!
|
29
|
-
@digest_class = klass
|
30
|
-
end
|
31
|
-
|
32
|
-
# The `Environment#version` is a custom value used for manually
|
33
|
-
# expiring all asset caches.
|
34
|
-
#
|
35
|
-
# Sprockets is able to track most file and directory changes and
|
36
|
-
# will take care of expiring the cache for you. However, its
|
37
|
-
# impossible to know when any custom helpers change that you mix
|
38
|
-
# into the `Context`.
|
39
|
-
#
|
40
|
-
# It would be wise to increment this value anytime you make a
|
41
|
-
# configuration change to the `Environment` object.
|
42
|
-
attr_reader :version
|
43
|
-
|
44
|
-
# Assign an environment version.
|
45
|
-
#
|
46
|
-
# environment.version = '2.0'
|
47
|
-
#
|
48
|
-
def version=(version)
|
49
|
-
expire_index!
|
50
|
-
@version = version
|
51
|
-
end
|
52
|
-
|
53
|
-
# Returns a `Digest` instance for the `Environment`.
|
54
|
-
#
|
55
|
-
# This value serves two purposes. If two `Environment`s have the
|
56
|
-
# same digest value they can be treated as equal. This is more
|
57
|
-
# useful for comparing environment states between processes rather
|
58
|
-
# than in the same. Two equal `Environment`s can share the same
|
59
|
-
# cached assets.
|
60
|
-
#
|
61
|
-
# The value also provides a seed digest for all `Asset`
|
62
|
-
# digests. Any change in the environment digest will affect all of
|
63
|
-
# its assets.
|
64
|
-
def digest
|
65
|
-
# Compute the initial digest using the implementation class. The
|
66
|
-
# Sprockets release version and custom environment version are
|
67
|
-
# mixed in. So any new releases will affect all your assets.
|
68
|
-
@digest ||= digest_class.new.update(VERSION).update(version.to_s)
|
69
|
-
|
70
|
-
# Returned a dupped copy so the caller can safely mutate it with `.update`
|
71
|
-
@digest.dup
|
72
|
-
end
|
73
|
-
|
74
|
-
# Get and set `Logger` instance.
|
75
|
-
attr_accessor :logger
|
76
|
-
|
77
|
-
# Get `Context` class.
|
78
|
-
#
|
79
|
-
# This class maybe mutated and mixed in with custom helpers.
|
80
|
-
#
|
81
|
-
# environment.context_class.instance_eval do
|
82
|
-
# include MyHelpers
|
83
|
-
# def asset_url; end
|
84
|
-
# end
|
85
|
-
#
|
86
|
-
attr_reader :context_class
|
17
|
+
include PathUtils, PathDependencyUtils, PathDigestUtils, DigestUtils
|
18
|
+
include Configuration
|
19
|
+
include Server
|
20
|
+
include Resolve, Loader
|
21
|
+
include Bower
|
87
22
|
|
88
23
|
# Get persistent cache store
|
89
24
|
attr_reader :cache
|
@@ -94,79 +29,56 @@ module Sprockets
|
|
94
29
|
# setters. Either `get(key)`/`set(key, value)`,
|
95
30
|
# `[key]`/`[key]=value`, `read(key)`/`write(key, value)`.
|
96
31
|
def cache=(cache)
|
97
|
-
|
98
|
-
@cache = cache
|
32
|
+
@cache = Cache.new(cache, logger)
|
99
33
|
end
|
100
34
|
|
101
|
-
# Return an `
|
102
|
-
def
|
35
|
+
# Return an `Cached`. Must be implemented by the subclass.
|
36
|
+
def cached
|
103
37
|
raise NotImplementedError
|
104
38
|
end
|
39
|
+
alias_method :index, :cached
|
105
40
|
|
106
|
-
|
107
|
-
# Define `default_external_encoding` accessor on 1.9.
|
108
|
-
# Defaults to UTF-8.
|
109
|
-
attr_accessor :default_external_encoding
|
110
|
-
end
|
111
|
-
|
112
|
-
# Works like `Dir.entries`.
|
113
|
-
#
|
114
|
-
# Subclasses may cache this method.
|
115
|
-
def entries(pathname)
|
116
|
-
trail.entries(pathname)
|
117
|
-
end
|
118
|
-
|
119
|
-
# Works like `File.stat`.
|
41
|
+
# Internal: Compute digest for path.
|
120
42
|
#
|
121
|
-
#
|
122
|
-
def stat(path)
|
123
|
-
trail.stat(path)
|
124
|
-
end
|
125
|
-
|
126
|
-
# Read and compute digest of filename.
|
43
|
+
# path - String filename or directory path.
|
127
44
|
#
|
128
|
-
#
|
45
|
+
# Returns a String digest or nil.
|
129
46
|
def file_digest(path)
|
130
47
|
if stat = self.stat(path)
|
131
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
#
|
136
|
-
|
137
|
-
|
138
|
-
digest.update(contents)
|
48
|
+
# Caveat: Digests are cached by the path's current mtime. Its possible
|
49
|
+
# for a files contents to have changed and its mtime to have been
|
50
|
+
# negligently reset thus appearing as if the file hasn't changed on
|
51
|
+
# disk. Also, the mtime is only read to the nearest second. Its
|
52
|
+
# also possible the file was updated more than once in a given second.
|
53
|
+
cache.fetch("file_digest:#{path}:#{stat.mtime.to_i}") do
|
54
|
+
self.stat_digest(path, stat)
|
139
55
|
end
|
140
56
|
end
|
141
57
|
end
|
142
58
|
|
143
|
-
#
|
144
|
-
def
|
145
|
-
|
59
|
+
# Find asset by logical path or expanded path.
|
60
|
+
def find_asset(path, options = {})
|
61
|
+
uri, _ = resolve(path, options.merge(compat: false))
|
62
|
+
if uri
|
63
|
+
load(uri)
|
64
|
+
end
|
146
65
|
end
|
147
66
|
|
148
|
-
|
149
|
-
|
150
|
-
attributes_for(path).content_type
|
151
|
-
end
|
67
|
+
def find_all_linked_assets(path, options = {})
|
68
|
+
return to_enum(__method__, path, options) unless block_given?
|
152
69
|
|
153
|
-
|
154
|
-
|
155
|
-
logical_path = path
|
156
|
-
pathname = Pathname.new(path)
|
70
|
+
asset = find_asset(path, options)
|
71
|
+
return unless asset
|
157
72
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
rescue FileNotFound
|
165
|
-
return nil
|
166
|
-
end
|
73
|
+
yield asset
|
74
|
+
stack = asset.links.to_a
|
75
|
+
|
76
|
+
while uri = stack.shift
|
77
|
+
yield asset = load(uri)
|
78
|
+
stack = asset.links.to_a + stack
|
167
79
|
end
|
168
80
|
|
169
|
-
|
81
|
+
nil
|
170
82
|
end
|
171
83
|
|
172
84
|
# Preferred `find_asset` shorthand.
|
@@ -177,131 +89,11 @@ module Sprockets
|
|
177
89
|
find_asset(*args)
|
178
90
|
end
|
179
91
|
|
180
|
-
def each_entry(root, &block)
|
181
|
-
return to_enum(__method__, root) unless block_given?
|
182
|
-
root = Pathname.new(root) unless root.is_a?(Pathname)
|
183
|
-
|
184
|
-
paths = []
|
185
|
-
entries(root).sort.each do |filename|
|
186
|
-
path = root.join(filename)
|
187
|
-
paths << path
|
188
|
-
|
189
|
-
if stat(path).directory?
|
190
|
-
each_entry(path) do |subpath|
|
191
|
-
paths << subpath
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
paths.sort_by(&:to_s).each(&block)
|
197
|
-
|
198
|
-
nil
|
199
|
-
end
|
200
|
-
|
201
|
-
def each_file
|
202
|
-
return to_enum(__method__) unless block_given?
|
203
|
-
paths.each do |root|
|
204
|
-
each_entry(root) do |path|
|
205
|
-
if !stat(path).directory?
|
206
|
-
yield path
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
nil
|
211
|
-
end
|
212
|
-
|
213
|
-
def each_logical_path(*args)
|
214
|
-
return to_enum(__method__, *args) unless block_given?
|
215
|
-
filters = args.flatten
|
216
|
-
files = {}
|
217
|
-
each_file do |filename|
|
218
|
-
if logical_path = logical_path_for_filename(filename, filters)
|
219
|
-
yield logical_path unless files[logical_path]
|
220
|
-
files[logical_path] = true
|
221
|
-
end
|
222
|
-
end
|
223
|
-
nil
|
224
|
-
end
|
225
|
-
|
226
92
|
# Pretty inspect
|
227
93
|
def inspect
|
228
94
|
"#<#{self.class}:0x#{object_id.to_s(16)} " +
|
229
95
|
"root=#{root.to_s.inspect}, " +
|
230
|
-
"paths=#{paths.inspect}
|
231
|
-
"digest=#{digest.to_s.inspect}" +
|
232
|
-
">"
|
96
|
+
"paths=#{paths.inspect}>"
|
233
97
|
end
|
234
|
-
|
235
|
-
protected
|
236
|
-
# Clear index after mutating state. Must be implemented by the subclass.
|
237
|
-
def expire_index!
|
238
|
-
raise NotImplementedError
|
239
|
-
end
|
240
|
-
|
241
|
-
def build_asset(logical_path, pathname, options)
|
242
|
-
pathname = Pathname.new(pathname)
|
243
|
-
|
244
|
-
# If there are any processors to run on the pathname, use
|
245
|
-
# `BundledAsset`. Otherwise use `StaticAsset` and treat is as binary.
|
246
|
-
if attributes_for(pathname).processors.any?
|
247
|
-
if options[:bundle] == false
|
248
|
-
circular_call_protection(pathname.to_s) do
|
249
|
-
ProcessedAsset.new(index, logical_path, pathname)
|
250
|
-
end
|
251
|
-
else
|
252
|
-
BundledAsset.new(index, logical_path, pathname)
|
253
|
-
end
|
254
|
-
else
|
255
|
-
StaticAsset.new(index, logical_path, pathname)
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def cache_key_for(path, options)
|
260
|
-
"#{path}:#{options[:bundle] ? '1' : '0'}"
|
261
|
-
end
|
262
|
-
|
263
|
-
def circular_call_protection(path)
|
264
|
-
reset = Thread.current[:sprockets_circular_calls].nil?
|
265
|
-
calls = Thread.current[:sprockets_circular_calls] ||= Set.new
|
266
|
-
if calls.include?(path)
|
267
|
-
raise CircularDependencyError, "#{path} has already been required"
|
268
|
-
end
|
269
|
-
calls << path
|
270
|
-
yield
|
271
|
-
ensure
|
272
|
-
Thread.current[:sprockets_circular_calls] = nil if reset
|
273
|
-
end
|
274
|
-
|
275
|
-
def logical_path_for_filename(filename, filters)
|
276
|
-
logical_path = attributes_for(filename).logical_path.to_s
|
277
|
-
|
278
|
-
if matches_filter(filters, logical_path)
|
279
|
-
return logical_path
|
280
|
-
end
|
281
|
-
|
282
|
-
# If filename is an index file, retest with alias
|
283
|
-
if File.basename(logical_path)[/[^\.]+/, 0] == 'index'
|
284
|
-
path = logical_path.sub(/\/index\./, '.')
|
285
|
-
if matches_filter(filters, path)
|
286
|
-
return path
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
nil
|
291
|
-
end
|
292
|
-
|
293
|
-
def matches_filter(filters, filename)
|
294
|
-
return true if filters.empty?
|
295
|
-
|
296
|
-
filters.any? do |filter|
|
297
|
-
if filter.is_a?(Regexp)
|
298
|
-
filter.match(filename)
|
299
|
-
elsif filter.respond_to?(:call)
|
300
|
-
filter.call(filename)
|
301
|
-
else
|
302
|
-
File.fnmatch(filter.to_s, filename)
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
98
|
end
|
307
99
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Sprockets
|
4
|
+
module Bower
|
5
|
+
# Internal: All supported bower.json files.
|
6
|
+
#
|
7
|
+
# https://github.com/bower/json/blob/0.4.0/lib/json.js#L7
|
8
|
+
POSSIBLE_BOWER_JSONS = ['bower.json', 'component.json', '.bower.json']
|
9
|
+
|
10
|
+
# Internal: Override resolve_alternates to install bower.json behavior.
|
11
|
+
#
|
12
|
+
# load_path - String environment path
|
13
|
+
# logical_path - String path relative to base
|
14
|
+
#
|
15
|
+
# Returns candiate filenames.
|
16
|
+
def resolve_alternates(load_path, logical_path)
|
17
|
+
candidates, deps = super
|
18
|
+
|
19
|
+
# bower.json can only be nested one level deep
|
20
|
+
if !logical_path.index('/')
|
21
|
+
dirname = File.join(load_path, logical_path)
|
22
|
+
|
23
|
+
if directory?(dirname)
|
24
|
+
filenames = POSSIBLE_BOWER_JSONS.map { |basename| File.join(dirname, basename) }
|
25
|
+
filename = filenames.detect { |fn| self.file?(fn) }
|
26
|
+
|
27
|
+
if filename
|
28
|
+
deps << build_file_digest_uri(filename)
|
29
|
+
read_bower_main(dirname, filename) do |path|
|
30
|
+
candidates << path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
return candidates, deps
|
37
|
+
end
|
38
|
+
|
39
|
+
# Internal: Read bower.json's main directive.
|
40
|
+
#
|
41
|
+
# dirname - String path to component directory.
|
42
|
+
# filename - String path to bower.json.
|
43
|
+
#
|
44
|
+
# Returns nothing.
|
45
|
+
def read_bower_main(dirname, filename)
|
46
|
+
bower = JSON.parse(File.read(filename), create_additions: false)
|
47
|
+
|
48
|
+
case bower['main']
|
49
|
+
when String
|
50
|
+
yield File.expand_path(bower['main'], dirname)
|
51
|
+
when Array
|
52
|
+
bower['main'].each do |name|
|
53
|
+
yield File.expand_path(name, dirname)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'sprockets/utils'
|
3
|
+
|
4
|
+
module Sprockets
|
5
|
+
# Internal: Bundle processor takes a single file asset and prepends all the
|
6
|
+
# `:required` URIs to the contents.
|
7
|
+
#
|
8
|
+
# Uses pipeline metadata:
|
9
|
+
#
|
10
|
+
# :required - Ordered Set of asset URIs to prepend
|
11
|
+
# :stubbed - Set of asset URIs to substract from the required set.
|
12
|
+
#
|
13
|
+
# Also see DirectiveProcessor.
|
14
|
+
class Bundle
|
15
|
+
def self.call(input)
|
16
|
+
env = input[:environment]
|
17
|
+
type = input[:content_type]
|
18
|
+
dependencies = Set.new(input[:metadata][:dependencies])
|
19
|
+
|
20
|
+
processed_uri, deps = env.resolve(input[:filename], accept: type, pipeline: :self, compat: false)
|
21
|
+
dependencies.merge(deps)
|
22
|
+
|
23
|
+
find_required = proc { |uri| env.load(uri).metadata[:required] }
|
24
|
+
required = Utils.dfs(processed_uri, &find_required)
|
25
|
+
stubbed = Utils.dfs(env.load(processed_uri).metadata[:stubbed], &find_required)
|
26
|
+
required.subtract(stubbed)
|
27
|
+
assets = required.map { |uri| env.load(uri) }
|
28
|
+
|
29
|
+
(required + stubbed).each do |uri|
|
30
|
+
dependencies.merge(env.load(uri).metadata[:dependencies])
|
31
|
+
end
|
32
|
+
|
33
|
+
reducers = Hash[env.match_mime_type_keys(env.config[:bundle_reducers], type).flat_map(&:to_a)]
|
34
|
+
process_bundle_reducers(assets, reducers).merge(dependencies: dependencies, included: assets.map(&:uri))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Internal: Run bundle reducers on set of Assets producing a reduced
|
38
|
+
# metadata Hash.
|
39
|
+
#
|
40
|
+
# assets - Array of Assets
|
41
|
+
# reducers - Array of [initial, reducer_proc] pairs
|
42
|
+
#
|
43
|
+
# Returns reduced asset metadata Hash.
|
44
|
+
def self.process_bundle_reducers(assets, reducers)
|
45
|
+
initial = {}
|
46
|
+
reducers.each do |k, (v, _)|
|
47
|
+
initial[k] = v if !v.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
assets.reduce(initial) do |h, asset|
|
51
|
+
reducers.each do |k, (_, block)|
|
52
|
+
value = k == :data ? asset.source : asset.metadata[k]
|
53
|
+
if h.key?(k)
|
54
|
+
if !value.nil?
|
55
|
+
h[k] = block.call(h[k], value)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
h[k] = value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
h
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -1,32 +1,183 @@
|
|
1
|
-
require 'digest/md5'
|
2
1
|
require 'fileutils'
|
3
|
-
require '
|
2
|
+
require 'logger'
|
3
|
+
require 'sprockets/encoding_utils'
|
4
|
+
require 'sprockets/path_utils'
|
5
|
+
require 'zlib'
|
4
6
|
|
5
7
|
module Sprockets
|
6
|
-
|
7
|
-
# A
|
8
|
+
class Cache
|
9
|
+
# Public: A file system cache store that automatically cleans up old keys.
|
10
|
+
#
|
11
|
+
# Assign the instance to the Environment#cache.
|
8
12
|
#
|
9
13
|
# environment.cache = Sprockets::Cache::FileStore.new("/tmp")
|
10
14
|
#
|
15
|
+
# See Also
|
16
|
+
#
|
17
|
+
# ActiveSupport::Cache::FileStore
|
18
|
+
#
|
11
19
|
class FileStore
|
12
|
-
|
13
|
-
|
20
|
+
# Internal: Default key limit for store.
|
21
|
+
DEFAULT_MAX_SIZE = 25 * 1024 * 1024
|
22
|
+
|
23
|
+
# Internal: Default standard error fatal logger.
|
24
|
+
#
|
25
|
+
# Returns a Logger.
|
26
|
+
def self.default_logger
|
27
|
+
logger = Logger.new($stderr)
|
28
|
+
logger.level = Logger::FATAL
|
29
|
+
logger
|
14
30
|
end
|
15
31
|
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
32
|
+
# Public: Initialize the cache store.
|
33
|
+
#
|
34
|
+
# root - A String path to a directory to persist cached values to.
|
35
|
+
# max_size - A Integer of the maximum number of keys the store will hold.
|
36
|
+
# (default: 1000).
|
37
|
+
def initialize(root, max_size = DEFAULT_MAX_SIZE, logger = self.class.default_logger)
|
38
|
+
@root = root
|
39
|
+
@size = find_caches.inject(0) { |n, (_, stat)| n + stat.size }
|
40
|
+
@max_size = max_size
|
41
|
+
@gc_size = max_size * 0.75
|
42
|
+
@logger = logger
|
20
43
|
end
|
21
44
|
|
22
|
-
#
|
23
|
-
|
45
|
+
# Public: Retrieve value from cache.
|
46
|
+
#
|
47
|
+
# This API should not be used directly, but via the Cache wrapper API.
|
48
|
+
#
|
49
|
+
# key - String cache key.
|
50
|
+
#
|
51
|
+
# Returns Object or nil or the value is not set.
|
52
|
+
def get(key)
|
53
|
+
path = File.join(@root, "#{key}.cache")
|
54
|
+
|
55
|
+
value = safe_open(path) do |f|
|
56
|
+
begin
|
57
|
+
EncodingUtils.unmarshaled_deflated(f.read, Zlib::MAX_WBITS)
|
58
|
+
rescue Exception => e
|
59
|
+
@logger.error do
|
60
|
+
"#{self.class}[#{path}] could not be unmarshaled: " +
|
61
|
+
"#{e.class}: #{e.message}"
|
62
|
+
end
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if value
|
68
|
+
FileUtils.touch(path)
|
69
|
+
value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Public: Set a key and value in the cache.
|
74
|
+
#
|
75
|
+
# This API should not be used directly, but via the Cache wrapper API.
|
76
|
+
#
|
77
|
+
# key - String cache key.
|
78
|
+
# value - Object value.
|
79
|
+
#
|
80
|
+
# Returns Object value.
|
81
|
+
def set(key, value)
|
82
|
+
path = File.join(@root, "#{key}.cache")
|
83
|
+
|
24
84
|
# Ensure directory exists
|
25
|
-
FileUtils.mkdir_p
|
85
|
+
FileUtils.mkdir_p File.dirname(path)
|
86
|
+
|
87
|
+
# Check if cache exists before writing
|
88
|
+
exists = File.exist?(path)
|
89
|
+
|
90
|
+
# Serialize value
|
91
|
+
marshaled = Marshal.dump(value)
|
92
|
+
|
93
|
+
# Compress if larger than 4KB
|
94
|
+
if marshaled.bytesize > 4 * 1024
|
95
|
+
deflater = Zlib::Deflate.new(
|
96
|
+
Zlib::BEST_COMPRESSION,
|
97
|
+
Zlib::MAX_WBITS,
|
98
|
+
Zlib::MAX_MEM_LEVEL,
|
99
|
+
Zlib::DEFAULT_STRATEGY
|
100
|
+
)
|
101
|
+
deflater << marshaled
|
102
|
+
raw = deflater.finish
|
103
|
+
else
|
104
|
+
raw = marshaled
|
105
|
+
end
|
106
|
+
|
107
|
+
# Write data
|
108
|
+
PathUtils.atomic_write(path) do |f|
|
109
|
+
f.write(raw)
|
110
|
+
@size += f.size unless exists
|
111
|
+
end
|
112
|
+
|
113
|
+
# GC if necessary
|
114
|
+
gc! if @size > @max_size
|
26
115
|
|
27
|
-
@root.join(key).open('w') { |f| Marshal.dump(value, f)}
|
28
116
|
value
|
29
117
|
end
|
118
|
+
|
119
|
+
# Public: Pretty inspect
|
120
|
+
#
|
121
|
+
# Returns String.
|
122
|
+
def inspect
|
123
|
+
"#<#{self.class} size=#{@size}/#{@max_size}>"
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
# Internal: Get all cache files along with stats.
|
128
|
+
#
|
129
|
+
# Returns an Array of [String filename, File::Stat] pairs sorted by
|
130
|
+
# mtime.
|
131
|
+
def find_caches
|
132
|
+
Dir.glob(File.join(@root, '**/*.cache')).reduce([]) { |stats, filename|
|
133
|
+
stat = safe_stat(filename)
|
134
|
+
# stat maybe nil if file was removed between the time we called
|
135
|
+
# dir.glob and the next stat
|
136
|
+
stats << [filename, stat] if stat
|
137
|
+
stats
|
138
|
+
}.sort_by { |_, stat| stat.mtime.to_i }
|
139
|
+
end
|
140
|
+
|
141
|
+
def compute_size(caches)
|
142
|
+
caches.inject(0) { |sum, (_, stat)| sum + stat.size }
|
143
|
+
end
|
144
|
+
|
145
|
+
def safe_stat(fn)
|
146
|
+
File.stat(fn)
|
147
|
+
rescue Errno::ENOENT
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
|
151
|
+
def safe_open(path, &block)
|
152
|
+
if File.exist?(path)
|
153
|
+
File.open(path, 'rb', &block)
|
154
|
+
end
|
155
|
+
rescue Errno::ENOENT
|
156
|
+
end
|
157
|
+
|
158
|
+
def gc!
|
159
|
+
start_time = Time.now
|
160
|
+
|
161
|
+
caches = find_caches
|
162
|
+
size = compute_size(caches)
|
163
|
+
|
164
|
+
delete_caches, keep_caches = caches.partition { |filename, stat|
|
165
|
+
deleted = size > @gc_size
|
166
|
+
size -= stat.size
|
167
|
+
deleted
|
168
|
+
}
|
169
|
+
|
170
|
+
return if delete_caches.empty?
|
171
|
+
|
172
|
+
FileUtils.remove(delete_caches.map(&:first), force: true)
|
173
|
+
@size = compute_size(keep_caches)
|
174
|
+
|
175
|
+
@logger.warn do
|
176
|
+
secs = Time.now.to_f - start_time.to_f
|
177
|
+
"#{self.class}[#{@root}] garbage collected " +
|
178
|
+
"#{delete_caches.size} files (#{(secs * 1000).to_i}ms)"
|
179
|
+
end
|
180
|
+
end
|
30
181
|
end
|
31
182
|
end
|
32
183
|
end
|