s3-light 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +184 -0
- data/Rakefile +8 -0
- data/lefthook.yml +7 -0
- data/lib/s3-light/bucket.rb +61 -0
- data/lib/s3-light/client/buckets_list.rb +118 -0
- data/lib/s3-light/client/objects_list.rb +206 -0
- data/lib/s3-light/client.rb +54 -0
- data/lib/s3-light/concurrent_result.rb +21 -0
- data/lib/s3-light/configuration.rb +11 -0
- data/lib/s3-light/connection/body.rb +64 -0
- data/lib/s3-light/connection/response.rb +25 -0
- data/lib/s3-light/connection.rb +191 -0
- data/lib/s3-light/connections_manager/main.rb +43 -0
- data/lib/s3-light/connections_manager/threaded.rb +44 -0
- data/lib/s3-light/md5_calculator.rb +49 -0
- data/lib/s3-light/object.rb +133 -0
- data/lib/s3-light/version.rb +5 -0
- data/lib/s3-light.rb +28 -0
- data/sig/s3/light.rbs +6 -0
- data/tmp/.keep +0 -0
- metadata +112 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module S3Light
|
4
|
+
class ConcurrentResult
|
5
|
+
def initialize
|
6
|
+
@hash = Concurrent::Hash.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(key, value)
|
10
|
+
@hash[key] = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
@hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
"#<#{self.class.name} @hash=#{@hash}>"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module S3Light
|
4
|
+
class Connection
|
5
|
+
class Body
|
6
|
+
def initialize(body)
|
7
|
+
@body = body
|
8
|
+
validate_body_type!
|
9
|
+
@size = calculate_size
|
10
|
+
@io = to_io
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :size
|
14
|
+
|
15
|
+
def read(length = nil, outbuf = nil)
|
16
|
+
@io.read(length, outbuf)
|
17
|
+
end
|
18
|
+
|
19
|
+
def rewind
|
20
|
+
@io.rewind
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def to_io
|
25
|
+
case @body
|
26
|
+
when String
|
27
|
+
StringIO.new(@body)
|
28
|
+
when StringIO, IO
|
29
|
+
@body
|
30
|
+
when Enumerable
|
31
|
+
StringIO.new(@body.to_a.join)
|
32
|
+
when nil
|
33
|
+
StringIO.new('')
|
34
|
+
else
|
35
|
+
raise RequestError, "body of wrong type: #{@body.class}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def calculate_size
|
40
|
+
case @body
|
41
|
+
when String
|
42
|
+
@body.bytesize
|
43
|
+
when StringIO, IO
|
44
|
+
@body.size
|
45
|
+
when Enumerable
|
46
|
+
@body.to_a.join.bytesize
|
47
|
+
when nil
|
48
|
+
0
|
49
|
+
else
|
50
|
+
raise RequestError, 'cannot determine size of body'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_body_type!
|
55
|
+
return if @body.is_a?(String)
|
56
|
+
return if @body.respond_to?(:read)
|
57
|
+
return if @body.is_a?(Enumerable)
|
58
|
+
return if @body.nil?
|
59
|
+
|
60
|
+
raise RequestError, "body of wrong type: #{@body.class}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module S3Light
|
4
|
+
class Connection
|
5
|
+
class Response
|
6
|
+
def initialize(http_response)
|
7
|
+
@http_response = http_response
|
8
|
+
end
|
9
|
+
|
10
|
+
def xml
|
11
|
+
return @xml if defined? @xml
|
12
|
+
|
13
|
+
@xml = Nokogiri::XML(@http_response.body.to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
def code
|
17
|
+
@http_response.code
|
18
|
+
end
|
19
|
+
|
20
|
+
def body
|
21
|
+
@http_response.body
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module S3Light
|
4
|
+
class Connection
|
5
|
+
class HttpError < StandardError
|
6
|
+
attr_reader :response
|
7
|
+
|
8
|
+
def initialize(response)
|
9
|
+
@response = response
|
10
|
+
super("HTTP Error: #{response.code} - #{response.body}")
|
11
|
+
end
|
12
|
+
|
13
|
+
def code
|
14
|
+
@response.code
|
15
|
+
end
|
16
|
+
end
|
17
|
+
attr_reader :endpoint
|
18
|
+
|
19
|
+
def initialize(endpoint:, access_key_id:, secret_access_key:, ssl_context:)
|
20
|
+
@endpoint = URI(endpoint)
|
21
|
+
@access_key_id = access_key_id
|
22
|
+
@secret_access_key = secret_access_key
|
23
|
+
@ssl_context = ssl_context
|
24
|
+
@opened = false
|
25
|
+
end
|
26
|
+
|
27
|
+
def persistent_connection
|
28
|
+
return @persistent_connection if @persistent_connection
|
29
|
+
|
30
|
+
@persistent_connection = HTTP.persistent(@endpoint).headers(
|
31
|
+
'User-Agent' => "S3Light/#{S3Light::VERSION}",
|
32
|
+
'Host' => @endpoint.host
|
33
|
+
)
|
34
|
+
ObjectSpace.define_finalizer(self, self.class.close_connection(@persistent_connection))
|
35
|
+
|
36
|
+
@persistent_connection
|
37
|
+
end
|
38
|
+
|
39
|
+
def make_request(method, path, headers: {}, body: nil)
|
40
|
+
@opened = true
|
41
|
+
full_path = URI.join(@endpoint, path).to_s
|
42
|
+
request_time = Time.now.utc
|
43
|
+
body = body.is_a?(Body) ? body : Body.new(body)
|
44
|
+
|
45
|
+
headers = build_headers(method, full_path, headers, body, request_time)
|
46
|
+
|
47
|
+
response = stream_request(method, full_path, headers, body)
|
48
|
+
|
49
|
+
handle_response(response)
|
50
|
+
end
|
51
|
+
|
52
|
+
def download_file(path, output_path)
|
53
|
+
@opened = true
|
54
|
+
full_path = URI.join(@endpoint, path).to_s
|
55
|
+
request_time = Time.now.utc
|
56
|
+
|
57
|
+
headers = build_headers('GET', full_path, {}, Body.new(nil), request_time)
|
58
|
+
|
59
|
+
File.open(output_path, 'wb') do |file|
|
60
|
+
response = persistent_connection.headers(headers).get(full_path, ssl_context: @ssl_context)
|
61
|
+
raise HttpError.new(response) if response.code > 399
|
62
|
+
|
63
|
+
response.body.each do |chunk|
|
64
|
+
file.write(chunk)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
output_path
|
69
|
+
end
|
70
|
+
|
71
|
+
def close
|
72
|
+
@persistent_connection&.close
|
73
|
+
@opened = false
|
74
|
+
end
|
75
|
+
|
76
|
+
def ==(other_connection)
|
77
|
+
@endpoint == other_connection.endpoint
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#<#{self.class.name} @endpoint=#{@endpoint} @opened=#{@opened}>"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.close_connection(connection)
|
85
|
+
proc do
|
86
|
+
connection.close
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def stream_request(method, full_path, headers, body)
|
92
|
+
if body.size > 0
|
93
|
+
headers['Content-Length'] = body.size.to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
persistent_connection.headers(headers).request(
|
97
|
+
method,
|
98
|
+
full_path,
|
99
|
+
ssl_context: @ssl_context,
|
100
|
+
body: body
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_headers(method, path, headers, body, request_time)
|
105
|
+
canonical_headers = {
|
106
|
+
'host' => @endpoint.host,
|
107
|
+
'x-amz-date' => request_time.strftime('%Y%m%dT%H%M%SZ'),
|
108
|
+
'x-amz-content-sha256' => 'UNSIGNED-PAYLOAD'
|
109
|
+
}
|
110
|
+
|
111
|
+
canonical_headers['content-length'] = body.size.to_s if body.size > 0
|
112
|
+
|
113
|
+
signed_headers = canonical_headers.keys.sort.join(';')
|
114
|
+
|
115
|
+
auth_header = authorization_header(
|
116
|
+
method,
|
117
|
+
path,
|
118
|
+
canonical_headers,
|
119
|
+
signed_headers,
|
120
|
+
body,
|
121
|
+
request_time
|
122
|
+
)
|
123
|
+
|
124
|
+
headers.merge(canonical_headers).merge('Authorization' => auth_header)
|
125
|
+
end
|
126
|
+
|
127
|
+
def authorization_header(method, path, canonical_headers, signed_headers, body, request_time)
|
128
|
+
algorithm = 'AWS4-HMAC-SHA256'
|
129
|
+
credential_scope = credential_scope(request_time)
|
130
|
+
string_to_sign = string_to_sign(method, path, canonical_headers, signed_headers, body, request_time)
|
131
|
+
signature = signature(string_to_sign, request_time)
|
132
|
+
|
133
|
+
"#{algorithm} Credential=#{@access_key_id}/#{credential_scope}, SignedHeaders=#{signed_headers}, Signature=#{signature}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def credential_scope(request_time)
|
137
|
+
date = request_time.strftime('%Y%m%d')
|
138
|
+
region = @endpoint.host.split('.')[1] || 'us-east-1'
|
139
|
+
"#{date}/#{region}/s3/aws4_request"
|
140
|
+
end
|
141
|
+
|
142
|
+
def string_to_sign(method, path, canonical_headers, signed_headers, body, request_time)
|
143
|
+
uri = URI(path)
|
144
|
+
canonical_request = [
|
145
|
+
method.to_s.upcase,
|
146
|
+
uri.path,
|
147
|
+
uri.query || '',
|
148
|
+
canonical_headers.sort.map { |k, v| "#{k}:#{v.strip}" }.join("\n") + "\n",
|
149
|
+
signed_headers,
|
150
|
+
'UNSIGNED-PAYLOAD'
|
151
|
+
].join("\n")
|
152
|
+
|
153
|
+
[
|
154
|
+
'AWS4-HMAC-SHA256',
|
155
|
+
request_time.strftime('%Y%m%dT%H%M%SZ'),
|
156
|
+
credential_scope(request_time),
|
157
|
+
Digest::SHA256.hexdigest(canonical_request)
|
158
|
+
].join("\n")
|
159
|
+
end
|
160
|
+
|
161
|
+
def signature(string_to_sign, request_time)
|
162
|
+
date = request_time.strftime('%Y%m%d')
|
163
|
+
region = @endpoint.host.split('.')[1] || 'us-east-1'
|
164
|
+
service = 's3'
|
165
|
+
|
166
|
+
k_date = hmac("AWS4#{@secret_access_key}", date)
|
167
|
+
k_region = hmac(k_date, region)
|
168
|
+
k_service = hmac(k_region, service)
|
169
|
+
k_signing = hmac(k_service, 'aws4_request')
|
170
|
+
|
171
|
+
OpenSSL::HMAC.hexdigest('sha256', k_signing, string_to_sign)
|
172
|
+
end
|
173
|
+
|
174
|
+
def hmac(key, value)
|
175
|
+
OpenSSL::HMAC.digest('sha256', key, value)
|
176
|
+
end
|
177
|
+
|
178
|
+
def handle_response(response)
|
179
|
+
case response.code
|
180
|
+
when 200..299
|
181
|
+
Connection::Response.new(response)
|
182
|
+
else
|
183
|
+
raise HttpError.new(response)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def chunked?(headers)
|
188
|
+
headers['Transfer-Encoding'] == 'chunked'
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module S3Light
|
4
|
+
module ConnectionsManager
|
5
|
+
class Main
|
6
|
+
def initialize
|
7
|
+
@connections = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def yield_connection(client, &block)
|
11
|
+
client_connection = nil
|
12
|
+
@connections.each do |existing_client, connection|
|
13
|
+
if client.endpoint == existing_client.endpoint
|
14
|
+
client_connection = connection
|
15
|
+
break
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
client_connection ||= create_new_connection(client)
|
20
|
+
@connections[client] = client_connection
|
21
|
+
|
22
|
+
block.call(client_connection)
|
23
|
+
end
|
24
|
+
|
25
|
+
def close_all_connections
|
26
|
+
return if @connections.empty?
|
27
|
+
@connections.each do |_, connection|
|
28
|
+
connection.close
|
29
|
+
end
|
30
|
+
|
31
|
+
@connections = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def create_new_connection(client)
|
36
|
+
Connection.new(
|
37
|
+
endpoint: client.endpoint, access_key_id: client.access_key_id, secret_access_key: client.secret_access_key,
|
38
|
+
ssl_context: client.ssl_context
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module S3Light
|
4
|
+
module ConnectionsManager
|
5
|
+
class Threaded
|
6
|
+
def initialize(client, concurrency)
|
7
|
+
@client = client
|
8
|
+
@thread_pool = Concurrent::FixedThreadPool.new(concurrency)
|
9
|
+
@connections = Concurrent::Array.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def with_connection
|
13
|
+
@thread_pool.post do
|
14
|
+
existing_connection = Thread.current[:connection]
|
15
|
+
yield existing_connection if existing_connection
|
16
|
+
|
17
|
+
new_connection = create_new_connection
|
18
|
+
@connections << new_connection
|
19
|
+
Thread.current[:connection] = new_connection
|
20
|
+
|
21
|
+
yield new_connection
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
@connections.each(&:close)
|
27
|
+
@connections = Concurrent::Array.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def wait_to_finish
|
31
|
+
@thread_pool.shutdown
|
32
|
+
@thread_pool.wait_for_termination
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def create_new_connection
|
37
|
+
Connection.new(
|
38
|
+
endpoint: @client.endpoint, access_key_id: @client.access_key_id, secret_access_key: @client.secret_access_key,
|
39
|
+
ssl_context: @client.ssl_context
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module S3Light
|
4
|
+
class Md5Calculator
|
5
|
+
def initialize(input)
|
6
|
+
@input = input
|
7
|
+
end
|
8
|
+
|
9
|
+
def md5
|
10
|
+
@md5 ||=
|
11
|
+
case @input
|
12
|
+
when String
|
13
|
+
Digest::MD5.hexdigest(@input)
|
14
|
+
when IO, StringIO
|
15
|
+
compute_md5_for_io
|
16
|
+
when Pathname
|
17
|
+
compute_md5_for_file_path
|
18
|
+
else
|
19
|
+
raise ArgumentError, "Unsupported input type: #{@input.class}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
"#<#{self.class.name}>"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def compute_md5_for_io
|
29
|
+
Digest::MD5.new.tap do |md5|
|
30
|
+
io_buffer_size = S3Light.configuration.io_buffer_size
|
31
|
+
while chunk = @input.read(io_buffer_size)
|
32
|
+
md5 << chunk
|
33
|
+
end
|
34
|
+
@input.rewind
|
35
|
+
end.hexdigest
|
36
|
+
end
|
37
|
+
|
38
|
+
def compute_md5_for_file_path
|
39
|
+
Digest::MD5.new.tap do |md5|
|
40
|
+
io_buffer_size = S3Light.configuration.io_buffer_size
|
41
|
+
File.open(@input, 'rb') do |file|
|
42
|
+
while chunk = file.read(io_buffer_size)
|
43
|
+
md5 << chunk
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end.hexdigest
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module S3Light
|
4
|
+
Object = Struct.new(:client, :bucket, :key, :input, :persisted) do
|
5
|
+
def initialize(client, bucket, id, input, persisted = false)
|
6
|
+
super(client, bucket, id, input, persisted)
|
7
|
+
end
|
8
|
+
|
9
|
+
def persisted?
|
10
|
+
persisted
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"#<#{self.class.name} @id=#{key} @bucket=#{bucket.name}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
def save!
|
18
|
+
raise S3Light::Error, 'Input is empty' if input.nil?
|
19
|
+
|
20
|
+
self.client.with_connection do |connection|
|
21
|
+
__save!(connection)
|
22
|
+
end
|
23
|
+
self.persisted = true
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def destroy!
|
29
|
+
unless persisted
|
30
|
+
raise S3Light::Error, 'Object does not exist'
|
31
|
+
end
|
32
|
+
|
33
|
+
self.client.with_connection do |connection|
|
34
|
+
__destroy!(connection)
|
35
|
+
rescue S3Light::Connection::HttpError => e
|
36
|
+
raise e unless e.code == 404
|
37
|
+
end
|
38
|
+
|
39
|
+
self.persisted = false
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def acl
|
44
|
+
return @acl if defined?(@acl)
|
45
|
+
|
46
|
+
response = client.with_connection do |connection|
|
47
|
+
connection.make_request(:get, "/#{bucket.name}/#{key}?acl=1")
|
48
|
+
end
|
49
|
+
|
50
|
+
@acl = response.xml.remove_namespaces!.xpath('//AccessControlList/Grant').each_with_object({}) do |grant, result|
|
51
|
+
permission = grant.at_xpath('Permission').text
|
52
|
+
grantee = grant.at_xpath('Grantee')
|
53
|
+
|
54
|
+
grantee_type = grantee['type']
|
55
|
+
|
56
|
+
identifier =
|
57
|
+
case grantee_type
|
58
|
+
when 'CanonicalUser'
|
59
|
+
grantee.at_xpath('ID')&.text || 'CanonicalUser'
|
60
|
+
when 'Group'
|
61
|
+
grantee.at_xpath('URI')&.text
|
62
|
+
else
|
63
|
+
grantee.at_xpath('EmailAddress')&.text
|
64
|
+
end
|
65
|
+
|
66
|
+
result[identifier] = permission if identifier
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def acl=(new_acl)
|
72
|
+
xml_body = build_acl_xml(new_acl)
|
73
|
+
|
74
|
+
client.with_connection do |connection|
|
75
|
+
connection.make_request(:put, "/#{bucket.name}/#{key}?acl=1", body: xml_body)
|
76
|
+
end
|
77
|
+
|
78
|
+
@acl = new_acl
|
79
|
+
end
|
80
|
+
|
81
|
+
def download(to: nil)
|
82
|
+
raise S3Light::Error, 'Object does not exist' unless persisted
|
83
|
+
|
84
|
+
to ||= "/tmp/#{SecureRandom.hex}-#{key}"
|
85
|
+
|
86
|
+
self.client.with_connection do |connection|
|
87
|
+
__download(to, connection)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def open(mode = 'r', &block)
|
92
|
+
raise S3Light::Error, 'Object does not exist' unless persisted
|
93
|
+
download_path = download
|
94
|
+
file = File.open(download_path, mode)
|
95
|
+
yield file
|
96
|
+
self.input = file
|
97
|
+
file
|
98
|
+
end
|
99
|
+
|
100
|
+
def __save!(connection)
|
101
|
+
connection.make_request(:put, "/#{bucket.name}/#{key}", body: input)
|
102
|
+
end
|
103
|
+
|
104
|
+
def __destroy!(connection)
|
105
|
+
connection.make_request(:delete, "/#{bucket.name}/#{key}")
|
106
|
+
end
|
107
|
+
|
108
|
+
def __download(to, connection)
|
109
|
+
connection.download_file("/#{bucket.name}/#{key}", to)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
def build_acl_xml(acl)
|
114
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
115
|
+
xml.AccessControlPolicy {
|
116
|
+
xml.AccessControlList {
|
117
|
+
acl.each do |grantee, permission|
|
118
|
+
xml.Grant {
|
119
|
+
xml.Grantee('xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:type' => 'CanonicalUser') {
|
120
|
+
xml.ID grantee
|
121
|
+
xml.DisplayName grantee
|
122
|
+
}
|
123
|
+
xml.Permission permission
|
124
|
+
}
|
125
|
+
end
|
126
|
+
}
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
builder.to_xml
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/s3-light.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
require 'pathname'
|
5
|
+
require 'stringio'
|
6
|
+
require 'digest'
|
7
|
+
require 'base64'
|
8
|
+
require 'time'
|
9
|
+
require 'openssl/hmac'
|
10
|
+
require 'nokogiri'
|
11
|
+
require 'concurrent-ruby'
|
12
|
+
|
13
|
+
Dir.glob(File.join(File.dirname(__FILE__), 's3-light', '**', '*.rb')).each do |file|
|
14
|
+
require file
|
15
|
+
end
|
16
|
+
|
17
|
+
module S3Light
|
18
|
+
class Error < StandardError; end
|
19
|
+
class << self
|
20
|
+
def self.configure
|
21
|
+
yield(configuration)
|
22
|
+
end
|
23
|
+
|
24
|
+
def configuration
|
25
|
+
@configuration ||= Configuration.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/sig/s3/light.rbs
ADDED
data/tmp/.keep
ADDED
File without changes
|