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