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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHENTICATION.md +17 -30
  3. data/CHANGELOG.md +312 -0
  4. data/CONTRIBUTING.md +4 -5
  5. data/LOGGING.md +1 -1
  6. data/OVERVIEW.md +37 -5
  7. data/TROUBLESHOOTING.md +2 -8
  8. data/lib/google/cloud/storage/bucket/acl.rb +40 -40
  9. data/lib/google/cloud/storage/bucket/cors.rb +4 -1
  10. data/lib/google/cloud/storage/bucket/lifecycle.rb +259 -44
  11. data/lib/google/cloud/storage/bucket/list.rb +3 -3
  12. data/lib/google/cloud/storage/bucket.rb +1096 -172
  13. data/lib/google/cloud/storage/convert.rb +4 -3
  14. data/lib/google/cloud/storage/credentials.rb +16 -14
  15. data/lib/google/cloud/storage/errors.rb +7 -2
  16. data/lib/google/cloud/storage/file/acl.rb +181 -20
  17. data/lib/google/cloud/storage/file/list.rb +10 -8
  18. data/lib/google/cloud/storage/file/signer_v2.rb +36 -18
  19. data/lib/google/cloud/storage/file/signer_v4.rb +249 -61
  20. data/lib/google/cloud/storage/file/verifier.rb +2 -2
  21. data/lib/google/cloud/storage/file.rb +450 -84
  22. data/lib/google/cloud/storage/hmac_key/list.rb +182 -0
  23. data/lib/google/cloud/storage/hmac_key.rb +316 -0
  24. data/lib/google/cloud/storage/policy/binding.rb +246 -0
  25. data/lib/google/cloud/storage/policy/bindings.rb +196 -0
  26. data/lib/google/cloud/storage/policy/condition.rb +138 -0
  27. data/lib/google/cloud/storage/policy.rb +277 -24
  28. data/lib/google/cloud/storage/post_object.rb +20 -2
  29. data/lib/google/cloud/storage/project.rb +249 -50
  30. data/lib/google/cloud/storage/service.rb +479 -288
  31. data/lib/google/cloud/storage/version.rb +1 -1
  32. data/lib/google/cloud/storage.rb +86 -16
  33. data/lib/google-cloud-storage.rb +54 -7
  34. metadata +74 -27
@@ -25,12 +25,13 @@ module Google
25
25
  def storage_class_for str
26
26
  return nil if str.nil?
27
27
  return str.map { |s| storage_class_for s } if str.is_a? Array
