duracloud-client 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,23 +1,46 @@
1
- require "csv"
2
-
3
1
  module Duracloud
4
2
  class Manifest
5
3
 
6
- CSV_OPTS = {
7
- col_sep: '\t',
8
- headers: :first_row,
9
- write_headers: true,
10
- return_headers: true,
11
- }
4
+ BAGIT = "BAGIT".freeze
5
+ TSV = "TSV".freeze
6
+
7
+ attr_reader :space_id, :store_id
8
+
9
+ def initialize(space_id, store_id = nil)
10
+ @space_id = space_id
11
+ @store_id = store_id
12
+ @tsv_response = nil
13
+ @bagit_response = nil
14
+ end
15
+
16
+ def csv(opts = {})
17
+ CSVReader.call(tsv, opts)
18
+ end
19
+
20
+ def tsv
21
+ tsv_response.body
22
+ end
23
+
24
+ def bagit
25
+ bagit_response.body
26
+ end
27
+
28
+ private
29
+
30
+ def tsv_response
31
+ @tsv_response ||= get_response(TSV)
32
+ end
33
+
34
+ def bagit_response
35
+ @bagit_response ||= get_response(BAGIT)
36
+ end
12
37
 
13
- def self.csv(space_id, csv_opts: {})
14
- data = raw(space_id)
15
- CSV.new(data, CSV_OPTS.merge(csv_opts))
38
+ def get_response(format)
39
+ Client.get_manifest(space_id, query(format))
16
40
  end
17
41
 
18
- def self.raw(space_id)
19
- response = Client.get_manifest(space_id)
20
- response.body
42
+ def query(format)
43
+ { storeID: store_id, format: format }
21
44
  end
22
45
 
23
46
  end
@@ -1,4 +1,4 @@
1
- require "active_model/callbacks"
1
+ require "active_model"
2
2
 
3
3
  module Duracloud
4
4
  module Persistence
@@ -3,7 +3,11 @@ require 'time'
3
3
  require 'date'
4
4
 
5
5
  module Duracloud
6
+ #
7
+ # Encapsulates Duracloud "properties" which are transmitted via HTTP headers.
8
+ #
6
9
  # @abstract
10
+ #
7
11
  class Properties < Hashie::Mash
8
12
 
9
13
  PREFIX = "x-dura-meta-".freeze
@@ -23,38 +27,69 @@ module Duracloud
23
27
  # Properties set by the DuraCloud SyncTool
24
28
  SYNCTOOL = /\A#{PREFIX}(creator|(content-file-(created|modified|last-accessed-path)))\z/
25
29
 
30
+ # Is the property valid for this class of properties?
31
+ # @note Subclasses should override this method rather than the `#property?'
32
+ # instance method.
33
+ # @param prop [String] the property name
34
+ # @return [Boolean]
26
35
  def self.property?(prop)
27
- duraspace_property?(prop) && !internal_property?(prop)
36
+ duracloud_property?(prop) && !internal_property?(prop)
28
37
  end
29
38
 
39
+ # Filter the hash of properties, selecting only the properties valid
40
+ # for this particular usage (subclass).
41
+ # @param hsh [Hash] the unfiltered properties
42
+ # @return [Hash] the filtered properties
30
43
  def self.filter(hsh)
31
44
  hsh.select { |k, v| property?(k) }
32
45
  end
33
46
 
34
- def self.duraspace_property?(prop)
47
+ # Is the property a (theoretically) valid DuraCloud property?
48
+ # @param prop [String] the property name
49
+ # @return [Boolean]
50
+ def self.duracloud_property?(prop)
35
51
  prop.start_with?(PREFIX)
36
52
  end
37
53
 
54
+ # Is the property a reserved "internal" DuraCloud property?
55
+ # @param prop [String] the property name
56
+ # @return [Boolean]
38
57
  def self.internal_property?(prop)
39
58
  INTERNAL =~ prop
40
59
  end
41
60
 
61
+ # Is the property a space property?
62
+ # @param prop [String] the property name
63
+ # @return [Boolean]
42
64
  def self.space_property?(prop)
