gin 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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