fieldview 0.0.0 → 0.0.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: c291877633d7f4126668164ee7cc60c7c5c67bac
4
- data.tar.gz: df16497405345a46fab2c65066eec659ffc074b7
3
+ metadata.gz: 05858ad578d5fc9f1a42d4f15c8d9ee0b7299f01
4
+ data.tar.gz: be48bc245a2c2a38c6b850b51f735ca4d8db193a
5
5
  SHA512:
6
- metadata.gz: 883b9f63fcaf3af60958bc8407660a76608afca697fa043afae906240d6c7f2f36ed3116dea2de6c76630c3e4ba82effe08d6d11ab3e349c4cb03b962eae052d
7
- data.tar.gz: 8d027e67cdabf34fd66a7284196c34533d82b2f002df8826b2d91b8005ae4246fb8632225f5b67041dd518c465a4826244f5e5eabc9e7a2d6927acb91d63808d
6
+ metadata.gz: d1893ef1af5bdcc6a9ca2790e9806d58828d24a859ad944109b611a1af556688a45761290f0694821a5690c44c3b3e86d59a69135178eec56bcad0598890997c
7
+ data.tar.gz: cad12cb23d33a5569cdd3583429bab951ede0d58c5fcc4ca6ea80e27266fff1554717d49edc4b28c76a4c5dfbaf2c7fc72507c5585e380d18e832f3f43866341
data/Gemfile.lock CHANGED
@@ -2,7 +2,6 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  fieldview (0.0.0)
5
- faraday (~> 0.9)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
@@ -12,11 +11,8 @@ GEM
12
11
  byebug (9.0.6)
13
12
  crack (0.4.3)
14
13
  safe_yaml (~> 1.0.0)
15
- faraday (0.12.0.1)
16
- multipart-post (>= 1.2, < 3)
17
14
  hashdiff (0.3.2)
18
15
  minitest (5.8.4)
19
- multipart-post (2.0.0)
20
16
  public_suffix (2.0.5)
21
17
  rake (11.3.0)
22
18
  safe_yaml (1.0.4)
data/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  The FieldView Ruby library provides convenient access to the FieldView API from applications written in the Ruby language. It includes a pre-defined set of classes for API resources that are available currently from the API. You will need to get access from a Climate Corporation representative and the interface utilize OAUTH 2.0.
4
4
 
5
+ OAuth token refreshing is handled for your if for some reason the token expires in the middle of requests (this requires that you provide the `refresh_token` with the creation of the `AuthToken`).
6
+
7
+ ## Listable Objects
8
+
9
+ FieldView returns pages of objects that are tracked via the headers, you cannot go back in pages easily (perhaps we could be tracking the next-token?). Use `more_pages?` on the listable object to see if there are more pages to be acquired by using `next_page!`. The `next_token` on listable objects can also be preserved when reaching the end of a list to see if there are any additional changes since the `next_token` was saved.
10
+
11
+ The raw data can be acquired by using `.data` on a listable object.
12
+
13
+ When finished you will want to store information of the `AuthToken` somewhere as it may have changed with requests.
14
+
5
15
  ## Usage
6
16
 
7
17
  The library needs to be configured with your FieldView account's client secret,
@@ -25,9 +35,10 @@ auth_token = FieldView::AuthToken.new_auth_token_with_code_from_redirect_code(<C
25
35
  # Or initialize with previous information
26
36
  auth_token = FieldView::AuthToken.new(
27
37
  access_token: <ATOKEN>,
28
- expiration_at: <DATETIME>,
38
+ expiration_at: <DATETIME>, # not required, but should be known
29
39
  refresh_token: <RTOKEN>,
30
- refresh_token_expiration_at: <DATETIME>)
40
+ refresh_token_expiration_at: <DATETIME> # Not required, but should be known
41
+ )
31
42
 
32
43
  # Or with just auth_token (assuming it hasn't expired)
33
44
  auth_token = FieldView::AuthToken.new(access_token: <ATOKEN>)
