lifter 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8f28fdb4670f7959c6eef5fff3ac85894c016293
4
- data.tar.gz: 763a8953d4ba2ed82f969cd28c5cf2150dd0dc81
3
+ metadata.gz: 795dbedbc9fb64ba72602c68ca1eb1352a1e135d
4
+ data.tar.gz: 6a4c454a3a9c5005f393c3de0a17f5797e50cf3d
5
5
  SHA512:
6
- metadata.gz: b3f02a5c37031f345a3a3320ce4155f67fab09f40dcd98d91a5b12b87a081fbad616416779c9ff47a65ae0a4c1b264dc8d746cfff3a43140243d88676925c330
7
- data.tar.gz: ce2838c2521b7aa19926af9e589199fd51c7158e6fb96618cf7f04d62f7309b07824e76ece43afe277f3828db41668e8eed843d1ce3e5091d452ab5516ba0deb
6
+ metadata.gz: 2b8879248daabb268a9b0ac9bafcea9bf752ad1253d7293374b01d81e5a8b50d11d969f7a7123df42314b4193821b6e883c2b39b08a70c2b86a3321cd6fb9752
7
+ data.tar.gz: f10d0d10f850e39471e7bf001cc898db9dee34a21ba22fc77348fb143b612d5e6352b6e36e2bfb6c6fb2897da839263b246d9229df3433cd2ed711e206605a49
data/lib/lifter.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'multipart_parser/parser'
2
2
  require 'multipart_parser/reader'
3
3
 
4
+ require 'lifter/errors'
4
5
  require 'lifter/thread_pool'
5
6
  require 'lifter/file_pool'
6
7
  require 'lifter/webhook'
@@ -3,9 +3,10 @@ require 'http/parser'
3
3
 
4
4
  module Lifter
5
5
  class Connection < EventMachine::Connection
6
- Request = Struct.new(:headers, :params, :remote_ip)
6
+ Request = Struct.new(:headers, :params, :remote_ip, :origin)
7
7
 
8
8
  PAYLOAD_METHODS = ['put', 'post'].freeze
9
+ OPTIONS_METHOD = 'options'.freeze
9
10
  CONTENT_TYPE_KEY = 'content-type'.freeze
10
11
  MULTIPART_CONTENT_TYPE = 'multipart/form-data'.freeze
11
12
 
@@ -16,6 +17,7 @@ module Lifter
16
17
 
17
18
  @is_multipart = nil
18
19
  @has_payload = nil
20
+ @options_request = nil
19
21
 
20
22
  @multipart_reader = nil
21
23
  @inline_reader = nil
@@ -23,6 +25,8 @@ module Lifter
23
25
  @server = nil
24
26
  @request = Request.new
25
27
 
28
+ @gracefully_close = false
29
+
26
30
  @parser = HTTP::Parser.new
27
31
 
28
32
  @parser.on_message_begin = proc do
@@ -31,7 +35,16 @@ module Lifter
31
35
 
32
36
  @parser.on_headers_complete = proc do
33
37
  process_headers
34
- start_payload if payload?
38
+
39
+ if payload?
40
+ start_payload
41
+ else
42
+ if request_method?(:options)
43
+ handle_options_request
44
+ else
45
+ close(gracefully: true)
46
+ end
47
+ end
35
48
  end
36
49
 
37
50
  @parser.on_body = proc do |data|
@@ -60,7 +73,24 @@ module Lifter
60
73
  end
61
74
 
62
75
  def unbind
76
+ @payload.cancel if !gracefully_close? && !@payload.nil?
77
+ end
78
+
79
+ def close(opts = {})
80
+ @gracefully_close = opts[:gracefully] == true
81
+
82
+ EventMachine.next_tick do
83
+ close_connection(true)
84
+ end
85
+ end
86
+
87
+ def cancel
63
88
  @payload.cancel if !@payload.nil?
89
+ close(gracefully: true)
90
+ end
91
+
92
+ def gracefully_close?
93
+ @gracefully_close == true
64
94
  end
65
95
 
66
96
  def http_version
@@ -71,16 +101,23 @@ module Lifter
71
101
  '127.0.0.1'
72
102
  end
