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,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