conveyor 0.1.4 → 0.2.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.
@@ -2,53 +2,54 @@ require 'net/http'
2
2
 
3
3
  module Conveyor
4
4
  class Client
5
- def initialize host, port = 8011
6
- @host = host
7
- @port = port
5
+ def initialize host, channel, port = 8011
6
+ @host = host
7
+ @port = port
8
+ @channel = channel
8
9
  connect!
9
10
  end
10
-
11
+
11
12
  def connect!
12
13
  @conn = Net::HTTP.start(@host, @port)
13
14
  end
14
15
 
15
- def create_channel channel_name
16
- @conn.put("/channels/#{channel_name}", nil, {'Content-Type' => 'application/octet-stream'})
16
+ def create_channel
17
+ @conn.put("/channels/#{@channel}", nil, {'Content-Type' => 'application/octet-stream'})
17
18
  end
18
19
 
19
- def post channel_name, content
20
- @conn.post("/channels/#{channel_name}", content, {'Content-Type' => 'application/octet-stream', 'Date' => Time.now.to_s})
20
+ def post content
21
+ @conn.post("/channels/#{@channel}", content, {'Content-Type' => 'application/octet-stream', 'Date' => Time.now.gmtime.to_s})
21
22
  end
22
23
 
23
- def get channel_name, id
24
- @conn.get("/channels/#{channel_name}/#{id}").body
24
+ def get id
25
+ @conn.get("/channels/#{@channel}/#{id}").body
25
26
  end
26
27
 
27
- def get_next channel_name, group=nil
28
+ def get_next group=nil
28
29
  if group
29
- @conn.get("/channels/#{channel_name}?next&group=#{group}").body
30
+ @conn.get("/channels/#{@channel}?next&group=#{group}").body
30
31
  else
31
- @conn.get("/channels/#{channel_name}?next").body
32
+ @conn.get("/channels/#{@channel}?next").body
32
33
  end
33
34
  end
34
35
 
35
- def channel_status channel_name
36
- JSON::parse(@conn.get("/channels/#{channel_name}").body)
36
+ def status
37
+ JSON::parse(@conn.get("/channels/#{@channel}").body)
37
38
  end
38
39
 
39
- def get_next_n channel_name, n = 10, group = nil
40
+ def get_next_n n = 10, group = nil
40
41
  if group
41
- JSON.parse(@conn.get("/channels/#{channel_name}?next&n=#{n}&group=#{group}").body)
42
+ JSON.parse(@conn.get("/channels/#{@channel}?next&n=#{n}&group=#{group}").body)
42
43
  else
43
- JSON.parse(@conn.get("/channels/#{channel_name}?next&n=#{n}").body)
44
+ JSON.parse(@conn.get("/channels/#{@channel}?next&n=#{n}").body)
44
45
  end
45
46
  end
46
47
 
47
- def rewind channel_name, id, group=nil
48
+ def rewind id, group=nil
48
49
  if group
49
- @conn.post("/channels/#{channel_name}?rewind_id=#{id}&group=#{group}", nil)
50
+ @conn.post("/channels/#{@channel}?rewind_id=#{id}&group=#{group}", nil)
50
51
  else
51
- @conn.post("/channels/#{channel_name}?rewind_id=#{id}", nil)
52
+ @conn.post("/channels/#{@channel}?rewind_id=#{id}", nil)
52
53
  end
53
54
  end
54
55
  end
@@ -5,171 +5,160 @@ require 'fileutils'
5
5
  require 'json'
6
6
  require 'logger'
7
7
 
8
- class Mongrel::HttpRequest
9
- def put?
10
- params["REQUEST_METHOD"] == "PUT"
11
- end
12
-
13
- def post?
14
- params["REQUEST_METHOD"] == "POST"
15
- end
16
-
17
- def get?
18
- params["REQUEST_METHOD"] == "GET"
19
- end
20
-
21
- def path_match pattern
22
- params["REQUEST_PATH"].match(pattern)
23
- end
24
- end
25
-
26
8
  module Conveyor