73
103
 
74
- def respond(code, status)
104
+ def respond(code, status, headers = {}, body = '')
105
+ return if gracefully_close?
106
+
75
107
  EventMachine.next_tick do
76
108
  response = "HTTP/#{http_version.join('.')} #{code} #{status}"
77
- send_data(response)
78
- end
79
- end
80
109
 
81
- def close
82
- EventMachine.next_tick do
83
- close_connection(true)
110
+ headers = {} if headers.empty?
111
+ headers[:content_length] = body.bytesize if headers[:content_length].nil?
112
+ headers[:access_control_allow_origin] = @request.origin if !@request.origin.nil?
113
+
114
+ formatted_headers = format_response_headers(headers)
115
+
116
+ response << "\r\n" << formatted_headers
117
+ response << "\r\n\r\n"
118
+ response << body
119
+
120
+ send_data(response)
84
121
  end
85
122
  end
86
123
 
@@ -95,9 +132,6 @@ module Lifter
95
132
  end
96
133
 
97
134
  private def finish_request
98
- clear_request
99
- clear_multipart
100
- clear_payload
101
135
  end
102
136
 
103
137
  private def start_payload
@@ -123,6 +157,8 @@ module Lifter
123
157
  @request.headers = headers
124
158
 
125
159
  @request.remote_ip = remote_ip
160
+
161
+ @request.origin = @request.headers['origin']
126
162
  end
127
163
 
128
164
  private def payload?
@@ -133,6 +169,10 @@ module Lifter
133
169
  @has_payload
134
170
  end
135
171
 
172
+ private def request_method?(method)
173
+ @parser.http_method.to_s.downcase == method.to_s.downcase
174
+ end
175
+
136
176
  private def clear_multipart
137
177
  @is_multipart = nil
138
178
  end
@@ -166,5 +206,27 @@ module Lifter
166
206
  private def start_inline_payload
167
207
  @payload = Payloads::InlinePayload.new(self, file_manager)
168
208
  end
209
+
210
+ private def handle_options_request
211
+ cors_headers = {
212
+ access_control_allow_methods: 'PUT, POST',
213
+ access_control_allow_headers: 'accept, content-type',
214
+ }
215
+
216
+ respond(200, 'OK', cors_headers)
217
+ end
218
+
219
+ private def format_response_headers(headers = {})
220
+ formatted_headers = []
221
+
222
+ headers.each_pair do |key, value|
223
+ formatted_key = key.to_s.strip.downcase.gsub(/[:_\- ]/, '-').squeeze('-').gsub(/[\s]+/, '')
224
+ formatted_key = formatted_key.split('-').map { |s| s.capitalize }.join('-')
225
+
226
+ formatted_headers << "#{formatted_key}: #{value}"
227
+ end
228
+
229
+ formatted_headers.join("\r\n")
230
+ end
169
231
  end
170
232
  end
@@ -0,0 +1,6 @@
1
+ module Lifter
2
+ module Errors
3
+ class WebhookFailed < StandardError; end
4
+ class InvalidWebhookMethod < StandardError; end
5
+ end
6
+ end
@@ -43,10 +43,7 @@ module Lifter
43
43
  file.write(data)
44
44
 
45
45
  if file.prologue.length >= @upload_prologue_size
46
- @webhooks.push(file_id) do
47
- webhook = create_authorize_webhook(connection, file)
48
- webhook.deliver
49
- end
46
+ authorize_file(connection, file_id, file)
50
47
  end
51
48
  end
52
49
  end
@@ -60,21 +57,13 @@ module Lifter
60
57
 
61
58
  file.mv("#{@working_dir}/completed/#{file_id}")
62
59
 
63
- @files.remove(file_id)
64
-
65
60
  # In the event the upload was too small for the prologue size to have been met previously,
66
- # ensure the authorize webhook is fired off before completed.
61
+ # ensure the authorize webhook is fired off before the completed webhook.
67
62
  if file.prologue.length < @upload_prologue_size
68
- @webhooks.push(file_id) do
69
- webhook = create_authorize_webhook(connection, file)
70
- webhook.deliver
71
- end
63
+ authorize_file(connection, file_id, file)
72
64
  end
