gin 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,178 @@
1
+ require 'yaml'
2
+ require 'digest/md5'
3
+
4
+ class Gin::AssetManifest
5
+
6
+ class Asset
7
+
8
+ attr_reader :path, :mtime, :digest, :dependencies
9
+ attr_accessor :target_file
10
+
11
+ def initialize path, opts={}
12
+ @path = path
13
+ @rtime = opts[:rtime] || Time.now
14
+ @mtime = opts[:mtime] || File.mtime(path)
15
+ @digest = opts[:digest] || Digest::MD5.file(path).hexdigest
16
+ @target_file = opts[:target_file]
17
+ @dependencies = opts[:dependencies] || []
18
+ end
19
+
20
+
21
+ def update!
22
+ return unless File.file?(@path)
23
+ @rtime = Time.now
24
+ @mtime = File.mtime(@path)
25
+ @digest = Digest::MD5.file(@path).hexdigest
26
+ end
27
+
28
+
29
+ def outdated?
30
+ return true if !File.file?(@path)
31
+ return true if @target_file && !File.file?(@target_file)
32
+ return @mtime != File.mtime(@path) if @rtime - @mtime > 0
33
+ @digest != Digest::MD5.file(@path).hexdigest
34
+ end
35
+
36
+
37
+ def to_hash
38
+ hash = {
39
+ :target_file => @target_file,
40
+ :rtime => @rtime,
41
+ :mtime => @mtime,
42
+ :digest => @digest }
43
+
44
+ hash[:dependencies] = @dependencies.dup unless @dependencies.empty?
45
+
46
+ hash
47
+ end
48
+ end
49
+
50
+
51
+ attr_reader :assets, :filepath
52
+ attr_accessor :render_dir, :asset_globs
53
+
54
+
55
+ def initialize filepath, render_dir, asset_globs
56
+ @staged = []
57
+ @assets = {}
58
+ @filepath = filepath
59
+ @render_dir = render_dir
60
+ @asset_globs = asset_globs
61
+
62
+ load_file! if File.file?(@filepath)
63
+ end
64
+
65
+
66
+ def set asset_file, target_file, dependencies=[]
67
+ squash_similar_targets(target_file)
68
+
69
+ asset = @assets[asset_file] =
70
+ Asset.new(asset_file, :target_file => target_file)
71
+
72
+ Array(dependencies).each do |path|
73
+ @assets[path] ||= Asset.new(path)
74
+ asset.dependencies << path
75
+ end
76
+ end
77
+
78
+
79
+ def squash_similar_targets target_file
80
+ matcher = Regexp.escape( target_file.sub(/-\w+\.(\w+)$/, '') )
81
+ matcher = Regexp.new "#{matcher}-\\w+\\.#{$1}"
82
+
83
+ @assets.each do |name, asset|
84
+ if asset.target_file && matcher =~ asset.target_file
85
+ asset.target_file = nil
86
+ end
87
+ end
88
+ end
89
+
90
+
91
+ def stage asset_file, target_file, dependencies=[]
92
+ @staged << [asset_file, target_file, dependencies]
93
+ end
94
+
95
+
96
+ def delete asset_file
97
+ @assets.delete asset_file.to_s
98
+ end
99
+
100
+
101
+ def commit!
102
+ until @staged.empty?
103
+ set(*@staged.shift)
104
+ end
105
+ end
106
+
107
+
108
+ def outdated?
109
+ source_changed? || @assets.keys.any?{|f| asset_outdated?(f) }
110
+ end
111
+
112
+
113
+ def asset_outdated? asset_file, checked=[]
114
+ # Check for circular dependencies
115
+ return false if checked.include?(asset_file)
116
+ checked << asset_file
117
+
118
+ asset = @assets[asset_file]
119
+ return true if !asset ||
120
+ asset.target_file && !asset.target_file.start_with?(@render_dir) ||
121
+ asset.outdated?
122
+
123
+ asset.dependencies.any? do |path|
124
+ return true if asset_outdated?(path, checked)
125
+ end
126
+ end
127
+
128
+
129
+ def source_changed?
130
+ source_files.sort != @assets.keys.sort
131
+ end
132
+
133
+
134
+ def source_files
135
+ globs = @asset_globs.map{|gl|
136
+ (gl =~ /\.(\*|\w+)$/) ? gl : File.join(gl, '**', '*') }
137
+
138
+ Dir.glob(globs).reject{|path| !File.file?(path) }.uniq
139
+ end
140
+
141
+
142
+ def source_dirs
143
+ Dir.glob(@asset_globs).map{|pa| pa[-1] == ?/ ? pa[0..-2] : pa }.uniq
144
+ end
145
+
146
+
147
+ def to_hash
148
+ assets_hash = {}
149
+
150
+ @assets.each do |path, asset|
151
+ assets_hash[path] = asset.to_hash
152
+ end
153
+
154
+ assets_hash
155
+ end
156
+
157
+
158
+ def filepath= new_file
159
+ FileUtils.mv(@filepath, new_file) if
160
+ @filepath && new_file != @filepath && File.file?(@filepath)
161
+ @filepath = new_file
162
+ end
163
+
164
+
165
+ def save_file!
166
+ File.open(@filepath, 'w'){|f| f.write self.to_hash.to_yaml }
167
+ end
168
+
169
+
170
+ def load_file!
171
+ yaml = YAML.load_file(@filepath)
172
+ @assets.clear
173
+
174
+ yaml.each do |path, info|
175
+ @assets[path] = Asset.new path, info
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,235 @@
1
+ require 'sprockets'
2
+ require 'fileutils'
3
+ require 'gin/asset_manifest'
4
+
5
+
6
+ class Gin::AssetPipeline
7
+
8
+ attr_accessor :logger
9
+
10
+ def initialize manifest_file, render_dir, asset_paths, sprockets, &block
11
+ @rendering = 0
12
+ @logger = $stderr
13
+ @listen = false
14
+ @thread = false
15
+ @sprockets = nil
16
+ @flag_update = false
17
+
18
+ @render_lock = Gin::RWLock.new
19
+ @listen_lock = Gin::RWLock.new
20
+
21
+ @manifest = Gin::AssetManifest.new(manifest_file, render_dir, asset_paths)
22
+
23
+ setup_listener(asset_paths, sprockets, &block)
24
+ end
25
+
26
+
27
+ def setup_listener asset_paths=[], spr=nil, &block
28
+ spr = Sprockets::Environment.new unless Sprockets::Environment === spr
29
+
30
+ @manifest.asset_globs = asset_paths
31
+
32
+ @manifest.source_dirs.each do |path|
33
+ spr.append_path path
34
+ end
35
+
36
+ yield spr if block_given?
37
+
38
+ @manifest.asset_globs |= spr.paths
39
+ @sprockets = spr
40
+ end
41
+
42
+
43
+ def manifest_file
44
+ @manifest.filepath
45
+ end
46
+
47
+
48
+ def manifest_file= new_file
49
+ @manifest.filepath = new_file
50
+ end
51
+
52
+
53
+ def render_dir
54
+ @manifest.render_dir
55
+ end
56
+
57
+
58
+ def render_dir= new_dir
59
+ new_dir = File.expand_path(new_dir)
60
+
61
+ if @manifest.render_dir != new_dir
62
+ @listen_lock.write_sync do
63
+ # TODO: instead of re-rendering everything, maybe move rendered assets?
64
+ @flag_update = true
65
+ @manifest.render_dir = new_dir
66
+ end
67
+ end
68
+ end
69
+
70
+
71
+ def log str
72
+ @logger << "#{str}\n"
73
+ end
74
+
75
+
76
+ ##
77
+ # Returns true if in the middle of rendering, otherwise false.
78
+
79
+ def rendering?
80
+ @render_lock.read_sync{ @rendering != 0 || @flag_update }
81
+ end
82
+
83
+
84
+ def listen
85
+ stop if listen?
86
+
87
+ @thread = Thread.new do
88
+ listen!
89
+ end
90
+
91
+ @thread.abort_on_exception = true
92
+ end
93
+
94
+
95
+ def listen!
96
+ @listen_lock.write_sync{ @listen = true }
97
+
98
+ while listen? do
99
+ render_all if outdated?
100
+ sleep 0.2
101
+ end
102
+ end
103
+
104
+
105
+ def listen?
106
+ @listen_lock.read_sync{ @listen }
107
+ end
108
+
109
+
110
+ def stop
111
+ @listen_lock.write_sync{ @listen = false }
112
+ end
113
+
114
+
115
+ def stop!
116
+ stop
117
+ @thread.join if @thread && @thread.alive?
118
+ end
119
+
120
+
121
+ def outdated?
122
+ @flag_update || @manifest.outdated?
123
+ end
124
+
125
+
126
+ def update_sprockets
127
+ paths = @manifest.source_dirs
128
+ return if @sprockets.paths == paths
129
+
130
+ @sprockets.clear_paths
131
+ paths.each{|path| @sprockets.append_path(path) }
132
+ end
133
+
134
+
135
+ def with_render_lock &block
136
+ @render_lock.write_sync{ @rendering += 1 }
137
+ start = Time.now
138
+ block.call
139
+
140
+ log "Assets rendered in (#{(Time.now.to_f - start.to_f).round(3)} sec)"
141
+
142
+ ensure
143
+ @render_lock.write_sync do
144
+ @rendering -= 1
145
+ @rendering = 0 if @rendering < 0
146
+ @flag_update = false
147
+ end
148
+ end
149
+
150
+
151
+ def remove_path path
152
+ log "Deleting asset: #{path}"
153
+ File.delete(path)
154
+
155
+ dir = File.dirname(path)
156
+ while dir != self.render_dir && Dir[File.join(dir,'*')].empty?
157
+ FileUtils.rm_r(dir)
158
+ dir = File.dirname(dir)
159
+ end
160
+ end
161
+
162
+
163
+ ##
164
+ # Looks at all rendered, added, and modified assets and compiles those
165
+ # out of date or missing.
166
+
167
+ def render_all
168
+ update_sprockets
169
+
170
+ with_render_lock do
171
+ sp_files = @sprockets.each_file.map(&:to_s).uniq
172
+
173
+ # delete rendered files that aren't in the asset_dirs
174
+ @manifest.assets.each do |asset_file, asset|
175
+ valid_asset = sp_files.include?(asset_file)
176
+ next if valid_asset
177
+
178
+ target_file = asset.target_file
179
+ remove_path target_file if target_file
180
+ @manifest.delete(asset_file)
181
+ end
182
+
183
+ # render assets and update tree index
184
+ sp_files.each do |asset_file|
185
+ next unless @manifest.asset_outdated?(asset_file)
186
+ sp_asset = @sprockets[asset_file] # First time render
187
+
188
+ if target_file = render(asset_file)
189
+ @manifest.stage asset_file, target_file,
190
+ sp_asset.dependencies.map{|d| d.pathname.to_s }
191
+ end
192
+ end
193
+
194
+ @manifest.commit!
195
+
196
+ # save cache to disk
197
+ @manifest.save_file!
198
+ end
199
+ end
200
+
201
+
202
+ def render path
203
+ asset = @sprockets[path]
204
+ return unless asset
205
+
206
+ ext = render_ext(asset)
207
+ render_path = File.join(self.render_dir, asset.logical_path)
208
+
209
+ file_glob = render_path.sub(/(\.\w+)$/, "-*#{ext}")
210
+ file_name = Dir[file_glob].first
211
+
212
+ digest = asset.digest[0..7]
213
+ return file_name if file_name && file_name.include?(digest)
214
+
215
+ log "Rendering asset: #{path}"
216
+ render_filename = file_glob.sub('*', digest)
217
+
218
+ FileUtils.mkdir_p File.dirname(render_filename)
219
+ File.open(render_filename, 'wb'){|f| f.write asset.source }
220
+
221
+ File.delete(file_name) if file_name
222
+
223
+ return render_filename
224
+ end
225
+
226
+
227
+ private
228
+
229
+ def render_ext asset
230
+ path = asset.pathname.to_s
231
+ ctype = asset.content_type
232
+ ctype == 'application/octet-stream' ?
233
+ File.extname(path) : @sprockets.extension_for_mime_type(ctype)
234
+ end
235
+ end
@@ -11,6 +11,14 @@ class Gin::Cache
11
11
  end
