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,148 @@
1
+ require "fileutils"
2
+ module BucketClient
3
+ class LocalClient < Client
4
+
5
+ attr_reader :path, :principal
6
+
7
+ def initialize(path, principal = nil)
8
+ @path = path
9
+ @principal = principal || path
10
+ unless File.directory? path
11
+ FileUtils.mkdir_p path
12
+ end
13
+ end
14
+
15
+ # @param [String] key
16
+ def exist_bucket(key)
17
+ raise ArgumentError.new("Cannot contain slash") if key.count("/") > 0 || key.count("\\") > 0
18
+ File.directory? bucket_dir(key)
19
+ end
20
+
21
+ def get_bucket!(key)
22
+ LocalBucket.new(self, key)
23
+ end
24
+
25
+ def put_bucket(key)
26
+ val = get_bucket! key
27
+ dir = bucket_dir key
28
+ FileUtils.mkdir_p dir unless File.directory? dir
29
+ OperationResult.new(true, "OK", val, 200)
30
+ end
31
+
32
+ def create_bucket(key)
33
+ exist = exist_bucket key
34
+ if exist
35
+ OperationResult.new(false, "Bucket already exist", nil, 409)
36
+ else
37
+ put_bucket key
38
+ end
39
+ end
40
+
41
+ def delete_bucket_if_exist(key)
42
+ dir = bucket_dir key
43
+ FileUtils.remove_dir dir if File.directory? dir
44
+ OperationResult.new(true, "OK", nil, 204)
45
+ end
46
+
47
+ def delete_bucket(key)
48
+ exist = exist_bucket key
49
+ if exist
50
+ delete_bucket_if_exist key
51
+ else
52
+ OperationResult.new(false, "Bucket does not exist", nil, 404)
53
+ end
54
+ end
55
+
56
+ def set_read_policy(key, access)
57
+ raise ArgumentError.new("Read Policy not accepted") if access != :public && access != :private
58
+ exist = exist_bucket key
59
+ OperationResult.new(exist, "", nil, exist ? 200 : 404)
60
+ end
61
+
62
+ def set_get_cors(key, _)
63
+ exist = exist_bucket key
64
+ OperationResult.new(exist, "", nil, exist ? 200 : 404)
65
+ end
66
+
67
+ def get_blob(uri)
68
+ begin
69
+ path = get_blob_path uri
70
+ bin = IO.binread path
71
+ OperationResult.new(true, "OK", bin, 200)
72
+ rescue StandardError => e
73
+ OperationResult.new(false, e.message, nil, 400)
74
+ end
75
+ end
76
+
77
+ def exist_blob(uri)
78
+ path = get_blob_path uri
79
+ Pathname.new(path).exist?
80
+ end
81
+
82
+ def put_blob(payload, uri)
83
+ begin
84
+ path = get_blob_path uri
85
+ dir = File.dirname path
86
+ FileUtils.mkdir_p dir unless File.directory? dir
87
+ IO.binwrite path, payload
88
+ OperationResult.new(true, "OK", uri, 204)
89
+ rescue StandardError => e
90
+ OperationResult.new(false, e.message, nil, 400)
91
+ end
92
+ end
93
+
94
+ def update_blob(payload, uri)
95
+ exist = exist_blob uri
96
+ if exist
97
+ put_blob payload, uri
98
+ else
99
+ OperationResult.new(false, "Blob does not exist", nil, 404)
100
+ end
101
+ end
102
+
103
+ def delete_blob_if_exist(uri)
104
+ begin
105
+ path = get_blob_path uri
106
+ FileUtils.rm_f path if exist_blob uri
107
+ OperationResult.new(true, "Deleted", nil, 204)
108
+ rescue StandardError => e
109
+ OperationResult.new(false, e.message, nil, 400)
110
+ end
111
+ end
112
+
113
+ def delete_blob(uri)
114
+ exist = exist_blob uri
115
+ if exist
116
+ delete_blob_if_exist uri
117
+ else
118
+ OperationResult.new(false, "Bucket does not exist", nil, 404)
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def get_blob_path(uri)
125
+ data = break_uri uri
126
+ blob_dir data[:bucket], data[:blob]
127
+ end
128
+
129
+ def break_uri(uri)
130
+ url = Addressable::URI.parse uri
131
+ path = ("/" + url.path).split(principal).drop(1).join(principal)
132
+ arr = path.split "/"
133
+ {:bucket => arr[0], :blob => arr.drop(1).join("/")}
134
+ end
135
+
136
+ def bucket_dir(key)
137
+ combine(@path, key)
138
+ end
139
+
140
+ def blob_dir(bucket, key)
141
+ combine(@path, bucket, key)
142
+ end
143
+
144
+ def combine(*args)
145
+ Pathname.new(File.join(args)).cleanpath.to_s
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,39 @@
1
+ class DigitalOceanACLFactory
2
+ def generate_acl(access, owner_id)
3
+ if access === :public
4
+ "<AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">
5
+ <Owner>
6
+ <ID>#{owner_id}</ID>
7
+ </Owner>
8
+ <AccessControlList>
9
+ <Grant>
10
+ <Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">
11
+ <ID>#{owner_id}</ID>
12
+ </Grantee>
13
+ <Permission>FULL_CONTROL</Permission>
14
+ </Grant>
15
+ <Grant>
16
+ <Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\">
17
+ <URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>
18
+ </Grantee>
19
+ <Permission>READ</Permission>
20
+ </Grant>
21
+ </AccessControlList>
22
+ </AccessControlPolicy>"
23
+ else
24
+ "<AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">
25
+ <Owner>
26
+ <ID>#{owner_id}</ID>
27
+ </Owner>
28
+ <AccessControlList>
29
+ <Grant>
30
+ <Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">
31
+ <ID>#{owner_id}</ID>
32
+ </Grantee>
33
+ <Permission>FULL_CONTROL</Permission>
34
+ </Grant>
35
+ </AccessControlList>
36
+ </AccessControlPolicy>"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,81 @@
1
+ module BucketClient
2
+ class DigitalOceanBucket < Bucket
3
+ attr_reader :key
4
+ # @param [String] region the region of the bucket
5
+ # @param [DigitalOceanHttpClient] http the Digital Ocean http client
6
+ # @param [DigitalOceanClient] client the parent client
7
+ # @param [String] key the key of this bucket
8
+ def initialize(region, http, client, key)
9
+ @region = region
10
+ @http = http
11
+ @bucket_client = client
12
+ @key = key
13
+ end
14
+
15
+ def get_uri(key)
16
+ "https://#{@region}.digitaloceanspaces.com/#{@key}/#{key}"
17
+ end
18
+
19
+ def get_blob_with_uri(uri)
20
+ @bucket_client.get_blob uri
21
+ end
22
+
23
+ def exist_blob_with_uri(uri)
24
+ @bucket_client.exist_blob(uri)
25
+ end
26
+
27
+ def put_blob_with_uri(payload, uri)
28
+ @bucket_client.put_blob payload, uri
29
+ end
30
+
31
+ def update_blob_with_uri(payload, uri)
32
+ exist = exist_blob_with_uri uri
33
+ if exist
34
+ put_blob_with_uri payload, uri
35
+ else
36
+ OperationResult.new false, "Blob does not exist", nil, 400
37
+ end
38
+ end
39
+
40
+ def delete_blob_with_uri(uri)
41
+ @bucket_client.delete_blob(uri)
42
+ end
43
+
44
+ def delete_blob_if_exist_with_uri(uri)
45
+ @bucket_client.delete_blob_if_exist(uri)
46
+ end
47
+
48
+ def get_blob(key)
49
+ get_blob_with_uri(get_uri key)
50
+ end
51
+
52
+ def exist_blob(key)
53
+ exist_blob_with_uri(get_uri key)
54
+ end
55
+
56
+ def put_blob(payload, key)
57
+ put_blob_with_uri(payload, get_uri(key))
58
+ end
59
+
60
+ def create_blob(payload, key)
61
+ exist = exist_blob key
62
+ if exist
63
+ OperationResult.new false, "Blob already exist", nil, 400
64
+ else
65
+ put_blob payload, key
66
+ end
67
+ end
68
+
69
+ def update_blob(payload, key)
70
+ update_blob_with_uri(payload, get_uri(key))
71
+ end
72
+
73
+ def delete_blob(key)
74
+ delete_blob_with_uri(get_uri key)
75
+ end
76
+
77
+ def delete_blob_if_exist(key)
78
+ delete_blob_if_exist_with_uri(get_uri key)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,275 @@
1
+ require "bucket_client/client"
2
+ require "bucket_client/aws4_request_signer"
3
+ require "bucket_client/operation_result"
4
+ require "json"
5
+ require "mimemagic"
6
+ require "bucket_client/digital_ocean/digital_ocean_http_client"
7
+ require "bucket_client/digital_ocean/digital_ocean_bucket"
8
+ require "bucket_client/digital_ocean/digital_ocean_acl_factory"
9
+ require "addressable/uri"
10
+ require "ox"
11
+
12
+ module ArrayHash
13
+ def access(symbol)
14
+ raise ArgumentError "Cannot find symbol #{symbol.to_s}" unless has_key symbol
15
+ r = find {|x| !x[symbol].nil?}[symbol]
16
+ r.extend(ArrayHash)
17
+ r
18
+ end
19
+
20
+ def get(symbol)
21
+ raise ArgumentError "Cannot find symbol #{symbol.to_s}" if self[symbol].nil?
22
+ r = self[symbol]
23
+ r.extend(ArrayHash)
24
+ r
25
+ end
26
+
27
+ def has_key(symbol)
28
+ self.any? {|x| !x[symbol].nil?}
29
+ end
30
+ end
31
+
32
+ module BucketClient
33
+ class DigitalOceanClient < Client
34
+ # @param [KirinHttp::Client] client Basic http client
35
+ # @param [String] id AWS id
36
+ # @param [String] secret AWS secret
37
+ # @param [String] region Region for bucket
38
+ def initialize(client, id, secret, region)
39
+ signer = AWS4RequestSigner.new(id, secret)
40
+ @region = region
41
+ @http = DigitalOceanHttpClient.new(signer, region, client)
42
+ @acl_factory = DigitalOceanACLFactory.new
43
+ end
44
+
45
+ def exist_bucket(key)
46
+ resp = @http.query(:head, "https://#{@region}.digitaloceanspaces.com/#{key}")
47
+ resp.code == 200
48
+ end
49
+
50
+ def put_bucket(key)
51
+ exist = exist_bucket key
52
+ if exist
53
+ bucket = get_bucket! key
54
+ OperationResult.new(true, "OK", bucket, 200)
55
+ else
56
+ create_bucket key
57
+ end
58
+ end
59
+
60
+ def create_bucket(key)
61
+ endpoint = "https://#{@region}.digitaloceanspaces.com/#{key}"
62
+ resp = @http.query(:put, endpoint)
63
+ success = resp.code === 200
64
+ if success
65
+ bucket = get_bucket! key
66
+ OperationResult.new(success, "Bucket created", bucket, 200)
67
+ else
68
+ OperationResult.new(success, resp.content, nil, resp.code)
69
+ end
70
+ end
71
+
72
+ def delete_bucket_if_exist(key)
73
+ exist = exist_bucket key
74
+ if exist
75
+ delete_bucket key
76
+ else
77
+ OperationResult.new(true, "Bucket already deleted", nil, 204)
78
+ end
79
+ end
80
+
81
+ def delete_bucket(key)
82
+ endpoint = "https://#{@region}.digitaloceanspaces.com/#{key}"
83
+ resp = @http.query(:delete, endpoint)
84
+ success = resp.code === 204
85
+ if success
86
+ OperationResult.new(success, "Bucket deleted", nil, resp.code)
87
+ else
88
+ OperationResult.new(success, resp.content, nil, resp.code)
89
+ end
90
+ end
91
+
92
+ def get_bucket!(key)
93
+ DigitalOceanBucket.new(@region, @http, self, key)
94
+ end
95
+
96
+ def set_get_cors(key, cors)
97
+ resp = set_cors key, cors, 10
98
+ OperationResult.new(resp.code === 200, resp.content, nil, resp.code)
99
+ end
100
+
101
+ def set_read_policy(key, access)
102
+ raise ArgumentError.new("Read Policy not accepted") if access != :public && access != :private
103
+ resp = set_bucket_acl key, access, 10
104
+ return resp unless resp.success
105
+ object_uri_resp = get_all_object_uri key
106
+ return object_uri_resp unless object_uri_resp.success
107
+ object_uri_resp.value.each do |uri|
108
+ acl_resp = set_blob_acl uri, access, 10
109
+ return acl_resp unless acl_resp.success
110
+ end
111
+ resp
112
+ end
113
+
114
+ def get_blob(uri)
115
+ resp = @http.query :get, uri
116
+ success = resp.code === 200
117
+ if success
118
+ OperationResult.new(success, "OK", resp.content, resp.code)
119
+ else
120
+ OperationResult.new(success, resp.content, nil, resp.code)
121
+ end
122
+ end
123
+
124
+ def exist_blob(uri)
125
+ @http.query(:head, uri).code === 200
126
+ end
127
+
128
+ def put_blob(payload, uri)
129
+ mime = MimeMagic.by_magic payload
130
+ resp = @http.query :put, uri, payload, mime.type, "application/xml"
131
+ return OperationResult.new(false, resp.content, nil, 400) unless resp.code === 200
132
+ url = Addressable::URI.parse uri
133
+ bucket = url.path.split("/").find {|x| !x.nil? && !x.empty?}
134
+ domain = url.host.split(".").first
135
+ bucket = domain unless domain == @region
136
+ is_public = bucket_public bucket
137
+ return is_public unless is_public.success
138
+ access = is_public.value ? :public : :private
139
+ resp = set_blob_acl uri, access, 10
140
+ return resp unless resp.code === 200
141
+ OperationResult.new(true, "OK", uri, resp.code)
142
+ end
143
+
144
+ def update_blob(payload, uri)
145
+ exist = exist_blob uri
146
+ if exist
147
+ put_blob payload, uri
148
+ else
149
+ OperationResult.new(false, "Blob does not exist", nil, 404)
150
+ end
151
+ end
152
+
153
+ def delete_blob_if_exist(uri)
154
+ resp = @http.query :delete, uri
155
+ success = resp.code === 204
156
+ if success
157
+ OperationResult.new(success, "Blob deleted", nil, resp.code)
158
+ else
159
+ OperationResult.new(success, resp.content, nil, resp.code)
160
+ end
161
+ end
162
+
163
+ def delete_blob(uri)
164
+ exist = exist_blob uri
165
+ if exist
166
+ delete_blob_if_exist uri
167
+ else
168
+ OperationResult.new(false, "Blob does not exist", nil, 404)
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def bucket_public(key)
175
+ endpoint = "https://#{@region}.digitaloceanspaces.com/#{key}/?acl="
176
+ resp = @http.query :get, endpoint
177
+ success = resp.code === 200
178
+ if success
179
+ acl_xml = Ox.load(resp.content, mode: :hash)[:AccessControlPolicy]
180
+ acl_xml.extend(ArrayHash)
181
+ begin
182
+ uri = acl_xml.access(:AccessControlList).get(:Grant).access(:Grantee).access(:URI)
183
+ value = uri === "http://acs.amazonaws.com/groups/global/AllUsers"
184
+ rescue
185
+ value = false
186
+ end
187
+ OperationResult.new(success, "OK", value, 200)
188
+ else
189
+ OperationResult.new(success, resp.content, nil, resp.code)
190
+ end
191
+ end
192
+
193
+ def set_cors(key, cors, max, count = 0)
194
+ failure = "Failed too many times due to conflict"
195
+ return OperationResult.new(false, failure, nil, 400) if ++count === max
196
+ endpoint = "https://#{@region}.digitaloceanspaces.com/#{key}/?cors="
197
+ if cors.length > 0
198
+ xml = cors_xml cors
199
+ resp = @http.query :put, endpoint, xml
200
+ else
201
+ resp = @http.query :delete, endpoint, nil, "text/plain"
202
+ end
203
+ if resp.code === 409
204
+ set_cors key, cors, max, count
205
+ else
206
+ resp
207
+ end
208
+ end
209
+
210
+ def cors_xml(cors)
211
+ rules = cors.map(&method(:cors_rule))
212
+ "<CORSConfiguration>#{rules.join "\n"}</CORSConfiguration>"
213
+ end
214
+
215
+ def cors_rule(cors)
216
+ "<CORSRule>
217
+ <AllowedOrigin>#{cors}</AllowedOrigin>
218
+ <AllowedMethod>GET</AllowedMethod>
219
+ <AllowedHeader>*</AllowedHeader>
220
+ <MaxAgeSeconds>3000</MaxAgeSeconds>
221
+ </CORSRule>"
222
+ end
223
+
224
+ def get_all_object_uri(key)
225
+ endpoint = "https://#{@region}.digitaloceanspaces.com/#{key}/"
226
+ resp = @http.query :get, endpoint
227
+ if resp.code === 200
228
+ value = Ox.load(resp.content, mode: :hash)[:ListBucketResult]
229
+ .select {|k| !k[:Contents].nil?}
230
+ .map {|k| endpoint + k[:Contents][:Key]}
231
+ OperationResult.new(true, "OK", value, resp.code)
232
+ else
233
+ OperationResult.new(false, resp.content, nil, resp.code)
234
+ end
235
+ end
236
+
237
+ def set_blob_acl(uri, access, max, count = 0)
238
+ endpoint = uri + "?acl="
239
+ set_acl endpoint, access, max, count
240
+ end
241
+
242
+ def set_bucket_acl(key, access, max, count = 0)
243
+ endpoint = "https://#{@region}.digitaloceanspaces.com/#{key}/?acl="
244
+ set_acl endpoint, access, max, count
245
+ end
246
+
247
+ def set_acl(endpoint, access, max, count)
248
+ error = "Failed too many times due to conflict"
249
+ return OperationResult.new(false, error, nil, 400) if ++count === max
250
+ acl = @http.query :get, endpoint
251
+ return OperationResult.new(false, acl.content, nil, acl.code) if acl.code != 200
252
+ acl_xml = Ox.load(acl.content, mode: :hash)
253
+ return OperationResult.new(false, "ACL XML Malformed? Check value for XML", acl_xml, 400) if acl_malformed acl_xml
254
+ owner_id = acl_owner acl_xml
255
+ new_xml = @acl_factory.generate_acl access, owner_id
256
+ resp = @http.query :put, endpoint, new_xml
257
+ if resp.code === 409
258
+ set_blob_acl uri, access, max, count
259
+ else
260
+ OperationResult.new resp.code === 200, resp.content, nil, resp.code
261
+ end
262
+ end
263
+
264
+ def acl_malformed(acl_xml)
265
+ policy = acl_xml[:AccessControlPolicy]
266
+ policy.nil? || policy.select {|k| !k[:Owner].nil?}.length == 0
267
+ end
268
+
269
+ def acl_owner(acl_xml)
270
+ acl_xml[:AccessControlPolicy].select {|k| !k[:Owner].nil?}.first[:Owner][:ID]
271
+ end
272
+
273
+
274
+ end
275
+ end