bucket_client 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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.gitlab-ci.yml +70 -0
- data/.idea/bucket_client.iml +105 -0
- data/.idea/encodings.xml +4 -0
- data/.idea/misc.xml +7 -0
- data/.idea/modules.xml +8 -0
- data/.idea/runConfigurations/Integration_Test.xml +37 -0
- data/.idea/runConfigurations/Unit_Test.xml +37 -0
- data/.rspec +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +114 -0
- data/LICENSE.txt +21 -0
- data/README.md +870 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bucket_client.gemspec +46 -0
- data/integration/aws_blob_spec.rb +134 -0
- data/integration/aws_bucket_spec.rb +145 -0
- data/integration/azure_blob_spec.rb +132 -0
- data/integration/azure_bucket_spec.rb +132 -0
- data/integration/dev_blob_spec.rb +131 -0
- data/integration/dev_bucket_spec.rb +140 -0
- data/integration/do_blob_spec.rb +134 -0
- data/integration/do_bucket_spec.rb +144 -0
- data/integration/gcp_blob_spec.rb +132 -0
- data/integration/gcp_bucket_spec.rb +132 -0
- data/integration/img.jpg +0 -0
- data/lib/bucket_client.rb +66 -0
- data/lib/bucket_client/aws/aws_bucket.rb +85 -0
- data/lib/bucket_client/aws/aws_client.rb +195 -0
- data/lib/bucket_client/aws/aws_http_client.rb +32 -0
- data/lib/bucket_client/aws/aws_policy_factory.rb +26 -0
- data/lib/bucket_client/aws4_request_signer.rb +133 -0
- data/lib/bucket_client/azure/azure_bucket.rb +83 -0
- data/lib/bucket_client/azure/azure_client.rb +197 -0
- data/lib/bucket_client/bucket.rb +388 -0
- data/lib/bucket_client/bucket_operation_exception.rb +8 -0
- data/lib/bucket_client/client.rb +408 -0
- data/lib/bucket_client/dev/local_bucket.rb +84 -0
- data/lib/bucket_client/dev/local_client.rb +148 -0
- data/lib/bucket_client/digital_ocean/digital_ocean_acl_factory.rb +39 -0
- data/lib/bucket_client/digital_ocean/digital_ocean_bucket.rb +81 -0
- data/lib/bucket_client/digital_ocean/digital_ocean_client.rb +275 -0
- data/lib/bucket_client/digital_ocean/digital_ocean_http_client.rb +31 -0
- data/lib/bucket_client/gcp/gcp_bucket.rb +79 -0
- data/lib/bucket_client/gcp/gcp_client.rb +171 -0
- data/lib/bucket_client/operation_result.rb +33 -0
- data/lib/bucket_client/version.rb +3 -0
- metadata +246 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
module BucketClient
|
2
|
+
class AWSBucket < Bucket
|
3
|
+
|
4
|
+
attr_reader :key
|
5
|
+
|
6
|
+
# @param [String] region the region of the bucket
|
7
|
+
# @param [AWSHttpClient] http the AWS http client
|
8
|
+
# @param [AWSClient] client the parent client
|
9
|
+
# @param [String] key the key of this bucket
|
10
|
+
def initialize(region, http, client, key)
|
11
|
+
@region = region
|
12
|
+
@http = http
|
13
|
+
@bucket_client = client
|
14
|
+
@key = key
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_uri(key)
|
18
|
+
"https://s3-#{@region}.amazonaws.com/#{@key}/#{key}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_blob_with_uri(uri)
|
22
|
+
@bucket_client.get_blob uri
|
23
|
+
end
|
24
|
+
|
25
|
+
def exist_blob_with_uri(uri)
|
26
|
+
@bucket_client.exist_blob(uri)
|
27
|
+
end
|
28
|
+
|
29
|
+
def put_blob_with_uri(payload, uri)
|
30
|
+
@bucket_client.put_blob payload, uri
|
31
|
+
end
|
32
|
+
|
33
|
+
def update_blob_with_uri(payload, uri)
|
34
|
+
exist = exist_blob_with_uri uri
|
35
|
+
if exist
|
36
|
+
put_blob_with_uri payload, uri
|
37
|
+
else
|
38
|
+
OperationResult.new false, "Blob does not exist", nil, 400
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete_blob_with_uri(uri)
|
43
|
+
@bucket_client.delete_blob(uri)
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete_blob_if_exist_with_uri(uri)
|
47
|
+
@bucket_client.delete_blob_if_exist(uri)
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_blob(key)
|
51
|
+
get_blob_with_uri(get_uri key)
|
52
|
+
end
|
53
|
+
|
54
|
+
def exist_blob(key)
|
55
|
+
exist_blob_with_uri(get_uri key)
|
56
|
+
end
|
57
|
+
|
58
|
+
def put_blob(payload, key)
|
59
|
+
put_blob_with_uri(payload, get_uri(key))
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_blob(payload, key)
|
63
|
+
exist = exist_blob key
|
64
|
+
if exist
|
65
|
+
OperationResult.new false, "Blob already exist", nil, 400
|
66
|
+
else
|
67
|
+
put_blob payload, key
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_blob(payload, key)
|
72
|
+
update_blob_with_uri(payload, get_uri(key))
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete_blob(key)
|
76
|
+
delete_blob_with_uri(get_uri key)
|
77
|
+
end
|
78
|
+
|
79
|
+
def delete_blob_if_exist(key)
|
80
|
+
delete_blob_if_exist_with_uri(get_uri key)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require "bucket_client/client"
|
2
|
+
require "bucket_client/aws4_request_signer"
|
3
|
+
require "bucket_client/aws/aws_http_client"
|
4
|
+
require "bucket_client/aws/aws_policy_factory"
|
5
|
+
require "bucket_client/aws/aws_bucket"
|
6
|
+
require "bucket_client/aws/aws_http_client"
|
7
|
+
require "bucket_client/operation_result"
|
8
|
+
require "mimemagic"
|
9
|
+
require "json"
|
10
|
+
|
11
|
+
module BucketClient
|
12
|
+
class AWSClient < Client
|
13
|
+
|
14
|
+
# @param [KirinHttp::Client] client Basic http client
|
15
|
+
# @param [String] id AWS id
|
16
|
+
# @param [String] secret AWS secret
|
17
|
+
# @param [String] region Region for bucket
|
18
|
+
def initialize(client, id, secret, region)
|
19
|
+
signer = AWS4RequestSigner.new(id, secret)
|
20
|
+
@region = region
|
21
|
+
@http = AWSHttpClient.new(signer, region, client)
|
22
|
+
@policy_factory = AWSPolicyFactory.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def exist_bucket(key)
|
26
|
+
resp = @http.query(:head, "https://s3-#{@region}.amazonaws.com/#{key}")
|
27
|
+
resp.code == 200
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_bucket!(key)
|
31
|
+
AWSBucket.new(@region, @http, self, key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def put_bucket(key)
|
35
|
+
exist = exist_bucket key
|
36
|
+
if exist
|
37
|
+
bucket = get_bucket! key
|
38
|
+
OperationResult.new(true, "OK", bucket, 200)
|
39
|
+
else
|
40
|
+
create_bucket key
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_bucket(key)
|
45
|
+
content = "<CreateBucketConfiguration xmlns='http://s3.amazonaws.com/doc/2006-03-01/'>
|
46
|
+
<LocationConstraint>#{@region}</LocationConstraint>
|
47
|
+
</CreateBucketConfiguration>"
|
48
|
+
endpoint = "https://s3-#{@region}.amazonaws.com/#{key}"
|
49
|
+
resp = @http.query(:put, endpoint, content)
|
50
|
+
success = resp.code === 200
|
51
|
+
if success
|
52
|
+
bucket = get_bucket! key
|
53
|
+
OperationResult.new(success, "Bucket created", bucket, 200)
|
54
|
+
else
|
55
|
+
OperationResult.new(success, resp.content, nil, resp.code)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete_bucket(key)
|
60
|
+
endpoint = "https://s3-#{@region}.amazonaws.com/#{key}"
|
61
|
+
resp = @http.query(:delete, endpoint)
|
62
|
+
success = resp.code === 204
|
63
|
+
if success
|
64
|
+
OperationResult.new(success, "Bucket deleted", nil, resp.code)
|
65
|
+
else
|
66
|
+
OperationResult.new(success, resp.content, nil, resp.code)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def delete_bucket_if_exist(key)
|
71
|
+
exist = exist_bucket key
|
72
|
+
if exist
|
73
|
+
delete_bucket key
|
74
|
+
else
|
75
|
+
OperationResult.new(true, "Bucket already deleted", nil, 204)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def set_read_policy(key, access)
|
80
|
+
raise ArgumentError.new("Read Policy not accepted") if access != :public && access != :private
|
81
|
+
resp = set_policy key, access, 10
|
82
|
+
OperationResult.new(resp.code === 204, resp.content, nil, resp.code)
|
83
|
+
end
|
84
|
+
|
85
|
+
def set_get_cors(key, cors)
|
86
|
+
resp = set_cors key, cors, 10
|
87
|
+
OperationResult.new(resp.code === 200, resp.content, nil, resp.code)
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_blob(uri)
|
91
|
+
resp = @http.query(:get, uri)
|
92
|
+
success = resp.code === 200
|
93
|
+
if success
|
94
|
+
OperationResult.new(success, "OK", resp.content, resp.code)
|
95
|
+
else
|
96
|
+
OperationResult.new(success, resp.content, nil, resp.code)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def exist_blob(uri)
|
101
|
+
@http.query(:head, uri).code === 200
|
102
|
+
end
|
103
|
+
|
104
|
+
def put_blob(payload, uri)
|
105
|
+
mime = MimeMagic.by_magic payload
|
106
|
+
resp = @http.query(:put, uri, payload, mime.type, "text/plain")
|
107
|
+
success = resp.code === 200
|
108
|
+
if success
|
109
|
+
OperationResult.new(success, "Blob put", uri, resp.code)
|
110
|
+
else
|
111
|
+
OperationResult.new(success, resp.content, nil, resp.code)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def update_blob(payload, uri)
|
116
|
+
exist = exist_blob uri
|
117
|
+
if exist
|
118
|
+
put_blob payload, uri
|
119
|
+
else
|
120
|
+
OperationResult.new(false, "Blob does not exist", nil, 404)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def delete_blob_if_exist(uri)
|
125
|
+
resp = @http.query(:delete, uri)
|
126
|
+
success = resp.code === 204
|
127
|
+
if success
|
128
|
+
OperationResult.new(success, "Blob deleted", nil, resp.code)
|
129
|
+
else
|
130
|
+
OperationResult.new(success, resp.content, nil, resp.code)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def delete_blob(uri)
|
135
|
+
exist = exist_blob uri
|
136
|
+
if exist
|
137
|
+
delete_blob_if_exist uri
|
138
|
+
else
|
139
|
+
OperationResult.new(false, "Blob does not exist", nil, 404)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def set_policy(key, access, max, count = 0)
|
146
|
+
failure = "Failed too many times due to conflict"
|
147
|
+
return OperationResult.new(false, failure, nil, 400) if ++count === max
|
148
|
+
endpoint = "https://s3-#{@region}.amazonaws.com/#{key}/?policy="
|
149
|
+
if access === :public
|
150
|
+
policy = @policy_factory.generate_policy access, key
|
151
|
+
resp = @http.query(:put, endpoint, policy.to_json, "application/json")
|
152
|
+
else
|
153
|
+
resp = @http.query :delete, endpoint
|
154
|
+
end
|
155
|
+
if resp.code === 409
|
156
|
+
set_policy key, access, max, count
|
157
|
+
else
|
158
|
+
resp
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def set_cors(key, cors, max, count = 0)
|
163
|
+
failure = "Failed too many times due to conflict"
|
164
|
+
return OperationResult.new(false, failure, nil, 400) if ++count === max
|
165
|
+
endpoint = "https://s3-#{@region}.amazonaws.com/#{key}/?cors="
|
166
|
+
if cors.length > 0
|
167
|
+
xml = cors_xml cors
|
168
|
+
resp = @http.query :put, endpoint, xml
|
169
|
+
else
|
170
|
+
resp = @http.query :delete, endpoint
|
171
|
+
end
|
172
|
+
if resp.code === 409
|
173
|
+
set_cors key, cors, max, count
|
174
|
+
else
|
175
|
+
resp
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def cors_xml(cors)
|
180
|
+
rules = cors.map(&method(:cors_rule))
|
181
|
+
"<CORSConfiguration>#{rules.join "\n"}</CORSConfiguration>"
|
182
|
+
end
|
183
|
+
|
184
|
+
def cors_rule(cors)
|
185
|
+
"<CORSRule>
|
186
|
+
<AllowedOrigin>#{cors}</AllowedOrigin>
|
187
|
+
<AllowedMethod>GET</AllowedMethod>
|
188
|
+
<AllowedHeader>*</AllowedHeader>
|
189
|
+
<MaxAgeSeconds>3000</MaxAgeSeconds>
|
190
|
+
</CORSRule>"
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "kirin_http"
|
2
|
+
|
3
|
+
module BucketClient
|
4
|
+
class AWSHttpClient
|
5
|
+
|
6
|
+
# @param [Client::AWS4RequestSigner] signer aws4 signer
|
7
|
+
# @param [String] region region of the service
|
8
|
+
# @param [KirinHttp::Client] http Http client to send http Message
|
9
|
+
def initialize(signer, region, http)
|
10
|
+
@signer = signer
|
11
|
+
@region = region
|
12
|
+
@http = http
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Symbol] method
|
16
|
+
# @param [String] endpoint
|
17
|
+
# @param [Object] content
|
18
|
+
# @param [String] type
|
19
|
+
# @return [KirinHttp::Response]
|
20
|
+
def query(method, endpoint, content = nil, type = "text/plain", accept = nil)
|
21
|
+
accept = type if accept.nil?
|
22
|
+
header = {
|
23
|
+
"Content-Type": type,
|
24
|
+
"Accept": accept
|
25
|
+
}
|
26
|
+
message = KirinHttp::Message.new(endpoint, method, content, header)
|
27
|
+
message = @signer.sign message, "s3", @region
|
28
|
+
@http.send message
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
class AWSPolicyFactory
|
4
|
+
def generate_policy(access, key)
|
5
|
+
statements = []
|
6
|
+
if access === :public
|
7
|
+
# uuid_2 = "69cab402-0674-40e6-9915-982b92016d6a"
|
8
|
+
uuid_2 = SecureRandom.uuid.to_s
|
9
|
+
policy_statement = {
|
10
|
+
"Sid": "AllowPublicRead#{uuid_2}",
|
11
|
+
"Action": ["s3:GetObject"],
|
12
|
+
"Effect": "Allow",
|
13
|
+
"Resource": "arn:aws:s3:::#{key}/*",
|
14
|
+
"Principal": "*"
|
15
|
+
}
|
16
|
+
statements.push policy_statement
|
17
|
+
end
|
18
|
+
uuid_1 = SecureRandom.uuid.to_s
|
19
|
+
# uuid_1 = "668c8c5d-3efb-458d-bebb-6fa194b55732"
|
20
|
+
{
|
21
|
+
"Id": "ReadPolicy#{uuid_1}",
|
22
|
+
"Version": "2012-10-17",
|
23
|
+
"Statement": statements
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'openssl'
|
3
|
+
require "pathname"
|
4
|
+
require 'base64'
|
5
|
+
require 'kirin_http'
|
6
|
+
require "addressable/uri"
|
7
|
+
|
8
|
+
module BucketClient
|
9
|
+
class AWS4RequestSigner
|
10
|
+
def initialize(id, secret)
|
11
|
+
@id = id
|
12
|
+
@secret = secret
|
13
|
+
@algorithm = "AWS4-HMAC-SHA256"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Signs the http request using the AWS4 signing protocol
|
17
|
+
#
|
18
|
+
# @param [KirinHttp::Message] request http request message to sign
|
19
|
+
# @param [String] service service type
|
20
|
+
# @param [String] region region of service
|
21
|
+
# @return [KirinHttp::Message]
|
22
|
+
def sign(request, service, region)
|
23
|
+
|
24
|
+
request.header["x-amz-date"] = amz_date
|
25
|
+
request.header["Content-MD5"] = request_md5(request)
|
26
|
+
request.header["x-amz-content-sha256"] = sha256 request.content
|
27
|
+
|
28
|
+
cred = credential region, service
|
29
|
+
canonical_req = canonical_request request
|
30
|
+
signed_payload = string_to_sign(cred, canonical_req)
|
31
|
+
signature = get_signature @secret, date_stamp, region, service, signed_payload
|
32
|
+
request.header["Authorization"] = auth cred, signed_headers(request), signature
|
33
|
+
request
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def auth(cred, signed_headers, signature)
|
39
|
+
[
|
40
|
+
"#{@algorithm} Credential=#{@id}/#{cred}",
|
41
|
+
"SignedHeaders=#{signed_headers}",
|
42
|
+
"Signature=#{signature}"
|
43
|
+
].join ","
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_signature(key, date, region, service, signed_payload)
|
47
|
+
k_date = hmac("AWS4" + key, date)
|
48
|
+
k_region = hmac(k_date, region)
|
49
|
+
k_service = hmac(k_region, service)
|
50
|
+
token = hmac(k_service, "aws4_request")
|
51
|
+
hmac_hex token, signed_payload
|
52
|
+
end
|
53
|
+
|
54
|
+
def string_to_sign(cred, can_req)
|
55
|
+
[
|
56
|
+
@algorithm,
|
57
|
+
amz_date,
|
58
|
+
cred,
|
59
|
+
sha256(can_req)
|
60
|
+
].join "\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
def credential(region, service)
|
64
|
+
[
|
65
|
+
date_stamp,
|
66
|
+
region,
|
67
|
+
service,
|
68
|
+
"aws4_request"
|
69
|
+
].join "/"
|
70
|
+
end
|
71
|
+
|
72
|
+
def canonical_request(request)
|
73
|
+
[
|
74
|
+
request.method.to_s.upcase,
|
75
|
+
message_path(request),
|
76
|
+
canonical_query_params(request),
|
77
|
+
header_format(request),
|
78
|
+
signed_headers(request),
|
79
|
+
sha256(request.content || '')
|
80
|
+
].join "\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def header_format(request)
|
87
|
+
sorted = request.header.dup.transform_keys {|k| k.to_s.downcase}.sort_by {|k| k}
|
88
|
+
sorted.map {|k, v| "#{k}:#{v.strip}"}.join("\n") + "\n"
|
89
|
+
end
|
90
|
+
|
91
|
+
def signed_headers(request)
|
92
|
+
request.header.dup.to_h.transform_keys(&:to_s).keys.map(&:downcase).sort.join(";")
|
93
|
+
end
|
94
|
+
|
95
|
+
def message_path(request)
|
96
|
+
Addressable::URI.parse(request.uri).path
|
97
|
+
end
|
98
|
+
|
99
|
+
def sha256(payload)
|
100
|
+
Digest::SHA256.new.update(payload || '').hexdigest
|
101
|
+
end
|
102
|
+
|
103
|
+
def request_md5(request)
|
104
|
+
np = Digest::MD5.new
|
105
|
+
np << (request.content || "")
|
106
|
+
Base64.encode64(np.digest)
|
107
|
+
end
|
108
|
+
|
109
|
+
def amz_date
|
110
|
+
Time.now.utc.strftime('%Y%m%dT%H%M%SZ')
|
111
|
+
end
|
112
|
+
|
113
|
+
def date_stamp
|
114
|
+
Time.now.utc.strftime('%Y%m%d')
|
115
|
+
end
|
116
|
+
|
117
|
+
def hmac(key, data)
|
118
|
+
OpenSSL::HMAC.digest('sha256', key, data)
|
119
|
+
end
|
120
|
+
|
121
|
+
def hmac_hex(key, data)
|
122
|
+
OpenSSL::HMAC.hexdigest('sha256', key, data)
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def canonical_query_params(request)
|
127
|
+
q = Addressable::URI.parse(request.uri).query || ""
|
128
|
+
param_map = Hash[q.split("&").map {|x| x.split("=")}].sort_by(&:to_s)
|
129
|
+
param_map.select {|k| !k.nil?}.map {|k, v| "#{k}=#{v}"}.to_a.join "&"
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|