12
12
 
13
13
 
14
+ ##
15
+ # Clear all cache entries.
16
+
17
+ def clear
18
+ @lock.write_sync{ @data.clear }
19
+ end
20
+
21
+
14
22
  ##
15
23
  # Set the write timeout when waiting for reader thread locks.
16
24
  # Defaults to 0.05 sec. See Gin::RWLock for more details.
@@ -45,6 +53,34 @@ class Gin::Cache
45
53
  end
46
54
 
47
55
 
56
+ ##
57
+ # Increases the value of the given key in a thread-safe manner.
58
+ # Only Numerics and nil values are supported.
59
+ # Treats nil or non-existant values as 0.
60
+
61
+ def increase key, amount=1
62
+ @lock.write_sync do
63
+ return unless @data[key].nil? || Numeric === @data[key]
64
+ @data[key] ||= 0
65
+ @data[key] += amount
66
+ end
67
+ end
68
+
69
+
70
+ ##
71
+ # Decreases the value of the given key in a thread-safe manner.
72
+ # Only Numerics and nil values are supported.
73
+ # Treats nil or non-existant values as 0.
74
+
75
+ def decrease key, amount=1
76
+ @lock.write_sync do
77
+ return unless @data[key].nil? || Numeric === @data[key]
78
+ @data[key] ||= 0
79
+ @data[key] -= amount
80
+ end
81
+ end
82
+
83
+
48
84
  ##
49
85
  # Check if the current key exists in the cache.
50
86