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']
         |