duracloud-client 0.3.0 → 0.4.0

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: 328f7556852dc8ca4dbdf2fed3314aa6ca2c160b
4
- data.tar.gz: d10ae8810f1e964d495b8b88142246d42fc2c410
3
+ metadata.gz: 850dd8765ce4680a9a81f7453970ac9d8a33ec6d
4
+ data.tar.gz: 64f6a963ed6453f7f77af4ed9efbc93200417dfc
5
5
  SHA512:
6
- metadata.gz: efa0688d30ec628915d6497422377c9edf95c06233af784947103ab110a4ea8925b7ff99854ccf829e024414eba6b370edd409dc525ff81b14c8707c1f6ccd93
7
- data.tar.gz: 08bfcbd0b87934908c9b229b09400648699f90dd32e903b77d362033b8dcdbb1c737e3727e095b4a1343b32930dac68066895e5b5edc84f3d4e04c6e6a2db8e5
6
+ metadata.gz: a938f8b35fe0b32f4d6e73ac80c741adedd32c91513508f64e6cb63486299cff015cb38a394b22df2f1b56fe76024939ce06c1c3968a247406d0d069e08bd9bb
7
+ data.tar.gz: 303f6bbedfb8db0950ab9ffece57ba5989fa575dd0ed4b290004f4d13a91dfc88a252b83034bfc1503e1c81c6acfe5fe0b897da2b004ab9ab6234c86587c4128
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # duracloud-ruby-client
2
+
2
3
  Ruby client for communicating with DuraCloud
3
4
 
4
5
  ## Installation