73
65
 
74
- @webhooks.push(file_id) do
75
- webhook = create_completed_webhook(connection, file)
76
- webhook.deliver
77
- end
66
+ complete_file(connection, file_id, file)
78
67
  end
79
68
  end
80
69
 
@@ -101,18 +90,28 @@ module Lifter
101
90
  end
102
91
  end
103
92
 
104
- private def create_authorize_webhook(connection, file)
93
+ private def authorize_file(connection, file_id, file)
94
+ return if file.authorized? || file.pending_authorization?
95
+
96
+ @webhooks.push(file_id) do
97
+ webhook = create_authorize_webhook(connection, file_id, file)
98
+ file.pending_authorization
99
+ webhook.deliver
100
+ end
101
+ end
102
+
103
+ private def create_authorize_webhook(connection, file_id, file)
105
104
  webhook = Webhook.new(@authorize_webhook_endpoint)
106
105
 
107
106
  headers = file.original_request.headers.dup
108
107
  params = file.original_request.params.dup
109
108
 
110
- headers = clean_request_headers(headers)
109
+ headers = scrub_request_headers(headers)
111
110
  headers['x-upload-ip'] = file.original_request.remote_ip
112
111
 
113
112
  params[file.param] = {
114
113
  file_name: file.original_name,
115
- file_prologue: file.prologue.length
114
+ file_prologue: file.prologue
116
115
  }
117
116
 
118
117
  webhook.headers = headers
@@ -120,18 +119,30 @@ module Lifter
120
119
 
121
120
  webhook.on_failure do
122
121
  connection.cancel
122
+ cancel_file(connection, file_id)
123
+ end
124
+
125
+ webhook.on_success do
126
+ file.authorize
123
127
  end
124
128
 
125
129
  webhook
126
130
  end
127
131
 
128
- private def create_completed_webhook(connection, file)
132
+ private def complete_file(connection, file_id, file)
133
+ @webhooks.push(file_id) do
134
+ webhook = create_completed_webhook(connection, file_id, file)
135
+ webhook.deliver
136
+ end
137
+ end
138
+
139
+ private def create_completed_webhook(connection, file_id, file)
129
140
  webhook = Webhook.new(@completed_webhook_endpoint)
130
141
 
131
142
  headers = file.original_request.headers.dup
132
143
  params = file.original_request.params.dup
133
144
 
134
- headers = clean_request_headers(headers)
145
+ headers = scrub_request_headers(headers)
135
146
  headers['x-upload-ip'] = file.original_request.remote_ip
136
147
 
137
148
  params[file.param] = {
@@ -140,14 +151,23 @@ module Lifter
140
151
  file_hash: file.hash
141
152
  }
142
153
 
154
+ webhook.headers = headers
155
+ webhook.params = params
156
+
143
157
  webhook.on_failure do
144
158
  connection.cancel
159
+ @files.remove(file_id)
160
+ end
161
+
162
+ webhook.on_success do
163
+ connection.respond(200, 'OK')
164
+ @files.remove(file_id)
145
165
  end
146
166
 
147
167
  webhook
148
168
  end
149
169
 
150
- private def clean_request_headers(headers)
170
+ private def scrub_request_headers(headers)
151
171
  SCRUB_HEADERS.each do |header_key|
152
172
  headers.delete(header_key)
153
173
  end
@@ -10,7 +10,13 @@ module Lifter
10
10
  attr_reader :prologue, :original_request, :original_name, :param
11
11
 
12
12
  def initialize(file, opts = {})
13
+ @mutex = Mutex.new
14
+
13
15
  @file = file
16
+ @path = file.path
17
+
18
+ @authorized = false
19
+ @pending_authorization = false
14
20
 
15
21
  @hash = setup_hash(opts[:hash_method])
16
22
 
@@ -37,25 +43,59 @@ module Lifter
37
43
  end
38
44
 
39
45
  def close
40
- @file.close
46
+ @file.close if !@file.closed?
41
47
  end
42
48
 
43
49
  def rm
44
50
  FileUtils.rm(full_path)
51
+ @path = nil
45
52
  end
46
53
 
47
54
  def mv(new_path)