27
-
28
- # An HTTP server for Conveyor.
29
- class Server < Mongrel::HttpServer
9
+ class App
10
+ def initialize(data_directory, *options)
11
+ options = options.inject(){|(k, v), m| m[k] = v; m}
12
+ @data_directory = data_directory
13
+ @log_directory = options[:log_directory]
14
+ @unsafe_mode = options[:unsafe_mode] # allows deleting of channels. REALLY UNSAFE!
30
15
 
31
- # A Mongrel handler for multiple Conveyor Channels.
32
- class ChannelsHandler < Mongrel::HttpHandler
33
-
34
- def initialize data_directory, log_directory=nil
35
- @data_directory = data_directory
36
- if log_directory
37
- @logger = Logger.new File.join(log_directory, 'conveyor.log')
38
- else
39
- @logger = Logger.new '/dev/null'
40
- end
16
+ if @log_directory
17
+ @logger = Logger.new File.join(@log_directory, 'conveyor.log')
18
+ else
19
+ @logger = Logger.new '/dev/null'
20
+ end
41
21
 
42
- @channels = {}
43
- Dir.entries(@data_directory).each do |e|
44
- if !['.', '..'].include?(e) && File.directory?(File.join(@data_directory, e))
45
- @channels[e] = Channel.new(File.join(@data_directory, e))
46
- end
22
+ @channels = {}
23
+ Dir.entries(@data_directory).each do |e|
24
+ if !['.', '..'].include?(e) && File.directory?(File.join(@data_directory, e)) && Channel.valid_channel_name?(e)
25
+ @channels[e] = Channel.new(File.join(@data_directory, e))
47
26
  end
48
27
  end
49
28
 
50
- def create_new_channel channel_name
51
- @channels[channel_name] = Conveyor::Channel.new(File.join(@data_directory, channel_name))
52
- end
29
+ @requests = 0
30
+ end
31
+
32
+ def path_match env, pattern
33
+ env["REQUEST_PATH"].match(pattern)
34
+ end
53
35
 
54
- def i str
55
- @logger.info str
36
+ def create_new_channel channel_name
37
+ @channels[channel_name] = Conveyor::Channel.new(File.join(@data_directory, channel_name))
38
+ end
39
+
40
+ def i msg
41
+ @logger.info msg
42
+ end
43
+
44
+ def put env, m
45
+ if Channel.valid_channel_name?(m.captures[0])
46
+ if !@channels.key?(m.captures[0])
47
+ create_new_channel m.captures[0]
48
+ i "#{env["REMOTE_ADDR"]} PUT #{env["REQUEST_PATH"]} 201"
49
+ [201, {}, "created channel #{m.captures[0]}"]
50
+ else
51
+ i "#{env["REMOTE_ADDR"]} PUT #{env["REQUEST_PATH"]}"
52
+ [202, {}, "channel already exists. didn't do anything"]
53
+ end
54
+ else
55
+ i "#{env["REMOTE_ADDR"]} GET #{env["REQUEST_PATH"]} 406"
56
+ [406, {}, "invalid channel name. must match #{Channel::NAME_PATTERN}"]
56
57
  end
58
+ end
57
59
 
58
- def process request, response
59
- if request.put? && m = request.path_match(%r{/channels/(.*)})
60
- if Channel.valid_channel_name?(m.captures[0])
61
- if !@channels.key?(m.captures[0])
62
- create_new_channel m.captures[0]
63
- response.start(201) do |head, out|
64
- out.write("created channel #{m.captures[0]}")
65
- end
66
- i "#{request.params["REMOTE_ADDR"]} PUT #{request.params["REQUEST_PATH"]} 201"
67
- else
68
- response.start(202) do |head, out|
69
- out.write("channel already exists. didn't do anything")
70
- end
71
- i "#{request.params["REMOTE_ADDR"]} PUT #{request.params["REQUEST_PATH"]} "
72
- end
60
+ def post env, m
61
+ if @channels.key?(m.captures[0])
62
+ params = Mongrel::HttpRequest.query_parse(env['QUERY_STRING'])
63
+ if params.key?('rewind_id')
64
+ if params['group']
65
+ @channels[m.captures[0]].rewind(:id => params['rewind_id'], :group => params['group']).to_i # TODO make sure this is an integer
66
+ [200, {}, "iterator rewound to #{params['rewind_id']}"]
73
67
  else
