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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.gitlab-ci.yml +70 -0
  4. data/.idea/bucket_client.iml +105 -0
  5. data/.idea/encodings.xml +4 -0
  6. data/.idea/misc.xml +7 -0
  7. data/.idea/modules.xml +8 -0
  8. data/.idea/runConfigurations/Integration_Test.xml +37 -0
  9. data/.idea/runConfigurations/Unit_Test.xml +37 -0
  10. data/.rspec +3 -0
  11. data/CODE_OF_CONDUCT.md +74 -0
  12. data/Gemfile +6 -0
  13. data/Gemfile.lock +114 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +870 -0
  16. data/Rakefile +6 -0
  17. data/bin/console +14 -0
  18. data/bin/setup +8 -0
  19. data/bucket_client.gemspec +46 -0
  20. data/integration/aws_blob_spec.rb +134 -0
  21. data/integration/aws_bucket_spec.rb +145 -0
  22. data/integration/azure_blob_spec.rb +132 -0
  23. data/integration/azure_bucket_spec.rb +132 -0
  24. data/integration/dev_blob_spec.rb +131 -0
  25. data/integration/dev_bucket_spec.rb +140 -0
  26. data/integration/do_blob_spec.rb +134 -0
  27. data/integration/do_bucket_spec.rb +144 -0
  28. data/integration/gcp_blob_spec.rb +132 -0
  29. data/integration/gcp_bucket_spec.rb +132 -0
  30. data/integration/img.jpg +0 -0
  31. data/lib/bucket_client.rb +66 -0
  32. data/lib/bucket_client/aws/aws_bucket.rb +85 -0
  33. data/lib/bucket_client/aws/aws_client.rb +195 -0
  34. data/lib/bucket_client/aws/aws_http_client.rb +32 -0
  35. data/lib/bucket_client/aws/aws_policy_factory.rb +26 -0
  36. data/lib/bucket_client/aws4_request_signer.rb +133 -0
  37. data/lib/bucket_client/azure/azure_bucket.rb +83 -0
  38. data/lib/bucket_client/azure/azure_client.rb +197 -0
  39. data/lib/bucket_client/bucket.rb +388 -0
  40. data/lib/bucket_client/bucket_operation_exception.rb +8 -0
  41. data/lib/bucket_client/client.rb +408 -0
  42. data/lib/bucket_client/dev/local_bucket.rb +84 -0
  43. data/lib/bucket_client/dev/local_client.rb +148 -0
  44. data/lib/bucket_client/digital_ocean/digital_ocean_acl_factory.rb +39 -0
  45. data/lib/bucket_client/digital_ocean/digital_ocean_bucket.rb +81 -0
  46. data/lib/bucket_client/digital_ocean/digital_ocean_client.rb +275 -0
  47. data/lib/bucket_client/digital_ocean/digital_ocean_http_client.rb +31 -0
  48. data/lib/bucket_client/gcp/gcp_bucket.rb +79 -0
  49. data/lib/bucket_client/gcp/gcp_client.rb +171 -0
  50. data/lib/bucket_client/operation_result.rb +33 -0
  51. data/lib/bucket_client/version.rb +3 -0
  52. 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