fog 0.3.1 → 0.3.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.
- data/Gemfile.lock +1 -1
- data/README.rdoc +1 -0
- data/fog.gemspec +44 -1
- data/lib/fog.rb +2 -1
- data/lib/fog/bin.rb +2 -0
- data/lib/fog/google.rb +22 -0
- data/lib/fog/google/bin.rb +20 -0
- data/lib/fog/google/models/storage/directories.rb +43 -0
- data/lib/fog/google/models/storage/directory.rb +48 -0
- data/lib/fog/google/models/storage/file.rb +87 -0
- data/lib/fog/google/models/storage/files.rb +94 -0
- data/lib/fog/google/parsers/storage/access_control_list.rb +46 -0
- data/lib/fog/google/parsers/storage/copy_object.rb +22 -0
- data/lib/fog/google/parsers/storage/get_bucket.rb +46 -0
- data/lib/fog/google/parsers/storage/get_bucket_logging.rb +40 -0
- data/lib/fog/google/parsers/storage/get_bucket_object_versions.rb +88 -0
- data/lib/fog/google/parsers/storage/get_bucket_versioning.rb +24 -0
- data/lib/fog/google/parsers/storage/get_request_payment.rb +20 -0
- data/lib/fog/google/parsers/storage/get_service.rb +32 -0
- data/lib/fog/google/requests/storage/copy_object.rb +72 -0
- data/lib/fog/google/requests/storage/delete_bucket.rb +46 -0
- data/lib/fog/google/requests/storage/delete_object.rb +50 -0
- data/lib/fog/google/requests/storage/get_bucket.rb +110 -0
- data/lib/fog/google/requests/storage/get_bucket_acl.rb +55 -0
- data/lib/fog/google/requests/storage/get_object.rb +104 -0
- data/lib/fog/google/requests/storage/get_object_acl.rb +66 -0
- data/lib/fog/google/requests/storage/get_object_torrent.rb +55 -0
- data/lib/fog/google/requests/storage/get_object_url.rb +54 -0
- data/lib/fog/google/requests/storage/get_service.rb +53 -0
- data/lib/fog/google/requests/storage/head_object.rb +64 -0
- data/lib/fog/google/requests/storage/put_bucket.rb +68 -0
- data/lib/fog/google/requests/storage/put_bucket_acl.rb +80 -0
- data/lib/fog/google/requests/storage/put_object.rb +71 -0
- data/lib/fog/google/requests/storage/put_object_url.rb +54 -0
- data/lib/fog/google/storage.rb +192 -0
- data/spec/google/models/storage/directories_spec.rb +49 -0
- data/spec/google/models/storage/directory_spec.rb +83 -0
- data/spec/google/models/storage/file_spec.rb +121 -0
- data/spec/google/models/storage/files_spec.rb +141 -0
- data/spec/google/requests/storage/copy_object_spec.rb +61 -0
- data/spec/google/requests/storage/delete_bucket_spec.rb +35 -0
- data/spec/google/requests/storage/delete_object_spec.rb +38 -0
- data/spec/google/requests/storage/get_bucket_spec.rb +110 -0
- data/spec/google/requests/storage/get_object_spec.rb +58 -0
- data/spec/google/requests/storage/get_service_spec.rb +32 -0
- data/spec/google/requests/storage/head_object_spec.rb +26 -0
- data/spec/google/requests/storage/put_bucket_spec.rb +21 -0
- data/spec/google/requests/storage/put_object_spec.rb +43 -0
- metadata +45 -2
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'pp'
|
2
|
+
module Fog
|
3
|
+
module Google
|
4
|
+
class Storage
|
5
|
+
class Real
|
6
|
+
|
7
|
+
require 'fog/google/parsers/storage/get_bucket'
|
8
|
+
|
9
|
+
# List information about objects in an Google Storage bucket
|
10
|
+
#
|
11
|
+
# ==== Parameters
|
12
|
+
# * bucket_name<~String> - name of bucket to list object keys from
|
13
|
+
# * options<~Hash> - config arguments for list. Defaults to {}.
|
14
|
+
# * 'delimiter'<~String> - causes keys with the same string between the prefix
|
15
|
+
# value and the first occurence of delimiter to be rolled up
|
16
|
+
# * 'marker'<~String> - limits object keys to only those that appear
|
17
|
+
# lexicographically after its value.
|
18
|
+
# * 'max-keys'<~Integer> - limits number of object keys returned
|
19
|
+
# * 'prefix'<~String> - limits object keys to those beginning with its value.
|
20
|
+
#
|
21
|
+
# ==== Returns
|
22
|
+
# * response<~Excon::Response>:
|
23
|
+
# * body<~Hash>:
|
24
|
+
# * 'Delimeter'<~String> - Delimiter specified for query
|
25
|
+
# * 'IsTruncated'<~Boolean> - Whether or not the listing is truncated
|
26
|
+
# * 'Marker'<~String> - Marker specified for query
|
27
|
+
# * 'MaxKeys'<~Integer> - Maximum number of keys specified for query
|
28
|
+
# * 'Name'<~String> - Name of the bucket
|
29
|
+
# * 'Prefix'<~String> - Prefix specified for query
|
30
|
+
# * 'Contents'<~Array>:
|
31
|
+
# * 'ETag'<~String>: Etag of object
|
32
|
+
# * 'Key'<~String>: Name of object
|
33
|
+
# * 'LastModified'<~String>: Timestamp of last modification of object
|
34
|
+
# * 'Owner'<~Hash>:
|
35
|
+
# * 'DisplayName'<~String> - Display name of object owner
|
36
|
+
# * 'ID'<~String> - Id of object owner
|
37
|
+
# * 'Size'<~Integer> - Size of object
|
38
|
+
# * 'StorageClass'<~String> - Storage class of object
|
39
|
+
#
|
40
|
+
def get_bucket(bucket_name, options = {})
|
41
|
+
unless bucket_name
|
42
|
+
raise ArgumentError.new('bucket_name is required')
|
43
|
+
end
|
44
|
+
request({
|
45
|
+
:expects => 200,
|
46
|
+
:headers => {},
|
47
|
+
:host => "#{bucket_name}.#{@host}",
|
48
|
+
:idempotent => true,
|
49
|
+
:method => 'GET',
|
50
|
+
:parser => Fog::Parsers::Google::Storage::GetBucket.new,
|
51
|
+
:query => options
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
class Mock
|
58
|
+
|
59
|
+
def get_bucket(bucket_name, options = {})
|
60
|
+
unless bucket_name
|
61
|
+
raise ArgumentError.new('bucket_name is required')
|
62
|
+
end
|
63
|
+
response = Excon::Response.new
|
64
|
+
name = /(\w+\.?)*/.match(bucket_name)
|
65
|
+
if bucket_name == name.to_s
|
66
|
+
if bucket = @data[:buckets][bucket_name]
|
67
|
+
contents = bucket[:objects].values.sort {|x,y| x['Key'] <=> y['Key']}.reject do |object|
|
68
|
+
(options['prefix'] && object['Key'][0...options['prefix'].length] != options['prefix']) ||
|
69
|
+
(options['marker'] && object['Key'] <= options['marker'])
|
70
|
+
end.map do |object|
|
71
|
+
data = object.reject {|key, value| !['ETag', 'Key', 'LastModified', 'Size', 'StorageClass'].include?(key)}
|
72
|
+
data.merge!({
|
73
|
+
'LastModified' => Time.parse(data['LastModified']),
|
74
|
+
'Owner' => bucket['Owner'],
|
75
|
+
'Size' => data['Size'].to_i
|
76
|
+
})
|
77
|
+
data
|
78
|
+
end
|
79
|
+
max_keys = options['max-keys'] || 1000
|
80
|
+
size = [max_keys, 1000].min
|
81
|
+
truncated_contents = contents[0...size]
|
82
|
+
|
83
|
+
response.status = 200
|
84
|
+
response.body = {
|
85
|
+
'Contents' => truncated_contents,
|
86
|
+
'IsTruncated' => truncated_contents.size != contents.size,
|
87
|
+
'Marker' => options['marker'],
|
88
|
+
'MaxKeys' => max_keys,
|
89
|
+
'Name' => bucket['Name'],
|
90
|
+
'Prefix' => options['prefix']
|
91
|
+
}
|
92
|
+
if options['max-keys'] && options['max-keys'] < response.body['Contents'].length
|
93
|
+
response.body['IsTruncated'] = true
|
94
|
+
response.body['Contents'] = response.body['Contents'][0...options['max-keys']]
|
95
|
+
end
|
96
|
+
else
|
97
|
+
response.status = 404
|
98
|
+
raise(Excon::Errors.status_error({:expects => 200}, response))
|
99
|
+
end
|
100
|
+
else
|
101
|
+
response.status = 400
|
102
|
+
raise(Excon::Errors.status_error({:expects => 200}, response))
|
103
|
+
end
|
104
|
+
response
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Fog
|
2
|
+
module Google
|
3
|
+
class Storage
|
4
|
+
class Real
|
5
|
+
|
6
|
+
require 'fog/google/parsers/storage/access_control_list'
|
7
|
+
|
8
|
+
# Get access control list for an Google Storage bucket
|
9
|
+
#
|
10
|
+
# ==== Parameters
|
11
|
+
# * bucket_name<~String> - name of bucket to get access control list for
|
12
|
+
#
|
13
|
+
# ==== Returns
|
14
|
+
# * response<~Excon::Response>:
|
15
|
+
# * body<~Hash>:
|
16
|
+
# * 'AccessControlPolicy'<~Hash>
|
17
|
+
# * 'Owner'<~Hash>:
|
18
|
+
# * 'DisplayName'<~String> - Display name of object owner
|
19
|
+
# * 'ID'<~String> - Id of object owner
|
20
|
+
# * 'AccessControlList'<~Array>:
|
21
|
+
# * 'Grant'<~Hash>:
|
22
|
+
# * 'Grantee'<~Hash>:
|
23
|
+
# * 'DisplayName'<~String> - Display name of grantee
|
24
|
+
# * 'ID'<~String> - Id of grantee
|
25
|
+
# or
|
26
|
+
# * 'URI'<~String> - URI of group to grant access for
|
27
|
+
# * 'Permission'<~String> - Permission, in [FULL_CONTROL, WRITE, WRITE_ACP, READ, READ_ACP]
|
28
|
+
#
|
29
|
+
def get_bucket_acl(bucket_name)
|
30
|
+
unless bucket_name
|
31
|
+
raise ArgumentError.new('bucket_name is required')
|
32
|
+
end
|
33
|
+
request({
|
34
|
+
:expects => 200,
|
35
|
+
:headers => {},
|
36
|
+
:host => "#{bucket_name}.#{@host}",
|
37
|
+
:idempotent => true,
|
38
|
+
:method => 'GET',
|
39
|
+
:parser => Fog::Parsers::Google::Storage::AccessControlList.new,
|
40
|
+
:query => {'acl' => nil}
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class Mock
|
47
|
+
|
48
|
+
def get_bucket_acl(bucket_name)
|
49
|
+
Fog::Mock.not_implemented
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Fog
|
2
|
+
module Google
|
3
|
+
class Storage
|
4
|
+
class Real
|
5
|
+
|
6
|
+
# Get an object from Google Storage
|
7
|
+
#
|
8
|
+
# ==== Parameters
|
9
|
+
# * bucket_name<~String> - Name of bucket to read from
|
10
|
+
# * object_name<~String> - Name of object to read
|
11
|
+
# * options<~Hash>:
|
12
|
+
# * 'If-Match'<~String> - Returns object only if its etag matches this value, otherwise returns 412 (Precondition Failed).
|
13
|
+
# * 'If-Modified-Since'<~Time> - Returns object only if it has been modified since this time, otherwise returns 304 (Not Modified).
|
14
|
+
# * 'If-None-Match'<~String> - Returns object only if its etag differs from this value, otherwise returns 304 (Not Modified)
|
15
|
+
# * 'If-Unmodified-Since'<~Time> - Returns object only if it has not been modified since this time, otherwise returns 412 (Precodition Failed).
|
16
|
+
# * 'Range'<~String> - Range of object to download
|
17
|
+
# * 'versionId'<~String> - specify a particular version to retrieve
|
18
|
+
#
|
19
|
+
# ==== Returns
|
20
|
+
# * response<~Excon::Response>:
|
21
|
+
# * body<~String> - Contents of object
|
22
|
+
# * headers<~Hash>:
|
23
|
+
# * 'Content-Length'<~String> - Size of object contents
|
24
|
+
# * 'Content-Type'<~String> - MIME type of object
|
25
|
+
# * 'ETag'<~String> - Etag of object
|
26
|
+
# * 'Last-Modified'<~String> - Last modified timestamp for object
|
27
|
+
#
|
28
|
+
def get_object(bucket_name, object_name, options = {}, &block)
|
29
|
+
unless bucket_name
|
30
|
+
raise ArgumentError.new('bucket_name is required')
|
31
|
+
end
|
32
|
+
unless object_name
|
33
|
+
raise ArgumentError.new('object_name is required')
|
34
|
+
end
|
35
|
+
if version_id = options.delete('versionId')
|
36
|
+
query = {'versionId' => version_id}
|
37
|
+
end
|
38
|
+
headers = {}
|
39
|
+
headers['If-Modified-Since'] = options['If-Modified-Since'].utc.strftime("%a, %d %b %Y %H:%M:%S +0000") if options['If-Modified-Since']
|
40
|
+
headers['If-Unmodified-Since'] = options['If-Unmodified-Since'].utc.strftime("%a, %d %b %Y %H:%M:%S +0000") if options['If-Modified-Since']
|
41
|
+
headers.merge!(options)
|
42
|
+
request({
|
43
|
+
:expects => 200,
|
44
|
+
:headers => headers,
|
45
|
+
:host => "#{bucket_name}.#{@host}",
|
46
|
+
:idempotent => true,
|
47
|
+
:method => 'GET',
|
48
|
+
:path => CGI.escape(object_name),
|
49
|
+
:query => query
|
50
|
+
}, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
class Mock
|
56
|
+
|
57
|
+
def get_object(bucket_name, object_name, options = {}, &block)
|
58
|
+
unless bucket_name
|
59
|
+
raise ArgumentError.new('bucket_name is required')
|
60
|
+
end
|
61
|
+
unless object_name
|
62
|
+
raise ArgumentError.new('object_name is required')
|
63
|
+
end
|
64
|
+
response = Excon::Response.new
|
65
|
+
if (bucket = @data[:buckets][bucket_name]) && (object = bucket[:objects][object_name])
|
66
|
+
if options['If-Match'] && options['If-Match'] != object['ETag']
|
67
|
+
response.status = 412
|
68
|
+
elsif options['If-Modified-Since'] && options['If-Modified-Since'] > Time.parse(object['LastModified'])
|
69
|
+
response.status = 304
|
70
|
+
elsif options['If-None-Match'] && options['If-None-Match'] == object['ETag']
|
71
|
+
response.status = 304
|
72
|
+
elsif options['If-Unmodified-Since'] && options['If-Unmodified-Since'] < Time.parse(object['LastModified'])
|
73
|
+
response.status = 412
|
74
|
+
else
|
75
|
+
response.status = 200
|
76
|
+
response.headers = {
|
77
|
+
'Content-Length' => object['Size'],
|
78
|
+
'Content-Type' => object['Content-Type'],
|
79
|
+
'ETag' => object['ETag'],
|
80
|
+
'Last-Modified' => object['LastModified']
|
81
|
+
}
|
82
|
+
unless block_given?
|
83
|
+
response.body = object[:body]
|
84
|
+
else
|
85
|
+
data = StringIO.new(object[:body])
|
86
|
+
remaining = data.length
|
87
|
+
while remaining > 0
|
88
|
+
chunk = data.read([remaining, Excon::CHUNK_SIZE].min)
|
89
|
+
block.call(chunk)
|
90
|
+
remaining -= Excon::CHUNK_SIZE
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
else
|
95
|
+
response.status = 404
|
96
|
+
raise(Excon::Errors.status_error({:expects => 200}, response))
|
97
|
+
end
|
98
|
+
response
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Fog
|
2
|
+
module Google
|
3
|
+
class Storage
|
4
|
+
class Real
|
5
|
+
|
6
|
+
require 'fog/google/parsers/storage/access_control_list'
|
7
|
+
|
8
|
+
# Get access control list for an Google Storage object
|
9
|
+
#
|
10
|
+
# ==== Parameters
|
11
|
+
# * bucket_name<~String> - name of bucket containing object
|
12
|
+
# * object_name<~String> - name of object to get access control list for
|
13
|
+
# * options<~Hash>:
|
14
|
+
# * 'versionId'<~String> - specify a particular version to retrieve
|
15
|
+
#
|
16
|
+
# ==== Returns
|
17
|
+
# * response<~Excon::Response>:
|
18
|
+
# * body<~Hash>:
|
19
|
+
# * 'AccessControlPolicy'<~Hash>
|
20
|
+
# * 'Owner'<~Hash>:
|
21
|
+
# * 'DisplayName'<~String> - Display name of object owner
|
22
|
+
# * 'ID'<~String> - Id of object owner
|
23
|
+
# * 'AccessControlList'<~Array>:
|
24
|
+
# * 'Grant'<~Hash>:
|
25
|
+
# * 'Grantee'<~Hash>:
|
26
|
+
# * 'DisplayName'<~String> - Display name of grantee
|
27
|
+
# * 'ID'<~String> - Id of grantee
|
28
|
+
# or
|
29
|
+
# * 'URI'<~String> - URI of group to grant access for
|
30
|
+
# * 'Permission'<~String> - Permission, in [FULL_CONTROL, WRITE, WRITE_ACP, READ, READ_ACP]
|
31
|
+
#
|
32
|
+
def get_object_acl(bucket_name, object_name, options = {})
|
33
|
+
unless bucket_name
|
34
|
+
raise ArgumentError.new('bucket_name is required')
|
35
|
+
end
|
36
|
+
unless object_name
|
37
|
+
raise ArgumentError.new('object_name is required')
|
38
|
+
end
|
39
|
+
query = {'acl' => nil}
|
40
|
+
if version_id = options.delete('versionId')
|
41
|
+
query['versionId'] = version_id
|
42
|
+
end
|
43
|
+
request({
|
44
|
+
:expects => 200,
|
45
|
+
:headers => {},
|
46
|
+
:host => "#{bucket_name}.#{@host}",
|
47
|
+
:idempotent => true,
|
48
|
+
:method => 'GET',
|
49
|
+
:parser => Fog::Parsers::Google::Storage::AccessControlList.new,
|
50
|
+
:path => CGI.escape(object_name),
|
51
|
+
:query => query
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
class Mock
|
58
|
+
|
59
|
+
def get_object_acl(bucket_name, object_name)
|
60
|
+
Fog::Mock.not_implemented
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Fog
|
2
|
+
module Google
|
3
|
+
class Storage
|
4
|
+
class Real
|
5
|
+
|
6
|
+
# Get torrent for an Google Storage object
|
7
|
+
#
|
8
|
+
# ==== Parameters
|
9
|
+
# * bucket_name<~String> - name of bucket containing object
|
10
|
+
# * object_name<~String> - name of object to get torrent for
|
11
|
+
#
|
12
|
+
# ==== Returns
|
13
|
+
# * response<~Excon::Response>:
|
14
|
+
# * body<~Hash>:
|
15
|
+
# * 'AccessControlPolicy'<~Hash>
|
16
|
+
# * 'Owner'<~Hash>:
|
17
|
+
# * 'DisplayName'<~String> - Display name of object owner
|
18
|
+
# * 'ID'<~String> - Id of object owner
|
19
|
+
# * 'AccessControlList'<~Array>:
|
20
|
+
# * 'Grant'<~Hash>:
|
21
|
+
# * 'Grantee'<~Hash>:
|
22
|
+
# * 'DisplayName'<~String> - Display name of grantee
|
23
|
+
# * 'ID'<~String> - Id of grantee
|
24
|
+
# * 'Permission'<~String> - Permission, in [FULL_CONTROL, WRITE, WRITE_ACP, READ, READ_ACP]
|
25
|
+
#
|
26
|
+
def get_object_torrent(bucket_name, object_name)
|
27
|
+
unless bucket_name
|
28
|
+
raise ArgumentError.new('bucket_name is required')
|
29
|
+
end
|
30
|
+
unless object_name
|
31
|
+
raise ArgumentError.new('object_name is required')
|
32
|
+
end
|
33
|
+
request({
|
34
|
+
:expects => 200,
|
35
|
+
:headers => {},
|
36
|
+
:host => "#{bucket_name}.#{@host}",
|
37
|
+
:idempotent => true,
|
38
|
+
:method => 'GET',
|
39
|
+
:path => CGI.escape(object_name),
|
40
|
+
:query => {'torrent' => nil}
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class Mock
|
47
|
+
|
48
|
+
def get_object_object(bucket_name, object_name)
|
49
|
+
Fog::Mock.not_implemented
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Fog
|
2
|
+
module Google
|
3
|
+
class Storage
|
4
|
+
class Real
|
5
|
+
|
6
|
+
# Get an expiring object url from Google Storage
|
7
|
+
#
|
8
|
+
# ==== Parameters
|
9
|
+
# * bucket_name<~String> - Name of bucket containing object
|
10
|
+
# * object_name<~String> - Name of object to get expiring url for
|
11
|
+
# * expires<~Time> - An expiry time for this url
|
12
|
+
#
|
13
|
+
# ==== Returns
|
14
|
+
# * response<~Excon::Response>:
|
15
|
+
# * body<~String> - url for object
|
16
|
+
#
|
17
|
+
def get_object_url(bucket_name, object_name, expires)
|
18
|
+
unless bucket_name
|
19
|
+
raise ArgumentError.new('bucket_name is required')
|
20
|
+
end
|
21
|
+
unless object_name
|
22
|
+
raise ArgumentError.new('object_name is required')
|
23
|
+
end
|
24
|
+
url({
|
25
|
+
:headers => {},
|
26
|
+
:host => "#{bucket_name}.#{@host}",
|
27
|
+
:method => 'GET',
|
28
|
+
:path => CGI.escape(object_name)
|
29
|
+
}, expires)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
class Mock
|
35
|
+
|
36
|
+
def get_object_url(bucket_name, object_name, expires)
|
37
|
+
unless bucket_name
|
38
|
+
raise ArgumentError.new('bucket_name is required')
|
39
|
+
end
|
40
|
+
unless object_name
|
41
|
+
raise ArgumentError.new('object_name is required')
|
42
|
+
end
|
43
|
+
url({
|
44
|
+
:headers => {},
|
45
|
+
:host => "#{bucket_name}.#{@host}",
|
46
|
+
:method => 'GET',
|
47
|
+
:path => CGI.escape(object_name)
|
48
|
+
}, expires)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|