@@ -84,7 +85,7 @@ D, [2016-04-29T12:12:32.641574 #28275] DEBUG -- : Duracloud::Client PUT https://
84
85
  => #<Duracloud::Space space_id="rest-api-testing2", store_id="(default)">
85
86
  ```
86
87
 
87
- A `Duracloud::BadRequestError` is raise if the space ID is invalid (illegal characters, too long, etc.).
88
+ A `Duracloud::BadRequestError` exception is raised if the space ID is invalid (illegal characters, too long, etc.).
88
89
 
89
90
  #### Retrieve a space and view its properties
90
91
 
@@ -100,7 +101,7 @@ D, [2016-04-29T12:15:12.593075 #28275] DEBUG -- : Duracloud::Client HEAD https:/
100
101
  => #<DateTime: 2016-04-05T17:59:11+00:00 ((2457484j,64751s,0n),+0s,2299161j)>
101
102
  ```
102
103
 
103
- A `Duracloud::NotFoundError` exception is raise if the space does not exist.
104
+ A `Duracloud::NotFoundError` exception is raised if the space does not exist.
104
105
 
105
106
  #### Enumerate the content IDs of the space
106
107
 
@@ -153,6 +154,12 @@ If the space or content ID does not exist, a `Duracloud::NotFoundError` is raise
153
154
  If an MD5 digest is provided (:md5 attribute), a `Duracloud::MessageDigestError` is
154
155
  raised if the content ID exists and the stored digest does not match.
155
156
 
157
+ *Added in v0.4.0*
158
+
159
+ If a content item is not found at the content ID, `Duracloud::Content.find` will look for a "content manifest"
160
+ by appending ".dura-manifest" to the content ID. If the manifest is found, the content item is marked as
161
+ "chunked". **Caution: Working with chunked files should be considered EXPERIMENTAL.**
162
+
156
163
  #### Update the properties for a content item
157
164
 
158
165
  ```
@@ -177,6 +184,48 @@ D, [2016-04-29T18:32:06.465928 #32379] DEBUG -- : Duracloud::Client HEAD https:/
177
184
  => "bob@example.com"
178
185
  ```
179
186
 
187
+ #### Copy a content item
188
+
189
+ *Added in v0.3.0; Changed in v0.4.0.*
190
+
191
+ Accepts same keywords as `.find` and `.new` -- `:space_id`, `:content_id`, `:store_id` -- plus `:force`.
192
+
193
+ The `:force` argument is a boolean (default `false`) indicating whether to replace existing content (if found) at the target location. If `:force` is false and content exists at the target location, the operation raises a `Duracloud::Content::CopyError` exception.
194
+
195
+ Also, `:space_id` and `:content_id` arguments are not required, but default to the values of the current content object's attributes. An exception is raised if the source and destination locations are the same (regardless of the value of `:force`).
196
+
197
+ ```
198
+ >> content = Duracloud::Content.find(space_id: 'rest-api-testing', content_id: 'contentItem.txt')
199
+ D, [2017-01-27T17:16:45.846459 #93283] DEBUG -- : Duracloud::Client HEAD https://duke.duracloud.org/durastore/rest-api-testing/contentItem.txt 200 OK
200
+ => #<Duracloud::Content space_id="rest-api-testing", content_id="contentItem.txt", store_id=(default)>
201
+
202
+ >> content.copy(space_id: 'rest-api-testing2')
203
+ D, [2017-01-27T17:17:59.848741 #93283] DEBUG -- : Duracloud::Client PUT https://duke.duracloud.org/durastore/rest-api-testing2/contentItem.txt 201 Created
204
+ => #<Duracloud::Content space_id="rest-api-testing2", content_id="contentItem.txt", store_id=(default)>
205
+ ```
206
+
207
+ #### Move a content item
208
+
209
+ *Added in v0.3.0; Changed in v0.4.0.*
210
+
211
+ See also *Copy a content item, above.
212
+
213
+ ```
214
+ This is a convenience operation -- copy and delete -- not directly supported by the DuraCloud REST API.
215
+
216
+ >> content = Duracloud::Content.find(space_id: 'rest-api-testing', content_id: 'contentItem.txt')
217
+ D, [2017-01-27T17:19:41.926994 #93286] DEBUG -- : Duracloud::Client HEAD https://duke.duracloud.org/durastore/rest-api-testing/contentItem.txt 200 OK
218
+ => #<Duracloud::Content space_id="rest-api-testing", content_id="contentItem.txt", store_id=(default)>
219
+
220
+ >> content.move(space_id: 'rest-api-testing2')
221
+ D, [2017-01-27T17:20:07.542468 #93286] DEBUG -- : Duracloud::Client PUT https://duke.duracloud.org/durastore/rest-api-testing2/contentItem.txt 201 Created
222
+ D, [2017-01-27T17:20:08.442504 #93286] DEBUG -- : Duracloud::Client DELETE https://duke.duracloud.org/durastore/rest-api-testing/contentItem.txt 200 OK
223
+ => #<Duracloud::Content space_id="rest-api-testing2", content_id="contentItem.txt", store_id=(default)>
224
+
225
+ >> content.deleted?
226
+ => true
227
+ ```
228
+
180
229
  #### Delete a content item
181
230
 
182
231
  ```
@@ -191,7 +240,7 @@ D, [2016-04-29T18:28:31.459962 #32379] DEBUG -- : Duracloud::Client DELETE https
191
240
  I, [2016-04-29T18:28:31.460069 #32379] INFO -- : Content foo2 deleted successfully
192
241
  => #<Duracloud::Content space_id="rest-api-testing", content_id="foo2", store_id=(default)>
193
242
 
194
- >> Duracloud::Content.exist?("rest-api-testing", "foo2")
243
+ >> Duracloud::Content.exist?(space_id: "rest-api-testing", content_id: "foo2")
195
244
  D, [2016-04-29T18:29:03.935451 #32379] DEBUG -- : Duracloud::Client HEAD https://foo.duracloud.org/durastore/rest-api-testing/foo2 404 Not Found
196
245
  => false
197
246
  ```
@@ -0,0 +1,92 @@
1
+ require "active_model"
2
+
3
+ module Duracloud
4
+ class AbstractEntity
5
+ include ActiveModel::Model
6
+ extend ActiveModel::Callbacks
7
+
8
+ define_model_callbacks :save, :delete, :load_properties
9
+ after_save :persisted!
10
+ after_save :reset_properties
11
+ after_load_properties :persisted!
12
+ before_delete :reset_properties
13
+ after_delete :deleted!
14
+ after_delete :freeze
15
+
16
+ def save
17
+ raise Error, "Cannot save deleted #{self.class}." if deleted?
18
+ run_callbacks :save do
19
+ do_save
20
+ end
21
+ end
22
+
23
+ def delete
24
+ raise Error, "Cannot delete, already deleted." if deleted?
25
+ run_callbacks :delete do
26
+ do_delete
27
+ end
28
+ end
29
+
30
+ def persisted?
31
+ !!@persisted
32
+ end
33
+
34
+ def deleted?
35
+ !!@deleted
36
+ end
37
+
38
+ # Return the properties associated with this resource,
39
+ # loading from Duracloud if necessary.
40
+ # @return [Duracloud::Properties] the properties
41
+ # @raise [Duracloud::NotFoundError] if the resource is marked persisted
42
+ # but does not exist in Duracloud
43
+ def properties
44
+ load_properties if persisted? && @properties.nil?
45
+ @properties ||= properties_class.new
46
+ end
47
+
48
+
49
+ def load_properties
50
+ run_callbacks :load_properties do
51
+ do_load_properties
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def do_load_properties
58
+ raise NotImplementedError, "Subclasses must implement `#do_load_properties` private method."
59
+ end
60
+
61
+ def persisted!
62
+ @persisted = true
63
+ end
64
+
65
+ def deleted!
66
+ @deleted = true
67
+ @persisted = false
68
+ end
69
+
70
+ def do_delete
71
+ raise NotImplementedError, "Subclasses must implement `do_delete`."
72
+ end
73
+
74
+ def do_save
75
+ raise NotImplementedError, "Subclasses must implement `do_save`."
76
+ end
77
+
78
+ def properties=(props)
79
+ filtered = props ? properties_class.filter(props) : props
80
+ @properties = properties_class.new(filtered)
81
+ end
82
+
83
+ def reset_properties
84
+ @properties = nil
85
+ end
86
+
87
+ def properties_class
88
+ Properties
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,35 @@
1
+ module Duracloud
2
+ class ChunkedContent < Content
3
+
4
+ def self.find(**kwargs)
5
+ new(**kwargs).tap do |content|
6
+ content.manifest
7
+ end
8
+ end
9
+
10
+ def manifest
11
+ if @manifest.nil?
12
+ @manifest = ContentManifest.find(space_id: space_id,
13
+ manifest_id: content_id + MANIFEST_EXT,
14
+ store_id: store_id)
15
+ load_properties
16
+ end
17
+ @manifest
18
+ end
19
+
20
+ private
21
+
22
+ def do_load_properties
23
+ if md5
24
+ if md5 != manifest.source.md5
25
+ raise MessageDigestError, "Expected MD5: {#{md5}}; DuraCloud MD5: {#{manifest.source.md5}}."
26
+ end
27
+ else
28
+ self.md5 = manifest.source.md5
29
+ end
30
+ self.properties = manifest.properties.dup
31
+ self.content_type = manifest.source.content_type
32
+ end
33
+
34
+ end
35
+ end
@@ -6,8 +6,8 @@ module Duracloud
6
6
  extend RestMethods
7
7
  include RestMethods
8
8
 
9
- def self.execute(request_class, http_method, url, **options)
10
- new.execute(request_class, http_method, url, **options)
9
+ def self.execute(request_class, http_method, url, **options, &block)
10
+ new.execute(request_class, http_method, url, **options, &block)
11
11
  end
12
12
 
13
13
  def self.configure
@@ -22,9 +22,9 @@ module Duracloud
22
22
  @config = Configuration.new(**options)
23
23
  end
24
24
 
25
- def execute(request_class, http_method, url, **options)
25
+ def execute(request_class, http_method, url, **options, &block)
26
26
  request = request_class.new(self, http_method, url, **options)
27
- response = request.execute
27
+ response = request.execute(&block)
28
28
  handle_response(response)
29
29
  response
30
30
  end
@@ -4,48 +4,57 @@ module Duracloud
4
4
  #
5
5
  # A piece of content in DuraCloud
6
6
  #
7
- class Content
8
- include ActiveModel::Model
9
- include ActiveModel::Dirty
10
- include Persistence
11
- include HasProperties
7
+ class Content < AbstractEntity
12
8
 
13
- CHUNK_SIZE = 1024 * 16
9
+ class CopyError < Error; end
14
10
 
15
- after_save :changes_applied
11
+ CHUNK_SIZE = 1024 * 16
12
+ COPY_SOURCE_HEADER = "x-dura-meta-copy-source"
13
+ COPY_SOURCE_STORE_HEADER = "x-dura-meta-copy-source-store"
14
+ MANIFEST_EXT = ".dura-manifest"
16
15
 
17
16
  # Does the content exist in DuraCloud?
18
- # @return [Boolean] whether the content exists
19
- # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 attribute
20
- # does not match the stored value
21
- def self.exist?(params={})
22
- find(params) && true
17
+ # @return [Boolean] whether the content exists.
18
+ # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 keyword option,
19
+ # if given, does not match the stored value.
20
+ def self.exist?(**kwargs)
21
+ find(**kwargs) && true
23
22
  rescue NotFoundError
24
23
  false
25
24
  end
26
25
 
27
26
  # Find content in DuraCloud.
28
27
  # @return [Duraclound::Content] the content
29
- # @raise [Duracloud::NotFoundError] the space, content, or store does not exist.
30
- # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 attribute
31
- # does not match the stored value
32
- def self.find(params={})
33
- new(params).tap do |content|
28
+ # @raise [Duracloud::NotFoundError] the space, content, or store (if given) does not exist.
29
+ # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 keyword option,
30
+ # if given, does not match the stored value.
31
+ def self.find(**kwargs)
32
+ new(**kwargs).tap do |content|
34
33
  content.load_properties
35
34
  end
35
+ rescue NotFoundError => e
36
+ ChunkedContent.find(**kwargs)
36
37
  end
37
38
 
38
- attr_accessor :space_id, :content_id, :store_id
39
+ # Create new content in DuraCloud.
40
+ # @return [Duraclound::Content] the content
41
+ # @raise [Duracloud::NotFoundError] the space or store (if given) does not exist.
42
+ # @raise [Duracloud::MessageDigestError] the provided digest in the :md5 keyword option,
43
+ # if given, does not match the stored value.
44
+ def self.create(**kwargs)
45
+ new(**kwargs).save
46
+ end
47
+
48
+ attr_accessor :space_id, :content_id, :store_id,
49
+ :body, :md5, :content_type
39
50
  alias_method :id, :content_id
40
51
  validates_presence_of :space_id, :content_id
41
52
 
42
- define_attribute_methods :content_type, :body, :md5
43
-
44
53
  # Return the space associated with this content.
45
54
  # @return [Duracloud::Space] the space.
46
55
  # @raise [Duracloud::NotFoundError] the space or store does not exist.
47
56
  def space
48
- Space.find(space_id, store_id)
57
+ @space ||= Space.find(space_id, store_id)
49
58
  end
50
59
 
51
60
  def inspect
@@ -54,67 +63,33 @@ module Duracloud
54
63
  " store_id=#{store_id || '(default)'}>"
55
64
  end
56
65
 
57
- # @api private
58
- # @raise [Duracloud::NotFoundError] the content does not exist in DuraCloud.
59
- def load_body
60
- response = Client.get_content(*args, **query)
61
- set_md5!(response)
62
- @body = response.body # don't use setter b/c marks as dirty
63
- persisted!
64
- end
65
-
66
- def load_properties
67
- super do |response|
68
- # don't mark content_type or md5 as changed
69
- set_md5!(response)
70
- @content_type = response.content_type
71
- end
72
- end
73
-
74
- def body=(str_or_io)
75
- @body = str_or_io
76
- body_will_change!
77
- end
78
-
79
- # Return the content body, loading from DuraCloud if necessary.
80
- # @return [String, StringIO] the content body
81
- def body
82
- load_body if persisted? && empty?
83
- @body
84
- end
85
-
86
66
  # Is the content empty?
87
67
  # @return [Boolean] whether the content is empty (nil or empty string)
88
68
  def empty?
89
- @body.nil? || @body.size == 0
90
- end
91
-
92
- def content_type=(val)
93
- content_type_will_change! unless val == @content_type
94
- @content_type = val
69
+ body.nil? || ( body.respond_to?(:size) && body.size == 0 )
95
70
  end
96
71
 
97
- def content_type
98
- @content_type
99
- end
100
-
101
- def md5=(val)
102
- md5_will_change! unless val == @md5
103
- @md5 = val
104
- end
105
-
106
- def md5
107
- @md5
72
+ # Downloads the remote content
73
+ # @yield [String] chunk of the remote content, if block given.
74
+ # @return [Duracloud::Response] the response to the content request.
75
+ # @raise [Duracloud::NotFoundError]
76
+ def download(&block)
77
+ Client.get_content(*args, **query, &block)
108
78
  end
109
79
 
110
80
  # @return [Duracloud::Content] the copied content
111
81
  # The current instance still represents the original content.
112
- def copy(target_space_id:, target_content_id:, target_store_id: nil)
113
- copy_headers = {'x-dura-meta-copy-source'=>[space_id, content_id].join('/')}
114
- copy_headers['x-dura-meta-copy-source-store'] = store_id if store_id
115
- options = { storeID: target_store_id, headers: copy_headers }
116
- Client.copy_content(target_space_id, target_content_id, **options)
117
- Content.find(space_id: target_space_id, content_id: target_content_id, store_id: target_store_id, md5: md5)
82
+ def copy(**args)
83
+ dest = args.except(:force)
84
+ dest[:space_id] ||= space_id
85
+ dest[:content_id] ||= content_id
86
+ raise CopyError, "Destination is the same as the source." if dest == copy_source
87
+ if !args[:force] && Content.exist?(**dest)
88
+ raise CopyError, "Destination exists and :false option is false."
89
+ end
90
+ options = { storeID: dest[:store_id], headers: copy_headers }
91
+ Client.copy_content(dest[:space_id], dest[:content_id], **options)
92
+ Content.new(dest.merge(md5: md5))
118
93
  end
119
94
 
120
95
  # @return [Duracloud::Content] the moved content
@@ -127,28 +102,6 @@ module Duracloud
127
102
 
128
103
  private
129
104
 
130
- def set_md5!(response)
131
- if md5
132
- if md5 != response.md5
133
- raise MessageDigestError,
134
- "Expected MD5 digest (#{md5}) does not match response header: #{response.md5}"
135
- end
136
- else
137
- @md5 = response.md5
138
- end
139
- end
140
-
141
- def io_like?
142
- body.respond_to?(:read) && body.respond_to?(:rewind)
143
- end
144
-
145
- def set_properties
146
- headers = properties.to_h
147
- headers["Content-Type"] = content_type if content_type_changed?
148
- options = { headers: headers, query: query }
149
- Client.set_content_properties(*args, **options)
150
- end
151
-
152
105
  def store
153
106
  headers = {
154
107
  "Content-MD5" => md5 || calculate_md5,
@@ -159,6 +112,24 @@ module Duracloud
159
112
  Client.store_content(*args, **options)
160
113
  end
161
114
 
115
+ def copy_headers
116
+ ch = { COPY_SOURCE_HEADER=>"#{space_id}/#{content_id}" }
117
+ ch[COPY_SOURCE_STORE_HEADER] = store_id if store_id
118
+ ch
119
+ end
120
+
121
+ def copy_source
122
+ { space_id: space_id, content_id: content_id, store_id: store_id }
123
+ end
124
+
125
+ def io_like?
126
+ body.respond_to?(:read) && body.respond_to?(:rewind)
127
+ end
128
+
129
+ def set_properties
130
+ Client.set_content_properties(*args, headers: properties, query: query)
131
+ end
132
+
162
133
  def calculate_md5
163
134
  digest = Digest::MD5.new
164
135
  if io_like?
@@ -177,8 +148,17 @@ module Duracloud
177
148
  ContentProperties
178
149
  end
179
150
 
180
- def get_properties_response
181
- Client.get_content_properties(*args, **query)
151
+ def do_load_properties
152
+ response = Client.get_content_properties(*args, **query)
153
+ if md5
154
+ if md5 != response.md5
155
+ raise MessageDigestError, "Expected MD5: {#{md5}}; DuraCloud MD5: {#{response.md5}}."
156
+ end
157
+ else
158
+ self.md5 = response.md5
159
+ end
160
+ self.properties = response.headers
161
+ self.content_type = response.content_type
182
162
  end
183
163
 
184
164
  def do_delete
@@ -186,7 +166,7 @@ module Duracloud
186
166
  end
187
167
 
188
168
  def do_save
189
- if !empty? && body_changed?
169
+ if !empty?
190
170
  store
191
171
  elsif persisted?
192
172
  set_properties