s3-light 1.0.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 +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
|