duracloud-client 0.6.0 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/duracloud/abstract_entity.rb +2 -3
- data/lib/duracloud/chunked_content.rb +1 -0
- data/lib/duracloud/content.rb +2 -1
- data/lib/duracloud/properties.rb +7 -95
- data/lib/duracloud/response.rb +5 -0
- data/lib/duracloud/space.rb +6 -4
- data/lib/duracloud/space_acls.rb +3 -6
- data/lib/duracloud/version.rb +1 -1
- data/lib/duracloud.rb +0 -2
- data/spec/spec_helper.rb +12 -0
- data/spec/unit/content_spec.rb +20 -4
- data/spec/unit/properties_spec.rb +9 -7
- data/spec/unit/space_spec.rb +17 -16
- metadata +2 -4
- data/lib/duracloud/content_properties.rb +0 -7
- data/lib/duracloud/space_properties.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37ba6ce5cc5015994ed96ce7d5fc1ad987b5ef96
|
4
|
+
data.tar.gz: 34d6471b5dbc0a84b65c06f8c2604f7e3c5b0f88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 239504e09b5efb3d8db2e46afd0d24d935dcd8258c67ab996b302946ba9bb3a1322f6f385864e1f955be4d073a0790e8fd57202f6a788c7b6303189c7a137c23
|
7
|
+
data.tar.gz: 85b33cbfb16b8f4529fb88b6120904929407fe5532c7699c0a4a350d51fc9dcf9c645a7e1d25ec31eea399dd569de3cfd07339a4751ad51c22ba29fdce2e92bb
|
data/README.md
CHANGED
@@ -192,13 +192,13 @@ D, [2016-04-29T18:31:16.975749 #32379] DEBUG -- : Duracloud::Client HEAD https:/
|
|
192
192
|
>> content.properties
|
193
193
|
=> #<Duracloud::ContentProperties x-dura-meta-owner="ellen@example.com">
|
194
194
|
|
195
|
-
>> content.properties
|
195
|
+
>> content.properties["x-dura-meta-creator"] = "bob@example.com"
|
196
196
|
>> content.save
|
197
197
|
D, [2016-04-29T18:31:52.770195 #32379] DEBUG -- : Duracloud::Client POST https://foo.duracloud.org/durastore/rest-api-testing/foo3 200 OK
|
198
198
|
I, [2016-04-29T18:31:52.770293 #32379] INFO -- : Content foo3 updated successfully
|
199
199
|
=> true
|
200
200
|
|
201
|
-
>> content.properties
|
201
|
+
>> content.properties["x-dura-meta-creator"]
|
202
202
|
D, [2016-04-29T18:32:06.465928 #32379] DEBUG -- : Duracloud::Client HEAD https://foo.duracloud.org/durastore/rest-api-testing/foo3 200 OK
|
203
203
|
=> "bob@example.com"
|
204
204
|
```
|
@@ -42,7 +42,7 @@ module Duracloud
|
|
42
42
|
# but does not exist in Duracloud
|
43
43
|
def properties
|
44
44
|
load_properties if persisted? && @properties.nil?
|
45
|
-
@properties ||=
|
45
|
+
@properties ||= Properties.new
|
46
46
|
end
|
47
47
|
|
48
48
|
|
@@ -76,8 +76,7 @@ module Duracloud
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def properties=(props)
|
79
|
-
|
80
|
-
@properties = properties_class.new(filtered)
|
79
|
+
@properties = Properties.new(props)
|
81
80
|
end
|
82
81
|
|
83
82
|
def reset_properties
|
data/lib/duracloud/content.rb
CHANGED
@@ -46,7 +46,7 @@ module Duracloud
|
|
46
46
|
end
|
47
47
|
|
48
48
|
attr_accessor :space_id, :content_id, :store_id,
|
49
|
-
:body, :md5, :content_type, :size
|
49
|
+
:body, :md5, :content_type, :size, :modified
|
50
50
|
alias_method :id, :content_id
|
51
51
|
validates_presence_of :space_id, :content_id
|
52
52
|
|
@@ -168,6 +168,7 @@ module Duracloud
|
|
168
168
|
self.properties = response.headers
|
169
169
|
self.content_type = response.content_type
|
170
170
|
self.size = response.size
|
171
|
+
self.modified = response.modified
|
171
172
|
end
|
172
173
|
|
173
174
|
def do_delete
|
data/lib/duracloud/properties.rb
CHANGED
@@ -6,109 +6,30 @@ module Duracloud
|
|
6
6
|
#
|
7
7
|
# Encapsulates Duracloud "properties" which are transmitted via HTTP headers.
|
8
8
|
#
|
9
|
-
# @abstract
|
10
|
-
#
|
11
9
|
class Properties < Hashie::Mash
|
12
10
|
|
13
11
|
PREFIX = "x-dura-meta-".freeze
|
12
|
+
PREFIX_RE = /\A#{PREFIX}/i
|
14
13
|
|
15
|
-
#
|
16
|
-
SPACE = /\A#{PREFIX}space-(count|created)\z/
|
17
|
-
|
18
|
-
# Space ACL headers
|
19
|
-
SPACE_ACLS = /\A#{PREFIX}acl-/
|
20
|
-
|
21
|
-
# Copy Content headers
|
22
|
-
COPY_CONTENT = /\A#{PREFIX}copy-source(-store)?\z/
|
23
|
-
|
24
|
-
# DuraCloud internal content properties
|
25
|
-
INTERNAL = /\A#{PREFIX}content-(mimetype|size|checksum|modified)\z/
|
26
|
-
|
27
|
-
# Properties set by the DuraCloud SyncTool
|
28
|
-
SYNCTOOL = /\A#{PREFIX}(creator|(content-file-(created|modified|last-accessed|path)))\z/
|
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.
|
14
|
+
# Is the property name valid?
|
33
15
|
# @param prop [String] the property name
|
34
16
|
# @return [Boolean]
|
35
17
|
def self.property?(prop)
|
36
|
-
|
37
|
-
end
|
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
|
43
|
-
def self.filter(hsh)
|
44
|
-
hsh.select { |k, v| property?(k) }
|
45
|
-
end
|
46
|
-
|
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)
|
51
|
-
prop.start_with?(PREFIX)
|
52
|
-
end
|
53
|
-
|
54
|
-
# Is the property a reserved "internal" DuraCloud property?
|
55
|
-
# @param prop [String] the property name
|
56
|
-
# @return [Boolean]
|
57
|
-
def self.internal_property?(prop)
|
58
|
-
INTERNAL =~ prop
|
18
|
+
!!( PREFIX_RE =~ prop.to_s )
|
59
19
|
end
|
60
20
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
def self.space_property?(prop)
|
65
|
-
SPACE =~ prop
|
21
|
+
def initialize(source = nil, default = nil, &block)
|
22
|
+
source.select! { |k, v| property?(k) } if source
|
23
|
+
super
|
66
24
|
end
|
67
25
|
|
68
|
-
# Is the property a space ACL?
|
69
|
-
# @param prop [String] the property name
|
70
|
-
# @return [Boolean]
|
71
|
-
def self.space_acl?(prop)
|
72
|
-
SPACE_ACLS =~ prop
|
73
|
-
end
|
74
|
-
|
75
|
-
# Is the property used for copying content?
|
76
|
-
# @param prop [String] the property name
|
77
|
-
# @return [Boolean]
|
78
|
-
def self.copy_content_property?(prop)
|
79
|
-
COPY_CONTENT =~ prop
|
80
|
-
end
|
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
|
88
26
|
def property?(prop)
|
89
27
|
self.class.property?(prop)
|
90
28
|
end
|
91
29
|
|
92
|
-
# Filter the hash of properties, selecting only the properties valid
|
93
|
-
# for this particular usage (subclass).
|
94
|
-
# @param hsh [Hash] the unfiltered properties
|
95
|
-
# @return [Hash] the filtered properties
|
96
|
-
def filter(hsh)
|
97
|
-
self.class.filter(hsh)
|
98
|
-
end
|
99
|
-
|
100
|
-
# @api private
|
101
|
-
def regular_writer(key, value)
|
102
|
-
if property?(key)
|
103
|
-
super
|
104
|
-
else
|
105
|
-
raise Error, "#{self.class}: Unrecognized or restricted property \"#{key}\"."
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
30
|
# @api private
|
110
31
|
def convert_key(key)
|
111
|
-
force_ascii(
|
32
|
+
force_ascii(key)
|
112
33
|
end
|
113
34
|
|
114
35
|
# @api private
|
@@ -127,15 +48,6 @@ module Duracloud
|
|
127
48
|
|
128
49
|
private
|
129
50
|
|
130
|
-
# Coerce to a DuraCloud property
|
131
|
-
def duracloud_property!(prop)
|
132
|
-
prop.dup.tap do |p|
|
133
|
-
p.gsub!(/_/, '-')
|
134
|
-
p.downcase!
|
135
|
-
p.prepend(PREFIX) unless self.class.duracloud_property?(p)
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
51
|
def convert_array(value)
|
140
52
|
value.uniq!
|
141
53
|
if value.length > 1
|
data/lib/duracloud/response.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "forwardable"
|
2
|
+
require "date"
|
2
3
|
|
3
4
|
module Duracloud
|
4
5
|
class Response
|
@@ -43,5 +44,9 @@ module Duracloud
|
|
43
44
|
def size
|
44
45
|
header["content-length"].first.to_i rescue nil
|
45
46
|
end
|
47
|
+
|
48
|
+
def modified
|
49
|
+
DateTime.parse(header["last-modified"].first) rescue nil
|
50
|
+
end
|
46
51
|
end
|
47
52
|
end
|
data/lib/duracloud/space.rb
CHANGED
@@ -141,16 +141,18 @@ module Duracloud
|
|
141
141
|
|
142
142
|
# Return the number of items in the space
|
143
143
|
# @return [Fixnum] the number of items
|
144
|
+
# @note If the count is over 1000, DuraCloud sets the
|
145
|
+
# x-dura-meta-space-count header to "1000+".
|
146
|
+
# This method will in that case return 1000, indicating
|
147
|
+
# that the exact count must be retrieved by other means.
|
144
148
|
def count
|
145
|
-
properties.
|
149
|
+
properties["x-dura-meta-space-count"].to_i
|
146
150
|
end
|
147
151
|
|
148
152
|
# Return the creation date of the space, if persisted, or nil.
|
149
153
|
# @return [DateTime] the date
|
150
154
|
def created
|
151
|
-
|
152
|
-
DateTime.parse(space_created)
|
153
|
-
end
|
155
|
+
DateTime.parse(properties["x-dura-meta-space-created"]) rescue nil
|
154
156
|
end
|
155
157
|
|
156
158
|
# Find a content item in the space
|
data/lib/duracloud/space_acls.rb
CHANGED
@@ -6,18 +6,15 @@ module Duracloud
|
|
6
6
|
|
7
7
|
ACL_PREFIX = (PREFIX + "acl-").freeze
|
8
8
|
|
9
|
-
def self.property?(prop)
|
10
|
-
space_acl?(prop)
|
11
|
-
end
|
12
|
-
|
13
9
|
attr_reader :space
|
14
10
|
|
15
11
|
def initialize(space)
|
16
|
-
super()
|
17
12
|
@space = space
|
18
13
|
if space.persisted?
|
19
14
|
response = Client.get_space_acls(space.space_id, **query)
|
20
|
-
|
15
|
+
super(response.headers)
|
16
|
+
else
|
17
|
+
super()
|
21
18
|
end
|
22
19
|
end
|
23
20
|
|
data/lib/duracloud/version.rb
CHANGED
data/lib/duracloud.rb
CHANGED
@@ -13,7 +13,6 @@ module Duracloud
|
|
13
13
|
autoload :Connection, "duracloud/connection"
|
14
14
|
autoload :Content, "duracloud/content"
|
15
15
|
autoload :ContentManifest, "duracloud/content_manifest"
|
16
|
-
autoload :ContentProperties, "duracloud/content_properties"
|
17
16
|
autoload :DurastoreRequest, "duracloud/durastore_request"
|
18
17
|
autoload :ErrorHandler, "duracloud/error_handler"
|
19
18
|
autoload :HasProperties, "duracloud/has_properties"
|
@@ -25,7 +24,6 @@ module Duracloud
|
|
25
24
|
autoload :RestMethods, "duracloud/rest_methods"
|
26
25
|
autoload :Space, "duracloud/space"
|
27
26
|
autoload :SpaceAcls, "duracloud/space_acls"
|
28
|
-
autoload :SpaceProperties, "duracloud/space_properties"
|
29
27
|
autoload :Store, "duracloud/store"
|
30
28
|
autoload :SyncValidation, "duracloud/sync_validation"
|
31
29
|
autoload :TSV, "duracloud/tsv"
|
data/spec/spec_helper.rb
CHANGED
@@ -4,6 +4,18 @@ require "webmock/rspec"
|
|
4
4
|
|
5
5
|
WebMock.disable_net_connect!(allow_localhost: true)
|
6
6
|
|
7
|
+
# Monkey-patches WebMock to not alter header names
|
8
|
+
WebMock::Util::Headers.class_eval do
|
9
|
+
def self.normalize_headers(headers)
|
10
|
+
return nil unless headers
|
11
|
+
headers.each do |name, value|
|
12
|
+
if value.is_a?(Array) && value.size == 1
|
13
|
+
headers[name] = value.first
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
7
19
|
Duracloud::Client.configure do |config|
|
8
20
|
config.host = "example.com"
|
9
21
|
config.user = "testuser"
|
data/spec/unit/content_spec.rb
CHANGED
@@ -8,21 +8,37 @@ module Duracloud
|
|
8
8
|
describe "when it exists" do
|
9
9
|
subject { described_class.find(space_id: "foo", content_id: "bar") }
|
10
10
|
describe "and it is not chunked" do
|
11
|
-
before {
|
11
|
+
before {
|
12
|
+
stub_request(:head, url)
|
13
|
+
.to_return(headers: {
|
14
|
+
"Content-MD5"=>"164e9aee34c0c42915716e11d5d539b5",
|
15
|
+
"Content-Length"=>"1811970",
|
16
|
+
"Content-Type"=>"image/jpeg",
|
17
|
+
"Last-Modified"=>"2017-06-12T17:36:58",
|
18
|
+
})
|
19
|
+
}
|
12
20
|
it { is_expected.to be_a described_class }
|
13
21
|
it { is_expected.to_not be_chunked }
|
22
|
+
its(:md5) { is_expected.to eq "164e9aee34c0c42915716e11d5d539b5" }
|
23
|
+
its(:size) { is_expected.to eq 1811970 }
|
24
|
+
its(:content_type) { is_expected.to eq "image/jpeg" }
|
25
|
+
its(:modified) { is_expected.to eq DateTime.parse("2017-06-12T17:36:58") }
|
14
26
|
end
|
15
27
|
describe "and it is chunked" do
|
16
28
|
let(:manifest_xml) { File.read(File.expand_path("../../fixtures/content_manifest.xml", __FILE__)) }
|
17
29
|
before do
|
18
30
|
stub_request(:head, url).to_return(status: 404)
|
19
31
|
stub_request(:head, manifest_url)
|
32
|
+
.to_return(headers: {
|
33
|
+
"Last-Modified"=>"2017-06-26T16:38:42",
|
34
|
+
})
|
20
35
|
stub_request(:get, manifest_url).to_return(body: manifest_xml)
|
21
36
|
end
|
22
37
|
it { is_expected.to be_a described_class }
|
23
38
|
its(:md5) { is_expected.to eq "164e9aee34c0c42915716e11d5d539b5" }
|
24
39
|
its(:size) { is_expected.to eq 4227858432 }
|
25
40
|
its(:content_type) { is_expected.to eq "application/octet-stream" }
|
41
|
+
its(:modified) { is_expected.to eq DateTime.parse("2017-06-26T16:38:42") }
|
26
42
|
it { is_expected.to be_chunked }
|
27
43
|
end
|
28
44
|
end
|
@@ -153,7 +169,7 @@ module Duracloud
|
|
153
169
|
.with(headers: {'x-dura-meta-creator'=>'testuser'})
|
154
170
|
}
|
155
171
|
it "updates the properties" do
|
156
|
-
subject.properties
|
172
|
+
subject.properties["x-dura-meta-creator"] = "testuser"
|
157
173
|
subject.save
|
158
174
|
end
|
159
175
|
end
|
@@ -199,9 +215,9 @@ module Duracloud
|
|
199
215
|
'Content-MD5'=>'08a008a01d498c404b0c30852b39d3b8'})
|
200
216
|
end
|
201
217
|
specify {
|
202
|
-
pending "Research Webmock problem with return headers"
|
203
218
|
content = Content.find(space_id: "foo", content_id: "bar")
|
204
|
-
|
219
|
+
puts content.properties.to_h
|
220
|
+
expect(content.properties["x-dura-meta-creator"]).to eq('testuser')
|
205
221
|
}
|
206
222
|
end
|
207
223
|
|
@@ -1,13 +1,15 @@
|
|
1
1
|
module Duracloud
|
2
2
|
RSpec.describe Properties do
|
3
3
|
|
4
|
-
describe "
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
describe "
|
10
|
-
|
4
|
+
describe "ignores non-DuraCloud keys" do
|
5
|
+
subject { described_class.new("access-control-allow-headers"=>"Content-Type, Authorization", "access-control-allow-methods"=>"GET, POST, PUT, DELETE", "access-control-allow-origin"=>"*", "cache-control"=>"no-cache=\"set-cookie\"", "content-type"=>"application/octet-stream", "date"=>"Wed, 12 Jul 2017 14:52:41 GMT", "expires"=>"0", "pragma"=>"no-cache", "server"=>"Apache-Coyote/1.1", "strict-transport-security"=>"max-age=31536000 ; includeSubDomains", "vary"=>"Accept-Encoding", "x-content-type-options"=>"nosniff", "x-dura-meta-space-count"=>"1000+", "x-dura-meta-space-created"=>"2017-07-06T20:35:39", "x-frame-options"=>"DENY", "x-xss-protection"=>"1; mode=block", "connection"=>"keep-alive") }
|
6
|
+
its(:to_h) { is_expected.to eq({"x-dura-meta-space-count"=>"1000+", "x-dura-meta-space-created"=>"2017-07-06T20:35:39"}) }
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "preserves original keys" do
|
10
|
+
subject { described_class.new("x-dura-meta-acl-foo_bar"=>"READ", "x-dura-meta-acl-group-spam-eggs"=>"WRITE") }
|
11
|
+
its(:to_h) { is_expected.to eq({"x-dura-meta-acl-foo_bar"=>"READ", "x-dura-meta-acl-group-spam-eggs"=>"WRITE"}) }
|
12
|
+
end
|
11
13
|
|
12
14
|
end
|
13
15
|
end
|
data/spec/unit/space_spec.rb
CHANGED
@@ -52,9 +52,15 @@ EOS
|
|
52
52
|
describe "when found" do
|
53
53
|
before {
|
54
54
|
stub_request(:head, url)
|
55
|
+
.to_return(headers: {
|
56
|
+
"x-dura-meta-space-count"=>"1000+",
|
57
|
+
"x-dura-meta-space-created"=>"2017-05-18T20:03:18",
|
58
|
+
})
|
55
59
|
}
|
56
60
|
it { is_expected.to be_a(Space) }
|
57
61
|
its(:space_id) { is_expected.to eq("foo") }
|
62
|
+
its(:count) { is_expected.to eq 1000 }
|
63
|
+
its(:created) { is_expected.to eq DateTime.parse("2017-05-18T20:03:18") }
|
58
64
|
end
|
59
65
|
describe "when not found" do
|
60
66
|
before {
|
@@ -123,22 +129,17 @@ EOS
|
|
123
129
|
stub_request(:head, "#{url}/foo1")
|
124
130
|
stub_request(:head, "#{url}/foo2")
|
125
131
|
stub_request(:head, "#{url}/foo3")
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
headers: {
|
138
|
-
'x-dura-meta-space-count'=>'3',
|
139
|
-
'x-dura-meta-space-created'=>'2016-04-05T17:59:11'
|
140
|
-
})
|
141
|
-
}
|
132
|
+
stub_request(:get, "#{url}?maxResults=1000")
|
133
|
+
.to_return(body: body,
|
134
|
+
headers: {
|
135
|
+
'X-Dura-Meta-Space-Count'=>'3',
|
136
|
+
'X-Dura-Meta-Space-Created'=>'2016-04-05T17:59:11'
|
137
|
+
})
|
138
|
+
stub_request(:head, url)
|
139
|
+
.to_return(headers: {
|
140
|
+
'x-dura-meta-space-count'=>'3',
|
141
|
+
'x-dura-meta-space-created'=>'2016-04-05T17:59:11'
|
142
|
+
})
|
142
143
|
}
|
143
144
|
describe "class methods" do
|
144
145
|
specify {
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: duracloud-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Chandek-Stark
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashie
|
@@ -174,7 +174,6 @@ files:
|
|
174
174
|
- lib/duracloud/connection.rb
|
175
175
|
- lib/duracloud/content.rb
|
176
176
|
- lib/duracloud/content_manifest.rb
|
177
|
-
- lib/duracloud/content_properties.rb
|
178
177
|
- lib/duracloud/durastore_request.rb
|
179
178
|
- lib/duracloud/error.rb
|
180
179
|
- lib/duracloud/error_handler.rb
|
@@ -185,7 +184,6 @@ files:
|
|
185
184
|
- lib/duracloud/rest_methods.rb
|
186
185
|
- lib/duracloud/space.rb
|
187
186
|
- lib/duracloud/space_acls.rb
|
188
|
-
- lib/duracloud/space_properties.rb
|
189
187
|
- lib/duracloud/store.rb
|
190
188
|
- lib/duracloud/sync_validation.rb
|
191
189
|
- lib/duracloud/tsv.rb
|