43
65
  SPACE =~ prop
44
66
  end
45
67
 
68
+ # Is the property a space ACL?
69
+ # @param prop [String] the property name
70
+ # @return [Boolean]
46
71
  def self.space_acl?(prop)
47
72
  SPACE_ACLS =~ prop
48
73
  end
49
74
 
75
+ # Is the property used for copying content?
76
+ # @param prop [String] the property name
77
+ # @return [Boolean]
50
78
  def self.copy_content_property?(prop)
51
79
  COPY_CONTENT =~ prop
52
80
  end
53
81
 
82
+ # Is the property valid for this class of properties?
83
+ # @note Subclasses should not override this method, but instead
84
+ # override the `.property?' class method.
85
+ # @param prop [String] the property name
86
+ # @return [Boolean]
87
+ # @api private
54
88
  def property?(prop)
55
89
  self.class.property?(prop)
56
90
  end
57
91
 
92
+ # @api private
58
93
  def regular_writer(key, value)
59
94
  if property?(key)
60
95
  super
@@ -63,10 +98,12 @@ module Duracloud
63
98
  end
64
99
  end
65
100
 
101
+ # @api private
66
102
  def convert_key(key)
67
- force_ascii(duraspace_property!(super))
103
+ force_ascii(duracloud_property!(super))
68
104
  end
69
105
 
106
+ # @api private
70
107
  def convert_value(value, _ = nil)
71
108
  case value
72
109
  when Array
@@ -82,12 +119,12 @@ module Duracloud
82
119
 
83
120
  private
84
121
 
85
- # coerce to a Duraspace property
86
- def duraspace_property!(prop)
122
+ # Coerce to a DuraCloud property
123
+ def duracloud_property!(prop)
87
124
  prop.dup.tap do |p|
88
125
  p.gsub!(/_/, '-')
89
126
  p.downcase!
90
- p.prepend(PREFIX) unless self.class.duraspace_property?(p)
127
+ p.prepend(PREFIX) unless self.class.duracloud_property?(p)
91
128
  end
92
129
  end
93
130
 
@@ -8,32 +8,44 @@ module Duracloud
8
8
  # @param body [String] the body of the request
9
9
  # @param headers [Hash] HTTP headers
10
10
  # @param query [Hash] Query string parameters
11
- def initialize(client, http_method, url, body: nil, headers: nil, query: nil)
11
+ # def initialize(client, http_method, url, body: nil, headers: nil, query: nil)
12
+ def initialize(client, http_method, url, **options)
12
13
  @client = client
13
14
  @http_method = http_method
14
15
  @url = url
15
- @body = body
16
- @headers = headers
17
- @query = query
16
+ set_options(options.dup)
18
17
  end
19
18
 
20
19
  def execute
21
- begin
22
- original_response = connection.send(http_method,
23
- url,
24
- body: body,
25
- query: query,
26
- header: headers)
27
- Response.new(original_response)
28
- end
20
+ response_class.new(original_response)
29
21
  end
30
22
 
31
23
  private
32
24
 
25
+ def original_response
26
+ connection.send(http_method,
27
+ url,
28
+ body: body,
29
+ query: query,
30
+ header: headers)
31
+ end
32
+
33
+ def set_options(options)
34
+ @body = options.delete(:body)
35
+ @headers = options.delete(:headers)
36
+ query = options.delete(:query) || {}
37
+ # Treat other keywords args as query params and ignore empty params
38
+ @query = query.merge(options).reject { |k, v| v.to_s.empty? }
39
+ end
40
+
33
41
  def base_path
34
42
  '/'
35
43
  end
36
44
 
45
+ def response_class
46
+ Response
47
+ end
48
+
37
49
  def connection
38
50
  @connection ||= Connection.new(client, base_path)
39
51
  end
@@ -2,79 +2,86 @@ module Duracloud
2
2
  module RestMethods
3
3
 
4
4
  def get_stores
5
- durastore :get, "stores"
5
+ durastore(:get, "stores")
6
6
  end