@@ -35,6 +46,15 @@ auth_token = FieldView::AuthToken.new(access_token: <ATOKEN>)
35
46
  # refresh token and a new access/refresh token will be associated with the object
36
47
  auth_token = FieldView::AuthToken.new(refresh_token: <RTOKEN>)
37
48
 
49
+ # Then you can do something like this:
50
+
51
+ fields = FieldView::Fields.list(auth_token)
52
+
53
+ fields.each do |field|
54
+ puts field.boundary
55
+ end
56
+
57
+ fields.has_more?
38
58
 
39
59
  ```
40
60
 
@@ -52,6 +72,13 @@ Run a single test:
52
72
 
53
73
  bundle exec ruby -Ilib/ test/field_view_test.rb -n /client.id/
54
74
 
75
+ ## TODOs
76
+
77
+ - [ ] Thread safety of the auth token object since it's passed to all created objects
78
+ - [ ] Probably add configurable behavior for auto-refresh
79
+ - [ ] Change the configuration to non-static variables (at least the items that are required)
80
+ - [ ] Use faraday so that we are middleware agnostic
81
+
55
82
  ## Disclaimer
56
83
 
57
84
  This Gem is in no way associated with The Climate Corporation, and they are in no way associated with it's support, maintenance, or updates.
data/fieldview.gemspec CHANGED
@@ -1,18 +1,18 @@
1
1
  $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
2
 
3
+ require 'fieldview/version'
4
+
3
5
  spec = Gem::Specification.new do |s|
4
6
  s.name = 'fieldview'
5
- s.version = "0.0.0"
7
+ s.version = FieldView::VERSION
6
8
  s.required_ruby_version = '>= 1.9.3'
7
9
  s.summary = 'Ruby bindings for the FieldView API'
8
- s.description = ' FieldView is used make data-driven decisions to maximize your return on every acre.'
10
+ s.description = ' FieldView is used to make data-driven decisions to maximize your return on every acre. This Ruby Gem is provided as a convenient way to access their API.'
9
11
  s.author = 'Paul Susmarski'
10
12
  s.email = 'paul@susmarski.com'
11
13
  s.homepage = 'http://rubygems.org/gems/fielview'
12
14
  s.license = 'MIT'
13
15
 
14
- s.add_dependency('faraday', '~> 0.9')
15
-
16
16
  s.files = Dir['lib/**/*.rb']
17
17
  s.files = `git ls-files`.split("\n")
18
18
  s.test_files = `git ls-files -- test/*`.split("\n")
@@ -12,14 +12,13 @@ module FieldView
12
12
  # When the access token expires we'll need to refresh,
13
13
  # can be specified as part of the object, 14399 is what is being
14
14
  # returned at the time of writing this (2017-04-11)
15
- self.access_token_expiration_at = params[:access_token_expiration_at] || (now + (params[:expires_in]||14399) - 10)
15
+ self.access_token_expiration_at = params[:access_token_expiration_at] || (now + (params[:expires_in]||14399))
16
16
 
17
17
  # Refresh token isn't required, but can be initialized with this
18
18
  self.refresh_token = params[:refresh_token]
19
19
 
20
- # Refresh token technically expires in 30 days, but
21
- # we'll subtract one day for safety
22
- self.refresh_token_expiration_at = params[:access_token_expiration_at] || (now + (30-1)*24*60*60)
20
+ # Refresh token technically expires in 30 days
21
+ self.refresh_token_expiration_at = params[:access_token_expiration_at] || (now + (30)*24*60*60)
23
22
  end
24
23
 
25
24
  def access_token_expired?
@@ -61,15 +60,27 @@ module FieldView
61
60
  http = Net::HTTP.new(uri.host, uri.port)
62
61
  http.use_ssl = FieldView.api_base.start_with?("https")
63
62
  request = Net::HTTP.class_eval(method.to_s.capitalize).new(uri.request_uri)
63
+ if method == :get then
64
+ new_query_ar = URI.decode_www_form(uri.query || '')
65
+ params.each do |key,value|
66
+ new_query_ar << [key.to_s, value.to_s]
67
+ end
68
+ uri.query = URI.encode_www_form(new_query_ar)
69
+ request = Net::HTTP.class_eval(method.to_s.capitalize).new(uri.request_uri)
70
+ else
71
+ if params.is_a?(Hash)
72
+ request.body = params.to_json()
73
+ else
74
+ request.body = params
75
+ end
76
+ end
64
77
  request["Accept"] = "*/*"