28
- { "durable_reduced_availability" => "DURABLE_REDUCED_AVAILABILITY",
28
+ { "archive" => "ARCHIVE",
29
+ "coldline" => "COLDLINE",
29
30
  "dra" => "DURABLE_REDUCED_AVAILABILITY",
30
31
  "durable" => "DURABLE_REDUCED_AVAILABILITY",
31
- "nearline" => "NEARLINE",
32
- "coldline" => "COLDLINE",
32
+ "durable_reduced_availability" => "DURABLE_REDUCED_AVAILABILITY",
33
33
  "multi_regional" => "MULTI_REGIONAL",
34
+ "nearline" => "NEARLINE",
34
35
  "regional" => "REGIONAL",
35
36
  "standard" => "STANDARD" }[str.to_s.downcase] || str.to_s
36
37
  end
@@ -38,20 +38,22 @@ module Google
38
38
  # storage.project_id #=> "my-project"
39
39
  #
40
40
  class Credentials < Google::Auth::Credentials
41
- SCOPE = \
42
- ["https://www.googleapis.com/auth/devstorage.full_control"].freeze
43
- PATH_ENV_VARS = %w[STORAGE_CREDENTIALS
44
- STORAGE_KEYFILE
45
- GOOGLE_CLOUD_CREDENTIALS
46
- GOOGLE_CLOUD_KEYFILE
47
- GCLOUD_KEYFILE].freeze
48
- JSON_ENV_VARS = %w[STORAGE_CREDENTIALS_JSON
49
- STORAGE_KEYFILE_JSON
50
- GOOGLE_CLOUD_CREDENTIALS_JSON
51
- GOOGLE_CLOUD_KEYFILE_JSON
52
- GCLOUD_KEYFILE_JSON].freeze
53
- DEFAULT_PATHS = \
54
- ["~/.config/gcloud/application_default_credentials.json"].freeze
41
+ SCOPE = ["https://www.googleapis.com/auth/devstorage.full_control"].freeze
42
+ PATH_ENV_VARS = [
43
+ "STORAGE_CREDENTIALS",
44
+ "STORAGE_KEYFILE",
45
+ "GOOGLE_CLOUD_CREDENTIALS",
46
+ "GOOGLE_CLOUD_KEYFILE",
47
+ "GCLOUD_KEYFILE"
48
+ ].freeze
49
+ JSON_ENV_VARS = [
50
+ "STORAGE_CREDENTIALS_JSON",
51
+ "STORAGE_KEYFILE_JSON",
52
+ "GOOGLE_CLOUD_CREDENTIALS_JSON",
53
+ "GOOGLE_CLOUD_KEYFILE_JSON",
54
+ "GCLOUD_KEYFILE_JSON"
55
+ ].freeze
56
+ DEFAULT_PATHS = ["~/.config/gcloud/application_default_credentials.json"].freeze
55
57
  end
56
58
  end
57
59
  end
@@ -58,8 +58,13 @@ module Google
58
58
  ##
59
59
  # # SignedUrlUnavailable Error
60
60
  #
61
- # This is raised when File#signed_url is unable to generate a URL due to
62
- # missing credentials needed to create the URL.
61
+ # Raised by signed URL methods if the service account credentials
62
+ # are missing. Service account credentials are acquired by following the
63
+ # steps in [Service Account Authentication](
64
+ # https://cloud.google.com/iam/docs/service-accounts).
65
+ #
66
+ # @see https://cloud.google.com/storage/docs/access-control/signed-urls Signed URLs
67
+ #
63
68
  class SignedUrlUnavailable < Google::Cloud::Error
64
69
  end
65
70
  end
@@ -190,7 +190,7 @@ module Google
190
190
  generation: generation,
191
191
  user_project: user_project
192
192
  entity = gapi.entity
193
- @owners.push entity unless @owners.nil?
193
+ @owners&.push entity
194
194
  entity
195
195
  end
196
196
 
@@ -241,7 +241,7 @@ module Google
241
241
  generation: generation,
242
242
  user_project: user_project
243
243
  entity = gapi.entity
244
- @readers.push entity unless @readers.nil?
244
+ @readers&.push entity
245
245
  entity
246
246
  end
247
247
 
@@ -281,8 +281,8 @@ module Google
281
281
  @service.delete_file_acl \
282
282
  @bucket, @file, entity,
283
283
  generation: generation, user_project: user_project
284
- @owners.delete entity unless @owners.nil?
285
- @readers.delete entity unless @readers.nil?
284
+ @owners&.delete entity
285
+ @readers&.delete entity
286
286
  true
287
287
  end
288
288
 
@@ -297,6 +297,22 @@ module Google
297
297
  # Convenience method to apply the `authenticatedRead` predefined ACL
298
298
  # rule to the file.
299
299
  #
300
+ # @param [Integer] generation Select a specific revision of the file to
301
+ # update. The default is the latest version.
302
+ # @param [Integer] if_generation_match Makes the operation conditional
303
+ # on whether the file's current generation matches the given value.
304
+ # Setting to 0 makes the operation succeed only if there are no live
305
+ # versions of the file.
306
+ # @param [Integer] if_generation_not_match Makes the operation conditional
307
+ # on whether the file's current generation does not match the given
308
+ # value. If no live file exists, the precondition fails. Setting to 0
309
+ # makes the operation succeed only if there is a live version of the file.
310
+ # @param [Integer] if_metageneration_match Makes the operation conditional
311
+ # on whether the file's current metageneration matches the given value.
312
+ # @param [Integer] if_metageneration_not_match Makes the operation
313
+ # conditional on whether the file's current metageneration does not
314
+ # match the given value.
315
+ #
300
316
  # @example
301
317
  # require "google/cloud/storage"
302
318
  #
@@ -307,8 +323,17 @@ module Google
307
323
  # file = bucket.file "path/to/my-file.ext"
308
324
  # file.acl.auth!
309
325
  #
310
- def auth!
311
- update_predefined_acl! "authenticatedRead"
326
+ def auth! generation: nil,
327
+ if_generation_match: nil,
328
+ if_generation_not_match: nil,
329
+ if_metageneration_match: nil,
330
+ if_metageneration_not_match: nil
331
+ update_predefined_acl! "authenticatedRead",
332
+ generation: generation,
333
+ if_generation_match: if_generation_match,
334
+ if_generation_not_match: if_generation_not_match,
335
+ if_metageneration_match: if_metageneration_match,
336
+ if_metageneration_not_match: if_metageneration_not_match
312
337
  end
313
338
  alias authenticatedRead! auth!
314
339
  alias auth_read! auth!
@@ -319,6 +344,22 @@ module Google
319
344
  # Convenience method to apply the `bucketOwnerFullControl` predefined
320
345
  # ACL rule to the file.
321
346
  #
347
+ # @param [Integer] generation Select a specific revision of the file to
348
+ # update. The default is the latest version.
349
+ # @param [Integer] if_generation_match Makes the operation conditional
350
+ # on whether the file's current generation matches the given value.
351
+ # Setting to 0 makes the operation succeed only if there are no live
352
+ # versions of the file.
353
+ # @param [Integer] if_generation_not_match Makes the operation conditional
354
+ # on whether the file's current generation does not match the given
355
+ # value. If no live file exists, the precondition fails. Setting to 0
356
+ # makes the operation succeed only if there is a live version of the file.
357
+ # @param [Integer] if_metageneration_match Makes the operation conditional
358
+ # on whether the file's current metageneration matches the given value.
359
+ # @param [Integer] if_metageneration_not_match Makes the operation
360
+ # conditional on whether the file's current metageneration does not
361
+ # match the given value.
362
+ #
322
363
  # @example
323
364
  # require "google/cloud/storage"
324
365
  #
@@ -329,8 +370,17 @@ module Google
329
370
  # file = bucket.file "path/to/my-file.ext"
330
371
  # file.acl.owner_full!
331
372
  #
332
- def owner_full!
333
- update_predefined_acl! "bucketOwnerFullControl"
373
+ def owner_full! generation: nil,
374
+ if_generation_match: nil,
375
+ if_generation_not_match: nil,
376
+ if_metageneration_match: nil,
377
+ if_metageneration_not_match: nil
378
+ update_predefined_acl! "bucketOwnerFullControl",
379
+ generation: generation,
380
+ if_generation_match: if_generation_match,
381
+ if_generation_not_match: if_generation_not_match,
382
+ if_metageneration_match: if_metageneration_match,
383
+ if_metageneration_not_match: if_metageneration_not_match
334
384
  end
335
385
  alias bucketOwnerFullControl! owner_full!
336
386
 
@@ -338,6 +388,22 @@ module Google
338
388
  # Convenience method to apply the `bucketOwnerRead` predefined ACL
339
389
  # rule to the file.
340
390
  #
391
+ # @param [Integer] generation Select a specific revision of the file to
392
+ # update. The default is the latest version.
393
+ # @param [Integer] if_generation_match Makes the operation conditional
394
+ # on whether the file's current generation matches the given value.
395
+ # Setting to 0 makes the operation succeed only if there are no live
396
+ # versions of the file.
397
+ # @param [Integer] if_generation_not_match Makes the operation conditional
398
+ # on whether the file's current generation does not match the given
399
+ # value. If no live file exists, the precondition fails. Setting to 0
400
+ # makes the operation succeed only if there is a live version of the file.
401
+ # @param [Integer] if_metageneration_match Makes the operation conditional
402
+ # on whether the file's current metageneration matches the given value.
403
+ # @param [Integer] if_metageneration_not_match Makes the operation
404
+ # conditional on whether the file's current metageneration does not
405
+ # match the given value.
406
+ #
341
407
  # @example
342
408
  # require "google/cloud/storage"
343
409
  #
@@ -348,8 +414,17 @@ module Google
348
414
  # file = bucket.file "path/to/my-file.ext"
349
415
  # file.acl.owner_read!
350
416
  #
351
- def owner_read!
352
- update_predefined_acl! "bucketOwnerRead"
417
+ def owner_read! generation: nil,
418
+ if_generation_match: nil,
419
+ if_generation_not_match: nil,
420
+ if_metageneration_match: nil,
421
+ if_metageneration_not_match: nil
422
+ update_predefined_acl! "bucketOwnerRead",
423
+ generation: generation,
424
+ if_generation_match: if_generation_match,
425
+ if_generation_not_match: if_generation_not_match,
426
+ if_metageneration_match: if_metageneration_match,
427
+ if_metageneration_not_match: if_metageneration_not_match
353
428
  end
354
429
  alias bucketOwnerRead! owner_read!
355
430
 
@@ -357,6 +432,22 @@ module Google
357
432
  # Convenience method to apply the `private` predefined ACL
358
433
  # rule to the file.
359
434
  #
435
+ # @param [Integer] generation Select a specific revision of the file to
436
+ # update. The default is the latest version.
437
+ # @param [Integer] if_generation_match Makes the operation conditional
438
+ # on whether the file's current generation matches the given value.
439
+ # Setting to 0 makes the operation succeed only if there are no live
440
+ # versions of the file.
441
+ # @param [Integer] if_generation_not_match Makes the operation conditional
442
+ # on whether the file's current generation does not match the given
443
+ # value. If no live file exists, the precondition fails. Setting to 0
444
+ # makes the operation succeed only if there is a live version of the file.
445
+ # @param [Integer] if_metageneration_match Makes the operation conditional
446
+ # on whether the file's current metageneration matches the given value.
447
+ # @param [Integer] if_metageneration_not_match Makes the operation
448
+ # conditional on whether the file's current metageneration does not
449
+ # match the given value.
450
+ #
360
451
  # @example
361
452
  # require "google/cloud/storage"
362
453
  #
@@ -367,14 +458,39 @@ module Google
367
458
  # file = bucket.file "path/to/my-file.ext"
368
459
  # file.acl.private!
369
460
  #
370
- def private!
371
- update_predefined_acl! "private"
461
+ def private! generation: nil,
462
+ if_generation_match: nil,
463
+ if_generation_not_match: nil,
464
+ if_metageneration_match: nil,
465
+ if_metageneration_not_match: nil
466
+ update_predefined_acl! "private",
467
+ generation: generation,
468
+ if_generation_match: if_generation_match,
469
+ if_generation_not_match: if_generation_not_match,
470
+ if_metageneration_match: if_metageneration_match,
471
+ if_metageneration_not_match: if_metageneration_not_match
372
472
  end
373
473
 
374
474
  ##
375
475
  # Convenience method to apply the `projectPrivate` predefined ACL
376
476
  # rule to the file.
377
477
  #
478
+ # @param [Integer] generation Select a specific revision of the file to
479
+ # update. The default is the latest version.
480
+ # @param [Integer] if_generation_match Makes the operation conditional
481
+ # on whether the file's current generation matches the given value.
482
+ # Setting to 0 makes the operation succeed only if there are no live
483
+ # versions of the file.
484
+ # @param [Integer] if_generation_not_match Makes the operation conditional
485
+ # on whether the file's current generation does not match the given
486
+ # value. If no live file exists, the precondition fails. Setting to 0
487
+ # makes the operation succeed only if there is a live version of the file.
488
+ # @param [Integer] if_metageneration_match Makes the operation conditional
489
+ # on whether the file's current metageneration matches the given value.
490
+ # @param [Integer] if_metageneration_not_match Makes the operation
491
+ # conditional on whether the file's current metageneration does not
492
+ # match the given value.
493
+ #
378
494
  # @example
379
495
  # require "google/cloud/storage"
380
496
  #
@@ -385,8 +501,17 @@ module Google
385
501
  # file = bucket.file "path/to/my-file.ext"
386
502
  # file.acl.project_private!
387
503
  #
388
- def project_private!
389
- update_predefined_acl! "projectPrivate"
504
+ def project_private! generation: nil,
505
+ if_generation_match: nil,
506
+ if_generation_not_match: nil,
507
+ if_metageneration_match: nil,
508
+ if_metageneration_not_match: nil
509
+ update_predefined_acl! "projectPrivate",
510
+ generation: generation,
511
+ if_generation_match: if_generation_match,
512
+ if_generation_not_match: if_generation_not_match,
513
+ if_metageneration_match: if_metageneration_match,
514
+ if_metageneration_not_match: if_metageneration_not_match
390
515
  end
391
516
  alias projectPrivate! project_private!
392
517
 
@@ -394,6 +519,22 @@ module Google
394
519
  # Convenience method to apply the `publicRead` predefined ACL
395
520
  # rule to the file.
396
521
  #
522
+ # @param [Integer] generation Select a specific revision of the file to
523
+ # update. The default is the latest version.
524
+ # @param [Integer] if_generation_match Makes the operation conditional
525
+ # on whether the file's current generation matches the given value.
526
+ # Setting to 0 makes the operation succeed only if there are no live
527
+ # versions of the file.
528
+ # @param [Integer] if_generation_not_match Makes the operation conditional
529
+ # on whether the file's current generation does not match the given
530
+ # value. If no live file exists, the precondition fails. Setting to 0
531
+ # makes the operation succeed only if there is a live version of the file.
532
+ # @param [Integer] if_metageneration_match Makes the operation conditional
533
+ # on whether the file's current metageneration matches the given value.
534
+ # @param [Integer] if_metageneration_not_match Makes the operation
535
+ # conditional on whether the file's current metageneration does not
536
+ # match the given value.
537
+ #
397
538
  # @example
398
539
  # require "google/cloud/storage"
399
540
  #
@@ -404,8 +545,17 @@ module Google
404
545
  # file = bucket.file "path/to/my-file.ext"
405
546
  # file.acl.public!
406
547
  #
407
- def public!
408
- update_predefined_acl! "publicRead"
548
+ def public! generation: nil,
549
+ if_generation_match: nil,
550
+ if_generation_not_match: nil,
551
+ if_metageneration_match: nil,
552
+ if_metageneration_not_match: nil
553
+ update_predefined_acl! "publicRead",
554
+ generation: generation,
555
+ if_generation_match: if_generation_match,
556
+ if_generation_not_match: if_generation_not_match,
557
+ if_metageneration_match: if_metageneration_match,
558
+ if_metageneration_not_match: if_metageneration_not_match
409
559
  end
410
560
  alias publicRead! public!
411
561
  alias public_read! public!
@@ -418,9 +568,21 @@ module Google
418
568
  self
419
569
  end
420
570
 
421
- def update_predefined_acl! acl_role
571
+ def update_predefined_acl! acl_role,
572
+ generation: nil,
573
+ if_generation_match: nil,
574
+ if_generation_not_match: nil,
575
+ if_metageneration_match: nil,
576
+ if_metageneration_not_match: nil
422
577
  patched_file = Google::Apis::StorageV1::Object.new acl: []
423
- @service.patch_file @bucket, @file, patched_file,
578
+ @service.patch_file @bucket,
579
+ @file,
580
+ patched_file,
581
+ generation: generation,
582
+ if_generation_match: if_generation_match,
583
+ if_generation_not_match: if_generation_not_match,
584
+ if_metageneration_match: if_metageneration_match,
585
+ if_metageneration_not_match: if_metageneration_not_match,
424
586
  predefined_acl: acl_role,
425
587
  user_project: user_project
426
588
  clear!
@@ -428,8 +590,7 @@ module Google
428
590
 
429
591
  def entities_from_acls acls, role
430
592
  selected = acls.select { |acl| acl.role == role }
431
- entities = selected.map(&:entity)
432
- entities
593
+ selected.map(&:entity)
433
594
  end
434
595
  end
435
596
  end
@@ -77,11 +77,13 @@ module Google
77
77
  def next
78
78
  return nil unless next?
79
79
  ensure_service!
80
- options = {
81
- prefix: @prefix, delimiter: @delimiter, token: @token, max: @max,
82
- versions: @versions, user_project: @user_project
83
- }
84
- gapi = @service.list_files @bucket, options
80
+
81
+ gapi = @service.list_files @bucket, prefix: @prefix,
82
+ delimiter: @delimiter,
83
+ token: @token,
84
+ max: @max,
85
+ versions: @versions,
86
+ user_project: @user_project
85
87
  File::List.from_gapi gapi, @service, @bucket, @prefix,
86
88
  @delimiter, @max, @versions,
87
89
  user_project: @user_project
@@ -139,17 +141,17 @@ module Google
139
141
  # puts file.name
140
142
  # end
141
143
  #
142
- def all request_limit: nil
144
+ def all request_limit: nil, &block
143
145
  request_limit = request_limit.to_i if request_limit
144
146
  unless block_given?
145
147
  return enum_for :all, request_limit: request_limit
146
148
  end
147
149
  results = self
148
150
  loop do
149
- results.each { |r| yield r }
151
+ results.each(&block)
150
152
  if request_limit
151
153
  request_limit -= 1
152
- break if request_limit < 0
154
+ break if request_limit.negative?
153
155
  end
154
156
  break unless results.next?
155
157
  results = results.next
@@ -41,9 +41,20 @@ module Google
41
41
  end
42
42
 
43
43
  ##
44
- # The external path to the file.
44
+ # The external path to the file, URI-encoded.
45
+ # Will not URI encode the special `${filename}` variable.
46
+ # "You can also use the ${filename} variable..."
47
+ # https://cloud.google.com/storage/docs/xml-api/post-object
48
+ #
45
49
  def ext_path
46
- Addressable::URI.escape "/#{@bucket}/#{@path}"
50
+ path = "/#{@bucket}/#{@path}"
51
+ escaped = Addressable::URI.encode_component path, Addressable::URI::CharacterClasses::PATH
52
+ special_var = "${filename}"
53
+ # Restore the unencoded `${filename}` variable, if present.
54
+ if path.include? special_var
55
+ return escaped.gsub "$%7Bfilename%7D", special_var
56
+ end
57
+ escaped
47
58
  end
48
59
 
49
60
  ##
@@ -66,13 +77,21 @@ module Google
66
77
  end
67
78
 
68
79
  def determine_signing_key options = {}
69
- options[:signing_key] || options[:private_key] ||
70
- @service.credentials.signing_key
80
+ signing_key = options[:signing_key] || options[:private_key] ||
81
+ options[:signer] || @service.credentials.signing_key
82
+ raise SignedUrlUnavailable, error_msg("signing_key (private_key, signer)") unless signing_key
83
+ signing_key
71
84
  end
72
85
 
73
86
  def determine_issuer options = {}
74
- options[:issuer] || options[:client_email] ||
75
- @service.credentials.issuer
87
+ issuer = options[:issuer] || options[:client_email] || @service.credentials.issuer
88
+ raise SignedUrlUnavailable, error_msg("issuer (client_email)") unless issuer
89
+ issuer
90
+ end
91
+
92
+ def error_msg attr_name
93
+ "Service account credentials '#{attr_name}' is missing. To generate service account credentials " \
94
+ "see https://cloud.google.com/iam/docs/service-accounts"
76
95
  end
77
96
 
78
97
  def post_object options
@@ -88,8 +107,6 @@ module Google
88
107
  i = determine_issuer options
89
108
  s = determine_signing_key options
90
109
 
91
- raise SignedUrlUnavailable unless i && s
92
-
93
110
  policy_str = p.to_json
94
111
  policy = Base64.strict_encode64(policy_str).delete "\n"
95
112
 
@@ -108,18 +125,21 @@ module Google
108
125
  i = determine_issuer options
109
126
  s = determine_signing_key options
110
127
 
111
- raise SignedUrlUnavailable unless i && s
112
-
113
128
  sig = generate_signature s, signature_str(options)
114
129
  generate_signed_url i, sig, options[:expires], options[:query]
115
130
  end
116
131
 
117
132
  def generate_signature signing_key, secret
118
- unless signing_key.respond_to? :sign
119
- signing_key = OpenSSL::PKey::RSA.new signing_key
133
+ unencoded_signature = ""
134
+ if signing_key.is_a? Proc
135
+ unencoded_signature = signing_key.call secret
136
+ else
137
+ unless signing_key.respond_to? :sign
138
+ signing_key = OpenSSL::PKey::RSA.new signing_key
139
+ end
140
+ unencoded_signature = signing_key.sign OpenSSL::Digest::SHA256.new, secret
120
141
  end
121
- signature = signing_key.sign OpenSSL::Digest::SHA256.new, secret
122
- Base64.strict_encode64(signature).delete "\n"
142
+ Base64.strict_encode64(unencoded_signature).delete "\n"
123
143
  end
124
144
 
125
145
  def generate_signed_url issuer, signed_string, expires, query
@@ -127,10 +147,8 @@ module Google
127
147
  "&Expires=#{expires}" \
128
148
  "&Signature=#{url_escape signed_string}"
129
149
 
130
- if query
131
- query.each do |name, value|
132
- url << "&#{url_escape name}=#{url_escape value}"
133
- end
150
+ query&.each do |name, value|
151
+ url << "&#{url_escape name}=#{url_escape value}"
134
152
  end
135
153
 
136
154
  url