7
7
 
8
- def get_spaces
9
- durastore :get, "spaces"
8
+ def get_spaces(**query)
9
+ durastore(:get, "spaces", **query)
10
10
  end
11
11
 
12
- def get_space(space_id, query: nil)
13
- durastore :get, space_id, query: query
12
+ def get_space(space_id, **query)
13
+ durastore(:get, space_id, **query)
14
14
  end
15
15
 
16
- def get_space_properties(space_id)
17
- durastore :head, space_id
16
+ def get_space_properties(space_id, **query)
17
+ durastore(:head, space_id, **query)
18
18
  end
19
19
 
20
- def get_space_acls(space_id)
21
- durastore :head, "acl/#{space_id}"
20
+ def get_space_acls(space_id, **query)
21
+ durastore(:head, "acl/#{space_id}", **query)
22
22
  end
23
23
 
24
- def set_space_acls(space_id, acls)
25
- durastore :post, space_id, properties: acls
24
+ def set_space_acls(space_id, **options)
25
+ durastore(:post, "acl/#{space_id}", **options)
26
26
  end
27
27
 
28
- def create_space(space_id)
29
- durastore :put, space_id
28
+ def create_space(space_id, **query)
29
+ durastore(:put, space_id, **query)
30
30
  end
31
31
 
32
- def delete_space(space_id)
33
- durastore :delete, space_id
32
+ def delete_space(space_id, **query)
33
+ durastore(:delete, space_id, **query)
34
34
  end
35
35
 
36
- def get_content(url, **options)
37
- durastore :get, url, **options
36
+ def get_content(space_id, content_id, **options)
37
+ durastore_content(:get, space_id, content_id, **options)
38
38
  end
39
39
 
40
- def get_content_properties(url, **options)
41
- durastore :head, url, **options
40
+ def get_content_properties(space_id, content_id, **options)
41
+ durastore_content(:head, space_id, content_id, **options)
42
42
  end
43
43
 
44
- def set_content_properties(url, **options)
45
- durastore :post, url, **options
44
+ def set_content_properties(space_id, content_id, **options)
45
+ durastore_content(:post, space_id, content_id, **options)
46
46
  end
47
47
 
48
- def store_content(url, **options)
49
- durastore :put, url, **options
48
+ def store_content(space_id, content_id, **options)
49
+ durastore_content(:put, space_id, content_id, **options)
50
50
  end
51
51
 
52
- def delete_content(url, **options)
53
- durastore :delete, url, **options
52
+ def copy_content(space_id, content_id, **options)
53
+ raise NotImplementedError,
54
+ "The API method 'Copy Content' has not yet been implemented."
54
55
  end
55
56
 
56
- def get_audit_log
57
- durastore :get, "audit/#{space_id}"
57
+ def delete_content(space_id, content_id, **options)
58
+ durastore_content(:delete, space_id, content_id, **options)
58
59
  end
59
60
 
60
- def get_manifest(space_id)
61
- durastore :get, "manifest/#{space_id}"
61
+ def get_audit_log(space_id, **query)
62
+ durastore(:get, "audit/#{space_id}", **query)
62
63
  end
63
64
 
64
- def get_bit_integrity_report(space_id)
65
- durastore :get, "bit-integrity/#{space_id}"
65
+ def get_manifest(space_id, **query)
66
+ durastore(:get, "manifest/#{space_id}", **query)
66
67
  end
67
68
 
68
- def get_bit_integrity_report_properties(space_id)
69
- durastore :head, "bit-integrity/#{space_id}"
69
+ def get_bit_integrity_report(space_id, **query)
70
+ durastore(:get, "bit-integrity/#{space_id}", **query)
70
71
  end
71
72
 
72
- def get_tasks
73
- raise NotImplementedError, "The API method 'Get Tasks' has not been implemented."
73
+ def get_bit_integrity_report_properties(space_id, **query)
74
+ durastore(:head, "bit-integrity/#{space_id}", **query)
74
75
  end
75
76
 