65
78
  request["Authorization"] = "Bearer #{self.access_token}"
66
79
  request["X-Api-Key"] = FieldView.x_api_key
67
80
  request["Content-Type"] = "application/json"
68
-
69
81
  headers.each do |header,value|
70
82
  request[header] = value.to_s
71
83
  end
72
-
73
84
  response = http.request(request)
74
85
  FieldView.handle_response_error_codes(response)
75
86
  return FieldViewResponse.new(response)
@@ -92,10 +103,10 @@ module FieldView
92
103
  end
93
104
 
94
105
  # Code will be collected from the redirect to your server
95
- def self.new_auth_token_with_code_from_redirect_code(code)
106
+ def self.new_auth_token_with_code_from_redirect_code(code, redirect_uri: nil)
96
107
  http, request = build_token_request([
97
108
  ["grant_type", "authorization_code"],
98
- ["redirect_uri", FieldView.redirect_uri],
109
+ ["redirect_uri", FieldView.redirect_uri || redirect_uri],
99
110
  ["code", code]
100
111
  ])
101
112
  response = http.request(request)
@@ -107,5 +118,12 @@ module FieldView
107
118
  return AuthToken.new(json)
108
119
  end
109
120
  end
121
+
122
+ def to_s()
123
+ [
124
+ "AccessToken: #{self.access_token}, Expiration: #{self.access_token_expiration_at}",
125
+ "RefreshToken: #{self.refresh_token}, Expiration: #{self.refresh_token_expiration_at}"
126
+ ].join('\n')
127
+ end
110
128
  end
111
129
  end
@@ -14,8 +14,14 @@ module FieldView
14
14
  attr_accessor :response
15
15
  # Initializes a FieldViewError.
16
16
  def initialize(message=nil, http_status: nil, http_body: nil,
17
- http_headers: nil)
17
+ http_headers: nil, fieldview_response: nil)
18
18
  @message = message
19
+ if fieldview_response then
20
+ self.response = fieldview_response
21
+ http_status = fieldview_response.http_status
22
+ http_body = fieldview_response.http_body
23
+ http_headers = fieldview_response.http_headers
24
+ end
19
25
  @http_status = http_status
20
26
  @http_body = http_body
21
27
  @http_headers = http_headers || {}
@@ -61,4 +67,10 @@ module FieldView
61
67
  # Raised when the server is busy
62
68
  class ServerBusyError < FieldViewError
63
69
  end
70
+
71
+ # Raised when a response code that is non-breaking is outside
72
+ # API specifications, ex: We expect a 201 when creating an upload, but it
73
+ # returns a 200
74
+ class UnexpectedResponseError < FieldViewError
75
+ end
64
76
  end
@@ -1,14 +1,13 @@
1
1
  module FieldView
2
- class Field
2
+ class Field < Requestable
3
3
  attr_accessor :id
4
4
  attr_accessor :name
5
5
  attr_accessor :boundary_id
6
- attr_accessor :auth_token
7
6
  def initialize(json_object, auth_token = nil)
8
7
  self.id = json_object[:id]
9
8
  self.name = json_object[:name]
10
9
  self.boundary_id = json_object[:boundaryId]
11
- self.auth_token = auth_token
10
+ super(auth_token)
12
11
  end
13
12
 
14
13
  def boundary
@@ -12,9 +12,13 @@ module FieldView
12
12
  end
13
13
 
14
14
  self.http_body = response.body
15
- begin
16
- self.data = JSON.parse(response.body, symbolize_names: true)
17
- rescue JSON::ParserError
15
+ if response.body then
16
+ begin
17
+ self.data = JSON.parse(response.body, symbolize_names: true)
18
+ rescue JSON::ParserError
19
+ self.data = nil
20
+ end
21
+ else
18
22
  self.data = nil
