carbon-copy 0.0.2 → 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/.rspec +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +37 -0
- data/README.md +1 -1
- data/bin/carbon-copy +6 -8
- data/carbon-copy.gemspec +4 -0
- data/lib/carbon-copy.rb +32 -2
- data/lib/carbon-copy/cache_server.rb +13 -50
- data/lib/carbon-copy/request.rb +58 -0
- data/lib/carbon-copy/request_cacher.rb +85 -0
- data/spec/cache_server_spec.rb +0 -101
- data/spec/integration_spec.rb +59 -0
- data/spec/request_cacher_spec.rb +30 -0
- data/spec/request_spec.rb +90 -0
- data/spec/spec_helper.rb +20 -0
- metadata +64 -6
- data/lib/carbon-copy/http_cacher.rb +0 -91
- data/spec/http_cacher_spec.rb +0 -27
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
carbon-copy (0.0.2)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.1.3)
|
10
|
+
mime-types (1.19)
|
11
|
+
rack (1.4.1)
|
12
|
+
rack-protection (1.2.0)
|
13
|
+
rack
|
14
|
+
rest-client (1.6.7)
|
15
|
+
mime-types (>= 1.16)
|
16
|
+
rspec (2.11.0)
|
17
|
+
rspec-core (~> 2.11.0)
|
18
|
+
rspec-expectations (~> 2.11.0)
|
19
|
+
rspec-mocks (~> 2.11.0)
|
20
|
+
rspec-core (2.11.1)
|
21
|
+
rspec-expectations (2.11.2)
|
22
|
+
diff-lcs (~> 1.1.3)
|
23
|
+
rspec-mocks (2.11.2)
|
24
|
+
sinatra (1.3.3)
|
25
|
+
rack (~> 1.3, >= 1.3.6)
|
26
|
+
rack-protection (~> 1.2)
|
27
|
+
tilt (~> 1.3, >= 1.3.3)
|
28
|
+
tilt (1.3.3)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
ruby
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
carbon-copy!
|
35
|
+
rest-client
|
36
|
+
rspec
|
37
|
+
sinatra
|
data/README.md
CHANGED
data/bin/carbon-copy
CHANGED
@@ -2,19 +2,17 @@
|
|
2
2
|
require 'optparse'
|
3
3
|
require 'carbon-copy'
|
4
4
|
|
5
|
-
|
5
|
+
cc = CarbonCopy::CarbonCopy.new
|
6
6
|
|
7
7
|
opts = OptionParser.new do |opts|
|
8
8
|
opts.banner = "Carbon Copy: Cache them RESTs"
|
9
9
|
opts.on("-p", "--port Port", "Port to run server") do |v|
|
10
|
-
|
10
|
+
cc.port = v
|
11
|
+
end
|
12
|
+
opts.on("-l", "--cache Cache", "Cache files location") do |v|
|
13
|
+
cc.request_cacher = CarbonCopy::RequestCacher(v)
|
11
14
|
end
|
12
15
|
end
|
13
16
|
opts.parse!
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
if uri.to_s.strip.empty?
|
18
|
-
puts opts
|
19
|
-
exit 1
|
20
|
-
end
|
18
|
+
cc.run
|
data/carbon-copy.gemspec
CHANGED
@@ -14,4 +14,8 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.require_paths = ["lib"]
|
15
15
|
gem.version = CarbonCopy::VERSION
|
16
16
|
gem.executables = ['carbon-copy']
|
17
|
+
|
18
|
+
gem.add_development_dependency "rspec"
|
19
|
+
gem.add_development_dependency "rest-client"
|
20
|
+
gem.add_development_dependency "sinatra"
|
17
21
|
end
|
data/lib/carbon-copy.rb
CHANGED
@@ -1,6 +1,36 @@
|
|
1
1
|
require 'carbon-copy/cache_server'
|
2
|
-
require 'carbon-copy/
|
2
|
+
require 'carbon-copy/request_cacher'
|
3
|
+
|
4
|
+
#### CarbonCopy
|
5
|
+
#
|
6
|
+
# CarbonCopy is a simple server that sits between your outbound api requests
|
7
|
+
# and your file system. It was created in response to a frustration when
|
8
|
+
# developing frontend applications on untested backends. CarbonCopy stores your
|
9
|
+
# requests locally so if a server stops responding, or is too slow to test on,
|
10
|
+
# you can rely on cached data instead of making an http reqest for each
|
11
|
+
# refresh.
|
12
|
+
#
|
3
13
|
|
4
14
|
module CarbonCopy
|
5
|
-
VERSION = '0.0
|
15
|
+
VERSION = '0.1.0'
|
16
|
+
|
17
|
+
# connect all teh pieces
|
18
|
+
class CarbonCopy
|
19
|
+
attr_writer :request_cacher, :port
|
20
|
+
|
21
|
+
def run
|
22
|
+
CacheServer.new(port, request_cacher).run
|
23
|
+
end
|
24
|
+
|
25
|
+
# default to built in request cacher with current path as the request cache
|
26
|
+
# location
|
27
|
+
def request_cacher
|
28
|
+
@request_cacher || RequestCacher.new(Dir.pwd)
|
29
|
+
end
|
30
|
+
|
31
|
+
# default to port 7979
|
32
|
+
def port
|
33
|
+
@port || 7979
|
34
|
+
end
|
35
|
+
end
|
6
36
|
end
|
@@ -1,66 +1,29 @@
|
|
1
1
|
require 'socket'
|
2
|
-
require 'open-uri'
|
3
2
|
require 'openssl'
|
4
|
-
require 'carbon-copy/
|
3
|
+
require 'carbon-copy/request_cacher'
|
5
4
|
|
6
5
|
module CarbonCopy
|
7
6
|
class CacheServer
|
8
7
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
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
|
8
|
+
def initialize(port, request_cacher)
|
9
|
+
@port = port
|
10
|
+
@request_cacher = request_cacher
|
41
11
|
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
12
|
|
52
|
-
|
53
|
-
|
54
|
-
|
13
|
+
def run
|
14
|
+
webserver = TCPServer.new('127.0.0.1', @port)
|
15
|
+
puts "Running Carbon Copy on localhost port #{@port}"
|
16
|
+
while (session = webserver.accept)
|
17
|
+
Thread.new(session, &method(:handle))
|
55
18
|
end
|
56
|
-
header
|
57
19
|
end
|
58
20
|
|
59
21
|
def handle(session)
|
60
22
|
begin
|
61
|
-
|
62
|
-
|
63
|
-
|
23
|
+
request = Request.new(session)
|
24
|
+
request.parse
|
25
|
+
response = @request_cacher.connect(request)
|
26
|
+
session.write(response)
|
64
27
|
session.close
|
65
28
|
rescue => e
|
66
29
|
p e.message
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
module CarbonCopy
|
4
|
+
class Request
|
5
|
+
attr_accessor :verb, :host, :port, :path, :version, :url, :uri,
|
6
|
+
:request_str, :headers, :header_str, :request
|
7
|
+
|
8
|
+
def initialize(session)
|
9
|
+
@session = session
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse
|
13
|
+
request = @session.readline
|
14
|
+
|
15
|
+
#--- Initial host/uri information -------------------------------------
|
16
|
+
@verb = request.slice!(/^\w+\s/).strip
|
17
|
+
@host = request.slice!(/^\/[^\/: ]+/)[1..-1]
|
18
|
+
@port = request.slice!(/^:(\d+)/)
|
19
|
+
|
20
|
+
@port = ( @port.nil? ) ? '80' : @port[1..-1] # Remove the colon
|
21
|
+
|
22
|
+
@path = request.slice!(/^(\S)+/)
|
23
|
+
@version = request[/HTTP\/(1\.\d)\s*$/, 1]
|
24
|
+
@url = "#{@host}#{@path}"
|
25
|
+
@uri = "#{@path || '/'}"
|
26
|
+
|
27
|
+
uri = URI::parse(@uri)
|
28
|
+
@request_str = "#{@verb} #{uri.path}?#{uri.query} HTTP/#{@version}\r"
|
29
|
+
|
30
|
+
#--- Header and final response text -----------------------------------
|
31
|
+
@headers = parse_headers(@session)
|
32
|
+
|
33
|
+
#--- Update header info -----------------------------------------------
|
34
|
+
@headers["Host"] = @host
|
35
|
+
|
36
|
+
@header_str = @headers.map{|a, b| "#{a}: #{b}"}.join("\r\n")
|
37
|
+
@request = "#{@request_str}\n#{@header_str}\r\n\r\n"
|
38
|
+
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_headers(request)
|
43
|
+
header = {}
|
44
|
+
unless request.eof?
|
45
|
+
loop do
|
46
|
+
line = request.readline
|
47
|
+
if line.strip.empty?
|
48
|
+
break
|
49
|
+
end
|
50
|
+
|
51
|
+
/^(\S+): ([^\r\n]+)/.match(line)
|
52
|
+
header[$1] = $2
|
53
|
+
end
|
54
|
+
end
|
55
|
+
header
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module CarbonCopy
|
4
|
+
class RequestCacher
|
5
|
+
attr_reader :base_dir
|
6
|
+
|
7
|
+
def initialize(base_dir)
|
8
|
+
@base_dir = base_dir
|
9
|
+
end
|
10
|
+
|
11
|
+
# Setup cache directory
|
12
|
+
def cache_dir
|
13
|
+
"#{@base_dir}/.request_cache"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Determine final path
|
17
|
+
def path(request)
|
18
|
+
uri = ( request.uri == '/' ) ? '' : request.uri.gsub("\/", "_")
|
19
|
+
hash = Digest::MD5.new << request.header_str
|
20
|
+
# Cache directory structure
|
21
|
+
"""
|
22
|
+
#{cache_dir}/
|
23
|
+
#{request.host}/
|
24
|
+
#{request.verb.downcase}
|
25
|
+
#{uri}_
|
26
|
+
#{hash}
|
27
|
+
""".gsub(/\n|\s/, '')
|
28
|
+
end
|
29
|
+
|
30
|
+
# Ensure cached directories are created
|
31
|
+
def verify_cached_dir(request)
|
32
|
+
Dir.mkdir(cache_dir) unless File.exists?(cache_dir)
|
33
|
+
host_cache = "#{cache_dir}/#{request.host}"
|
34
|
+
Dir.mkdir(host_cache) unless File.exists?(host_cache)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_response(request)
|
38
|
+
a = TCPSocket.new(request.host, request.port)
|
39
|
+
a.write(request.request)
|
40
|
+
|
41
|
+
# Pull request data
|
42
|
+
content_len = nil
|
43
|
+
buff = ""
|
44
|
+
loop do
|
45
|
+
line = a.readline
|
46
|
+
buff += line
|
47
|
+
if line =~ /^Content-Length:\s+(\d+)\s*$/
|
48
|
+
content_len = $1.to_i
|
49
|
+
end
|
50
|
+
break if line.strip.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Pull response
|
54
|
+
if content_len
|
55
|
+
buff += a.read(content_len)
|
56
|
+
else
|
57
|
+
loop do
|
58
|
+
if a.eof? || line = a.readline || line.strip.empty?
|
59
|
+
break
|
60
|
+
end
|
61
|
+
buff += line
|
62
|
+
end
|
63
|
+
end
|
64
|
+
a.close
|
65
|
+
|
66
|
+
buff
|
67
|
+
end
|
68
|
+
|
69
|
+
def connect(request)
|
70
|
+
verify_cached_dir(request)
|
71
|
+
cached_path = path(request)
|
72
|
+
|
73
|
+
if File.exists?(cached_path) && !File.zero?(cached_path)
|
74
|
+
puts "Getting file #{cached_path} from cache"
|
75
|
+
IO.read( cached_path )
|
76
|
+
else
|
77
|
+
resp = get_response(request)
|
78
|
+
File.open( cached_path, 'w' ) do |f|
|
79
|
+
f.puts resp
|
80
|
+
end
|
81
|
+
resp
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/spec/cache_server_spec.rb
CHANGED
@@ -1,101 +0,0 @@
|
|
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,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'net/http'
|
4
|
+
require 'sinatra/base'
|
5
|
+
require 'rest-client'
|
6
|
+
|
7
|
+
class TestApp < Sinatra::Base
|
8
|
+
get '/awesome' do
|
9
|
+
'This is an awesome get request!'
|
10
|
+
end
|
11
|
+
|
12
|
+
options '/awesome' do
|
13
|
+
'This is an awesome options request!'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module CarbonCopy
|
18
|
+
describe CarbonCopy do
|
19
|
+
|
20
|
+
describe '#handle' do
|
21
|
+
before(:all) do
|
22
|
+
@cache_thread = Thread.new do
|
23
|
+
carbon_copy = CarbonCopy.new
|
24
|
+
carbon_copy.request_cacher = RequestCacher.new(support_path)
|
25
|
+
carbon_copy.port = 7979
|
26
|
+
carbon_copy.run
|
27
|
+
end
|
28
|
+
|
29
|
+
@sinatra_thread = Thread.new do
|
30
|
+
TestApp.run! host: 'localhost', port: 9898
|
31
|
+
end
|
32
|
+
sleep 1 # to allow for sinatra to boot
|
33
|
+
end
|
34
|
+
|
35
|
+
after(:all) do
|
36
|
+
@cache_thread.kill
|
37
|
+
@sinatra_thread.kill
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'caches get request' do
|
41
|
+
url = 'localhost:9898/awesome'
|
42
|
+
o_req = RestClient.get("http://#{url}").to_str
|
43
|
+
req = RestClient.get("http://localhost:7979/#{url}").to_str
|
44
|
+
req.should eq(o_req)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'caches options request' do
|
48
|
+
url = 'localhost:9898/awesome'
|
49
|
+
o_req = RestClient.options("http://#{url}").to_str
|
50
|
+
req = RestClient.options("http://localhost:7979/#{url}").to_str
|
51
|
+
req.should eq(o_req)
|
52
|
+
end
|
53
|
+
|
54
|
+
def support_path
|
55
|
+
File.expand_path('../support', __FILE__)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module CarbonCopy
|
6
|
+
describe RequestCacher do
|
7
|
+
let(:cacher) { RequestCacher.new('') }
|
8
|
+
let(:parsed) {
|
9
|
+
a = OpenStruct.new
|
10
|
+
a.verb = 'GET'
|
11
|
+
a.url = 'gist.github.com/74107'
|
12
|
+
a.host = 'gist.github.com'
|
13
|
+
a.version = '1.1'
|
14
|
+
a.uri = '/74107'
|
15
|
+
a.header_str = 'Test Header = Test Result'
|
16
|
+
a
|
17
|
+
}
|
18
|
+
it 'should have path with url' do
|
19
|
+
hash = Digest::MD5.new << parsed.header_str
|
20
|
+
cacher.path(parsed).should match(/\.request_cache\/gist\.github\.com\/get_74107_#{hash}/)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should reflect no path' do
|
24
|
+
hash = Digest::MD5.new << parsed.header_str
|
25
|
+
parsed.url = 'gist.github.com'
|
26
|
+
parsed.uri = '/'
|
27
|
+
cacher.path(parsed).should match(/\.request_cache\/gist\.github\.com\/get_#{hash}/)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'carbon-copy/request'
|
3
|
+
|
4
|
+
module CarbonCopy
|
5
|
+
describe Request do
|
6
|
+
describe '#parse_headers' do
|
7
|
+
let(:rs) { Request.new(stub.as_null_object) }
|
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' do
|
31
|
+
let(:rs) { Request.new(request).parse }
|
32
|
+
|
33
|
+
describe 'just host' do
|
34
|
+
let(:request) { create_host_IO("GET /apple.com HTTP/1.1\n") }
|
35
|
+
|
36
|
+
specify { rs.host.should eq('apple.com') }
|
37
|
+
specify { rs.uri.should eq('/') }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'host with port' do
|
41
|
+
let(:request) { create_host_IO("GET /apple.com:3000 HTTP/1.1\n") }
|
42
|
+
|
43
|
+
specify { rs.port.should eq('3000') }
|
44
|
+
specify { rs.host.should eq('apple.com') }
|
45
|
+
specify { rs.url.should eq('apple.com') }
|
46
|
+
specify { rs.uri.should eq('/') }
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'host with port and path' do
|
50
|
+
let(:request) { create_host_IO("GET /apple.com:18/awesome HTTP/1.1\n") }
|
51
|
+
|
52
|
+
specify { rs.port.should eq('18') }
|
53
|
+
specify { rs.host.should eq('apple.com') }
|
54
|
+
specify { rs.url.should eq('apple.com/awesome') }
|
55
|
+
specify { rs.uri.should eq('/awesome') }
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'host with get string' do
|
59
|
+
let(:request) { create_host_IO("POST /fazebook.com/google?p=test HTTP/1.1\n") }
|
60
|
+
|
61
|
+
specify { rs.verb.should eq('POST') }
|
62
|
+
specify { rs.port.should eq('80') }
|
63
|
+
specify { rs.url.should eq('fazebook.com/google?p=test') }
|
64
|
+
specify { rs.request_str.should eq("POST /google?p=test HTTP/1.1\r") }
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'host with path' do
|
68
|
+
let(:request) { create_host_IO("GET /apple.com/google/face/ HTTP/1.1\n") }
|
69
|
+
|
70
|
+
specify { rs.verb.should eq('GET') }
|
71
|
+
specify { rs.port.should eq('80') }
|
72
|
+
specify { rs.url.should eq('apple.com/google/face/') }
|
73
|
+
specify { rs.version.should eq('1.1') }
|
74
|
+
specify { rs.host.should eq('apple.com') }
|
75
|
+
specify { rs.uri.should eq('/google/face/') }
|
76
|
+
specify { rs.request_str.should eq("GET /google/face/? HTTP/1.1\r") }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def create_host_IO(host)
|
81
|
+
req = StringIO.new
|
82
|
+
host.split("\n").each do |host|
|
83
|
+
req << "#{host}\n"
|
84
|
+
end
|
85
|
+
req.rewind
|
86
|
+
req
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
require File.expand_path('../../lib/carbon-copy', __FILE__)
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
|
14
|
+
# Run specs in random order to surface order dependencies. If you find an
|
15
|
+
# order dependency and want to debug it, you can fix the order by providing
|
16
|
+
# the seed, which is printed after each run.
|
17
|
+
# --seed 1234
|
18
|
+
config.order = 'random'
|
19
|
+
end
|
20
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: carbon-copy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,56 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
13
|
-
dependencies:
|
12
|
+
date: 2012-09-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rest-client
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: sinatra
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
14
62
|
description: easily cache them REST calls
|
15
63
|
email:
|
16
64
|
- me@kdoubleyou.com
|
@@ -20,14 +68,21 @@ extensions: []
|
|
20
68
|
extra_rdoc_files: []
|
21
69
|
files:
|
22
70
|
- .gitignore
|
71
|
+
- .rspec
|
72
|
+
- Gemfile
|
73
|
+
- Gemfile.lock
|
23
74
|
- README.md
|
24
75
|
- bin/carbon-copy
|
25
76
|
- carbon-copy.gemspec
|
26
77
|
- lib/carbon-copy.rb
|
27
78
|
- lib/carbon-copy/cache_server.rb
|
28
|
-
- lib/carbon-copy/
|
79
|
+
- lib/carbon-copy/request.rb
|
80
|
+
- lib/carbon-copy/request_cacher.rb
|
29
81
|
- spec/cache_server_spec.rb
|
30
|
-
- spec/
|
82
|
+
- spec/integration_spec.rb
|
83
|
+
- spec/request_cacher_spec.rb
|
84
|
+
- spec/request_spec.rb
|
85
|
+
- spec/spec_helper.rb
|
31
86
|
homepage: https://github.com/rabidpraxis/carbon-copy
|
32
87
|
licenses: []
|
33
88
|
post_install_message:
|
@@ -54,5 +109,8 @@ specification_version: 3
|
|
54
109
|
summary: REST cache
|
55
110
|
test_files:
|
56
111
|
- spec/cache_server_spec.rb
|
57
|
-
- spec/
|
112
|
+
- spec/integration_spec.rb
|
113
|
+
- spec/request_cacher_spec.rb
|
114
|
+
- spec/request_spec.rb
|
115
|
+
- spec/spec_helper.rb
|
58
116
|
has_rdoc:
|
@@ -1,91 +0,0 @@
|
|
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
|
data/spec/http_cacher_spec.rb
DELETED
@@ -1,27 +0,0 @@
|
|
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
|