76
- def perform_task
77
- raise NotImplementedError, "The API method 'Perform Task' has not been implemented."
77
+ def get_tasks(**query)
78
+ raise NotImplementedError,
79
+ "The API method 'Get Tasks' has not been implemented."
80
+ end
81
+
82
+ def perform_task(task_name, **query)
83
+ raise NotImplementedError,
84
+ "The API method 'Perform Task' has not been implemented."
78
85
  end
79
86
 
80
87
  private
@@ -83,5 +90,10 @@ module Duracloud
83
90
  execute(DurastoreRequest, *args)
84
91
  end
85
92
 
93
+ def durastore_content(http_method, space_id, content_id, **options)
94
+ url = [ space_id, content_id ].join("/")
95
+ durastore(http_method, url, **options)
96
+ end
97
+
86
98
  end
87
99
  end
@@ -1,63 +1,224 @@
1
1
  require "date"
2
2
  require "nokogiri"
3
- require "forwardable"
4
3
 
5
4
  module Duracloud
5
+ #
6
+ # A "space" within a DuraCloud account.
7
+ #
6
8
  class Space
7
- extend Forwardable
8
9
  include Persistence
9
10
  include HasProperties
10
11
 
12
+ # Max size of content item list for one request.
13
+ # This limit is imposed by Duracloud.
11
14
  MAX_RESULTS = 1000
12
15
 
13
- def self.all
14
- response = Client.get_spaces
15
- doc = Nokogiri::XML(response.body)
16
- doc.css('space').map { |s| new(s['id']) }
16
+ class << self
17
+ # List all spaces
18
+ # @param store_id [String] the store ID (optional)
19
+ # @return [Array<Duracloud::Space>] the list of spaces
20
+ # @raise [Duracloud::Error] the store was not found
21
+ def all(store_id = nil)
22
+ ids(store_id).map { |id| new(id, store_id) }
23
+ end
24
+
25
+ # List all space IDs
26
+ # @param store_id [String] the store ID (optional)
27
+ # @return [Array<String>] the list of space IDs
28
+ # @raise [Duracloud::Error] the store was not found
29
+ def ids(store_id = nil)
30
+ response = Client.get_spaces(storeID: store_id)
31
+ doc = Nokogiri::XML(response.body)
32
+ doc.css('space').map { |s| s['id'] }
33
+ end
34
+
35
+ # Enumerates the content IDs in the space.
36
+ # @param space_id [String] the space ID
37
+ # @param store_id [String] the store ID (optional)
38
+ # @param prefix [String] the content ID prefix for filtering (optional)
39
+ # @param start_after [String] the content ID to be used as a "marker".
40
+ # Listing starts after this ID. (optional)
41
+ # @return [Enumerator] an enumerator.
42
+ # @raise [Duracloud::NotFoundError] the space or store does not exist.
43
+ def content_ids(space_id, store_id: nil, prefix: nil, start_after: nil)
44
+ space = find(space_id, store_id)
45
+ space.content_ids(prefix: prefix, start_after: start_after)
46
+ end
47
+
48
+ # Enumerates the content items in the space.
49
+ # @param space_id [String] the space ID
50
+ # @param store_id [String] the store ID (optional)
51
+ # @param prefix [String] the content ID prefix for filtering (optional)
52
+ # @param start_after [String] the content ID to be used as a "marker".
53
+ # Listing starts after this ID. (optional)
54
+ # @return [Enumerator] an enumerator.
55
+ # @raise [Duracloud::NotFoundError] the space does not exist in Duracloud.
56
+ def items(space_id, store_id: nil, prefix: nil, start_after: nil)
57
+ space = find(space_id, store_id)
58
+ space.items(prefix: prefix, start_after: start_after)
59
+ end
60
+
61
+ # Create a new space
62
+ # @see .new for arguments
63
+ # @return [Duracloud::Space] the space
64
+ # @raise [Duracloud::BadRequestError] the space ID is invalid.
65
+ def create(*args)
66
+ new(*args) do |space|
67
+ yield space if block_given?
68
+ space.save
69
+ end
70
+ end
71
+
72
+ # Does the space exist?
73
+ # @see .new for arguments
74
+ # @return [Boolean] whether the space exists.
75
+ def exist?(*args)
76
+ find(*args) && true
77
+ rescue NotFoundError
78
+ false
79
+ end
80
+
81
+ # Find a space
82
+ # @see .new for arguments
83
+ # @return [Duracloud::Space] the space
84
+ # @raise [Duracloud::NotFoundError] the space or store was not found
85
+ def find(*args)
86
+ new(*args) do |space|
87
+ space.load_properties
88
+ end
89
+ end
90
+
91
+ # Return the number of items in the space
92
+ # @return [Fixnum] the number of items
93
+ # @raise [Duracloud::NotFoundError] the space or store was not found
94
+ def count(*args)
95
+ find(*args).count
96
+ end
97
+
98
+ # Return the audit log for the space
99
+ # @return [Duracloud::AuditLog] the audit log
100
+ # @raise [Duracloud::NotFoundError] the space or store was not found
101
+ def audit_log(*args)
102
+ find(*args).audit_log
103
+ end
104
+
105
+ # Return the bit integrity report for the space
106
+ # @return [Duracloud::BitIntegrityReport] the report
107
+ # @raise [Duracloud::NotFoundError] the space or store was not found
108
+ def bit_integrity_report(*args)
109
+ find(*args).bit_integrity_report
110
+ end
111
+
112
+ # Return the manifest for the space
113
+ # @return [Duracloud::Manifest] the manifest
114
+ # @raise [Duracloud::NotFoundError] the space or store was not found
115
+ def manifest(*args)
116
+ find(*args).manifest
117
+ end
17
118
  end