19
23
  end
20
24
  self.http_status = response.code.to_i
@@ -1,16 +1,16 @@
1
1
  module FieldView
2
- class ListObject
2
+ class ListObject < Requestable
3
3
  attr_accessor :limit
4
- attr_reader :auth_token, :data, :last_http_status, :next_token, :listable
4
+ attr_reader :data, :last_http_status, :next_token, :listable
5
5
  include Enumerable
6
6
 
7
7
  def initialize(listable, auth_token, data, http_status, next_token: nil, limit: 100)
8
8
  @listable = listable
9
- @auth_token = auth_token
10
9
  @data = data
11
10
  @last_http_status = http_status
12
11
  @next_token = next_token
13
12
  @limit = 100
13
+ super(auth_token)
14
14
  end
15
15
 
16
16
  def each(&blk)
@@ -0,0 +1,9 @@
1
+ module FieldView
2
+ class Requestable
3
+ attr_accessor :auth_token
4
+
5
+ def initialize(auth_token = nil)
6
+ self.auth_token = auth_token
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,62 @@
1
+ module FieldView
2
+ class Upload < Requestable
3
+ # 5 Megabytes is the current chunk size
4
+ REQUIRED_CHUNK_SIZE = 5*1024*1024
5
+ CHUNK_CONTENT_TYPE = "application/octet-stream"
6
+
7
+ PATH = "uploads"
8
+ attr_accessor :id, :content_length, :content_type
9
+
10
+ # Creates an upload with the specified items, all required.
11
+ # will return an a new upload object that has it's ID which can be
12
+ # used to upload. The following are known content types:
13
+ # image/vnd.climate.thermal.geotiff, image/vnd.climate.ndvi.geotiff,
14
+ # image/vnd.climate.chlorophyll.geotiff, image/vnd.climate.cci.geotiff,
15
+ # image/vnd.climate.waterstress.geotiff, image/vnd.climate.infrared.geotiff
16
+ def self.create(auth_token, md5, content_length, content_type)
17
+ response = auth_token.execute_request!(:post, PATH,
18
+ params: {
19
+ "md5" => md5,
20
+ "length" => content_length,
21
+ "content-type" => content_type
22
+ })
23
+
24
+ Util.verify_response_with_code("Upload creation", response, 201)
25
+
26
+ return new(response.http_body.gsub('"', ''), content_length, auth_token)
27
+ end
28
+
29
+ def initialize(id, content_length, auth_token = nil)
30
+ self.id = id
31
+ self.content_length = content_length.to_i
32
+ if self.content_length <= 0 then
33
+ raise ArgumentError.new("You must set content_length to a non-zero value")
34
+ end
35
+ super(auth_token)
36
+ end
37
+
38
+ # Upload a chunk of a file, specify the range of the bytes, 0 based, for the bytes
39
+ # you're passing, for example, the first 256 bytes would be start_bytes = 0 and
40
+ # end_bytes = 255, with bytes = 256 byte size object
41
+ def upload_chunk(start_bytes, end_bytes, bytes)
42
+ if (end_bytes.to_i - start_bytes.to_i + 1) != bytes.bytesize ||
43
+ bytes.bytesize > REQUIRED_CHUNK_SIZE then
44
+ raise ArgumentError.new("End bytes (#{end_bytes}) - Start bytes (#{start_bytes})" \
45
+ " must be equal to bytes (#{bytes}) and no greater than #{REQUIRED_CHUNK_SIZE}")
46
+ end
47
+ response = auth_token.execute_request!(:put, "#{PATH}/#{self.id}",
48
+ headers: {
49
+ "Content-Range" => "bytes #{start_bytes}-#{end_bytes}/#{self.content_length}",
50
+ "Content-Length" => bytes.bytesize,
51
+ "Content-Type" => CHUNK_CONTENT_TYPE,
52
+ "Transfer-Encoding" => "chunked"
53
+ },
54
+ params: bytes)
55
+
56
+ # We expect a 204
57
+ Util.verify_response_with_code("Chunk upload", response, 204)
58
+
59
+ return response
60
+ end
61
+ end
62
+ end
@@ -3,5 +3,12 @@ module FieldView
3
3
  def self.http_status_is_more_in_list?(http_status)
