statica 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +3 -0
- data/README.md +41 -0
- data/Rakefile +46 -0
- data/lib/rack/statica_server.rb +20 -0
- data/lib/statica.rb +49 -0
- data/lib/statica/version.rb +5 -0
- data/statica.gemspec +20 -0
- data/test/helper.rb +52 -0
- data/test/test_statica.rb +67 -0
- metadata +57 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# statica
|
2
|
+
|
3
|
+
Statica is a static asset caching solution.
|
4
|
+
|
5
|
+
# Note
|
6
|
+
|
7
|
+
Not quite ready for production use, but not far off either. I will remove this notice when I feel it's production ready.
|
8
|
+
|
9
|
+
TODO:
|
10
|
+
|
11
|
+
* Keep track of assets Statica already has the digest for [production setting]
|
12
|
+
* Finish the README
|
13
|
+
* More tests
|
14
|
+
|
15
|
+
# Synopsis
|
16
|
+
|
17
|
+
The goal is to achieve the simplicity of appending a query parameter and the precision offered by implementing a per file digest system.
|
18
|
+
|
19
|
+
In order to achieve this, Statica takes the simple approach of appendin the hex digest to the end of the static asset path like so:
|
20
|
+
|
21
|
+
> Statica.digest_url("/javascripts/application.js")
|
22
|
+
=>"/javascripts/application.js/d79dbf6e6c051b22f1ab91fec4cf81855883cc7d0a3f3e242b56b97fbcd756cb"
|
23
|
+
|
24
|
+
Notice how the file name does not change. We are simply appending a qualifier to the resource for caching. In order to serve the asset, Statica comes with Rack middleware that simply removes the digest portion allowing your application to serve the asset as normal.
|
25
|
+
|
26
|
+
# If Statica path, remove the digest portion, allow app to serve normally
|
27
|
+
use Rack::StaticaServer
|
28
|
+
|
29
|
+
If you want an HTTP Server like [Apache](http://httpd.apache.org/) or [Nginx](http://wiki.nginx.org/Main) you can setup rules to ignore the digest and server the asset. I'll sample rules here later.
|
30
|
+
|
31
|
+
# About
|
32
|
+
|
33
|
+
More to come...
|
34
|
+
|
35
|
+
# Acknowledgement
|
36
|
+
|
37
|
+
Huge thanks to my company, [Primedia](http://primedia.com) for encouraging open source contributions.
|
38
|
+
|
39
|
+
# Contributors
|
40
|
+
|
41
|
+
I highly value contributions and will happily list all those who submit accepted pull requests.
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler'
|
3
|
+
Bundler::GemHelper.install_tasks
|
4
|
+
rescue Exception => e
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rake/testtask'
|
8
|
+
|
9
|
+
Rake::TestTask.new('test') do |t|
|
10
|
+
t.libs << 'lib' << 'test'
|
11
|
+
t.test_files = Dir.glob('test/**/test_*.rb')
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
task 'test:ci' do |t|
|
16
|
+
Rake::Task[ENV['TASK']].execute
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
require 'rcov/rcovtask'
|
21
|
+
Rcov::RcovTask.new do |t|
|
22
|
+
t.libs << 'lib' << 'test'
|
23
|
+
t.test_files = Dir.glob('test/**/test_*.rb')
|
24
|
+
t.verbose = true
|
25
|
+
end
|
26
|
+
rescue LoadError
|
27
|
+
task :rcov do
|
28
|
+
abort "RCov is not available. In order to run rcov, you must: gem install rcov"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
require 'yard'
|
34
|
+
YARD::Rake::YardocTask.new do |t|
|
35
|
+
t.files = %w(lib/**/*.rb)
|
36
|
+
end
|
37
|
+
rescue LoadError
|
38
|
+
task :yard do
|
39
|
+
abort "YARD is not available. In order to run yard, you must: gem install yard"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "Generate Documentation"
|
44
|
+
task :doc => :yard
|
45
|
+
|
46
|
+
task :default => 'test'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'statica'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
class StaticaServer
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
dup._call(env)
|
12
|
+
end
|
13
|
+
|
14
|
+
def _call(env)
|
15
|
+
env['PATH_INFO'] = Statica.resolve(env['PATH_INFO'])
|
16
|
+
|
17
|
+
@app.call(env)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/statica.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
|
3
|
+
module Statica
|
4
|
+
class ResourceNotFoundError < StandardError; end
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
# Fully qualified path to the root directory
|
9
|
+
attr_accessor :root_dir
|
10
|
+
|
11
|
+
def digest_class=(klass)
|
12
|
+
@digest = klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def digest
|
16
|
+
(@digest || ::Digest::SHA256).new
|
17
|
+
end
|
18
|
+
|
19
|
+
def sub_dirs
|
20
|
+
@sub_dirs ||= %w{javascripts stylesheets images}
|
21
|
+
end
|
22
|
+
|
23
|
+
def sub_dirs=(array)
|
24
|
+
@sub_dirs = array
|
25
|
+
end
|
26
|
+
|
27
|
+
def digest_for(path)
|
28
|
+
path.chop! if path.end_with?("/")
|
29
|
+
fname = File.join(root_dir, path)
|
30
|
+
|
31
|
+
raise ResourceNotFoundError, "#{fname} not found!" unless File.exist?(fname)
|
32
|
+
|
33
|
+
digest.file(fname).hexdigest
|
34
|
+
end
|
35
|
+
|
36
|
+
def digest_url(url)
|
37
|
+
"#{url}/#{digest_for(url)}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def resolve(path)
|
41
|
+
(path =~ regex) ? path.sub($2, '') : path
|
42
|
+
end
|
43
|
+
|
44
|
+
def regex
|
45
|
+
leaders = sub_dirs.collect{|t| Regexp.escape(t)}.join("|")
|
46
|
+
@regex ||= /\A\/?(#{leaders})\S*(\/[a-z0-9]+\/?)\Z/
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/statica.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.dirname(__FILE__) + '/lib/statica/version'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'statica'
|
7
|
+
s.version = Statica::VERSION
|
8
|
+
s.date = Date.today.to_s
|
9
|
+
s.authors = ['Andrew Stone']
|
10
|
+
s.email = ['andy@stonean.com']
|
11
|
+
s.summary = 'Statica is a static asset caching solution.'
|
12
|
+
s.homepage = 'http://github.com/stonean/statica'
|
13
|
+
s.extra_rdoc_files = %w(README.md)
|
14
|
+
s.rdoc_options = %w(--charset=UTF-8)
|
15
|
+
s.rubyforge_project = s.name
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = %w(lib)
|
20
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'minitest/unit'
|
2
|
+
require 'statica'
|
3
|
+
|
4
|
+
MiniTest::Unit.autorun
|
5
|
+
|
6
|
+
module R
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def tmp_dir
|
10
|
+
File.join(File.dirname(__FILE__), '..', 'tmp')
|
11
|
+
end
|
12
|
+
|
13
|
+
def maybe_mkdir(dir)
|
14
|
+
Dir.mkdir(dir) unless Dir.exist?(dir)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_file(name = 'test.txt')
|
18
|
+
out_dir = tmp_dir
|
19
|
+
|
20
|
+
maybe_mkdir(out_dir)
|
21
|
+
|
22
|
+
parts = name.split('/').delete_if{ |a| a.empty? }
|
23
|
+
name = parts.pop
|
24
|
+
|
25
|
+
parts.each do |subdir|
|
26
|
+
out_dir = File.join(out_dir, subdir)
|
27
|
+
maybe_mkdir(out_dir)
|
28
|
+
end
|
29
|
+
|
30
|
+
fname = File.join(out_dir, name)
|
31
|
+
f = File.open( fname, 'w')
|
32
|
+
|
33
|
+
letters = [('a'..'z'),('A'..'Z')].map{|i| i.to_a}.flatten
|
34
|
+
contents = []
|
35
|
+
(0..rand(1000)).each do
|
36
|
+
contents << (0..rand(12)).map{ letters[rand(letters.length)] }.join
|
37
|
+
end
|
38
|
+
|
39
|
+
f.write contents.join(' ')
|
40
|
+
f.flush
|
41
|
+
f.close
|
42
|
+
|
43
|
+
yield f
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
class TestBase < MiniTest::Unit::TestCase
|
49
|
+
def setup
|
50
|
+
Statica.root_dir = R.tmp_dir
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestStatica < TestBase
|
4
|
+
|
5
|
+
def test_digest_for_raises_error_on_resource_not_found
|
6
|
+
assert_raises Statica::ResourceNotFoundError do
|
7
|
+
Statica.digest_for('/javascripts/foo.js')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_digests_are_unique
|
12
|
+
digest1 = ''
|
13
|
+
digest2 = ''
|
14
|
+
|
15
|
+
path = "/javascripts/application.js"
|
16
|
+
|
17
|
+
R.test_file(path){ |f| digest1 = Statica.digest_for(path) }
|
18
|
+
R.test_file(path){ |f| digest2 = Statica.digest_for(path) }
|
19
|
+
|
20
|
+
refute_equal digest1, digest2
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_digest_path_format
|
24
|
+
url = "/javascripts/application.js"
|
25
|
+
digest = ""
|
26
|
+
buster = ""
|
27
|
+
|
28
|
+
R.test_file(url) do |f|
|
29
|
+
digest = Statica.digest_for(url)
|
30
|
+
buster = Statica.digest_url(url)
|
31
|
+
end
|
32
|
+
|
33
|
+
assert_equal "#{url}/#{digest}", buster
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_resolve_on_non_match
|
37
|
+
url = "/js/application.js"
|
38
|
+
assert_equal url, Statica.resolve(url)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_resolve_match_no_digest
|
42
|
+
url = "/javascripts/application.js"
|
43
|
+
assert_equal url, Statica.resolve(url)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_resolve_match_no_digest_no_leading_slash
|
47
|
+
url = "javascripts/application.js"
|
48
|
+
assert_equal url, Statica.resolve(url)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_resolve
|
52
|
+
urls = %w{
|
53
|
+
javascripts/application.js
|
54
|
+
/javascripts/third-party.js/
|
55
|
+
/images/home/logo.png
|
56
|
+
/stylesheets/CrazyStylz.css
|
57
|
+
/stylesheets/CrazyStylz.css
|
58
|
+
/images/my_precious.gif
|
59
|
+
}
|
60
|
+
|
61
|
+
urls.each do |url|
|
62
|
+
R.test_file(url) do |f|
|
63
|
+
assert_equal url, Statica.resolve( Statica.digest_url(url) )
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: statica
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andrew Stone
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-07 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email:
|
16
|
+
- andy@stonean.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files:
|
20
|
+
- README.md
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- Gemfile
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- lib/rack/statica_server.rb
|
27
|
+
- lib/statica.rb
|
28
|
+
- lib/statica/version.rb
|
29
|
+
- statica.gemspec
|
30
|
+
- test/helper.rb
|
31
|
+
- test/test_statica.rb
|
32
|
+
homepage: http://github.com/stonean/statica
|
33
|
+
licenses: []
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options:
|
36
|
+
- --charset=UTF-8
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project: statica
|
53
|
+
rubygems_version: 1.8.10
|
54
|
+
signing_key:
|
55
|
+
specification_version: 3
|
56
|
+
summary: Statica is a static asset caching solution.
|
57
|
+
test_files: []
|