18
119
 
19
- def self.create(id)
20
- # TODO
120
+ attr_reader :space_id, :store_id
121
+ alias_method :id, :space_id
122
+
123
+ after_save :reset_acls
124
+ before_delete :reset_acls
125
+
126
+ # @param space_id [String] the space ID
127
+ # @param store_id [String] the store ID (optional)
128
+ def initialize(space_id, store_id = nil)
129
+ @space_id = space_id
130
+ @store_id = store_id
131
+ yield self if block_given?
21
132
  end
22
133
 
23
- def self.find(id)
24
- space = new(id)
25
- space.load_properties
26
- space
134
+ def inspect
135
+ "#<#{self.class} space_id=#{space_id.inspect}," \
136
+ " store_id=#{(store_id || '(default)').inspect}>"
27
137
  end
28
138
 
29
- attr_reader :id
139
+ def to_s
140
+ space_id
141
+ end
30
142
 
31
- delegate [:count, :created] => :properties
143
+ # Return the number of items in the space
144
+ # @return [Fixnum] the number of items
145
+ def count
146
+ properties.space_count.to_i
147
+ end
32
148
 
33
- after_save :reset_acls
34
- before_delete :reset_acls
149
+ # Return the creation date of the space, if persisted, or nil.
150
+ # @return [DateTime] the date
151
+ def created
152
+ if space_created = properties.space_created
153
+ DateTime.parse(space_created)
154
+ end
155
+ end
156
+
157
+ # Find a content item in the space
158
+ # @return [Duracloud::Content] the content item.
159
+ # @raise [Duracloud::NotFoundError] if the content item does not exist.
160
+ def find_content(content_id)
161
+ Content.find(space_id, content_id, store_id)
162
+ end
163
+
164
+ # Return the audit log for the space
165
+ # @return [Duracloud::AuditLog] the audit log
166
+ def audit_log
167
+ AuditLog.new(space_id, store_id)
168
+ end
35
169
 
36
- def initialize(id)
37
- @id = id
170
+ # Return the bit integrity report for the space
171
+ # @return [Duracloud::BitIntegrityReport] the report
172
+ def bit_integrity_report
173
+ BitIntergrityReport.new(space_id, store_id)
38
174
  end
39
175
 