4
4
  return http_status.to_i == 206
5
5
  end
6
+
7
+ def self.verify_response_with_code(message, fieldview_response, *acceptable_response_codes)
8
+ if not acceptable_response_codes.include?(fieldview_response.http_status) then
9
+ raise UnexpectedResponseError.new("#{message} expects #{acceptable_response_codes}",
10
+ fieldview_response: fieldview_response)
11
+ end
12
+ end
6
13
  end
7
14
  end
@@ -0,0 +1,3 @@
1
+ module FieldView
2
+ VERSION = '0.0.1'
3
+ end
data/lib/fieldview.rb CHANGED
@@ -3,13 +3,18 @@ require 'json'
3
3
  require 'rbconfig'
4
4
  require 'base64'
5
5
 
6
+ # Version
7
+ require 'fieldview/version'
8
+
6
9
  # API Support Classes
7
10
  require 'fieldview/errors'
11
+ require 'fieldview/requestable'
8
12
  require 'fieldview/auth_token'
9
13
  require 'fieldview/fields'
10
14
  require 'fieldview/util'
11
15
  require 'fieldview/fieldview_response'
12
16
  require 'fieldview/field'
17
+ require 'fieldview/upload'
13
18
  require 'fieldview/list_object'
14
19
  require 'fieldview/boundary'
15
20
 
@@ -9,8 +9,8 @@ class TestAuthToken < Minitest::Test
9
9
  def check_token_matches_fixture(token)
10
10
  assert_equal FIXTURE[:access_token], token.access_token
11
11
  assert_equal FIXTURE[:refresh_token], token.refresh_token
12
- assert_equal FieldView.now + FIXTURE[:expires_in] - 15, token.access_token_expiration_at
13
- assert_equal FieldView.now - 5 + 29*24*60*60, token.refresh_token_expiration_at, "Should be 29 days in the future"
12
+ assert_equal FieldView.now + FIXTURE[:expires_in] - 5, token.access_token_expiration_at
13
+ assert_equal FieldView.now + 30*24*60*60 - 5, token.refresh_token_expiration_at, "Should be 30 days in the future"
14
14
  end
15
15
 
16
16
  def setup
data/test/test_helper.rb CHANGED
@@ -13,6 +13,9 @@ class Minitest::Test
13
13
  #
14
14
  API_FIXTURES = APIFixtures.new
15
15
 
16
+ def teardown
17
+ WebMock.reset!
18
+ end
16
19
 
17
20
  def next_token_headers(next_token = "AZXJKLA123")
18
21
  return {FieldView::NEXT_TOKEN_HEADER_KEY => next_token}
