dassets 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +56 -2
- data/bin/dassets +7 -0
- data/dassets.gemspec +9 -3
- data/lib/dassets/asset_file.rb +65 -0
- data/lib/dassets/cli.rb +109 -0
- data/lib/dassets/digests_file.rb +70 -0
- data/lib/dassets/root_path.rb +12 -0
- data/lib/dassets/runner/cache_command.rb +46 -0
- data/lib/dassets/runner/digest_command.rb +65 -0
- data/lib/dassets/runner.rb +42 -0
- data/lib/dassets/server/request.rb +48 -0
- data/lib/dassets/server/response.rb +36 -0
- data/lib/dassets/server.rb +37 -0
- data/lib/dassets/version.rb +1 -1
- data/lib/dassets.rb +32 -2
- data/test/helper.rb +3 -1
- data/test/support/app/assets/.digests +4 -0
- data/test/support/app/assets/public/file1.txt +1 -0
- data/test/support/app/assets/public/file2.txt +1 -0
- data/test/support/app/assets/public/grumpy_cat.jpg +0 -0
- data/test/support/app/assets/public/nested/file3.txt +0 -0
- data/test/support/app.rb +10 -0
- data/test/support/app_public/.gitkeep +0 -0
- data/test/support/config/assets.rb +7 -0
- data/test/support/example.digests +3 -0
- data/test/support/public/file1-daa05c683a4913b268653f7a7e36a5b4.txt +1 -0
- data/test/support/public/file2-9bbe1047cffbb590f59e0e5aeff46ae4.txt +1 -0
- data/test/support/public/grumpy_cat-b0d1f399a916f7a25c4c0f693c619013.jpg +0 -0
- data/test/support/public/nested/file3-d41d8cd98f00b204e9800998ecf8427e.txt +0 -0
- data/test/system/rack_tests.rb +78 -0
- data/test/unit/asset_file_tests.rb +76 -0
- data/test/unit/config_tests.rb +27 -0
- data/test/unit/dassets_tests.rb +49 -0
- data/test/unit/digests_file_tests.rb +90 -0
- data/test/unit/runner/cache_command_tests.rb +62 -0
- data/test/unit/runner/digest_command_tests.rb +83 -0
- data/test/unit/runner_tests.rb +29 -0
- data/test/unit/server/request_tests.rb +76 -0
- data/test/unit/server/response_tests.rb +70 -0
- data/test/unit/server_tests.rb +17 -0
- metadata +130 -10
data/README.md
CHANGED
@@ -1,10 +1,64 @@
|
|
1
1
|
# Dassets
|
2
2
|
|
3
|
-
Digest and serve asset files.
|
3
|
+
Digest and serve HTML asset files.
|
4
4
|
|
5
5
|
## Usage
|
6
6
|
|
7
|
-
|
7
|
+
You have some css, js, images, etc files. You want to update, deploy, and serve them in an efficient way. Dassets can help.
|
8
|
+
|
9
|
+
### Setup
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# in config/dassets.rb
|
13
|
+
require 'dassets'
|
14
|
+
|
15
|
+
Dassets.configure do |c|
|
16
|
+
|
17
|
+
# tell Dassets what the root path of your app is
|
18
|
+
c.root_path '/path/to/app/root'
|
19
|
+
|
20
|
+
# it works best to *not* keep the asset files in your public dir
|
21
|
+
c.files_path '/path/to/not/public' # default: '{root_path}/app/assets/public'
|
22
|
+
|
23
|
+
# you can choose the file to write the digests to, if you want
|
24
|
+
c.digests_file_path '/path/to/.digests' # default: '{files_path}/app/assets/.digests'
|
25
|
+
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
### Digest
|
30
|
+
|
31
|
+
```
|
32
|
+
$ dassets digest # rebuild the .digests for all asset files, OR
|
33
|
+
$ dassets digest /path/to/asset/file # update the digest for just one file
|
34
|
+
```
|
35
|
+
|
36
|
+
Use the CLI to build your digests file. Protip: use guard to auto rebuild digests every time you edit an asset file. TODO: link to some guard tools or docs.
|
37
|
+
|
38
|
+
### Link To
|
39
|
+
|
40
|
+
```rb
|
41
|
+
Dassets.init
|
42
|
+
Dassets['css/site.css'].href # => "/css/site-123abc.css"
|
43
|
+
Dassets['img/logos/main.jpg'].href # => "/img/logos/main-a1b2c3.jpg"
|
44
|
+
```
|
45
|
+
|
46
|
+
### Serve
|
47
|
+
|
48
|
+
In development, use the Dassets middleware to serve your digested asset files:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# `app` is a rack application
|
52
|
+
require 'dassets/server'
|
53
|
+
app.use Dassets::Server
|
54
|
+
```
|
55
|
+
|
56
|
+
In production, use the CLI to cache your digested asset files to the public dir:
|
57
|
+
|
58
|
+
```
|
59
|
+
# call the CLI in your deploy scripts or whatever
|
60
|
+
$ dassets cache /path/to/public/dir
|
61
|
+
```
|
8
62
|
|
9
63
|
## Installation
|
10
64
|
|
data/bin/dassets
ADDED
data/dassets.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.version = Dassets::VERSION
|
9
9
|
gem.authors = ["Kelly Redding", "Collin Redding"]
|
10
10
|
gem.email = ["kelly@kellyredding.com", "collin.redding@me.com"]
|
11
|
-
gem.description = %q{Digest and serve asset files}
|
12
|
-
gem.summary = %q{Digested
|
11
|
+
gem.description = %q{Digest and serve HTML asset files}
|
12
|
+
gem.summary = %q{Digested asset files}
|
13
13
|
gem.homepage = "http://github.com/redding/dassets"
|
14
14
|
|
15
15
|
gem.files = `git ls-files`.split($/)
|
@@ -17,6 +17,12 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
|
-
gem.add_development_dependency("assert")
|
20
|
+
gem.add_development_dependency("assert", ["~> 2.0"])
|
21
|
+
gem.add_development_dependency('assert-rack-test', ["~> 1.0"])
|
22
|
+
gem.add_development_dependency("sinatra", ["~> 1.4"])
|
23
|
+
|
24
|
+
|
25
|
+
gem.add_dependency('ns-options', ["~> 1.1"])
|
26
|
+
gem.add_dependency("rack", ["~> 1.0"])
|
21
27
|
|
22
28
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'rack/utils'
|
3
|
+
require 'rack/mime'
|
4
|
+
|
5
|
+
module Dassets; end
|
6
|
+
class Dassets::AssetFile
|
7
|
+
|
8
|
+
def self.from_abs_path(abs_path)
|
9
|
+
rel_path = abs_path.sub("#{Dassets.config.files_path}/", '')
|
10
|
+
md5 = Digest::MD5.file(abs_path).hexdigest
|
11
|
+
self.new(rel_path, md5)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :path, :md5, :dirname, :extname, :basename
|
15
|
+
attr_reader :files_path, :cache_path, :href
|
16
|
+
|
17
|
+
def initialize(rel_path, md5)
|
18
|
+
@path, @md5 = rel_path, md5
|
19
|
+
@dirname = File.dirname(@path)
|
20
|
+
@extname = File.extname(@path)
|
21
|
+
@basename = File.basename(@path, @extname)
|
22
|
+
|
23
|
+
file_name = "#{@basename}-#{@md5}#{@extname}"
|
24
|
+
@files_path = File.join(Dassets.config.files_path, @path)
|
25
|
+
@cache_path = File.join(@dirname, file_name).sub(/^\.\//, '').sub(/^\//, '')
|
26
|
+
@href = "/#{@cache_path}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def content
|
30
|
+
@content ||= if File.exists?(@files_path) && File.file?(@files_path)
|
31
|
+
File.read(@files_path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def mtime
|
36
|
+
@mtime ||= if File.exists?(@files_path) && File.file?(@files_path)
|
37
|
+
File.mtime(@files_path).httpdate
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# We check via File::size? whether this file provides size info via stat,
|
42
|
+
# otherwise we have to figure it out by reading the whole file into memory.
|
43
|
+
def size
|
44
|
+
@size ||= if File.exists?(@files_path) && File.file?(@files_path)
|
45
|
+
File.size?(@files_path) || Rack::Utils.bytesize(self.content)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def mime_type
|
50
|
+
@mime_type ||= if File.exists?(@files_path) && File.file?(@files_path)
|
51
|
+
Rack::Mime.mime_type(@extname)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def exists?
|
56
|
+
File.exists?(@files_path) && File.file?(@files_path)
|
57
|
+
end
|
58
|
+
|
59
|
+
def ==(other_asset_file)
|
60
|
+
other_asset_file.kind_of?(Dassets::AssetFile) &&
|
61
|
+
self.path == other_asset_file.path &&
|
62
|
+
self.md5 == other_asset_file.md5
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/lib/dassets/cli.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'dassets/version'
|
2
|
+
require 'dassets/runner'
|
3
|
+
|
4
|
+
module Dassets
|
5
|
+
|
6
|
+
class CLI
|
7
|
+
|
8
|
+
def self.run(*args)
|
9
|
+
self.new.run(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@cli = CLIRB.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(*args)
|
17
|
+
begin
|
18
|
+
@cli.parse!(args)
|
19
|
+
Dassets::Runner.new(@cli.args, @cli.opts).run
|
20
|
+
rescue CLIRB::HelpExit
|
21
|
+
puts help
|
22
|
+
rescue CLIRB::VersionExit
|
23
|
+
puts Dassets::VERSION
|
24
|
+
rescue Dassets::Runner::UnknownCmdError => err
|
25
|
+
$stderr.puts "#{err.message}\n\n"
|
26
|
+
$stderr.puts help
|
27
|
+
exit(1)
|
28
|
+
rescue Dassets::Runner::CmdError => err
|
29
|
+
$stderr.puts "#{err.message}"
|
30
|
+
exit(1)
|
31
|
+
rescue Dassets::Runner::CmdFail => err
|
32
|
+
exit(1)
|
33
|
+
rescue CLIRB::Error => exception
|
34
|
+
$stderr.puts "#{exception.message}\n\n"
|
35
|
+
$stderr.puts help
|
36
|
+
exit(1)
|
37
|
+
rescue Exception => exception
|
38
|
+
$stderr.puts "#{exception.class}: #{exception.message}"
|
39
|
+
$stderr.puts exception.backtrace.join("\n")
|
40
|
+
exit(1)
|
41
|
+
end
|
42
|
+
exit(0)
|
43
|
+
end
|
44
|
+
|
45
|
+
def help
|
46
|
+
"Usage: dassets [options] COMMAND\n"\
|
47
|
+
"\n"\
|
48
|
+
"Options:"\
|
49
|
+
"#{@cli}"
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class CLIRB # Version 1.0.0, https://github.com/redding/cli.rb
|
55
|
+
Error = Class.new(RuntimeError);
|
56
|
+
HelpExit = Class.new(RuntimeError); VersionExit = Class.new(RuntimeError)
|
57
|
+
attr_reader :argv, :args, :opts, :data
|
58
|
+
|
59
|
+
def initialize(&block)
|
60
|
+
@options = []; instance_eval(&block) if block
|
61
|
+
require 'optparse'
|
62
|
+
@data, @args, @opts = [], [], {}; @parser = OptionParser.new do |p|
|
63
|
+
p.banner = ''; @options.each do |o|
|
64
|
+
@opts[o.name] = o.value; p.on(*o.parser_args){ |v| @opts[o.name] = v }
|
65
|
+
end
|
66
|
+
p.on_tail('--version', ''){ |v| raise VersionExit, v.to_s }
|
67
|
+
p.on_tail('--help', ''){ |v| raise HelpExit, v.to_s }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def option(*args); @options << Option.new(*args); end
|
72
|
+
def parse!(argv)
|
73
|
+
@args = (argv || []).dup.tap do |args_list|
|
74
|
+
begin; @parser.parse!(args_list)
|
75
|
+
rescue OptionParser::ParseError => err; raise Error, err.message; end
|
76
|
+
end; @data = @args + [@opts]
|
77
|
+
end
|
78
|
+
def to_s; @parser.to_s; end
|
79
|
+
def inspect
|
80
|
+
"#<#{self.class}:#{'0x0%x' % (object_id << 1)} @data=#{@data.inspect}>"
|
81
|
+
end
|
82
|
+
|
83
|
+
class Option
|
84
|
+
attr_reader :name, :opt_name, :desc, :abbrev, :value, :klass, :parser_args
|
85
|
+
|
86
|
+
def initialize(name, *args)
|
87
|
+
settings, @desc = args.last.kind_of?(::Hash) ? args.pop : {}, args.pop || ''
|
88
|
+
@name, @opt_name, @abbrev = parse_name_values(name, settings[:abbrev])
|
89
|
+
@value, @klass = gvalinfo(settings[:value])
|
90
|
+
@parser_args = if [TrueClass, FalseClass, NilClass].include?(@klass)
|
91
|
+
["-#{@abbrev}", "--[no-]#{@opt_name}", @desc]
|
92
|
+
else
|
93
|
+
["-#{@abbrev}", "--#{@opt_name} #{@opt_name.upcase}", @klass, @desc]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def parse_name_values(name, custom_abbrev)
|
100
|
+
[ (processed_name = name.to_s.strip.downcase), processed_name.gsub('_', '-'),
|
101
|
+
custom_abbrev || processed_name.gsub(/[^a-z]/, '').chars.first || 'a'
|
102
|
+
]
|
103
|
+
end
|
104
|
+
def gvalinfo(v); v.kind_of?(Class) ? [nil,gklass(v)] : [v,gklass(v.class)]; end
|
105
|
+
def gklass(k); k == Fixnum ? Integer : k; end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'dassets/asset_file'
|
2
|
+
|
3
|
+
module Dassets
|
4
|
+
|
5
|
+
class DigestsFile
|
6
|
+
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
def initialize(file_path)
|
10
|
+
@path, @hash = file_path, decode(file_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](*args); @hash.send('[]', *args); end
|
14
|
+
def []=(*args); @hash.send('[]=', *args); end
|
15
|
+
def delete(*args); @hash.delete(*args); end
|
16
|
+
|
17
|
+
def each(*args, &block); @hash.each(*args, &block); end
|
18
|
+
|
19
|
+
def keys; @hash.keys; end
|
20
|
+
def values; @hash.values; end
|
21
|
+
def empty?; @hash.empty?; end
|
22
|
+
|
23
|
+
def asset_files
|
24
|
+
@hash.map{ |path, md5| Dassets::AssetFile.new(path, md5) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def asset_file(path)
|
28
|
+
Dassets::AssetFile.new(path, @hash[path] || '')
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
Hash.new.tap do |to_hash|
|
33
|
+
@hash.each{ |k, v| to_hash[k] = v }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def save!
|
38
|
+
encode(@hash, @path)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def decode(file_path)
|
44
|
+
Hash.new.tap do |h|
|
45
|
+
if File.exists?(file_path)
|
46
|
+
File.open(file_path, 'r').each_line do |l|
|
47
|
+
path, md5 = l.split(','); path ||= ''; path.strip!; md5 ||= ''; md5.strip!
|
48
|
+
h[path] = md5 if !path.empty?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def encode(hash, file_path)
|
55
|
+
File.open(file_path, 'w') do |f|
|
56
|
+
hash.keys.sort.each{ |path| f.write("#{path.strip},#{hash[path].strip}\n") }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
module NullDigestsFile
|
63
|
+
|
64
|
+
def self.new
|
65
|
+
DigestsFile.new('/dev/null')
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# This takes a path string relative to the configured root path and tranforms
|
2
|
+
# to the full qualifed root path. The goal here is to specify path options
|
3
|
+
# with root-relative path strings.
|
4
|
+
|
5
|
+
module Dassets; end
|
6
|
+
class Dassets::RootPath < String
|
7
|
+
|
8
|
+
def initialize(path_string)
|
9
|
+
super(Dassets.config.root_path.join(path_string).to_s)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'dassets/asset_file'
|
4
|
+
require 'dassets/digests_file'
|
5
|
+
|
6
|
+
module Dassets; end
|
7
|
+
class Dassets::Runner; end
|
8
|
+
class Dassets::Runner::CacheCommand
|
9
|
+
|
10
|
+
attr_reader :files_root_path, :cache_root_path, :digests_file, :asset_files
|
11
|
+
|
12
|
+
def initialize(cache_root_path)
|
13
|
+
unless cache_root_path && File.directory?(cache_root_path)
|
14
|
+
raise Dassets::Runner::CmdError, "specify an existing cache directory"
|
15
|
+
end
|
16
|
+
|
17
|
+
@files_root_path = Pathname.new(Dassets.config.files_path)
|
18
|
+
@cache_root_path = Pathname.new(cache_root_path)
|
19
|
+
@digests_file = Dassets::DigestsFile.new(Dassets.config.digests_file_path)
|
20
|
+
@asset_files = @digests_file.asset_files
|
21
|
+
end
|
22
|
+
|
23
|
+
def run(write_files=true)
|
24
|
+
begin
|
25
|
+
@asset_files.each do |file|
|
26
|
+
files_path = @files_root_path.join(file.path).to_s
|
27
|
+
cache_path = @cache_root_path.join(file.cache_path).to_s
|
28
|
+
|
29
|
+
if write_files
|
30
|
+
FileUtils.mkdir_p File.dirname(cache_path)
|
31
|
+
FileUtils.cp(files_path, cache_path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
unless ENV['DASSETS_TEST_MODE']
|
35
|
+
$stdout.puts "#{@asset_files.size} files written to #{@cache_root_path}"
|
36
|
+
end
|
37
|
+
return write_files
|
38
|
+
rescue Exception => e
|
39
|
+
unless ENV['DASSETS_TEST_MODE']
|
40
|
+
$stderr.puts e, *e.backtrace; $stderr.puts ""
|
41
|
+
end
|
42
|
+
raise Dassets::Runner::CmdFail
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'dassets/asset_file'
|
3
|
+
require 'dassets/digests_file'
|
4
|
+
|
5
|
+
module Dassets; end
|
6
|
+
class Dassets::Runner; end
|
7
|
+
class Dassets::Runner::DigestCommand
|
8
|
+
|
9
|
+
attr_reader :asset_files, :digests_file
|
10
|
+
|
11
|
+
def initialize(file_paths)
|
12
|
+
@pwd = ENV['PWD']
|
13
|
+
@asset_files = if (file_paths || []).empty?
|
14
|
+
get_asset_files([*Dassets.config.files_path])
|
15
|
+
else
|
16
|
+
get_asset_files(file_paths)
|
17
|
+
end
|
18
|
+
@digests_file = Dassets::DigestsFile.new(Dassets.config.digests_file_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def run(save=true)
|
22
|
+
begin
|
23
|
+
digest_paths = @digests_file.keys
|
24
|
+
asset_paths = @asset_files.map{ |f| f.path }
|
25
|
+
|
26
|
+
(digest_paths - asset_paths).each{ |file| @digests_file.delete(file) }
|
27
|
+
@asset_files.each{ |f| @digests_file[f.path] = f.md5 }
|
28
|
+
|
29
|
+
@digests_file.save! if save
|
30
|
+
unless ENV['DASSETS_TEST_MODE']
|
31
|
+
$stdout.puts "digested #{@asset_files.size} assets, saved to #{@digests_file.path}"
|
32
|
+
end
|
33
|
+
return save
|
34
|
+
rescue Exception => e
|
35
|
+
unless ENV['DASSETS_TEST_MODE']
|
36
|
+
$stderr.puts e, *e.backtrace; $stderr.puts ""
|
37
|
+
end
|
38
|
+
raise Dassets::Runner::CmdFail
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Get all file paths fuzzy-matching the given paths. Each path must be a
|
45
|
+
# file that exists and is in the `config.files_path` tree. Return them
|
46
|
+
# as sorted AssetFile objects.
|
47
|
+
def get_asset_files(paths)
|
48
|
+
fuzzy_paths(paths).
|
49
|
+
select{ |p| is_asset_file?(p) }.
|
50
|
+
sort.
|
51
|
+
map{ |p| Dassets::AssetFile.from_abs_path(p) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def fuzzy_paths(paths)
|
55
|
+
paths.inject(Set.new) do |paths, path|
|
56
|
+
p = File.expand_path(path, @pwd)
|
57
|
+
paths += Dir.glob("#{p}*") + Dir.glob("#{p}*/**/*")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def is_asset_file?(path)
|
62
|
+
File.file?(path) && path.include?("#{Dassets.config.files_path}/")
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'dassets'
|
2
|
+
|
3
|
+
ENV['DASSETS_CONFIG_FILE'] ||= 'config/assets'
|
4
|
+
|
5
|
+
module Dassets; end
|
6
|
+
class Dassets::Runner
|
7
|
+
UnknownCmdError = Class.new(ArgumentError)
|
8
|
+
CmdError = Class.new(RuntimeError)
|
9
|
+
CmdFail = Class.new(RuntimeError)
|
10
|
+
|
11
|
+
attr_reader :cmd_name, :cmd_args, :opts
|
12
|
+
|
13
|
+
def initialize(args, opts)
|
14
|
+
@opts = opts
|
15
|
+
@cmd_name = args.shift || ""
|
16
|
+
@cmd_args = args
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
require ENV['DASSETS_CONFIG_FILE']
|
21
|
+
|
22
|
+
case @cmd_name
|
23
|
+
when 'digest'
|
24
|
+
require 'dassets/runner/digest_command'
|
25
|
+
DigestCommand.new(@cmd_args).run
|
26
|
+
when 'cache'
|
27
|
+
require 'dassets/runner/cache_command'
|
28
|
+
CacheCommand.new(@cmd_args.first).run
|
29
|
+
when 'null'
|
30
|
+
NullCommand.new.run
|
31
|
+
else
|
32
|
+
raise UnknownCmdError, "unknown command `#{@cmd_name}`"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class NullCommand
|
37
|
+
def run
|
38
|
+
# if this was a real command it would do something here
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
|
3
|
+
class Dassets::Server
|
4
|
+
|
5
|
+
class Request < Rack::Request
|
6
|
+
|
7
|
+
# The HTTP request method. This is the standard implementation of this
|
8
|
+
# method but is respecified here due to libraries that attempt to modify
|
9
|
+
# the behavior to respect POST tunnel method specifiers. We always want
|
10
|
+
# the real request method.
|
11
|
+
def request_method; @env['REQUEST_METHOD']; end
|
12
|
+
def path_info; @env['PATH_INFO']; end
|
13
|
+
|
14
|
+
# Determine if the request is for an asset file
|
15
|
+
# This will be called on every request so speed is an issue
|
16
|
+
# - first check if the request is a GET or HEAD (fast)
|
17
|
+
# - then check if for a digest resource (kinda fast)
|
18
|
+
# - then check if on a path in the digests (slower)
|
19
|
+
def for_asset_file?
|
20
|
+
!!((get? || head?) && for_digest_file? && Dassets.digests[asset_path])
|
21
|
+
end
|
22
|
+
|
23
|
+
def asset_path
|
24
|
+
@asset_path ||= path_digest_match.captures.select{ |m| !m.empty? }.join
|
25
|
+
end
|
26
|
+
|
27
|
+
def asset_file
|
28
|
+
@asset_file ||= Dassets[asset_path]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def for_digest_file?
|
34
|
+
!path_digest_match.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def path_digest_match
|
38
|
+
@path_digest_match ||= begin
|
39
|
+
path_info.match(/\/(.+)-[a-f0-9]{32}(\..+|)$/i) || NullDigestMatch.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class NullDigestMatch
|
44
|
+
def captures; []; end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rack/response'
|
2
|
+
require 'rack/utils'
|
3
|
+
require 'rack/mime'
|
4
|
+
|
5
|
+
class Dassets::Server
|
6
|
+
|
7
|
+
class Response
|
8
|
+
attr_reader :asset_file, :status, :headers, :body
|
9
|
+
|
10
|
+
def initialize(env, asset_file)
|
11
|
+
@asset_file = asset_file
|
12
|
+
|
13
|
+
mtime = @asset_file.mtime.to_s
|
14
|
+
@status, @headers, @body = if env['HTTP_IF_MODIFIED_SINCE'] == mtime
|
15
|
+
[ 304, Rack::Utils::HeaderHash.new, [] ]
|
16
|
+
elsif !@asset_file.exists?
|
17
|
+
[ 404, Rack::Utils::HeaderHash.new, [] ]
|
18
|
+
else
|
19
|
+
[ 200,
|
20
|
+
Rack::Utils::HeaderHash.new.tap do |h|
|
21
|
+
h["Content-Type"] = @asset_file.mime_type.to_s
|
22
|
+
h["Content-Length"] = @asset_file.size.to_s
|
23
|
+
h["Last-Modified"] = mtime
|
24
|
+
end,
|
25
|
+
env["REQUEST_METHOD"] == "HEAD" ? [] : [ @asset_file.content ]
|
26
|
+
]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_rack
|
31
|
+
[@status, @headers.to_hash, @body]
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'dassets/server/request'
|
2
|
+
require 'dassets/server/response'
|
3
|
+
|
4
|
+
# Rack middleware for serving Dassets asset files
|
5
|
+
|
6
|
+
module Dassets
|
7
|
+
class Server
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
# The Rack call interface. The receiver acts as a prototype and runs
|
14
|
+
# each request in a clone object unless the +rack.run_once+ variable is
|
15
|
+
# set in the environment. Ripped from:
|
16
|
+
# http://github.com/rtomayko/rack-cache/blob/master/lib/rack/cache/context.rb
|
17
|
+
def call(env)
|
18
|
+
if env['rack.run_once']
|
19
|
+
call! env
|
20
|
+
else
|
21
|
+
clone.call! env
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# The real Rack call interface.
|
26
|
+
# if an asset file is being requested, this is an endpoint - otherwise, call
|
27
|
+
# on up to the app as normal
|
28
|
+
def call!(env)
|
29
|
+
if (request = Request.new(env)).for_asset_file?
|
30
|
+
Response.new(env, request.asset_file).to_rack
|
31
|
+
else
|
32
|
+
@app.call(env)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
data/lib/dassets/version.rb
CHANGED
data/lib/dassets.rb
CHANGED
@@ -1,5 +1,35 @@
|
|
1
|
-
require
|
1
|
+
require 'pathname'
|
2
|
+
require 'ns-options'
|
3
|
+
|
4
|
+
require 'dassets/version'
|
5
|
+
require 'dassets/root_path'
|
6
|
+
require 'dassets/digests_file'
|
2
7
|
|
3
8
|
module Dassets
|
4
|
-
|
9
|
+
|
10
|
+
def self.config; Config; end
|
11
|
+
def self.configure(&block); Config.define(&block); end
|
12
|
+
|
13
|
+
def self.init
|
14
|
+
@digests_file = DigestsFile.new(self.config.digests_file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.reset
|
18
|
+
@digests_file = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.digests; @digests_file || NullDigestsFile.new; end
|
22
|
+
def self.[](asset_path)
|
23
|
+
self.digests.asset_file(asset_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
class Config
|
27
|
+
include NsOptions::Proxy
|
28
|
+
|
29
|
+
option :root_path, Pathname, :required => true
|
30
|
+
option :files_path, RootPath, :default => proc{ "app/assets/public" }
|
31
|
+
option :digests_file_path, RootPath, :default => proc{ "app/assets/.digests" }
|
32
|
+
|
33
|
+
end
|
34
|
+
|
5
35
|
end
|
data/test/helper.rb
CHANGED
@@ -7,4 +7,6 @@ $LOAD_PATH.unshift(File.expand_path("../..", __FILE__))
|
|
7
7
|
# require pry for debugging (`binding.pry`)
|
8
8
|
require 'pry'
|
9
9
|
|
10
|
-
|
10
|
+
ENV['DASSETS_TEST_MODE'] = 'yes'
|
11
|
+
ENV['DASSETS_CONFIG_FILE'] = 'test/support/config/assets'
|
12
|
+
require ENV['DASSETS_CONFIG_FILE']
|