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.

Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +2 -2
  3. data/README.md +332 -115
  4. data/bin/sprockets +8 -0
  5. data/lib/rake/sprocketstask.rb +25 -13
  6. data/lib/sprockets/asset.rb +143 -205
  7. data/lib/sprockets/autoload/closure.rb +7 -0
  8. data/lib/sprockets/autoload/coffee_script.rb +7 -0
  9. data/lib/sprockets/autoload/eco.rb +7 -0
  10. data/lib/sprockets/autoload/ejs.rb +7 -0
  11. data/lib/sprockets/autoload/sass.rb +7 -0
  12. data/lib/sprockets/autoload/uglifier.rb +7 -0
  13. data/lib/sprockets/autoload/yui.rb +7 -0
  14. data/lib/sprockets/autoload.rb +11 -0
  15. data/lib/sprockets/base.rb +49 -257
  16. data/lib/sprockets/bower.rb +58 -0
  17. data/lib/sprockets/bundle.rb +65 -0
  18. data/lib/sprockets/cache/file_store.rb +165 -14
  19. data/lib/sprockets/cache/memory_store.rb +66 -0
  20. data/lib/sprockets/cache/null_store.rb +46 -0
  21. data/lib/sprockets/cache.rb +234 -0
  22. data/lib/sprockets/cached_environment.rb +69 -0
  23. data/lib/sprockets/closure_compressor.rb +53 -0
  24. data/lib/sprockets/coffee_script_processor.rb +25 -0
  25. data/lib/sprockets/coffee_script_template.rb +6 -0
  26. data/lib/sprockets/compressing.rb +74 -0
  27. data/lib/sprockets/configuration.rb +83 -0
  28. data/lib/sprockets/context.rb +125 -131
  29. data/lib/sprockets/dependencies.rb +73 -0
  30. data/lib/sprockets/digest_utils.rb +156 -0
  31. data/lib/sprockets/directive_processor.rb +209 -211
  32. data/lib/sprockets/eco_processor.rb +32 -0
  33. data/lib/sprockets/eco_template.rb +3 -35
  34. data/lib/sprockets/ejs_processor.rb +31 -0
  35. data/lib/sprockets/ejs_template.rb +3 -34
  36. data/lib/sprockets/encoding_utils.rb +258 -0
  37. data/lib/sprockets/engines.rb +45 -38
  38. data/lib/sprockets/environment.rb +17 -67
  39. data/lib/sprockets/erb_processor.rb +30 -0
  40. data/lib/sprockets/erb_template.rb +6 -0
  41. data/lib/sprockets/errors.rb +6 -13
  42. data/lib/sprockets/file_reader.rb +15 -0
  43. data/lib/sprockets/http_utils.rb +115 -0
  44. data/lib/sprockets/jst_processor.rb +35 -19
  45. data/lib/sprockets/legacy.rb +314 -0
  46. data/lib/sprockets/legacy_proc_processor.rb +35 -0
  47. data/lib/sprockets/legacy_tilt_processor.rb +29 -0
  48. data/lib/sprockets/loader.rb +176 -0
  49. data/lib/sprockets/manifest.rb +179 -98
  50. data/lib/sprockets/manifest_utils.rb +45 -0
  51. data/lib/sprockets/mime.rb +114 -32
  52. data/lib/sprockets/path_dependency_utils.rb +85 -0
  53. data/lib/sprockets/path_digest_utils.rb +47 -0
  54. data/lib/sprockets/path_utils.rb +282 -0
  55. data/lib/sprockets/paths.rb +81 -0
  56. data/lib/sprockets/processing.rb +157 -189
  57. data/lib/sprockets/processor_utils.rb +103 -0
  58. data/lib/sprockets/resolve.rb +208 -0
  59. data/lib/sprockets/sass_cache_store.rb +19 -15
  60. data/lib/sprockets/sass_compressor.rb +59 -0
  61. data/lib/sprockets/sass_functions.rb +2 -0
  62. data/lib/sprockets/sass_importer.rb +2 -29
  63. data/lib/sprockets/sass_processor.rb +285 -0
  64. data/lib/sprockets/sass_template.rb +4 -44
  65. data/lib/sprockets/server.rb +109 -84
  66. data/lib/sprockets/transformers.rb +145 -0
  67. data/lib/sprockets/uglifier_compressor.rb +63 -0
  68. data/lib/sprockets/uri_utils.rb +190 -0
  69. data/lib/sprockets/utils.rb +193 -44
  70. data/lib/sprockets/version.rb +1 -1
  71. data/lib/sprockets/yui_compressor.rb +65 -0
  72. data/lib/sprockets.rb +144 -53
  73. metadata +248 -238
  74. data/lib/sprockets/asset_attributes.rb +0 -126
  75. data/lib/sprockets/bundled_asset.rb +0 -79
  76. data/lib/sprockets/caching.rb +0 -96
  77. data/lib/sprockets/charset_normalizer.rb +0 -41
  78. data/lib/sprockets/index.rb +0 -99
  79. data/lib/sprockets/processed_asset.rb +0 -152
  80. data/lib/sprockets/processor.rb +0 -32
  81. data/lib/sprockets/safety_colons.rb +0 -28
  82. data/lib/sprockets/scss_template.rb +0 -13
  83. data/lib/sprockets/static_asset.rb +0 -57
  84. data/lib/sprockets/trail.rb +0 -90
