duracloud-client 0.0.3 → 0.1.0

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.
@@ -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