bucket_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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