74
- response.start(406) do |head, out|
75
- out.write("invalid channel name. must match #{Channel::NAME_PATTERN}")
76
- i "#{request.params["REMOTE_ADDR"]} GET #{request.params["REQUEST_PATH"]} 406"
77
- end
68
+ @channels[m.captures[0]].rewind(:id => params['rewind_id']).to_i # TODO make sure this is an integer
69
+ [200, {}, "iterator rewound to #{params['rewind_id']}"]
78
70
  end
79
- elsif request.post? && m = request.path_match(%r{/channels/(.*)})
80
- if @channels.key?(m.captures[0])
81
- params = Mongrel::HttpRequest.query_parse(request.params['QUERY_STRING'])
82
- if params.key?('rewind_id')
83
- if params['group']
84
- @channels[m.captures[0]].rewind(:id => params['rewind_id'], :group => params['group']).to_i # TODO make sure this is an integer
85
- response.start(200) do |head, out|
86
- out.write "iterator rewound to #{params['rewind_id']}"
87
- end
71
+ else
72
+ if env.key?('HTTP_DATE') && d = Time.parse(env['HTTP_DATE'])
73
+ id = @channels[m.captures[0]].post(env['rack.input'].read)
74
+ i "#{env["REMOTE_ADDR"]} POST #{env["REQUEST_PATH"]} 202"
75
+ [202, {"Location" => "/channels/#{m.captures[0]}/#{id}"}, ""]
76
+ else
77
+ i "#{env["REMOTE_ADDR"]} POST #{env["REQUEST_PATH"]} 400"
78
+ [400, {}, "A valid Date header is required for all POSTs."]
79
+ end
80
+ end
81
+ elsif Channel.valid_channel_name?(m.captures[0])
82
+ create_new_channel(m.captures[0])
83
+ post(env, m)
84
+ else
85
+ [404, {}, '']
86
+ end
87
+ end
88
+
89
+ def get env
90
+ headers = content = nil
91
+ if m = path_match(env, %r{/channels/(.*)/(\d+)})
92
+ if @channels.key?(m.captures[0])
93
+ headers, content = @channels[m.captures[0]].get(m.captures[1].to_i)
94
+ end
95
+ elsif m = path_match(env, %r{/channels/(.*)})
96
+ if @channels.key?(m.captures[0])
97
+ params = Mongrel::HttpRequest.query_parse(env['QUERY_STRING'])
98
+ if params.key? 'next'
99
+ if params.key? 'group'
100
+ if params.key? 'n'
101
+ list = @channels[m.captures[0]].get_next_n_by_group(params['n'].to_i, params['group'])
88
102
  else
89
- @channels[m.captures[0]].rewind(:id => params['rewind_id']).to_i # TODO make sure this is an integer
90
- response.start(200) do |head, out|
91
- out.write "iterator rewound to #{params['rewind_id']}"
92
- end
103
+ headers, content = @channels[m.captures[0]].get_next_by_group(params['group'])
93
104
  end
94
105
  else
95
- if request.params.include?('HTTP_DATE') && d = Time.parse(request.params['HTTP_DATE'])
96
- id = @channels[m.captures[0]].post(request.body.read)
97
- response.start(202) do |head, out|
98
- head["Location"] = "/channels/#{m.captures[0]}/#{id}"
106
+ if params.key? 'n'
107
+ list = @channels[m.captures[0]].get_next_n(params['n'].to_i).map do |i|
108
+ {:data => i[1], :hash => i[0][:hash], :id => i[0][:id]}
99
109
  end
100
- i "#{request.params["REMOTE_ADDR"]} POST #{request.params["REQUEST_PATH"]} 202"
101
110
  else
