carbon-copy 0.0.2
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/.gitignore +1 -0
- data/README.md +24 -0
- data/bin/carbon-copy +20 -0
- data/carbon-copy.gemspec +17 -0
- data/lib/carbon-copy.rb +6 -0
- data/lib/carbon-copy/cache_server.rb +71 -0
- data/lib/carbon-copy/http_cacher.rb +91 -0
- data/spec/cache_server_spec.rb +101 -0
- data/spec/http_cacher_spec.rb +27 -0
- metadata +58 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.request_cache/
|
data/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Carbon Copy - Easily cache them REST calls
|
2
|
+
|
3
|
+
We created Carbon Copy to allow for quick front end (or back end) development by
|
4
|
+
caching REST responses. We also wanted it to be easy and quick.
|
5
|
+
|
6
|
+
### How it works
|
7
|
+
|
8
|
+
Install the gem
|
9
|
+
|
10
|
+
gem install carbon-copy
|
11
|
+
|
12
|
+
Run the server with a specified port
|
13
|
+
|
14
|
+
carbon-copy -p 8989
|
15
|
+
|
16
|
+
Alter your REST calls on the front-end to pipe them locally through carbon-copy
|
17
|
+
|
18
|
+
$.get('http://localhost:8989/slow.server.com/resource')
|
19
|
+
|
20
|
+
On the first run, the requests will be cached in the `.request_cache` directory
|
21
|
+
in the same path as where you called the server. On subsequent requests, the
|
22
|
+
calls will pull from that cache instead of contacting the server.
|
23
|
+
|
24
|
+
You can restart the cache at any time by deleting the specific cache files.
|
data/bin/carbon-copy
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'carbon-copy'
|
4
|
+
|
5
|
+
server = CarbonCopy::CacheServer.new
|
6
|
+
|
7
|
+
opts = OptionParser.new do |opts|
|
8
|
+
opts.banner = "Carbon Copy: Cache them RESTs"
|
9
|
+
opts.on("-p", "--port Port", "Port to run server") do |v|
|
10
|
+
server.run(v)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
opts.parse!
|
14
|
+
|
15
|
+
uri = ARGV.shift
|
16
|
+
|
17
|
+
if uri.to_s.strip.empty?
|
18
|
+
puts opts
|
19
|
+
exit 1
|
20
|
+
end
|
data/carbon-copy.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require 'carbon-copy'
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Kevin Webster"]
|
6
|
+
gem.email = ["me@kdoubleyou.com"]
|
7
|
+
|
8
|
+
gem.description = "easily cache them REST calls"
|
9
|
+
gem.summary = "REST cache"
|
10
|
+
gem.homepage = 'https://github.com/rabidpraxis/carbon-copy'
|
11
|
+
gem.files = `git ls-files`.split("\n")
|
12
|
+
gem.test_files = `git ls-files -- spec/*`.split("\n")
|
13
|
+
gem.name = "carbon-copy"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = CarbonCopy::VERSION
|
16
|
+
gem.executables = ['carbon-copy']
|
17
|
+
end
|
data/lib/carbon-copy.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'openssl'
|
4
|
+
require 'carbon-copy/http_cacher'
|
5
|
+
|
6
|
+
module CarbonCopy
|
7
|
+
class CacheServer
|
8
|
+
|
9
|
+
def run(port)
|
10
|
+
webserver = TCPServer.new('127.0.0.1', port)
|
11
|
+
puts "Running Carbon Copy on localhost port #{port}"
|
12
|
+
while (session = webserver.accept)
|
13
|
+
Thread.new(session, &method(:handle))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_request(session)
|
18
|
+
request = session.readline
|
19
|
+
|
20
|
+
parsed = {}
|
21
|
+
#--- Initial host/uri information -------------------------------------
|
22
|
+
parsed[:verb] = request[/^\w+/]
|
23
|
+
parsed[:url] = request[/^#{parsed[:verb]}\s+\/(\S+)/, 1]
|
24
|
+
parsed[:host] = request[/^#{parsed[:verb]}\s+\/([^\/ ]+)/, 1]
|
25
|
+
parsed[:version] = request[/HTTP\/(1\.\d)\s*$/, 1]
|
26
|
+
parsed[:uri] = request[/^#{parsed[:verb]}\s+\/#{parsed[:host]}(\S+)\s+HTTP\/#{parsed[:version]}/, 1] || '/'
|
27
|
+
|
28
|
+
uri = URI::parse(parsed[:uri])
|
29
|
+
parsed[:request_str] = "#{parsed[:verb]} #{uri.path}?#{uri.query} HTTP/#{parsed[:version]}\r"
|
30
|
+
|
31
|
+
#--- Header and final response text -----------------------------------
|
32
|
+
parsed[:headers] = parse_headers(session)
|
33
|
+
|
34
|
+
#--- Update header info -----------------------------------------------
|
35
|
+
parsed[:headers]["Host"] = parsed[:host]
|
36
|
+
|
37
|
+
parsed[:header_str] = parsed[:headers].map{|a, b| "#{a}: #{b}"}.join("\r\n")
|
38
|
+
parsed[:response] = "#{parsed[:request_str]}\n#{parsed[:header_str]}\r\n\r\n"
|
39
|
+
|
40
|
+
parsed
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_headers(request)
|
44
|
+
header = {}
|
45
|
+
unless request.eof?
|
46
|
+
loop do
|
47
|
+
line = request.readline
|
48
|
+
if line.strip.empty?
|
49
|
+
break
|
50
|
+
end
|
51
|
+
|
52
|
+
/^(\S+): ([^\r\n]+)/.match(line)
|
53
|
+
header[$1] = $2
|
54
|
+
end
|
55
|
+
end
|
56
|
+
header
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle(session)
|
60
|
+
begin
|
61
|
+
req = parse_request(session)
|
62
|
+
resp = HTTPCacher.new.connect(req)
|
63
|
+
session.write(resp)
|
64
|
+
session.close
|
65
|
+
rescue => e
|
66
|
+
p e.message
|
67
|
+
p e.backtrace
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module CarbonCopy
|
4
|
+
class HTTPCacher
|
5
|
+
attr_reader :base_dir
|
6
|
+
|
7
|
+
def initialize(base_dir = Dir.pwd)
|
8
|
+
@base_dir = base_dir
|
9
|
+
end
|
10
|
+
|
11
|
+
#--------------------------------------------------------------------------
|
12
|
+
# Setup cache directory
|
13
|
+
#--------------------------------------------------------------------------
|
14
|
+
def cache_dir
|
15
|
+
"#{base_dir}/.request_cache"
|
16
|
+
end
|
17
|
+
|
18
|
+
#--------------------------------------------------------------------------
|
19
|
+
# Determine final path
|
20
|
+
#--------------------------------------------------------------------------
|
21
|
+
def path(parsed)
|
22
|
+
uri = ( parsed[:uri] == '/' ) ? '' : parsed[:uri].gsub("\/", "_")
|
23
|
+
hash = Digest::MD5.new << parsed[:header_str]
|
24
|
+
#--- Cache directory structure ----------------------------------------
|
25
|
+
"""
|
26
|
+
#{cache_dir}/
|
27
|
+
#{parsed[:host]}/
|
28
|
+
#{parsed[:verb].downcase}
|
29
|
+
#{uri}_
|
30
|
+
#{hash}
|
31
|
+
""".gsub(/\n|\s/, '')
|
32
|
+
end
|
33
|
+
|
34
|
+
#--------------------------------------------------------------------------
|
35
|
+
# Ensure cached directories are created
|
36
|
+
#--------------------------------------------------------------------------
|
37
|
+
def verify_cached_dir(parsed)
|
38
|
+
Dir.mkdir(cache_dir) unless File.exists?(cache_dir)
|
39
|
+
host_cache = "#{cache_dir}/#{parsed[:host]}"
|
40
|
+
Dir.mkdir(host_cache) unless File.exists?(host_cache)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_response(parsed)
|
44
|
+
a = TCPSocket.new(parsed[:host], 80)
|
45
|
+
a.write(parsed[:response])
|
46
|
+
|
47
|
+
#--- Pull request data ------------------------------------------------
|
48
|
+
content_len = nil
|
49
|
+
buff = ""
|
50
|
+
loop do
|
51
|
+
line = a.readline
|
52
|
+
buff += line
|
53
|
+
if line =~ /^Content-Length:\s+(\d+)\s*$/
|
54
|
+
content_len = $1.to_i
|
55
|
+
end
|
56
|
+
break if line.strip.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
#--- Pull response ----------------------------------------------------
|
60
|
+
if content_len
|
61
|
+
buff += a.read(content_len)
|
62
|
+
else
|
63
|
+
loop do
|
64
|
+
if a.eof? || line = a.readline || line.strip.empty?
|
65
|
+
break
|
66
|
+
end
|
67
|
+
buff += line
|
68
|
+
end
|
69
|
+
end
|
70
|
+
a.close
|
71
|
+
|
72
|
+
buff
|
73
|
+
end
|
74
|
+
|
75
|
+
def connect(parsed)
|
76
|
+
verify_cached_dir(parsed)
|
77
|
+
cached_path = path(parsed)
|
78
|
+
|
79
|
+
if File.exists?(cached_path) && !File.zero?(cached_path)
|
80
|
+
puts "Getting file #{cached_path} from cache"
|
81
|
+
IO.read( cached_path )
|
82
|
+
else
|
83
|
+
resp = get_response(parsed)
|
84
|
+
File.open( cached_path, 'w' ) do |f|
|
85
|
+
f.puts resp
|
86
|
+
end
|
87
|
+
resp
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require File.expand_path('../../lib/carbon-copy', __FILE__)
|
2
|
+
require 'open-uri'
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
describe CarbonCopy::CacheServer do
|
6
|
+
describe '#parse_headers' do
|
7
|
+
let(:rs) { CarbonCopy::CacheServer.new }
|
8
|
+
|
9
|
+
it 'should parse easy header strings' do
|
10
|
+
st = StringIO.new
|
11
|
+
st << "TestHeader: Header Result"
|
12
|
+
st << "\r\n\r\n"
|
13
|
+
st.rewind
|
14
|
+
rs.parse_headers(st).should eq({"TestHeader" => "Header Result"})
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should parse multi-line headers' do
|
18
|
+
st = StringIO.new
|
19
|
+
st << "TestHeader: Header Result\n"
|
20
|
+
st << "TestHeaders: Header Result"
|
21
|
+
st << "\r\n\r\n"
|
22
|
+
st.rewind
|
23
|
+
rs.parse_headers(st).should eq({
|
24
|
+
"TestHeader" => "Header Result",
|
25
|
+
"TestHeaders" => "Header Result"
|
26
|
+
})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#parse_request' do
|
31
|
+
let(:rs) { CarbonCopy::CacheServer.new }
|
32
|
+
let(:req) { rs.parse_request(request) }
|
33
|
+
|
34
|
+
describe 'host with path' do
|
35
|
+
let(:request) {
|
36
|
+
req = StringIO.new << "GET /apple.com/google/face/ HTTP/1.1\n"
|
37
|
+
req.rewind
|
38
|
+
req
|
39
|
+
}
|
40
|
+
|
41
|
+
it 'verb' do
|
42
|
+
req[:verb].should eq("GET")
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'url with path' do
|
46
|
+
req[:url].should eq('apple.com/google/face/')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'version' do
|
50
|
+
req[:version].should eq('1.1')
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'host' do
|
54
|
+
req[:host].should eq('apple.com')
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'uri' do
|
58
|
+
req[:uri].should eq('/google/face/')
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'request_str' do
|
62
|
+
req[:request_str].should eq("GET /google/face/? HTTP/1.1\r")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'just host' do
|
67
|
+
let(:request) {
|
68
|
+
req = StringIO.new << "GET /apple.com HTTP/1.1\n"
|
69
|
+
req.rewind
|
70
|
+
req
|
71
|
+
}
|
72
|
+
it 'host' do
|
73
|
+
req[:host].should eq('apple.com')
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'uri' do
|
77
|
+
req[:uri].should eq('/')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#handle' do
|
84
|
+
before(:all) do
|
85
|
+
Thread.new do
|
86
|
+
CarbonCopy::CacheServer.new.run(7979)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'caches google.com simple get request' do
|
91
|
+
url = 'www.apple.com'
|
92
|
+
o_req = get("http://#{url}").body
|
93
|
+
req = get("http://localhost:7979/#{url}").body
|
94
|
+
req.should eq(o_req)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def get(url)
|
100
|
+
Net::HTTP.get_response(URI.parse(url))
|
101
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require File.expand_path('../../lib/carbon-copy', __FILE__)
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
describe CarbonCopy::HTTPCacher do
|
5
|
+
let(:cacher) { CarbonCopy::HTTPCacher.new }
|
6
|
+
let(:parsed) {
|
7
|
+
{
|
8
|
+
verb: 'GET',
|
9
|
+
url: 'gist.github.com/74107',
|
10
|
+
host: 'gist.github.com',
|
11
|
+
version: '1.1',
|
12
|
+
uri: '/74107',
|
13
|
+
header_str: 'Test Header: Test Result'
|
14
|
+
}
|
15
|
+
}
|
16
|
+
it 'should have path with url' do
|
17
|
+
hash = Digest::MD5.new << parsed[:header_str]
|
18
|
+
cacher.path(parsed).should match(/\.request_cache\/gist\.github\.com\/get_74107_#{hash}/)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should reflect no path' do
|
22
|
+
hash = Digest::MD5.new << parsed[:header_str]
|
23
|
+
parsed[:url] = 'gist.github.com'
|
24
|
+
parsed[:uri] = '/'
|
25
|
+
cacher.path(parsed).should match(/\.request_cache\/gist\.github\.com\/get_#{hash}/)
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: carbon-copy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kevin Webster
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-02 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: easily cache them REST calls
|
15
|
+
email:
|
16
|
+
- me@kdoubleyou.com
|
17
|
+
executables:
|
18
|
+
- carbon-copy
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- README.md
|
24
|
+
- bin/carbon-copy
|
25
|
+
- carbon-copy.gemspec
|
26
|
+
- lib/carbon-copy.rb
|
27
|
+
- lib/carbon-copy/cache_server.rb
|
28
|
+
- lib/carbon-copy/http_cacher.rb
|
29
|
+
- spec/cache_server_spec.rb
|
30
|
+
- spec/http_cacher_spec.rb
|
31
|
+
homepage: https://github.com/rabidpraxis/carbon-copy
|
32
|
+
licenses: []
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options: []
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ! '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 1.8.24
|
52
|
+
signing_key:
|
53
|
+
specification_version: 3
|
54
|
+
summary: REST cache
|
55
|
+
test_files:
|
56
|
+
- spec/cache_server_spec.rb
|
57
|
+
- spec/http_cacher_spec.rb
|
58
|
+
has_rdoc:
|