lifter 0.1.0 → 0.1.1

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.
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