102
- response.start(400) do |head, out|
103
- out.write "A valid Date header is required for all POSTs."
104
- end
105
- i "#{request.params["REMOTE_ADDR"]} POST #{request.params["REQUEST_PATH"]} 400"
106
- end
107
- end
108
- end
109
-
110
- elsif request.get?
111
- headers = content = nil
112
- if m = request.path_match(%r{/channels/(.*)/(\d+)})
113
- if @channels.key?(m.captures[0])
114
- headers, content = @channels[m.captures[0]].get(m.captures[1].to_i)
115
- end
116
- elsif m = request.path_match(%r{/channels/(.*)})
117
- if @channels.key?(m.captures[0])
118
- params = Mongrel::HttpRequest.query_parse(request.params['QUERY_STRING'])
119
- if params.key? 'next'
120
- if params.key? 'group'
121
- if params.key? 'n'
122
- list = @channels[m.captures[0]].get_next_n_by_group(params['n'].to_i, params['group'])
123
- else
124
- headers, content = @channels[m.captures[0]].get_next_by_group(params['group'])
125
- end
126
- else
127
- if params.key? 'n'
128
- list = @channels[m.captures[0]].get_next_n(params['n'].to_i)
129
- list = list.map do |i|
130
- {:data => i[1], :hash => i[0][:hash], :id => i[0][:id]}
131
- end
132
- else
133
- headers, content = @channels[m.captures[0]].get_next
134
- end
135
- end
136
- else
137
- response.start(200) do |head, out|
138
- out.write @channels[m.captures[0]].status.to_json
139
- end
111
+ headers, content = @channels[m.captures[0]].get_next
140
112
  end
141
113
  end
142
114
  else
143
- response.start(200) do |head, out|
144
- out.write("fake!")
145
- end
146
- end
147
-
148
- if headers && content
149
- response.start(200) do |head, out|
150
- head['Content-Location'] = "/channels/#{m.captures[0]}/#{headers[:id]}"
151
- head['Content-MD5'] = headers[:hash]
152
- head['Content-Type'] = 'application/octet-stream'
153
- head['Last-Modified'] = Time.parse(headers[:time]).gmtime.to_s
154
- out.write content
155
- end
156
- i "#{request.params["REMOTE_ADDR"]} GET #{request.params["REQUEST_PATH"]} 200 #{headers[:id]} #{headers[:length]} #{headers[:hash]}"
157
- elsif list
158
- response.start(200) do |head, out|
159
- out.write list.to_json
160
- end
115
+ return [200, {}, @channels[m.captures[0]].status.to_json]
161
116
  end
162
-
163
117
  end
118
+ else
119
+ return [200, {}, "fake!"]
164
120
  end
121
+
122
+ if headers && content
123
+ i "#{env["REMOTE_ADDR"]} GET #{env["REQUEST_PATH"]} 200 #{headers[:id]} #{headers[:length]} #{headers[:hash]}"
124
+ return [
125
+ 200,
126
+ {
127
+ 'Content-Location' => "/channels/#{m.captures[0]}/#{headers[:id]}",
128
+ 'Content-MD5' => headers[:hash],
129
+ 'Content-Type' => 'application/octet-stream',
130
+ 'Last-Modified' => Time.at(headers[:time]).gmtime.to_s,
131
+ },
132
+ content
133
+ ]
134
+ elsif list
135
+ return [200, {}, list.to_json]
136
+ end
137
+
138
+ return [404, {}, '']
165
139
  end
166
140
 
167
- # +host+ and +port+ are passed along to Mongrel::HttpServer for TCP binding. +data_directory+ is used to store
168
- # all channel data and should be created before intializing a Server.
169
- def initialize(host, port, data_directory, log_directory = nil)
170
- super(host, port)
171
- ch = ChannelsHandler.new(data_directory, log_directory)
172
- register("/channels", ch)
141
+ def delete env, m
142
+ if @channels.key?(m.captures[0])
143
+ @channels[m.captures[0]].delete!
144
+ @channels.delete(m.captures[0])
145
+ [200, {}, "Channel deleted."]
146
+ end
147
+ end
148
+
149
+ def call(env)
150
+ @requests += 1
151
+ if env['REQUEST_METHOD'] == 'PUT' && m = path_match(env, %r{/channels/(.*)})
152
+ put(env, m)
153
+ elsif env['REQUEST_METHOD'] == 'POST' && m = path_match(env, %r{/channels/(.*)})
154
+ post(env, m)
155
+ elsif @unsafe_mode && env['REQUEST_METHOD'] == 'DELETE' && m = path_match(env, %r{/channels/(.*)})
156
+ delete(env, m)
157
+ elsif env['REQUEST_METHOD'] == 'GET'
158
+ get(env)
159
+ else
160
+ [404, {}, '']
161
+ end
173
162
  end