176
+ # Return the manifest for the space
177
+ # @return [Duracloud::Manifest] the manifest
178
+ def manifest
179
+ Manifest.new(space_id, store_id)
180
+ end
181
+
182
+ # Return the ACLs for the space
183
+ # @return [Duracloud::SpaceAcls] the ACLs
40
184
  def acls
41
185
  @acls ||= SpaceAcls.new(self)
42
186
  end
43
187
 
44
- def each(prefix: nil)
45
- raise Error, "Space not yet persisted." unless persisted?
46
- num = 0
47
- while num < count
48
- response = Client.get_space(id, query: {prefix: prefix, max_results: MAX_RESULTS})
49
- xml = Nokogiri::XML(response.body)
50
- content_ids = xml.css('item').map(&:text)
51
- content_ids.each do |content_id|
52
- yield content_id
188
+ # Enumerates the content IDs in the space.
189
+ # @param prefix [String] the content ID prefix for filtering (optional)
190
+ # @param start_after [String] the content ID to be used as a "marker".
191
+ # Listing starts after this ID. (optional)
192
+ # @return [Enumerator] an enumerator.
193
+ # @raise [Duracloud::NotFoundError] the space does not exist in Duracloud.
194
+ def content_ids(prefix: nil, start_after: nil)
195
+ Enumerator.new do |yielder|
196
+ num = 0
197
+ marker = start_after
198
+ while num < count
199
+ q = query.merge(prefix: prefix, maxResults: MAX_RESULTS, marker: marker)
200
+ response = Client.get_space(space_id, **q)
201
+ xml = Nokogiri::XML(response.body)
202
+ ids = xml.css('item').map(&:text)
203
+ break if ids.empty?
204
+ ids.each do |content_id|
205
+ yielder << content_id
206
+ end
207
+ num += ids.length
208
+ marker = ids.last
53
209
  end
54
- num += content_ids.length
55
210
  end
56
211
  end
57
212
 
58
- def each_item(prefix: nil)
59
- each(prefix: prefix) do |content_id|
60
- yield Content.find(self, content_id)
213
+ # Enumerates the content items in the space.
214
+ # @see #content_ids
215
+ # @return [Enumerator] an enumerator.
216
+ # @raise [Duracloud::NotFoundError] the space does not exist in Duracloud.
217
+ def items(*args)
218
+ Enumerator.new do |yielder|
219
+ content_ids(*args).each do |content_id|
220
+ yielder << find_content(content_id)
221
+ end
61
222
  end
62
223
  end
63
224
 
@@ -68,11 +229,12 @@ module Duracloud
68
229
  end
69
230
 
70
231
  def create
71
- Client.create_space(id)
232
+ Client.create_space(id, **query)
72
233
  end
73
234
 
74
235
  def update
75
- Client.set_space_acls(id, acls)
236
+ options = { headers: acls.to_h, query: query }
237
+ Client.set_space_acls(id, **options)
76
238
  end
77
239
 
78
240
  def properties_class
@@ -80,11 +242,11 @@ module Duracloud
80
242
  end
81
243
 
82
244
  def get_properties_response
83
- Client.get_space_properties(id)
245
+ Client.get_space_properties(id, **query)
84
246
  end
85
247
 
86
248
  def do_delete
87
- Client.delete_space(id)
249
+ Client.delete_space(id, **query)
88
250
  end
89
251
 
90
252
  def do_save
@@ -95,5 +257,9 @@ module Duracloud
95
257
  end
96
258
  end
97
259
 
260
+ def query
261
+ { storeID: store_id }
262
+ end
263
+
98
264
  end
99
265
  end
@@ -11,10 +11,14 @@ module Duracloud
11
11
  super()
12
12
  @space = space
13
13
  if space.persisted?
14
- response = Client.get_space_acls(space.id)
15
- update SpaceAcls.filter(response.headers)
14
+ response = Client.get_space_acls(space.space_id, **query)
15
+ update filter(response.headers)
16
16
  end
17
17
  end
18
18
 
19
+ def query
20
+ { storeID: space.store_id }
21
+ end
22
+
19
23
  end
20
24
  end