@@ -0,0 +1,89 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestUpload < Minitest::Test
4
+
5
+ def setup
6
+ setup_for_api_requests
7
+ end
8
+
9
+ def teardown
10
+ teardown_for_api_request
11
+ end
12
+
13
+ def test_expects_certain_status_code
14
+ stub_request(:post, /uploads/).
15
+ to_return(status: 200)
16
+
17
+ assert_raises(FieldView::UnexpectedResponseError) do
18
+ FieldView::Upload.create(new_auth_token, "dontcare", 5, "dontcare")
19
+ end
20
+ end
21
+
22
+ def test_create()
23
+ md5 = "eb782356d6011dab2b112bdcb098ec7d"
24
+ uuid = "1a2623b3-d2a7-4e52-ba21-a3eb521a3208"
25
+ content_length = 275
26
+ content_type = "image/vnd.climate.thermal.geotiff"
27
+ stub_request(:post, /uploads/).
28
+ with(body: {
29
+ "md5" => md5,
30
+ "length" => content_length,
31
+ "content-type" => content_type
32
+ }).
33
+ to_return(status: 201, body: %Q!"#{uuid}"!)
34
+
35
+ auth_token = new_auth_token
36
+
37
+ upload = FieldView::Upload.create(auth_token, md5, content_length, content_type)
38
+
39
+ assert_equal uuid, upload.id
40
+ assert_equal content_length, upload.content_length
41
+ assert_equal auth_token, upload.auth_token
42
+ end
43
+
44
+ def test_checks_content_length
45
+ assert_raises(ArgumentError) do
46
+ # nil is going to result in 0 when converting to integer
47
+ FieldView::Upload.new(1,nil,new_auth_token)
48
+ end
49
+ end
50
+
51
+ def test_uploading_wrong_chunk_size
52
+ upload = FieldView::Upload.new("dontcare",
53
+ 50000000, new_auth_token)
54
+
55
+ bytes = "12345678"
56
+ start_bytes = 0
57
+ end_bytes = 5
58
+ assert_raises(ArgumentError, "Wrong byte start/end") do
59
+ upload.upload_chunk(start_bytes, end_bytes, bytes)
60
+ end
61
+
62
+ bytes = "A" * (FieldView::Upload::REQUIRED_CHUNK_SIZE + 1)
63
+ end_bytes = bytes.bytesize - 1
64
+ assert_raises(ArgumentError, "Larger than the accepted chunk size") do
65
+ upload.upload_chunk(start_bytes, end_bytes, bytes)
66
+ end
67
+ end
68
+
69
+ def test_can_upload_chunk
70
+ uuid = "1a2623b3-d2a7-4e52-ba21-a3eb521a3208"
71
+ total_content_length = FieldView::Upload::REQUIRED_CHUNK_SIZE * 2
72
+ upload = FieldView::Upload.new(uuid,
73
+ total_content_length, new_auth_token)
74
+
75
+ chunk = "A"
76
+ start_bytes = 0
77
+ end_bytes = chunk.bytesize - 1
78
+
79
+ stub_request(:put, /uploads\/#{uuid}/).
80
+ with(headers: {
81
+ content_range: "bytes #{start_bytes}-#{end_bytes}/#{total_content_length}",
82
+ content_length: chunk.bytesize.to_s,
83
+ content_type: "application/octet-stream",
84
+ transfer_encoding: "chunked"
85
+ }).
86
+ to_return(status: 204, body: nil)
87
+ assert upload.upload_chunk(start_bytes, end_bytes, chunk)
88
+ end
89
+ end
metadata CHANGED
@@ -1,31 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fieldview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Susmarski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-15 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: faraday
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ~>
18
- - !ruby/object:Gem::Version
19
- version: '0.9'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ~>
25
- - !ruby/object:Gem::Version
26
- version: '0.9'
27
- description: ' FieldView is used make data-driven decisions to maximize your return
28
- on every acre.'
11
+ date: 2017-04-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: ' FieldView is used to make data-driven decisions to maximize your return
14
+ on every acre. This Ruby Gem is provided as a convenient way to access their API.'
29
15
  email: paul@susmarski.com
30
16
  executables: []
31
17
  extensions: []
@@ -46,7 +32,10 @@ files:
46
32
  - lib/fieldview/fields.rb
47
33
  - lib/fieldview/fieldview_response.rb
48
34
  - lib/fieldview/list_object.rb
35
+ - lib/fieldview/requestable.rb
36
+ - lib/fieldview/upload.rb
49
37
  - lib/fieldview/util.rb
38
+ - lib/fieldview/version.rb
50
39
  - spec/fixtures.json
51
40
  - test/api_fixtures.rb
52
41
  - test/test_auth_token.rb
@@ -56,6 +45,7 @@ files:
56
45
  - test/test_fieldview.rb
57
46
  - test/test_helper.rb
58
47
  - test/test_list_object.rb
48
+ - test/test_upload.rb
59
49
  homepage: http://rubygems.org/gems/fielview
60
50
  licenses:
61
51
  - MIT
@@ -89,3 +79,4 @@ test_files:
89
79
  - test/test_fieldview.rb
90
80
  - test/test_helper.rb
91
81
  - test/test_list_object.rb
82
+ - test/test_upload.rb