google-cloud-storage 1.18.1 → 1.44.0

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