google-cloud-storage 1.18.1 → 1.44.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 +4 -4
- data/AUTHENTICATION.md +17 -30
- data/CHANGELOG.md +312 -0
- data/CONTRIBUTING.md +4 -5
- data/LOGGING.md +1 -1
- data/OVERVIEW.md +37 -5
- data/TROUBLESHOOTING.md +2 -8
- data/lib/google/cloud/storage/bucket/acl.rb +40 -40
- data/lib/google/cloud/storage/bucket/cors.rb +4 -1
- data/lib/google/cloud/storage/bucket/lifecycle.rb +259 -44
- data/lib/google/cloud/storage/bucket/list.rb +3 -3
- data/lib/google/cloud/storage/bucket.rb +1096 -172
- data/lib/google/cloud/storage/convert.rb +4 -3
- data/lib/google/cloud/storage/credentials.rb +16 -14
- data/lib/google/cloud/storage/errors.rb +7 -2
- data/lib/google/cloud/storage/file/acl.rb +181 -20
- data/lib/google/cloud/storage/file/list.rb +10 -8
- data/lib/google/cloud/storage/file/signer_v2.rb +36 -18
- data/lib/google/cloud/storage/file/signer_v4.rb +249 -61
- data/lib/google/cloud/storage/file/verifier.rb +2 -2
- data/lib/google/cloud/storage/file.rb +450 -84
- data/lib/google/cloud/storage/hmac_key/list.rb +182 -0
- data/lib/google/cloud/storage/hmac_key.rb +316 -0
- data/lib/google/cloud/storage/policy/binding.rb +246 -0
- data/lib/google/cloud/storage/policy/bindings.rb +196 -0
- data/lib/google/cloud/storage/policy/condition.rb +138 -0
- data/lib/google/cloud/storage/policy.rb +277 -24
- data/lib/google/cloud/storage/post_object.rb +20 -2
- data/lib/google/cloud/storage/project.rb +249 -50
- data/lib/google/cloud/storage/service.rb +479 -288
- data/lib/google/cloud/storage/version.rb +1 -1
- data/lib/google/cloud/storage.rb +86 -16
- data/lib/google-cloud-storage.rb +54 -7
- metadata +74 -27
@@ -39,37 +39,83 @@ module Google
|
|
39
39
|
new bucket.name, file_name, bucket.service
|
40
40
|
end
|
41
41
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
def post_object issuer: nil,
|
43
|
+
client_email: nil,
|
44
|
+
signing_key: nil,
|
45
|
+
private_key: nil,
|
46
|
+
signer: nil,
|
47
|
+
expires: nil,
|
48
|
+
fields: nil,
|
49
|
+
conditions: nil,
|
50
|
+
scheme: "https",
|
51
|
+
virtual_hosted_style: nil,
|
52
|
+
bucket_bound_hostname: nil
|
53
|
+
i = determine_issuer issuer, client_email
|
54
|
+
s = determine_signing_key signing_key, private_key, signer
|
55
|
+
|
56
|
+
now = Time.now.utc
|
57
|
+
base_fields = required_fields i, now
|
58
|
+
post_fields = fields.dup || {}
|
59
|
+
post_fields.merge! base_fields
|
60
|
+
|
61
|
+
p = {}
|
62
|
+
p["conditions"] = policy_conditions base_fields, conditions, fields
|
63
|
+
expires ||= 60 * 60 * 24
|
64
|
+
p["expiration"] = (now + expires).strftime "%Y-%m-%dT%H:%M:%SZ"
|
65
|
+
|
66
|
+
policy_str = escape_characters p.to_json
|
67
|
+
|
68
|
+
policy = Base64.strict_encode64(policy_str).force_encoding "utf-8"
|
69
|
+
signature = generate_signature s, policy
|
70
|
+
|
71
|
+
post_fields["x-goog-signature"] = signature
|
72
|
+
post_fields["policy"] = policy
|
73
|
+
url = post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
|
74
|
+
hostname = "#{url}#{bucket_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
|
75
|
+
Google::Cloud::Storage::PostObject.new hostname, post_fields
|
76
|
+
end
|
77
|
+
|
78
|
+
def signed_url method: "GET",
|
79
|
+
expires: nil,
|
80
|
+
headers: nil,
|
81
|
+
issuer: nil,
|
82
|
+
client_email: nil,
|
83
|
+
signing_key: nil,
|
84
|
+
private_key: nil,
|
85
|
+
signer: nil,
|
86
|
+
query: nil,
|
87
|
+
scheme: "https",
|
88
|
+
virtual_hosted_style: nil,
|
89
|
+
bucket_bound_hostname: nil
|
90
|
+
raise ArgumentError, "method is required" unless method
|
91
|
+
issuer, signer = issuer_and_signer issuer, client_email, signing_key, private_key, signer
|
47
92
|
datetime_now = Time.now.utc
|
48
93
|
goog_date = datetime_now.strftime "%Y%m%dT%H%M%SZ"
|
49
94
|
datestamp = datetime_now.strftime "%Y%m%d"
|
50
95
|
# goog4_request is not checked.
|
51
96
|
scope = "#{datestamp}/auto/storage/goog4_request"
|
52
97
|
|
53
|
-
canonical_headers_str, signed_headers_str = \
|
54
|
-
|
98
|
+
canonical_headers_str, signed_headers_str = canonical_and_signed_headers \
|
99
|
+
headers, virtual_hosted_style, bucket_bound_hostname
|
55
100
|
|
56
101
|
algorithm = "GOOG4-RSA-SHA256"
|
57
102
|
expires = determine_expires expires
|
58
|
-
credential =
|
59
|
-
canonical_query_str = canonical_query query, algorithm,
|
60
|
-
credential, goog_date,
|
61
|
-
expires, signed_headers_str
|
103
|
+
credential = "#{issuer}/#{scope}"
|
104
|
+
canonical_query_str = canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
|
62
105
|
|
63
106
|
# From AWS: You don't include a payload hash in the Canonical
|
64
107
|
# Request, because when you create a presigned URL, you don't know
|
65
108
|
# the payload content because the URL is used to upload an arbitrary
|
66
109
|
# payload. Instead, you use a constant string UNSIGNED-PAYLOAD.
|
110
|
+
payload = headers&.key?("X-Goog-Content-SHA256") ? headers["X-Goog-Content-SHA256"] : "UNSIGNED-PAYLOAD"
|
111
|
+
|
67
112
|
canonical_request = [method,
|
68
|
-
|
113
|
+
file_path(!(virtual_hosted_style || bucket_bound_hostname)),
|
69
114
|
canonical_query_str,
|
70
115
|
canonical_headers_str,
|
71
116
|
signed_headers_str,
|
72
|
-
|
117
|
+
payload].join("\n")
|
118
|
+
|
73
119
|
# Construct string to sign
|
74
120
|
req_sha = Digest::SHA256.hexdigest canonical_request
|
75
121
|
string_to_sign = [algorithm, goog_date, scope, req_sha].join "\n"
|
@@ -78,65 +124,131 @@ module Google
|
|
78
124
|
signature = signer.call string_to_sign
|
79
125
|
|
80
126
|
# Construct signed URL
|
81
|
-
|
127
|
+
hostname = signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
|
128
|
+
"#{hostname}?#{canonical_query_str}&X-Goog-Signature=#{signature}"
|
129
|
+
end
|
130
|
+
|
131
|
+
# methods below are public visibility only for unit testing
|
132
|
+
# rubocop:disable Style/StringLiterals
|
133
|
+
def escape_characters str
|
134
|
+
str.split("").map do |s|
|
135
|
+
if s.ascii_only?
|
136
|
+
case s
|
137
|
+
when "\\"
|
138
|
+
'\\'
|
139
|
+
when "\b"
|
140
|
+
'\b'
|
141
|
+
when "\f"
|
142
|
+
'\f'
|
143
|
+
when "\n"
|
144
|
+
'\n'
|
145
|
+
when "\r"
|
146
|
+
'\r'
|
147
|
+
when "\t"
|
148
|
+
'\t'
|
149
|
+
when "\v"
|
150
|
+
'\v'
|
151
|
+
else
|
152
|
+
s
|
153
|
+
end
|
154
|
+
else
|
155
|
+
escape_special_unicode s
|
156
|
+
end
|
157
|
+
end.join
|
158
|
+
end
|
159
|
+
# rubocop:enable Style/StringLiterals
|
160
|
+
|
161
|
+
def escape_special_unicode str
|
162
|
+
str.unpack("U*").map { |i| "\\u#{i.to_s(16).rjust(4, '0')}" }.join
|
82
163
|
end
|
83
164
|
|
84
165
|
protected
|
85
166
|
|
167
|
+
def required_fields issuer, time
|
168
|
+
{
|
169
|
+
"key" => @file_name,
|
170
|
+
"x-goog-date" => time.strftime("%Y%m%dT%H%M%SZ"),
|
171
|
+
"x-goog-credential" => "#{issuer}/#{time.strftime '%Y%m%d'}/auto/storage/goog4_request",
|
172
|
+
"x-goog-algorithm" => "GOOG4-RSA-SHA256"
|
173
|
+
}.freeze
|
174
|
+
end
|
175
|
+
|
176
|
+
def policy_conditions base_fields, user_conditions, user_fields
|
177
|
+
# Convert each pair in base_fields hash to a single-entry hash in an array.
|
178
|
+
conditions = base_fields.to_a.map { |f| Hash[*f] }
|
179
|
+
# Add the bucket to the head of the base_fields. This is not returned in the PostObject fields.
|
180
|
+
conditions.unshift "bucket" => @bucket_name
|
181
|
+
# Add user-provided conditions to the head of the conditions array.
|
182
|
+
conditions = user_conditions + conditions if user_conditions
|
183
|
+
if user_fields
|
184
|
+
# Convert each pair in fields hash to a single-entry hash and add it to the head of the conditions array.
|
185
|
+
user_fields.to_a.reverse.each { |f| conditions.unshift Hash[*f] }
|
186
|
+
end
|
187
|
+
conditions.freeze
|
188
|
+
end
|
189
|
+
|
190
|
+
def signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
|
191
|
+
url = ext_url scheme, virtual_hosted_style, bucket_bound_hostname
|
192
|
+
"#{url}#{file_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
|
193
|
+
end
|
194
|
+
|
86
195
|
def determine_issuer issuer, client_email
|
87
196
|
# Parse the Service Account and get client id and private key
|
88
197
|
issuer = issuer || client_email || @service.credentials.issuer
|
89
|
-
unless issuer
|
90
|
-
raise SignedUrlUnavailable, "issuer (client_email) missing"
|
91
|
-
end
|
198
|
+
raise SignedUrlUnavailable, error_msg("issuer (client_email)") unless issuer
|
92
199
|
issuer
|
93
200
|
end
|
94
201
|
|
95
|
-
def determine_signing_key signing_key, private_key
|
96
|
-
signing_key = signing_key || private_key ||
|
97
|
-
|
98
|
-
unless signing_key
|
99
|
-
raise SignedUrlUnavailable, "signing_key (private_key) missing"
|
100
|
-
end
|
202
|
+
def determine_signing_key signing_key, private_key, signer
|
203
|
+
signing_key = signing_key || private_key || signer || @service.credentials.signing_key
|
204
|
+
raise SignedUrlUnavailable, error_msg("signing_key (private_key, signer)") unless signing_key
|
101
205
|
signing_key
|
102
206
|
end
|
103
207
|
|
208
|
+
def error_msg attr_name
|
209
|
+
"Service account credentials '#{attr_name}' is missing. To generate service account credentials " \
|
210
|
+
"see https://cloud.google.com/iam/docs/service-accounts"
|
211
|
+
end
|
212
|
+
|
104
213
|
def service_account_signer signer
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
214
|
+
if signer.is_a? Proc
|
215
|
+
lambda do |string_to_sign|
|
216
|
+
sig = signer.call string_to_sign
|
217
|
+
sig.unpack1 "H*"
|
218
|
+
end
|
219
|
+
else
|
220
|
+
signer = OpenSSL::PKey::RSA.new signer unless signer.respond_to? :sign
|
221
|
+
# Sign string to sign
|
222
|
+
lambda do |string_to_sign|
|
223
|
+
sig = signer.sign OpenSSL::Digest::SHA256.new, string_to_sign
|
224
|
+
sig.unpack1 "H*"
|
225
|
+
end
|
112
226
|
end
|
113
227
|
end
|
114
228
|
|
115
|
-
def issuer_and_signer issuer, client_email, signing_key, private_key
|
229
|
+
def issuer_and_signer issuer, client_email, signing_key, private_key, signer
|
116
230
|
issuer = determine_issuer issuer, client_email
|
117
|
-
signing_key = determine_signing_key signing_key, private_key
|
231
|
+
signing_key = determine_signing_key signing_key, private_key, signer
|
118
232
|
signer = service_account_signer signing_key
|
119
233
|
[issuer, signer]
|
120
234
|
end
|
121
235
|
|
122
|
-
def canonical_and_signed_headers headers
|
123
|
-
|
236
|
+
def canonical_and_signed_headers headers, virtual_hosted_style, bucket_bound_hostname
|
237
|
+
if virtual_hosted_style && bucket_bound_hostname
|
238
|
+
raise "virtual_hosted_style: #{virtual_hosted_style} and bucket_bound_hostname: " \
|
239
|
+
"#{bucket_bound_hostname} params cannot both be passed together"
|
240
|
+
end
|
241
|
+
|
124
242
|
canonical_headers = headers || {}
|
125
|
-
|
126
|
-
|
127
|
-
end]
|
128
|
-
canonical_headers["host"] = "storage.googleapis.com"
|
129
|
-
|
130
|
-
canonical_headers = canonical_headers.sort_by do |k, _|
|
131
|
-
k.downcase
|
132
|
-
end.to_h
|
133
|
-
canonical_headers_str = ""
|
134
|
-
canonical_headers.each do |k, v|
|
135
|
-
canonical_headers_str += "#{k}:#{v}\n"
|
243
|
+
headers_arr = canonical_headers.map do |k, v|
|
244
|
+
[k.downcase, v.strip.gsub(/[^\S\t]+/, " ").gsub(/\t+/, " ")]
|
136
245
|
end
|
137
|
-
|
138
|
-
canonical_headers
|
139
|
-
|
246
|
+
canonical_headers = Hash[headers_arr]
|
247
|
+
canonical_headers["host"] = host_name virtual_hosted_style, bucket_bound_hostname
|
248
|
+
|
249
|
+
canonical_headers = canonical_headers.sort_by(&:first).to_h
|
250
|
+
canonical_headers_str = canonical_headers.map { |k, v| "#{k}:#{v}\n" }.join
|
251
|
+
signed_headers_str = canonical_headers.keys.join ";"
|
140
252
|
[canonical_headers_str, signed_headers_str]
|
141
253
|
end
|
142
254
|
|
@@ -148,32 +260,108 @@ module Google
|
|
148
260
|
expires
|
149
261
|
end
|
150
262
|
|
151
|
-
def canonical_query query, algorithm, credential, goog_date, expires,
|
152
|
-
signed_headers_str
|
263
|
+
def canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
|
153
264
|
query ||= {}
|
154
265
|
query["X-Goog-Algorithm"] = algorithm
|
155
266
|
query["X-Goog-Credential"] = credential
|
156
267
|
query["X-Goog-Date"] = goog_date
|
157
268
|
query["X-Goog-Expires"] = expires
|
158
|
-
query["X-Goog-SignedHeaders"] =
|
159
|
-
query = query.
|
160
|
-
|
161
|
-
|
162
|
-
|
269
|
+
query["X-Goog-SignedHeaders"] = signed_headers_str
|
270
|
+
query = query.map { |k, v| [escape_query_param(k), escape_query_param(v)] }.sort_by(&:first).to_h
|
271
|
+
query.map { |k, v| "#{k}=#{v}" }.join "&"
|
272
|
+
end
|
273
|
+
|
274
|
+
##
|
275
|
+
# Only the characters in the regex set [A-Za-z0-9.~_-] must be left un-escaped; all others must be
|
276
|
+
# percent-encoded using %XX UTF-8 style.
|
277
|
+
def escape_query_param str
|
278
|
+
CGI.escape(str.to_s).gsub("%7E", "~").gsub "+", "%20"
|
279
|
+
end
|
280
|
+
|
281
|
+
def host_name virtual_hosted_style, bucket_bound_hostname
|
282
|
+
return bucket_bound_hostname if bucket_bound_hostname
|
283
|
+
virtual_hosted_style ? "#{@bucket_name}.storage.googleapis.com" : "storage.googleapis.com"
|
163
284
|
end
|
164
285
|
|
165
286
|
##
|
166
287
|
# The URI-encoded (percent encoded) external path to the file.
|
167
|
-
def
|
168
|
-
path =
|
169
|
-
path
|
170
|
-
|
288
|
+
def file_path path_style
|
289
|
+
path = []
|
290
|
+
path << "/#{@bucket_name}" if path_style
|
291
|
+
path << "/#{String(@file_name)}" if @file_name && !@file_name.empty?
|
292
|
+
CGI.escape(path.join).gsub("%2F", "/").gsub "+", "%20"
|
293
|
+
end
|
294
|
+
|
295
|
+
##
|
296
|
+
# The external path to the bucket, with trailing slash.
|
297
|
+
def bucket_path path_style
|
298
|
+
return "/#{@bucket_name}/" if path_style
|
171
299
|
end
|
172
300
|
|
173
301
|
##
|
174
302
|
# The external url to the file.
|
175
|
-
def ext_url
|
176
|
-
|
303
|
+
def ext_url scheme, virtual_hosted_style, bucket_bound_hostname
|
304
|
+
url = GOOGLEAPIS_URL.dup
|
305
|
+
if virtual_hosted_style
|
306
|
+
parts = url.split "//"
|
307
|
+
parts[1] = "#{@bucket_name}.#{parts[1]}"
|
308
|
+
parts.join "//"
|
309
|
+
elsif bucket_bound_hostname
|
310
|
+
raise ArgumentError, "scheme is required" unless scheme
|
311
|
+
URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}"
|
312
|
+
else
|
313
|
+
url
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def path_style? virtual_hosted_style, bucket_bound_hostname
|
318
|
+
!(virtual_hosted_style || bucket_bound_hostname)
|
319
|
+
end
|
320
|
+
|
321
|
+
##
|
322
|
+
# The external path to the file, URI-encoded.
|
323
|
+
# Will not URI encode the special `${filename}` variable.
|
324
|
+
# "You can also use the ${filename} variable..."
|
325
|
+
# https://cloud.google.com/storage/docs/xml-api/post-object
|
326
|
+
#
|
327
|
+
def post_object_ext_path
|
328
|
+
path = "/#{@bucket_name}/#{@file_name}"
|
329
|
+
escaped = Addressable::URI.encode_component path, Addressable::URI::CharacterClasses::PATH
|
330
|
+
special_var = "${filename}"
|
331
|
+
# Restore the unencoded `${filename}` variable, if present.
|
332
|
+
if path.include? special_var
|
333
|
+
return escaped.gsub "$%7Bfilename%7D", special_var
|
334
|
+
end
|
335
|
+
escaped
|
336
|
+
end
|
337
|
+
|
338
|
+
##
|
339
|
+
# The external url to the file.
|
340
|
+
def post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
|
341
|
+
url = GOOGLEAPIS_URL.dup
|
342
|
+
if virtual_hosted_style
|
343
|
+
parts = url.split "//"
|
344
|
+
parts[1] = "#{@bucket_name}.#{parts[1]}/"
|
345
|
+
parts.join "//"
|
346
|
+
elsif bucket_bound_hostname
|
347
|
+
raise ArgumentError, "scheme is required" unless scheme
|
348
|
+
URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}/"
|
349
|
+
else
|
350
|
+
url
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def generate_signature signing_key, data
|
355
|
+
packed_signature = nil
|
356
|
+
if signing_key.is_a? Proc
|
357
|
+
packed_signature = signing_key.call data
|
358
|
+
else
|
359
|
+
unless signing_key.respond_to? :sign
|
360
|
+
signing_key = OpenSSL::PKey::RSA.new signing_key
|
361
|
+
end
|
362
|
+
packed_signature = signing_key.sign OpenSSL::Digest::SHA256.new, data
|
363
|
+
end
|
364
|
+
packed_signature.unpack1("H*").force_encoding "utf-8"
|
177
365
|
end
|
178
366
|
end
|
179
367
|
end
|
@@ -50,7 +50,7 @@ module Google
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def self.md5_for local_file
|
53
|
-
if local_file.respond_to? :
|
53
|
+
if local_file.respond_to? :to_path
|
54
54
|
::File.open Pathname(local_file).to_path, "rb" do |f|
|
55
55
|
::Digest::MD5.file(f).base64digest
|
56
56
|
end
|
@@ -63,7 +63,7 @@ module Google
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def self.crc32c_for local_file
|
66
|
-
if local_file.respond_to? :
|
66
|
+
if local_file.respond_to? :to_path
|
67
67
|
::File.open Pathname(local_file).to_path, "rb" do |f|
|
68
68
|
::Digest::CRC32c.file(f).base64digest
|
69
69
|
end
|