duracloud-client 0.0.1 → 0.0.2

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: c6733dd180dfccf317acf1938b3f95265ee321a0
4
- data.tar.gz: a61cb47c8d1411efc8d031c1dea604d651b79a49
3
+ metadata.gz: b304d53dd688ed00817d59d286e32a5d53f88acc
4
+ data.tar.gz: 065f2f3e9f0307f83e863f8ba7b1a60b680414e5
5
5
  SHA512:
6
- metadata.gz: fb3b785bbbcc959e96fa2b13a550688375e1f1dff06cf44d10bc8e4f6b1aa5b9f23c41066402e2b67bb16f804cee2b20e6b427f32d0d1b6bf6b2016af3a10f2a
7
- data.tar.gz: 628e6c8c041862732675419e04bb0c404b5014097ba819c23f59dec17f3d208412e33e734edb657774d9853557e89aa4e59c7455e9fd11610d73860305e72836
6
+ metadata.gz: d73d05c4ef0bbd91a4928b79c2c260fdec0b755ece9a43423eac2229a56874c65e24e0e7cf8a7830dcabd02a293a0945a20a585c8bb2790493dae9a5d2be9220
7
+ data.tar.gz: 6f2edaffb35c789e94e264c4f97607129e403d6673f52dcf6a28af28719e50b82c4fe0b703555923aa417144a38962300c7e8fc11ef293cc18d8f50cbc24b13c
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1
4
+ - 2.2
5
+
data/README.md CHANGED
@@ -43,32 +43,71 @@ end
43
43
  => #<Duracloud::Client:0x007fe953a1c630 @config=#<Duracloud::Configuration host="foo.duracloud.org", port=nil, user="bob@example.com", password="******">>
44
44
  ```
45
45
 
46
- ### Create a new content item and store it in DuraCloud
46
+ #### Logging
47
47
 
48
- If a relative URL is given (`:url` keyword option, or a combination of `:space_id` and `:content_id` options), the fully-qualified URL is built in the standard way from the base URL `https://{host}:{port}/durastore/`.
48
+ By default, `Duracloud::Client` logs to `STDERR`. Use the `logger` config setting to change:
49
49
 
50
+ ```ruby
51
+ Duracloud::Client.configure do |config|
52
+ config.logger = Rails.logger
53
+ end
50
54
  ```
51
- > new_content = Duracloud::Content.new(space_id: "rest-api-testing", content_id: "ark:/99999/fk4zzzz")
52
- => #<Duracloud::Content url="rest-api-testing/ark:/99999/fk4zzzz">
53
- > new_content.body = "test"
55
+
56
+ ### List Storage Providers
57
+
58
+ ```
59
+ > stores = Duracloud::Store.all
60
+ => [#<Duracloud::Store:0x007faa592e9068 @owner_id="0", @primary="0", @id="1", @provider_type="AMAZON_GLACIER">, #<Duracloud::Store:0x007faa592dbd78 @owner_id="0", @primary="1", @id="2", @provider_type="AMAZON_S3">]
61
+
62
+ > stores.first.primary?
63
+ => false
64
+ ```
65
+
66
+ ### Space Methods
67
+
68
+ TODO
69
+
70
+ ### Content Methods
71
+
72
+ #### Create a new content item and store it in DuraCloud
73
+
74
+ 1. Initialize instance of `Duracloud::Content` and save:
75
+
76
+ ```
77
+ >> new_content = Duracloud::Content.new(space_id: "rest-api-testing", id: "ark:/99999/fk4zzzz")
78
+ => #<Duracloud::Content space_id="rest-api-testing", id="ark:/99999/fk4zzzz">
79
+
80
+ >> new_content.body = "test"
54
81
  => "test"
55
- > new_content.content_type = "text/plain"
82
+
83
+ >> new_content.content_type = "text/plain"
56
84
  => "text/plain"
57
- > new_content.store
58
- => #<Duracloud::Content url="rest-api-testing/ark:/99999/fk4zzzz">
85
+
86
+ >> new_content.save
87
+ => #<Duracloud::Content space_id="rest-api-testing", id="ark:/99999/fk4zzzz">
88
+ ```
89
+
90
+ 2. Create with class method `Duracloud::Content.create`:
91
+
92
+ ```
93
+ >> Duracloud::Content.create(space_id: "rest-api-testing", id="ark:/99999/fk4zzzz") do |c|
94
+ c.body = "test"
95
+ c.content_type = "text/plain"
96
+ end
97
+ => #<Duracloud::Content space_id="rest-api-testing", id="ark:/99999/fk4zzzz">
59
98
  ```