48
55
  FileUtils.mv(full_path, new_path)
56
+ @path = new_path
49
57
  end
50
58
 
51
59
  def full_path
52
- File.expand_path(@file.path)
60
+ File.expand_path(@path)
53
61
  end
54
62
 
55
63
  def hash
56
64
  @hash.hexdigest
57
65
  end
58
66
 
67
+ def authorize
68
+ @mutex.synchronize do
69
+ @authorized = true
70
+ end
71
+ end
72
+
73
+ def pending_authorization
74
+ @mutex.synchronize do
75
+ @pending_authorization = true
76
+ end
77
+ end
78
+
79
+ def pending_authorization?
80
+ pending = false
81
+
82
+ @mutex.synchronize do
83
+ pending = @pending_authorization == true
84
+ end
85
+
86
+ pending
87
+ end
88
+
89
+ def authorized?
90
+ authorized = false
91
+
92
+ @mutex.synchronize do
93
+ authorized = @authorized == true
94
+ end
95
+
96
+ authorized
97
+ end
98
+
59
99
  private def setup_hash(hash_type)
60
100
  case hash_type
61
101
  when :md5
@@ -23,7 +23,7 @@ module Lifter
23
23
  return if !current_part?
24
24
 
25
25
  if current_part.type == :file
26
- @file_manager.cancel_file(@current_part.id)
26
+ @file_manager.cancel_file(@connection, @current_part.id)
27
27
  end
28
28
 
29
29
  @current_part = nil
@@ -69,7 +69,7 @@ module Lifter
69
69
 
70
70
  @reader.on_end do
71
71
  @connection.respond(200, 'OK')
72
- @connection.close
72
+ @connection.close(gracefully: true)
73
73
  end
74
74
 
75
75
  @reader.on_error do |message|
@@ -97,9 +97,6 @@ module Lifter
97
97
  rescue StandardError => e
98
98
  puts e.to_s
99
99
  puts e.backtrace
100
- exit
101
- add_pending(job_tag)
102
- queue.push([job_tag, job])
103
100
  end
104
101
  end
105
102
  end
@@ -1,3 +1,3 @@
1
1
  module Lifter
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.1.1".freeze
3
3
  end
@@ -2,6 +2,32 @@ require 'http'
2
2
 
3
3
  module Lifter
4
4
  class Webhook
5
+ # Courtesy of: http://dev.mensfeld.pl/2012/01/converting-nested-hash-into-http-url-params-hash-version-in-ruby/
6
+ module ParamNester
7
+ def self.encode(value, key = nil, out_hash = {})
8
+ case value
9
+ when Hash
10
+ value.each { |k,v| encode(v, append_key(key, k), out_hash) }
11
+ out_hash
12
+ when Array
13
+ value.each { |v| encode(v, "#{key}[]", out_hash) }
14
+ out_hash
15
+ when nil
16
+ ''
17
+ else
18
+ out_hash[key] = value
19
+ out_hash
20
+ end
21
+ end
22
+
23
+ def self.append_key(root_key, key)
24
+ root_key.nil? ? :"#{key}" : :"#{root_key}[#{key.to_s}]"
25
+ end
26
+ end
27
+
28
+ RETRY_CODES = [500, 502, 503, 504]
29
+ RETRY_LIMIT = 3
30
+
5
31
  attr_reader :url, :method, :headers, :params
6
32
 
7
33
  def initialize(endpoint)
@@ -9,14 +35,24 @@ module Lifter
9
35
  @method = endpoint.method
10
36
  @headers = {}
11
37
  @params = {}
38
+
39
+ @retry_count = 0
40
+ @retry_limit = RETRY_LIMIT
41
+
42
+ @on_success = nil
43
+ @on_failure = nil
12
44
  end
13
45
 
14
46
  def headers=(headers)
15
47
  @headers = headers
16
48
  end
17
49
 
18
- def params=(params)
19
- @params = params
50
+ def params=(params = {})
51
+ @params = ParamNester.encode(params)
52
+ end
53
+
54
+ def on_success(&block)
55
+ @on_success = block
20
56
  end
21
57
 
22
58
  def on_failure(&block)
@@ -24,6 +60,33 @@ module Lifter
24
60
  end
