dassets 0.0.1 → 0.1.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.
- 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']
|