174
163
  end
175
164
  end
@@ -0,0 +1,119 @@
1
+ require 'conveyor/base_channel'
2
+
3
+ def String.random_alphanumeric(size=16)
4
+ s = ""
5
+ size.times { s << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }
6
+ s
7
+ end
8
+
9
+
10
+ module Conveyor
11
+ class Upgrader
12
+
13
+ def initialize directory
14
+ @directory = directory
15
+ end
16
+
17
+ def version_path
18
+ File.join(@directory, 'version')
19
+ end
20
+
21
+ def source_version
22
+ if File.exists?(version_path) && File.size(version_path) > 0
23
+ File.open(version_path).read.to_i
24
+ else
25
+ 0
26
+ end
27
+ end
28
+
29
+ def upgrade
30
+ case source_version
31
+ when 0
32
+ from_0
33
+ end
34
+ end
35
+
36
+ def parse_headers_0 str, index_file = false
37
+ pattern = '\A(\d+) (\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}[+\-]\d{2}\:\d{2}) (\d+) (\d+) ([a-f0-9]+)'
38
+ pattern += ' (\d+)' if index_file
39
+ pattern += '\Z'
40
+ m = str.match(Regexp.new(pattern))
41
+ {
42
+ :id => m.captures[0].to_i,
43
+ :time => m.captures[1],
44
+ :offset => m.captures[2].to_i,
45
+ :length => m.captures[3].to_i,
46
+ :hash => m.captures[4],
47
+ :file => (index_file ? m.captures[5].to_i : nil)
48
+ }.reject {|k,v| v == nil}
49
+ end
50
+
51
+ def from_0
52
+ Dir.glob(@directory + "/*").each do |d|
53
+
54
+ if File.directory?(d)
55
+ # create tmp dir
56
+ tmp_dir = create_tmp_dir!
57
+ puts "writing to #{tmp_dir}"
58
+ chan = Channel.new(tmp_dir)
59
+
60
+ Dir.glob("#{d}/[0-9]*").each do |f|
61
+ puts "upgrading #{f}"
62
+ size = File.size(f)
63
+ f = File.open(f)
64
+
65
+ while f.pos < size
66
+ l = f.readline.strip
67
+ header = parse_headers_0(l)
68
+ content = f.read(header[:length])
69
+ f.readline # newline chomp
70
+
71
+ chan.commit(content, Time.parse(header[:time]))
72
+ end
73
+ end
74
+
75
+ puts "upgrading iterator"
76
+ iterator = File.open(File.join(d, 'iterator')) do |f|
77
+ l = nil
78
+ while !f.eof? && l = f.readline
79
+ end
80
+ if l
81
+ chan.rewind :id => l.strip.to_i
82
+ end
83
+ end
84
+
85
+ Dir.glob(File.join(d, 'iterator-*')) do |i|
86
+ group = i.split('/').last.split('-').last
87
+ puts "upgrading group iterator for #{group}"
88
+ iterator = File.open(i) do |f|
89
+ l = nil
90
+ while !f.eof? && l = f.readline
91
+ end
92
+ if l
93
+ chan.rewind :id => l.strip.to_i, :group => group
94
+ end
95
+ end
96
+ end
97
+
98
+ puts "backing up #{d} to #{d}.bak"
99
+ FileUtils.mv d, d + ".bak"
100
+
101
+ puts "copying from #{tmp_dir} to #{d}"
102
+ FileUtils.cp_r tmp_dir, d
103
+
104
+ puts "deleting temp data"
105
+ FileUtils.rm_r tmp_dir
106
+ end
107
+ end
108
+ end
109
+
110
+ def create_tmp_dir!
111
+ loop do
112
+ tmp_dir = File.join('/tmp', String.random_alphanumeric)
113
+ if !File.exists?(tmp_dir)
114
+ return tmp_dir
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end