25
61
 
26
62
  def deliver
63
+ begin
64
+ start_delivery
65
+ rescue Errors::WebhookFailed => e
66
+ @on_failure.call if !@on_failure.nil?
67
+ end
68
+
69
+ @on_success.call if !@on_success.nil?
70
+ end
71
+
72
+ private def start_delivery
73
+ begin
74
+ response = finish_delivery
75
+ rescue StandardError => e
76
+ raise Errors::WebhookFailed.new
77
+ end
78
+
79
+ return if response.code == 200
80
+
81
+ if RETRY_CODES.include?(response.code) && @retry_count < @retry_limit
82
+ @retry_count += 1
83
+ start_delivery
84
+ else
85
+ raise Errors::WebhookFailed.new
86
+ end
87
+ end
88
+
89
+ private def finish_delivery
27
90
  http_stub = HTTP.headers(@headers)
28
91
 
29
92
  case @method.to_sym
@@ -34,7 +97,7 @@ module Lifter
34
97
  when :put
35
98
  http_stub.put(@url, form: @params)
36
99
  else
37
- raise StandardError.new('unsupported http method in webhook')
100
+ raise Errors::InvalidWebhookMethod.new
38
101
  end
39
102
  end
40
103
  end
data/test/test.rb CHANGED
@@ -1,3 +1,5 @@
1
+ lib = File.expand_path("../../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
1
3
  require 'lifter'
2
4
 
3
5
  server = Lifter::Server.new do |config|
@@ -13,14 +15,14 @@ server = Lifter::Server.new do |config|
13
15
  working_dir 'tmp/uploads'
14
16
 
15
17
  # Define maximum size in bytes of a file upload. Files larger than this will be automatically
16
- # removed, the connection closed, and no uploaded webhook will fire.
18
+ # removed, the connection closed, and no completed webhook will fire.
17
19
  #
18
20
  max_upload_size 500 * 1024 * 1024
19
21
 
20
22
  # Specify desired digest type for file uploads. Passed in uploaded webhook after upload completes.
21
23
  # Possible options: md5, sha1, sha256, sha512.
22
24
  #
23
- upload_hash_method :sha1
25
+ upload_hash_method :sha256
24
26
 
25
27
  # Configure maximum number of bytes to pass along in authorize webhook.
26
28
  #
@@ -38,7 +40,7 @@ server = Lifter::Server.new do |config|
38
40
  # as the first <prologue_limit> bytes of data is received. Non-200 responses for one part will not
39
41
  # remove data from other parts, although the connection will still be terminated.
40
42
  #
41
- authorize_webhook :post, 'http://127.0.0.1:8081/uploads/authorize'
43
+ authorize_webhook :post, 'http://127.0.0.1:3000/uploads/authorize'
42
44
 
43
45
  # A request to this webhook is made once a single file upload completes. In the event the upload
44
46
  # is multipart with multiple files, this endpoint will be called once for each file, upon
@@ -46,7 +48,7 @@ server = Lifter::Server.new do |config|
46
48
  #
47
49
  # An authorize webhook is always sent prior to sending this webhook.
48
50
  #
49
- completed_webhook :post, 'http://127.0.0.1:8081/uploads/ingest'
51
+ completed_webhook :post, 'http://127.0.0.1:3000/uploads/complete'
50
52
  end
51
53
 
52
54
  server.start
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lifter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Amundson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-20 00:00:00.000000000 Z
11
+ date: 2015-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http_parser.rb
@@ -80,6 +80,7 @@ files:
80
80
  - lib/lifter.rb
81
81
  - lib/lifter/config.rb
82
82
  - lib/lifter/connection.rb
83
+ - lib/lifter/errors.rb
83
84
  - lib/lifter/file_manager.rb
84
85
  - lib/lifter/file_pool.rb
85
86
  - lib/lifter/file_upload.rb
@@ -115,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
116
  version: '0'
116
117
  requirements: []
117
118
  rubyforge_project:
118
- rubygems_version: 2.4.5
119
+ rubygems_version: 2.4.5.1
119
120
  signing_key:
120
121
  specification_version: 4
121
122
  summary: Painless file uploads