60
99
 
61
- ### Retrieve an existing content item from DuraCloud
100
+ #### Retrieve an existing content item from DuraCloud
62
101
 
63
102
  ```ruby
64
- Duracloud::Content.find(**options) # :url, or :space_id and :content_id
103
+ Duracloud::Content.find(id: "contentID", space_id: "spaceID")
65
104
  ```
66
105
 
67
- ### Update the properties for an item
106
+ #### Update the properties for an item
68
107
 
69
108
  TODO
70
109
 
71
- ### Delete a content item
110
+ #### Delete a content item
72
111
 
73
112
  TODO
74
113
 
@@ -76,10 +115,14 @@ TODO
76
115
 
77
116
  We endeavor to follow semantic versioning. In particular, versions < 1.0 may introduce backward-incompatible changes without notice. Use at your own risk. Version 1.0 signals a stable API.
78
117
 
79
- ## Maintainers
118
+ ## Contributing
80
119
 
81
- * David Chandek-Stark (Duke University)
120
+ 1. Fork it ( https://github.com/[my-github-username]/duracloud-ruby-client/fork )
121
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
122
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
123
+ 4. Push to the branch (`git push origin my-new-feature`)
124
+ 5. Create a new Pull Request
82
125
 
83
- ## Contributing
126
+ ## Maintainers
84
127
 
85
- TODO
128
+ * [David Chandek-Stark](https://github.com/dchandekstark) (Duke University)
data/Rakefile CHANGED
@@ -1,2 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task default: :spec
7
+ rescue LoadError
8
+ end
data/duracloud.gemspec CHANGED
@@ -18,9 +18,15 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.required_ruby_version = ">= 2.1"
22
+
21
23
  spec.add_dependency "hashie", "~> 3.4"
22
24
  spec.add_dependency "httpclient", "~> 2.7"
25
+ spec.add_dependency "activemodel", "~> 4.2"
26
+ spec.add_dependency "nokogiri", "~> 1.6"
23
27
 
28
+ spec.add_development_dependency "rspec", "~> 3.4"
29
+ spec.add_development_dependency "rspec-its", "~> 1.2"
24
30
  spec.add_development_dependency "bundler", "~> 1.7"
25
- spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rake", "~> 11.1"
26
32
  end
@@ -1,11 +1,19 @@
1
1
  require "forwardable"
2
- require_relative "configuration"
3
- require_relative "connection"
4
- require_relative "content_request"
2
+
3
+ require "duracloud/configuration"
4
+ require "duracloud/connection"
5
+ require "duracloud/error_handler"
6
+ require "duracloud/rest_methods"
5
7
 
6
8
  module Duracloud
7
9
  class Client
8
10
  extend Forwardable
11
+ extend RestMethods
12
+ include RestMethods
13
+
14
+ def self.execute(request_class, http_method, url, **options)
15
+ new.execute(request_class, http_method, url, **options)
16
+ end
9
17
 
10
18
  def self.configure
11
19
  yield Configuration
@@ -13,35 +21,31 @@ module Duracloud
13
21
 
14
22
  attr_reader :config
15
23
 
16
- delegate [:host, :port, :user, :password, :base_url] => :config
24
+ delegate [:host, :port, :user, :password, :base_url, :logger] => :config
17
25
 
18
26
  def initialize(**options)
19
27
  @config = Configuration.new(**options)
20
28
  end
21
29
 
22
- def get_content(url, **options)
23
- execute ContentRequest, :get, url, **options
24
- #ContentRequest.get(self, url, **options)
25
- end
26
-
27
- def get_content_properties(url, **options)
28
- execute ContentRequest, :head, url, **options
29
- end
30
-
31
- def set_content_properties(url, **options)
32
- execute ContentRequest, :post, url, **options
33
- end
34
-
35
- def store_content(url, **options)
36
- execute ContentRequest, :put, url, **options
37
- end
38
-
39
- def delete_content(url, **options)
40
- execute ContentRequest, :delete, url, **options
41
- end
42
-
43
30
  def execute(request_class, http_method, url, **options)
44
- request_class.new(self, http_method, url, **options).execute
31
+ request = request_class.new(self, http_method, url, **options)
32
+ response = request.execute
33
+ handle_response(response)
34
+ response
35
+ end
36
+
37
+ private
38
+
39
+ def handle_response(response)
40
+ logger.debug([self.class.to_s, response.request_method, response.url,
41
+ response.status, response.reason].join(' '))
42
+ if response.error?
43
+ ErrorHandler.call(response)
44
+ elsif %w(POST PUT DELETE).include?(response.request_method) &&
45
+ response.plain_text? &&
46
+ response.has_body?
47
+ logger.info(response.body)
48
+ end
45
49
  end
46
50
  end
47
51
  end
@@ -1,16 +1,21 @@
1
+ require "logger"
2
+ require "uri"
3
+
1
4
  module Duracloud
2
5
  class Configuration
6
+
3
7
  class << self
4
- attr_accessor :host, :port, :user, :password
8
+ attr_accessor :host, :port, :user, :password, :logger
5
9
  end
6
10
 
7
- attr_reader :host, :port, :user, :password
11
+ attr_reader :host, :port, :user, :password, :logger
8
12
 
9
- def initialize(host: nil, port: nil, user: nil, password: nil)
13
+ def initialize(host: nil, port: nil, user: nil, password: nil, logger: nil)
10
14
  @host = host || default(:host)
11
15
  @port = port || default(:port)
12
16
  @user = user || default(:user)
13
17
  @password = password || default(:password)
18
+ @logger = logger || Logger.new(STDERR)
14
19
  freeze
15
20
  end
16
21
 
@@ -18,14 +23,16 @@ module Duracloud
18
23
  URI::HTTPS.build(host: host, port: port)
19
24
  end
20
25
 
26
+ def inspect
27
+ "#<#{self.class} host=#{host.inspect}, port=#{port.inspect}, user=#{user.inspect}," \
28
+ " password=\"******\", logger=#{logger.inspect}>"
29
+ end
30
+
21
31
  private
22
32
 
23
33
  def default(attr)
24
34
  self.class.send(attr) || ENV["DURACLOUD_#{attr.to_s.upcase}"]
25
35
  end
26
36
 
27
- def inspect
28
- "#<#{self.class} host=#{host.inspect}, port=#{port.inspect}, user=#{user.inspect}, password=\"******\">"
29
- end
30
37
  end
31
38
  end
@@ -9,20 +9,10 @@ module Duracloud
9
9
  # custom case-sensitive content property headers (x-dura-meta-*).
10
10
  #
11
11
  class Connection < HTTPClient
12
- # class << self
13
- # attr_accessor :base_path
14
- # end
15
-
16
- # self.base_path = '/'
17
-
18
12
  def initialize(client, base_path = '/')
19
13
  base_url = client.base_url + base_path
20
14
  super(base_url: base_url, force_basic_auth: true)
21
15
  set_auth(client.base_url, client.user, client.password)
22
16
  end
23
17
  end
24
-
25
- # class DurastoreConnection < Connection
26
- # self.base_path = '/durastore/'
27
- # end
28
18
  end
@@ -1,127 +1,189 @@
1
- require 'uri'
2
- require 'forwardable'
3
- require_relative 'content_properties'
1
+ require "uri"
2
+ require "stringio"
3
+ require "active_model"
4
+
5
+ require "duracloud/content_properties"
6
+ require "duracloud/persistence"
7
+ require "duracloud/has_properties"
4
8
 
5
9
  module Duracloud
6
10
  #
7
11
  # A piece of content in DuraCloud
8
12
  #
9
13
  class Content
10
- extend Forwardable
11
-
12
- class << self
13
- def from_response(response)
14
- new(url: response.url) { |c| c.load_from_response(response) }
14
+ include ActiveModel::Dirty
15
+ include Persistence
16
+ include HasProperties
17
+
18
+ after_save :changes_applied
19
+
20
+ # Find content in DuraCloud.
21
+ #
22
+ # @param id [String] the content ID
23
+ # @param space_id [String] the space ID.
24
+ # @return [Duraclound::Content] the content
25
+ # @raise [Duracloud::NotFoundError] the space or content ID does not exist.
26
+ def self.find(id:, space_id:)
27
+ new(id: id, space_id: space_id) do |content|
28
+ content.load_properties
15
29
  end
30
+ end
16
31
 
17
- def find(**options)
18
- new(**options).get
32
+ # Store content in DuraCloud
33
+ #
34
+ # @param id [String] The content ID
35
+ # @param space_id [String] The space ID.
36
+ # @param body [String, #read] The content body
37
+ # @return [Duracloud::Content] the content
38
+ # @raise [Duracloud::NotFoundError] if the space ID does not exist
39
+ # @raise [Duracloud::Error] if the body is empty.
40
+ def self.create(id:, space_id:, body:)
41
+ new(id: id, space_id: space_id) do |content|
42
+ content.body = body
43
+ yield content if block_given?
44
+ content.save
19
45
  end
46
+ end
20
47
 
21
- def create(**options)
22
- new(**options).store
23
- end
48
+ attr_reader :id, :space_id
49
+
50
+ define_attribute_methods :content_type, :body, :md5
51
+
52
+ # Initialize a new piece of content
53
+ #
54
+ # @param id [String] The content ID
55
+ # @param space_id [String] The space ID
56
+ #
57
+ # @example
58
+ # new(id: mycontent.txt", space_id: "myspace")
59
+ def initialize(id:, space_id:)
60
+ @id = id.freeze
61
+ @space_id = space_id.freeze
62
+ @body = nil
63
+ @content_type = nil
64
+ @md5 = nil
65
+ yield self if block_given?
24
66
  end
25
67
 
26
- attr_reader :url
27
- attr_accessor :md5, :content_type, :body
68
+ def space
69
+ Space.find(space_id)
70
+ end
28
71
 
29
- delegate :empty? => :body
72
+ def inspect
73
+ "#<#{self.class} id=#{id.inspect}, space_id=#{space_id.inspect}>"
74
+ end
30
75
 
31
- alias_method :checksum, :md5
76
+ # @api private
77
+ # @raise [Duracloud::NotFoundError] the content does not exist in DuraCloud.
78
+ def load_body
79
+ response = Client.get_content(url)
80
+ @body = response.body # don't use setter
81
+ persisted!
82
+ end
32
83
 
33
- def initialize(**options)
34
- url = if options[:url]
35
- options[:url]
36
- else
37
- unless options[:space_id] && options[:content_id]
38
- raise Error, "Content requires either :url OR both :space_id AND :content_id."
39
- end
40
- options.values_at(:space_id, :content_id).join('/')
41
- end
42
- @url = URI(url).path # might not be exactly what we want, but it works
43
- @body = options[:body] || options[:payload]
44
- @md5 = options[:md5]
45
- @content_type = options[:content_type]
46
- self.properties = options[:properties].to_h
47
- yield self if block_given?
84
+ def load_properties
85
+ super do |response|
86
+ # don't mark content_type as changed
87
+ @content_type = response.content_type
88
+ end
48
89
  end
49
90
 
50
- def inspect
51
- "#<#{self.class} url=#{url.inspect}>"
91
+ def body=(str_or_io)
92
+ val = read_string_or_io(str_or_io)
93
+ raise ArgumentError, "Cannot set body to empty string." if val.empty?
94
+ self.md5 = Digest::MD5.hexdigest(val)
95
+ body_will_change! if md5_changed?
96
+ @body = StringIO.new(val, "r")
52
97
  end
53
98
 
54
- def to_s
55
- body
99
+ def body
100
+ load_body if persisted? && empty?
101
+ @body
56
102
  end
57
103
 
58
- def get
59
- response = client.get_content(url)
60
- load_from_response(response)
61
- self
104
+ def empty?
105
+ @body.nil? || @body.size == 0
62
106
  end
63
- alias_method :reload, :get
64
107
 
65
- def get_properties
66
- response = client.get_content_properties(url)
67
- load_properties_from_response(response)
68
- self
108
+ def content_type=(val)
109
+ content_type_will_change! unless val == @content_type
110
+ @content_type = val
69
111
  end
70
- alias_method :reload_properties, :get_properties
71
112
 
72
- def set_properties
73
- self.content_type = "text/plain" unless content_type
74
- client.set_content_properties(url, properties: properties)
75
- self
113
+ def content_type
114
+ @content_type
76
115
  end
77
116
 
78
- def delete
79
- client.delete_content(url)
80
- reset
81
- freeze
82
- self
117
+ def md5
118
+ @md5
119
+ end
120
+
121
+ private
122
+
123
+ def md5=(val)
124
+ md5_will_change! unless val == @md5
125
+ @md5 = val
126
+ end
127
+
128
+ def set_properties
129
+ headers = properties.to_h
130
+ headers["Content-Type"] = content_type if content_type_changed?
131
+ response = Client.set_content_properties(url, headers: headers)
132
+ # response.body is a text message -- log?
83
133
  end
84
134
 
85
135
  def store
86
- raise Error, "Refusing to store empty content file!" unless body
87
- self.md5 = Digest::MD5.hexdigest(body) unless md5
88
- unless content_type
89
- self.content_type = body ? "application/octet-stream" : "text/plain"
90
- end
91
- options = {
92
- payload: body, md5: md5, content_type: content_type, properties: properties
93
- }
94
- client.store_content(url, **options)
95
- reload_properties
96
- self
136
+ headers = { "Content-MD5" => md5,
137
+ "Content-Type" => content_type || "application/octet-stream" }
138
+ headers.merge!(properties)
139
+ response = Client.store_content(url, body: body, headers: headers)
140
+ # response.body is a text message -- log?
97
141
  end
98
142
 
99
- def load_from_response(response)
100
- self.body = response.body
101
- self.md5 = response.md5
102
- self.content_type = response.content_type
103
- load_properties_from_response(response)
143
+ def url
144
+ [space_id, id].join("/")
104
145
  end
105
146
 
106
- def load_properties_from_response(response)
107
- self.properties = response.headers.select { |h, v| ContentProperties.property?(h) }
147
+ def properties_class
148
+ ContentProperties
108
149
  end
109
150
 
110
- def properties
111
- @properties ||= ContentProperties.new
151
+ def get_properties_response
152
+ Client.get_content_properties(url)
112
153
  end
113
154
 
114
- def properties=(props)
115
- properties.replace(props)
155
+ def do_delete
156
+ Client.delete_content(url)
116
157
  end
117
158
 
118
- def client
119
- @client ||= Client.new
159
+ def do_save
160
+ if !empty? && body_changed?
161
+ store
162
+ elsif persisted?
163
+ set_properties
164
+ else
165
+ raise Error, "Cannot store empty content."
166
+ end
167
+ end
168
+
169
+ def read_string_or_io(str_or_io)
170
+ if str_or_io.respond_to?(:read)
171
+ read_io_like(str_or_io)
172
+ elsif str_or_io.respond_to?(:to_str)
173
+ str_or_io.to_str
174
+ else
175
+ raise ArgumentError, "IO-like or String-like argument required."
176
+ end
120
177
  end
121
178
 
122
- def reset
123
- properties.clear
124
- @body, @md5, @content_type = nil, nil, nil
179
+ def read_io_like(io_like)
180
+ begin
181
+ io_like.rewind if io_like.respond_to?(:rewind)
182
+ io_like.read
183
+ ensure
184
+ io_like.rewind if io_like.respond_to?(:rewind)
185
+ end
125
186
  end
187
+
126
188
  end
127
189
  end