@@ -1,89 +1,24 @@
1
- require 'sprockets/asset_attributes'
2
- require 'sprockets/bundled_asset'
3
- require 'sprockets/caching'
4
- require 'sprockets/processed_asset'
5
- require 'sprockets/processing'
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 `Index`.
15
+ # `Base` class for `Environment` and `Cached`.
13
16
  class Base
14
- include Caching, Processing, Server, Trail
15
-
16
- # Returns a `Digest` implementation class.
17
- #
18
- # Defaults to `Digest::MD5`.
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
- expire_index!
98
- @cache = cache
32
+ @cache = Cache.new(cache, logger)
99
33
  end
100
34
 
101
- # Return an `Index`. Must be implemented by the subclass.
102
- def index
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
- if defined? Encoding.default_external
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
- # Subclasses may cache this method.
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
- # Subclasses may cache this method.
45
+ # Returns a String digest or nil.
129
46
  def file_digest(path)
130
47
  if stat = self.stat(path)
131
- # If its a file, digest the contents
132
- if stat.file?
133
- digest.file(path.to_s)
134
-
135
- # If its a directive, digest the list of filenames
136
- elsif stat.directory?
137
- contents = self.entries(path).join(',')
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
- # Internal. Return a `AssetAttributes` for `path`.
144
- def attributes_for(path)
145
- AssetAttributes.new(self, path)
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
- # Internal. Return content type of `path`.
149
- def content_type_of(path)
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
- # Find asset by logical path or expanded path.
154
- def find_asset(path, options = {})
155
- logical_path = path
156
- pathname = Pathname.new(path)
70
+ asset = find_asset(path, options)
71
+ return unless asset
157
72
 
158
- if pathname.absolute?
159
- return unless stat(pathname)
160
- logical_path = attributes_for(pathname).logical_path
161
- else
162
- begin
163
- pathname = resolve(logical_path)
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
- build_asset(logical_path, pathname, options)
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 'pathname'
2
+ require 'logger'
3
+ require 'sprockets/encoding_utils'
4
+ require 'sprockets/path_utils'
5
+ require 'zlib'
4
6
 
5
7
  module Sprockets
6
- module Cache
7
- # A simple file system cache store.
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
- def initialize(root)
13
- @root = Pathname.new(root)
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
- # Lookup value in cache
17
- def [](key)
18
- pathname = @root.join(key)
19
- pathname.exist? ? pathname.open('rb') { |f| Marshal.load(f) } : nil
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
- # Save value to cache
23
- def []=(key, value)
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 @root.join(key).dirname
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