duracloud-client 0.0.1 → 0.0.2

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