rubytus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ require 'json'
2
+
3
+ module Rubytus
4
+ class Info < Hash
5
+ def initialize(args = {})
6
+ self['Offset'] = args[:offset] || 0
7
+ self['FinalLength'] = args[:final_length] || 0
8
+ self['Meta'] = args[:meta] || nil
9
+ end
10
+
11
+ def offset=(value)
12
+ self['Offset'] = value.to_i
13
+ end
14
+
15
+ def offset
16
+ self['Offset']
17
+ end
18
+
19
+ def final_length=(value)
20
+ self['FinalLength'] = value.to_i
21
+ end
22
+
23
+ def final_length
24
+ self['FinalLength']
25
+ end
26
+
27
+ def remaining_length
28
+ final_length - offset
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ require 'rubytus/error'
2
+
3
+ module Rubytus
4
+ module Middlewares
5
+ class StorageBarrier
6
+ include Rubytus::Constants
7
+ include Goliath::Rack::AsyncMiddleware
8
+
9
+ def post_process(env, status, headers, body)
10
+ status = STATUS_OK
11
+ action = env['api.action']
12
+ storage = env['api.options'][:storage]
13
+
14
+ begin
15
+ case action
16
+ when :create
17
+ status = STATUS_CREATED
18
+ headers['Location'] = env['api.resource_url']
19
+ storage.create_file(env['api.uid'], env['api.final_length'])
20
+
21
+ when :head
22
+ info = storage.read_info(env['api.uid'])
23
+ headers['Offset'] = info.offset.to_s
24
+
25
+ when :get
26
+ body = storage.read_file(env['api.uid'])
27
+ end
28
+ rescue PermissionError => e
29
+ raise Goliath::Validation::Error.new(500, e.message)
30
+ end
31
+
32
+ [status, headers, body]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module Rubytus
2
+ module Middlewares
3
+ class TusBarrier
4
+ include Rubytus::Constants
5
+ include Goliath::Rack::AsyncMiddleware
6
+
7
+ def post_process(env, status, headers, body)
8
+ request = Rubytus::Request.new(env)
9
+
10
+ if request.collection?
11
+ unless request.options? || request.post?
12
+ status = STATUS_NOT_ALLOWED
13
+ body = "#{request.request_method} used against file creation url. Only POST is allowed."
14
+ headers['Allow'] = 'POST'
15
+ end
16
+ end
17
+
18
+ if request.resource?
19
+ unless request.options? || request.head? || request.patch? || request.get?
20
+ status = STATUS_NOT_ALLOWED
21
+ allowed = 'HEAD,PATCH'
22
+ body = "#{request.request_method} used against file creation url. Allowed: #{allowed}"
23
+ headers['Allow'] = allowed
24
+ end
25
+ end
26
+
27
+ if request.unknown?
28
+ status = STATUS_NOT_FOUND
29
+ body = "Unknown url: #{request.path_info} - does not match file pattern"
30
+ end
31
+
32
+ [status, headers, body]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,102 @@
1
+ require 'rubytus/constants'
2
+ require 'rubytus/common'
3
+
4
+ module Rubytus
5
+ class Request
6
+ include Rubytus::Constants
7
+ include Rubytus::Common
8
+
9
+ def initialize(env)
10
+ @env = env
11
+ end
12
+
13
+ def get?; request_method == 'GET'; end
14
+ def post?; request_method == 'POST'; end
15
+ def head?; request_method == 'HEAD'; end
16
+ def patch?; request_method == 'PATCH'; end
17
+ def options?; request_method == 'OPTIONS'; end
18
+
19
+ def resumable_content_type?
20
+ content_type == RESUMABLE_CONTENT_TYPE
21
+ end
22
+
23
+ def unknown?
24
+ !collection? && !resource?
25
+ end
26
+
27
+ def collection?
28
+ path_info.chomp('/') == base_path.chomp('/')
29
+ end
30
+
31
+ def resource?
32
+ !!(resource_uid =~ RESOURCE_UID_REGEX)
33
+ end
34
+
35
+ def resource_uid
36
+ rpath = path_info.dup
37
+ rpath.slice!(base_path)
38
+ rpath
39
+ end
40
+
41
+ def resource_url(uid)
42
+ "#{scheme}://#{host_with_port}#{base_path}#{uid}"
43
+ end
44
+
45
+ def final_length
46
+ fetch_positive_header('HTTP_FINAL_LENGTH')
47
+ end
48
+
49
+ def offset
50
+ fetch_positive_header('HTTP_OFFSET')
51
+ end
52
+
53
+ def base_path
54
+ @env['api.options'][:base_path]
55
+ end
56
+
57
+ def scheme
58
+ @env['HTTPS'] ? 'https' : 'http'
59
+ end
60
+
61
+ def path_info
62
+ @env['PATH_INFO']
63
+ end
64
+
65
+ def host_with_port
66
+ @env['HTTP_HOST'] || "#{@env['SERVER_NAME']}:#{@env['SERVER_PORT']}"
67
+ end
68
+
69
+ def request_method
70
+ @env['REQUEST_METHOD']
71
+ end
72
+
73
+ def content_type
74
+ @env['CONTENT_TYPE']
75
+ end
76
+
77
+ def content_length
78
+ @env['CONTENT_LENGTH'].to_i
79
+ end
80
+
81
+ protected
82
+ def fetch_positive_header(header_name)
83
+ header_val = @env[header_name] || ''
84
+ value = header_val.to_i
85
+ header_orig = normalize_header_name(header_name)
86
+
87
+ if header_val.empty?
88
+ error!(STATUS_BAD_REQUEST, "#{header_orig} header must not be empty")
89
+ end
90
+
91
+ if value < 0
92
+ error!(STATUS_BAD_REQUEST, "#{header_orig} header must be > 0")
93
+ end
94
+
95
+ value
96
+ end
97
+
98
+ def normalize_header_name(header_name)
99
+ header_name.gsub('HTTP_', '').split('_').map(&:capitalize).join('-')
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,117 @@
1
+ require 'json'
2
+ require 'rubytus/info'
3
+ require 'rubytus/error'
4
+ require 'pathname'
5
+
6
+ module Rubytus
7
+ module StorageHelper
8
+ def validates_data_dir(data_dir)
9
+ if Pathname.new(data_dir).relative?
10
+ data_dir = File.join(ENV['PWD'], data_dir)
11
+ end
12
+
13
+ begin
14
+ unless File.directory?(data_dir)
15
+ Dir.mkdir(data_dir)
16
+ end
17
+ rescue SystemCallError => _
18
+ raise PermissionError, "Couldn't create `data_dir` in #{data_dir}"
19
+ end
20
+
21
+ unless File.world_writable?(data_dir)
22
+ begin
23
+ File.chmod(0777, data_dir)
24
+ rescue Errno::EPERM
25
+ raise PermissionError, "Couldn't make `data_dir` in #{data_dir} writable"
26
+ end
27
+ end
28
+
29
+ data_dir
30
+ end
31
+
32
+ def file_path(uid)
33
+ File.join(@options[:data_dir], "#{uid}.bin")
34
+ end
35
+
36
+ def info_path(uid)
37
+ File.join(@options[:data_dir], "#{uid}.info")
38
+ end
39
+ end
40
+
41
+ class Storage
42
+ include StorageHelper
43
+
44
+ def initialize(options)
45
+ @options = options
46
+ end
47
+
48
+ def create_file(uid, final_length)
49
+ fpath = file_path(uid)
50
+ ipath = info_path(uid)
51
+ info = Rubytus::Info.new
52
+ info.final_length = final_length
53
+
54
+ begin
55
+ File.open(fpath, 'w') {}
56
+ File.open(ipath, 'w') do |f|
57
+ f.write(info.to_json)
58
+ end
59
+ rescue SystemCallError => e
60
+ raise(PermissionError, e.message) if e.class.name.start_with?('Errno::')
61
+ end
62
+ end
63
+
64
+ def read_file(uid)
65
+ fpath = file_path(uid)
66
+
67
+ begin
68
+ f = File.open(fpath, 'rb')
69
+ f.read
70
+ rescue SystemCallError => e
71
+ raise(PermissionError, e.message) if e.class.name.start_with?('Errno::')
72
+ ensure
73
+ f.close unless f.nil?
74
+ end
75
+ end
76
+
77
+ def patch_file(uid, data, offset = nil)
78
+ fpath = file_path(uid)
79
+ begin
80
+ f = File.open(fpath, 'r+b')
81
+ f.sync = true
82
+ f.seek(offset) unless offset.nil?
83
+ f.write(data)
84
+ size = f.size
85
+ f.close
86
+ update_info(uid, size)
87
+ rescue SystemCallError => e
88
+ raise(PermissionError, e.message) if e.class.name.start_with?('Errno::')
89
+ end
90
+ end
91
+
92
+ def read_info(uid)
93
+ ipath = info_path(uid)
94
+
95
+ begin
96
+ data = File.open(ipath, 'r') { |f| f.read }
97
+ JSON.parse(data, :object_class => Rubytus::Info)
98
+ rescue SystemCallError => e
99
+ raise(PermissionError, e.message) if e.class.name.start_with?('Errno::')
100
+ end
101
+ end
102
+
103
+ def update_info(uid, offset)
104
+ ipath = info_path(uid)
105
+ info = read_info(uid)
106
+ info.offset = offset
107
+
108
+ begin
109
+ File.open(ipath, 'w') do |f|
110
+ f.write(info.to_json)
111
+ end
112
+ rescue SystemCallError => e
113
+ raise(PermissionError, e.message) if e.class.name.start_with?('Errno::')
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ module Rubytus
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rubytus/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rubytus"
8
+ spec.version = Rubytus::VERSION
9
+ spec.authors = ["Alif Rachmawadi"]
10
+ spec.email = ["subosito@gmail.com"]
11
+ spec.description = %q{Resumable upload protocol implementation in Ruby}
12
+ spec.summary = %q{Resumable upload protocol implementation in Ruby}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "minitest"
24
+ spec.add_development_dependency "rr"
25
+ spec.add_development_dependency "simplecov"
26
+ spec.add_development_dependency "em-http-request"
27
+ spec.add_development_dependency "pry"
28
+ spec.add_development_dependency "coveralls"
29
+
30
+ spec.add_runtime_dependency "goliath", "~> 1.0.3"
31
+ end
@@ -0,0 +1,249 @@
1
+ require 'test_helper'
2
+ require 'rubytus/command'
3
+ require 'rubytus/info'
4
+
5
+ class TestRubytusCommand < MiniTest::Test
6
+ include Rubytus::Mock
7
+ include Rubytus::StorageHelper
8
+ include Goliath::TestHelper
9
+
10
+ def setup
11
+ @err = Proc.new { assert false, 'API request failed' }
12
+ end
13
+
14
+ def test_get_request_for_root
15
+ params = { :path => '/' }
16
+
17
+ with_api(Rubytus::Command, default_options) do
18
+ get_request(params, @err) do |c|
19
+ assert_equal 404, c.response_header.status
20
+ end
21
+ end
22
+ end
23
+
24
+ def test_options_request_for_collection
25
+ params = { :path => '/uploads/' }
26
+
27
+ with_api(Rubytus::Command, default_options) do
28
+ options_request(params, @err) do |c|
29
+ assert_equal 200, c.response_header.status
30
+ assert_equal '', c.response
31
+ end
32
+ end
33
+ end
34
+
35
+ def test_get_request_for_collection
36
+ params = { :path => '/uploads/' }
37
+
38
+ with_api(Rubytus::Command, default_options) do
39
+ get_request(params, @err) do |c|
40
+ assert_equal 405, c.response_header.status
41
+ assert_equal 'POST', c.response_header['ALLOW']
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_post_request_for_collection_without_final_length
47
+ params = { :path => '/uploads/' }
48
+
49
+ with_api(Rubytus::Command, default_options) do
50
+ post_request(params, @err) do |c|
51
+ assert_equal 400, c.response_header.status
52
+ end
53
+ end
54
+ end
55
+
56
+ def test_post_request_for_collection_with_negative_final_length
57
+ params = {
58
+ :path => '/uploads/',
59
+ :head => { 'Final-Length' => '-1'}
60
+ }
61
+
62
+ with_api(Rubytus::Command, default_options) do
63
+ post_request(params, @err) do |c|
64
+ assert_equal 400, c.response_header.status
65
+ end
66
+ end
67
+ end
68
+
69
+ def test_post_request_for_collection
70
+ params = {
71
+ :path => '/uploads/',
72
+ :head => { 'Final-Length' => '10' }
73
+ }
74
+
75
+ with_api(Rubytus::Command, default_options) do
76
+ post_request(params, @err) do |c|
77
+ assert_equal 201, c.response_header.status
78
+ assert c.response_header.location
79
+ end
80
+ end
81
+ end
82
+
83
+ def test_put_request_for_resource
84
+ with_api(Rubytus::Command, default_options) do
85
+ put_request({ :path => "/uploads/#{uid}" }, @err) do |c|
86
+ assert_equal 405, c.response_header.status
87
+ assert_equal 'HEAD,PATCH', c.response_header['ALLOW']
88
+ end
89
+ end
90
+ end
91
+
92
+ def test_patch_request_for_resource_without_valid_content_type
93
+ params = {
94
+ :path => "/uploads/#{uid}",
95
+ :body => 'abc',
96
+ :head => {
97
+ 'Offset' => '0',
98
+ 'Content-Type' => 'plain/text'
99
+ }
100
+ }
101
+
102
+ with_api(Rubytus::Command, default_options) do
103
+ patch_request(params, @err) do |c|
104
+ assert_equal 400, c.response_header.status
105
+ end
106
+ end
107
+ end
108
+
109
+ def test_patch_request_for_resource
110
+ options = default_options
111
+ ruid = uid
112
+
113
+ validates_data_dir(options[:data_dir])
114
+
115
+ storage = Rubytus::Storage.new(options)
116
+ storage.create_file(ruid, 3)
117
+
118
+ params = {
119
+ :path => "/uploads/#{ruid}",
120
+ :body => 'abc',
121
+ :head => {
122
+ 'Offset' => '0',
123
+ 'Content-Type' => 'application/offset+octet-stream'
124
+ }
125
+ }
126
+
127
+ with_api(Rubytus::Command, options) do
128
+ patch_request(params, @err) do |c|
129
+ assert_equal 200, c.response_header.status
130
+ end
131
+ end
132
+ end
133
+
134
+ def test_patch_request_for_resource_exceed_offset
135
+ ruid = uid
136
+ info = Rubytus::Info.new(:offset => 0)
137
+
138
+ any_instance_of(Rubytus::Storage) do |klass|
139
+ stub(klass).read_info(ruid) { info }
140
+ end
141
+
142
+ params = {
143
+ :path => "/uploads/#{ruid}",
144
+ :body => 'abc',
145
+ :head => {
146
+ 'Offset' => '3',
147
+ 'Content-Type' => 'application/offset+octet-stream'
148
+ }
149
+ }
150
+
151
+ with_api(Rubytus::Command, default_options) do
152
+ patch_request(params, @err) do |c|
153
+ assert_equal 403, c.response_header.status
154
+ end
155
+ end
156
+ end
157
+
158
+ def test_patch_request_for_resource_exceed_remaining_length
159
+ ruid = uid
160
+ info = Rubytus::Info.new(:offset => 0, :final_length => 2)
161
+
162
+ any_instance_of(Rubytus::Storage) do |klass|
163
+ stub(klass).read_info(ruid) { info }
164
+ end
165
+
166
+ params = {
167
+ :path => "/uploads/#{ruid}",
168
+ :body => 'abcdef',
169
+ :head => {
170
+ 'Offset' => '0',
171
+ 'Content-Type' => 'application/offset+octet-stream'
172
+ }
173
+ }
174
+
175
+ with_api(Rubytus::Command, default_options) do
176
+ patch_request(params, @err) do |c|
177
+ assert_equal 403, c.response_header.status
178
+ end
179
+ end
180
+ end
181
+
182
+ def test_patch_request_for_resource_failure
183
+ options = read_only_options
184
+ params = {
185
+ :path => "/uploads/#{uid}",
186
+ :body => 'abc',
187
+ :head => {
188
+ 'Offset' => '0',
189
+ 'Content-Type' => 'application/offset+octet-stream'
190
+ }
191
+ }
192
+
193
+ any_instance_of(Rubytus::Command) do |klass|
194
+ stub(klass).setup { true }
195
+ stub(klass).storage { Rubytus::Storage.new(options) }
196
+ end
197
+
198
+ with_api(Rubytus::Command, options) do
199
+ patch_request(params, @err) do |c|
200
+ assert_equal 500, c.response_header.status
201
+ end
202
+ end
203
+ end
204
+
205
+ def test_head_request_for_resource
206
+ ruid = uid
207
+ info = Rubytus::Info.new(:offset => 3)
208
+
209
+ any_instance_of(Rubytus::Storage) do |klass|
210
+ stub(klass).read_info(ruid) { info }
211
+ end
212
+
213
+ with_api(Rubytus::Command, default_options) do
214
+ head_request({ :path => "/uploads/#{ruid}" }, @err) do |c|
215
+ assert_equal 200, c.response_header.status
216
+ assert_equal '3', c.response_header['OFFSET']
217
+ end
218
+ end
219
+ end
220
+
221
+ def test_get_request_for_resource_failure
222
+ ruid = uid
223
+
224
+ any_instance_of(Rubytus::Storage) do |klass|
225
+ stub(klass).read_file(ruid) { raise Rubytus::PermissionError }
226
+ end
227
+
228
+ with_api(Rubytus::Command, default_options) do
229
+ get_request({ :path => "/uploads/#{ruid}" }, @err) do |c|
230
+ assert_equal 500, c.response_header.status
231
+ end
232
+ end
233
+ end
234
+
235
+ def test_get_request_for_resource
236
+ ruid = uid
237
+
238
+ any_instance_of(Rubytus::Storage) do |klass|
239
+ stub(klass).read_file(ruid) { 'abc' }
240
+ end
241
+
242
+ with_api(Rubytus::Command, default_options) do
243
+ get_request({ :path => "/uploads/#{ruid}" }, @err) do |c|
244
+ assert_equal 200, c.response_header.status
245
+ assert_equal 'abc', c.response
246
+ end
247
+ end
248
+ end
249
+ end